Merge branch 'main' into storefront-nuxt

This commit is contained in:
Egor Pavlovich Gorbunov 2025-10-03 16:56:42 +03:00
commit 949e077942
24 changed files with 204 additions and 79 deletions

View file

@ -100,7 +100,7 @@ before running installment scripts
### nginx ### nginx
Please comment-out SSL-related lines, then apply necessary configurations, run `certbot --cert-only --nginx`, Please comment-out SSL-related lines, then apply necessary configurations, run `certbot --cert-only --nginx`,
decomment previously commented lines and enjoy eVibes over HTTPS! decomment previously commented lines, and enjoy eVibes over HTTPS!
### .env ### .env

View file

@ -3,11 +3,13 @@ import traceback
from typing import Optional from typing import Optional
import requests import requests
from constance import config
from django.core.cache import cache from django.core.cache import cache
from django.db import transaction from django.db import transaction
from core.crm.exceptions import CRMException from core.crm.exceptions import CRMException
from core.models import CustomerRelationshipManagementProvider, Order, OrderCrmLink from core.models import CustomerRelationshipManagementProvider, Order, OrderCrmLink
from core.utils import is_status_code_success
logger = logging.getLogger("django") logger = logging.getLogger("django")
@ -25,7 +27,7 @@ class AmoCRM:
logger.warning("Multiple AMO CRM providers found") logger.warning("Multiple AMO CRM providers found")
raise CRMException("Multiple AMO CRM providers found") from mre raise CRMException("Multiple AMO CRM providers found") from mre
self.base = f"https://{self.instance.integration_url}" self.base = f"{self.instance.integration_url}"
self.client_id = self.instance.authentication.get("client_id") self.client_id = self.instance.authentication.get("client_id")
self.client_secret = self.instance.authentication.get("client_secret") self.client_secret = self.instance.authentication.get("client_secret")
@ -51,6 +53,7 @@ class AmoCRM:
payload = { payload = {
"client_id": self.client_id, "client_id": self.client_id,
"client_secret": self.client_secret, "client_secret": self.client_secret,
"redirect_uri": f"https://api.{config.BASE_DOMAIN}/",
} }
if self.refresh_token: if self.refresh_token:
payload["grant_type"] = "refresh_token" payload["grant_type"] = "refresh_token"
@ -59,7 +62,9 @@ class AmoCRM:
payload["grant_type"] = "authorization_code" payload["grant_type"] = "authorization_code"
payload["code"] = self.authorization_code payload["code"] = self.authorization_code
r = requests.post(f"{self.base}/oauth2/access_token", json=payload, timeout=15) r = requests.post(f"{self.base}/oauth2/access_token", json=payload, timeout=15)
r.raise_for_status() if not is_status_code_success(r.status_code):
logger.error(f"Unable to get AMO access token: {r.status_code} {r.text}")
raise CRMException("Unable to get AMO access token")
data = r.json() data = r.json()
self.access_token = data["access_token"] self.access_token = data["access_token"]
cache.set("amo_refresh_token", data["refresh_token"], 604800) cache.set("amo_refresh_token", data["refresh_token"], 604800)
@ -81,16 +86,32 @@ class AmoCRM:
return payload return payload
def _get_customer_name(self, order: Order) -> str: def _get_customer_name(self, order: Order) -> str:
if not order.attributes.get("business_identificator"): if type(order.attributes) is not dict:
return order.user.get_full_name() or ( raise ValueError("order.attributes must be a dict")
f"{order.attributes.get('customer_name')} | "
f"{order.attributes.get('customer_phone_number') or order.attributes.get('customer_email')}" business_identificator = (
order.attributes.get("business_identificator")
or order.attributes.get("businessIdentificator")
or order.user.attributes.get("business_identificator")
or order.user.attributes.get("businessIdentificator")
or ""
)
if not business_identificator:
return (
order.user.get_full_name()
if order.user
else None
or (
f"{order.attributes.get('customer_name')} | "
f"{order.attributes.get('customer_phone_number') or order.attributes.get('customer_email')}"
)
) )
try: try:
business_identificator = order.attributes.get("business_identificator")
r = requests.get( r = requests.get(
f"https://api-fns.ru/api/egr?req={business_identificator}&key={self.fns_api_key}", timeout=15 f"https://api-fns.ru/api/egr?req={business_identificator}&key={self.fns_api_key}", timeout=15
) )
r.raise_for_status()
body = r.json() body = r.json()
except requests.exceptions.RequestException as rex: except requests.exceptions.RequestException as rex:
logger.error(f"Unable to get company info with FNS: {rex}") logger.error(f"Unable to get company info with FNS: {rex}")
@ -137,6 +158,8 @@ class AmoCRM:
body = r.json() body = r.json()
return body.get("_embedded", {}).get("contacts", [{}])[0].get("id", None) return body.get("_embedded", {}).get("contacts", [{}])[0].get("id", None)
return None
else: else:
return None return None
@ -175,5 +198,5 @@ class AmoCRM:
if link.order.status == new_status: if link.order.status == new_status:
return return
link.order.status = self.STATUS_MAP.get(new_status) link.order.status = self.STATUS_MAP[new_status]
link.order.save(update_fields=["status"]) link.order.save(update_fields=["status"])

View file

@ -9,7 +9,6 @@ from django.db.models import (
Case, Case,
Exists, Exists,
FloatField, FloatField,
IntegerField,
Max, Max,
OuterRef, OuterRef,
Prefetch, Prefetch,
@ -61,6 +60,7 @@ class CaseInsensitiveListFilter(BaseInFilter, CharFilter):
return qs return qs
# noinspection PyUnusedLocal
class ProductFilter(FilterSet): class ProductFilter(FilterSet):
search = CharFilter(field_name="name", method="search_products", label=_("Search")) search = CharFilter(field_name="name", method="search_products", label=_("Search"))
uuid = UUIDFilter(field_name="uuid", lookup_expr="exact", label=_("UUID")) uuid = UUIDFilter(field_name="uuid", lookup_expr="exact", label=_("UUID"))
@ -95,7 +95,6 @@ class ProductFilter(FilterSet):
("price_order", "price"), ("price_order", "price"),
("sku", "sku"), ("sku", "sku"),
("?", "random"), ("?", "random"),
("personal_order_only", "personal_order_only"),
), ),
initial="uuid", initial="uuid",
) )
@ -153,7 +152,7 @@ class ProductFilter(FilterSet):
if not value: if not value:
return queryset return queryset
uuids = [product.get("uuid") for product in process_query(query=value, indexes=("products",))["products"]] uuids = [product.get("uuid") for product in process_query(query=value, indexes=("products",))["products"]] # type: ignore
return queryset.filter(uuid__in=uuids) return queryset.filter(uuid__in=uuids)
@ -280,21 +279,21 @@ class ProductFilter(FilterSet):
qs = qs.annotate( qs = qs.annotate(
has_stock=Max( has_stock=Max(
Case( Case(
When(stocks__quantity__gt=0, then=Value(1)), When(stocks__quantity__gt=0, then=Value(True)),
default=Value(0), default=Value(False),
output_field=IntegerField(), output_field=BooleanField(),
) )
), ),
has_price=Max( has_price=Max(
Case( Case(
When(stocks__price__gt=0, then=Value(1)), When(stocks__price__gt=0, then=Value(True)),
default=Value(0), default=Value(False),
output_field=IntegerField(), output_field=BooleanField(),
) )
), ),
).annotate( ).annotate(
personal_order_only=Case( personal_orders_only=Case(
When(has_stock=0, has_price=1, then=Value(True)), When(has_stock=False, has_price=False, then=Value(True)),
default=Value(False), default=Value(False),
output_field=BooleanField(), output_field=BooleanField(),
) )
@ -311,10 +310,11 @@ class ProductFilter(FilterSet):
key = "?" key = "?"
mapped_requested.append(key) mapped_requested.append(key)
continue continue
if key == "personal_orders_only":
continue
mapped_requested.append(f"-{key}" if desc else key) mapped_requested.append(f"-{key}" if desc else key)
has_personal_in_request = any(p.lstrip("-") == "personal_order_only" for p in mapped_requested) final_ordering = mapped_requested + ["personal_orders_only"]
final_ordering = (["personal_order_only"] if not has_personal_in_request else []) + mapped_requested
if final_ordering: if final_ordering:
qs = qs.order_by(*final_ordering) qs = qs.order_by(*final_ordering)
@ -396,6 +396,7 @@ class WishlistFilter(FilterSet):
fields = ["uuid", "user_email", "user", "order_by"] fields = ["uuid", "user_email", "user", "order_by"]
# noinspection PyUnusedLocal
class CategoryFilter(FilterSet): class CategoryFilter(FilterSet):
search = CharFilter(field_name="name", method="search_categories", label=_("Search")) search = CharFilter(field_name="name", method="search_categories", label=_("Search"))
uuid = UUIDFilter(field_name="uuid", lookup_expr="exact") uuid = UUIDFilter(field_name="uuid", lookup_expr="exact")
@ -429,7 +430,7 @@ class CategoryFilter(FilterSet):
if not value: if not value:
return queryset return queryset
uuids = [category.get("uuid") for category in process_query(query=value, indexes=("categories",))["categories"]] uuids = [category.get("uuid") for category in process_query(query=value, indexes=("categories",))["categories"]] # type: ignore
return queryset.filter(uuid__in=uuids) return queryset.filter(uuid__in=uuids)
@ -522,6 +523,7 @@ class CategoryFilter(FilterSet):
return queryset.filter(parent__uuid=uuid_val) return queryset.filter(parent__uuid=uuid_val)
# noinspection PyUnusedLocal
class BrandFilter(FilterSet): class BrandFilter(FilterSet):
search = CharFilter(field_name="name", method="search_brands", label=_("Search")) search = CharFilter(field_name="name", method="search_brands", label=_("Search"))
uuid = UUIDFilter(field_name="uuid", lookup_expr="exact") uuid = UUIDFilter(field_name="uuid", lookup_expr="exact")
@ -547,7 +549,7 @@ class BrandFilter(FilterSet):
if not value: if not value:
return queryset return queryset
uuids = [brand.get("uuid") for brand in process_query(query=value, indexes=("brands",))["brands"]] uuids = [brand.get("uuid") for brand in process_query(query=value, indexes=("brands",))["brands"]] # type: ignore
return queryset.filter(uuid__in=uuids) return queryset.filter(uuid__in=uuids)

View file

@ -546,6 +546,7 @@ class FeedbackProductAction(BaseMutation):
order_product = OrderProduct.objects.get(uuid=order_product_uuid) order_product = OrderProduct.objects.get(uuid=order_product_uuid)
if user != order_product.order.user: if user != order_product.order.user:
raise PermissionDenied(permission_denied_message) raise PermissionDenied(permission_denied_message)
feedback = None
match action: match action:
case "add": case "add":
feedback = order_product.do_feedback(comment=comment, rating=rating, action="add") feedback = order_product.do_feedback(comment=comment, rating=rating, action="add")

View file

@ -1,8 +1,9 @@
import logging import logging
from typing import Any
from constance import config from constance import config
from django.core.cache import cache from django.core.cache import cache
from django.db.models import Max, Min, QuerySet from django.db.models import Max, Min
from django.db.models.functions import Length from django.db.models.functions import Length
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from graphene import ( from graphene import (
@ -55,7 +56,6 @@ from core.utils.seo_builders import (
website_schema, website_schema,
) )
from payments.graphene.object_types import TransactionType from payments.graphene.object_types import TransactionType
from payments.models import Transaction
logger = logging.getLogger("django") logger = logging.getLogger("django")
@ -464,19 +464,19 @@ class OrderType(DjangoObjectType):
) )
description = _("orders") description = _("orders")
def resolve_total_price(self: Order, _info): def resolve_total_price(self: Order, _info) -> float:
return self.total_price return self.total_price
def resolve_total_quantity(self: Order, _info): def resolve_total_quantity(self: Order, _info) -> int:
return self.total_quantity return self.total_quantity
def resolve_notifications(self: Order, _info): def resolve_notifications(self: Order, _info) -> dict[str, Any]:
return camelize(self.notifications) return camelize(self.notifications)
def resolve_attributes(self: Order, _info): def resolve_attributes(self: Order, _info) -> dict[str, Any]:
return camelize(self.attributes) return camelize(self.attributes)
def resolve_payments_transactions(self: Order, _info) -> QuerySet[Transaction] | None: def resolve_payments_transactions(self: Order, _info):
if self.payments_transactions: if self.payments_transactions:
return self.payments_transactions.all() return self.payments_transactions.all()
return None return None

View file

@ -1,6 +1,7 @@
import logging import logging
from django.core.cache import cache from django.core.cache import cache
from django.db.models import Max, Case, When, Value, IntegerField, BooleanField
from django.utils import timezone from django.utils import timezone
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from graphene import Field, List, ObjectType, Schema from graphene import Field, List, ObjectType, Schema
@ -148,7 +149,7 @@ class Query(ObjectType):
product = Product.objects.get(uuid=kwargs["uuid"]) product = Product.objects.get(uuid=kwargs["uuid"])
if product.is_active and product.brand.is_active and product.category.is_active: if product.is_active and product.brand.is_active and product.category.is_active:
info.context.user.add_to_recently_viewed(product.uuid) info.context.user.add_to_recently_viewed(product.uuid)
return ( base_qs = (
Product.objects.all().select_related("brand", "category").prefetch_related("images", "stocks") Product.objects.all().select_related("brand", "category").prefetch_related("images", "stocks")
if info.context.user.has_perm("core.view_product") if info.context.user.has_perm("core.view_product")
else Product.objects.filter( else Product.objects.filter(
@ -162,6 +163,35 @@ class Query(ObjectType):
.prefetch_related("images", "stocks") .prefetch_related("images", "stocks")
) )
base_qs = (
base_qs.annotate(
has_stock=Max(
Case(
When(stocks__quantity__gt=0, then=Value(1)),
default=Value(0),
output_field=IntegerField(),
)
),
has_price=Max(
Case(
When(stocks__price__gt=0, then=Value(1)),
default=Value(0),
output_field=IntegerField(),
)
),
)
.annotate(
personal_order_only=Case(
When(has_stock=0, has_price=1, then=Value(True)),
default=Value(False),
output_field=BooleanField(),
)
)
.order_by("personal_order_only")
)
return base_qs
@staticmethod @staticmethod
def resolve_orders(_parent, info, **kwargs): def resolve_orders(_parent, info, **kwargs):
orders = Order.objects orders = Order.objects

View file

@ -2133,7 +2133,7 @@ msgstr "человекочитаемый идентификатор"
#: core/models.py:1261 #: core/models.py:1261
msgid "order" msgid "order"
msgstr "Заказать" msgstr "Заказ"
#: core/models.py:1282 #: core/models.py:1282
msgid "a user must have only one pending order at a time" msgid "a user must have only one pending order at a time"

View file

@ -9,6 +9,7 @@ def generate_unique_sku(make_candidate, taken):
return c return c
# noinspection PyUnusedLocal
def backfill_sku(apps, schema_editor): def backfill_sku(apps, schema_editor):
Product = apps.get_model("core", "Product") Product = apps.get_model("core", "Product")
from core.utils import generate_human_readable_id as make_candidate from core.utils import generate_human_readable_id as make_candidate
@ -35,6 +36,7 @@ def backfill_sku(apps, schema_editor):
last_pk = ids[-1] last_pk = ids[-1]
# noinspection PyUnusedLocal
def noop(apps, schema_editor): def noop(apps, schema_editor):
pass pass

View file

@ -541,7 +541,7 @@ class Product(ExportModelOperationsMixin("product"), NiceModel): # type: ignore
default=generate_human_readable_id, default=generate_human_readable_id,
) )
objects: ProductManager = ProductManager() objects = ProductManager()
class Meta: class Meta:
verbose_name = _("product") verbose_name = _("product")
@ -599,7 +599,7 @@ class Product(ExportModelOperationsMixin("product"), NiceModel): # type: ignore
@property @property
def personal_orders_only(self) -> bool: def personal_orders_only(self) -> bool:
return not self.quantity > 0 and self.price > 0.0 return not (self.quantity > 0 and self.price > 0.0)
class Attribute(ExportModelOperationsMixin("attribute"), NiceModel): # type: ignore [misc, django-manager-missing] class Attribute(ExportModelOperationsMixin("attribute"), NiceModel): # type: ignore [misc, django-manager-missing]
@ -1266,6 +1266,10 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi
@property @property
def is_business(self) -> bool: def is_business(self) -> bool:
if type(self.attributes) is not dict:
self.attributes = {}
self.save()
return False
with suppress(Exception): with suppress(Exception):
return (self.attributes.get("is_business", False) if self.attributes else False) or ( return (self.attributes.get("is_business", False) if self.attributes else False) or (
(self.user.attributes.get("is_business", False) and self.user.attributes.get("business_identificator")) (self.user.attributes.get("is_business", False) and self.user.attributes.get("business_identificator"))
@ -1306,7 +1310,7 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi
def add_product( def add_product(
self, self,
product_uuid=None, product_uuid=None,
attributes: list | None = None, attributes: list | dict | None = None,
update_quantity=True, update_quantity=True,
): ):
if attributes is None: if attributes is None:
@ -1776,7 +1780,10 @@ class OrderProduct(ExportModelOperationsMixin("order_product"), NiceModel): # t
def download_url(self: Self) -> str: def download_url(self: Self) -> str:
if self.product and self.product.stocks: if self.product and self.product.stocks:
if self.product.is_digital and self.product.stocks.first().digital_asset: # type: ignore [union-attr] if self.product.is_digital and self.product.stocks.first().digital_asset: # type: ignore [union-attr]
return self.download.url try:
return self.download.url
except self.download.RelatedObjectDoesNotExist:
return DigitalAssetDownload.objects.create(order_product=self).url
return "" return ""
def do_feedback(self, rating=10, comment="", action="add") -> Optional["Feedback"] | int: def do_feedback(self, rating=10, comment="", action="add") -> Optional["Feedback"] | int:
@ -1794,7 +1801,7 @@ class OrderProduct(ExportModelOperationsMixin("order_product"), NiceModel): # t
if action == "add": if action == "add":
if not feedback_exists: if not feedback_exists:
if self.order.status not in ["MOMENTAL", "PENDING", "FAILED"]: if self.order.status == "FINISHED":
return Feedback.objects.create(rating=rating, comment=comment, order_product=self) return Feedback.objects.create(rating=rating, comment=comment, order_product=self)
else: else:
raise ValueError(_("you cannot feedback an order which is not received")) raise ValueError(_("you cannot feedback an order which is not received"))
@ -1827,7 +1834,7 @@ class CustomerRelationshipManagementProvider(ExportModelOperationsMixin("crm_pro
verbose_name_plural = _("CRMs") verbose_name_plural = _("CRMs")
class OrderCrmLink(ExportModelOperationsMixin("order_crm_link"), NiceModel): class OrderCrmLink(ExportModelOperationsMixin("order_crm_link"), NiceModel): # type: ignore
order = ForeignKey(to=Order, on_delete=PROTECT, related_name="crm_links") order = ForeignKey(to=Order, on_delete=PROTECT, related_name="crm_links")
crm = ForeignKey(to=CustomerRelationshipManagementProvider, on_delete=PROTECT, related_name="order_links") crm = ForeignKey(to=CustomerRelationshipManagementProvider, on_delete=PROTECT, related_name="order_links")
crm_lead_id = CharField(max_length=30, unique=True, db_index=True) crm_lead_id = CharField(max_length=30, unique=True, db_index=True)

View file

@ -106,7 +106,7 @@ class CategoryDetailSerializer(ModelSerializer):
filterable_results = [] filterable_results = []
for attr in attributes: for attr in attributes:
vals = grouped.get(attr.id, []) vals = grouped.get(attr.id, []) # type: ignore
slice_vals = vals[:128] if len(vals) > 128 else vals slice_vals = vals[:128] if len(vals) > 128 else vals
filterable_results.append( filterable_results.append(
{ {

View file

@ -62,7 +62,7 @@ class AddressCreateSerializer(ModelSerializer):
write_only=True, write_only=True,
max_length=512, max_length=512,
) )
address_line_1 = CharField(write_only=True, max_length=128, required=False) address_line_1 = CharField(write_only=True, max_length=128, required=True)
address_line_2 = CharField(write_only=True, max_length=128, required=False) address_line_2 = CharField(write_only=True, max_length=128, required=False)
class Meta: class Meta:
@ -177,6 +177,7 @@ class BuyUnregisteredOrderSerializer(Serializer):
class BuyAsBusinessOrderSerializer(Serializer): class BuyAsBusinessOrderSerializer(Serializer):
products = AddOrderProductSerializer(many=True, required=True) products = AddOrderProductSerializer(many=True, required=True)
business_identificator = CharField(required=True) business_identificator = CharField(required=True)
promocode_uuid = UUIDField(required=False)
business_email = CharField(required=True) business_email = CharField(required=True)
business_phone_number = CharField(required=True) business_phone_number = CharField(required=True)
billing_business_address_uuid = CharField(required=False) billing_business_address_uuid = CharField(required=False)

View file

@ -255,3 +255,7 @@ def generate_human_readable_token() -> str:
str: A 20-character random token. str: A 20-character random token.
""" """
return "".join([secrets.choice(CROCKFORD) for _ in range(20)]) return "".join([secrets.choice(CROCKFORD) for _ in range(20)])
def is_status_code_success(status_code: int) -> bool:
return 200 <= status_code < 300

View file

@ -51,10 +51,20 @@ def send_order_created_email(order_pk: str) -> tuple[bool, str]:
except Order.DoesNotExist: except Order.DoesNotExist:
return False, f"Order not found with the given pk: {order_pk}" return False, f"Order not found with the given pk: {order_pk}"
if type(order.attributes) is not dict:
order.attributes = {}
if not any([order.user, order.attributes.get("email", None), order.attributes.get("customer_email", None)]): if not any([order.user, order.attributes.get("email", None), order.attributes.get("customer_email", None)]):
return False, f"Order's user not found with the given pk: {order_pk}" return False, f"Order's user not found with the given pk: {order_pk}"
activate(order.user.language) language = settings.LANGUAGE_CODE
recipient = order.attributes.get("customer_email", "")
if order.user:
recipient = order.user.email
language = order.user.language
activate(language)
set_email_settings() set_email_settings()
connection = mail.get_connection() connection = mail.get_connection()
@ -71,7 +81,7 @@ def send_order_created_email(order_pk: str) -> tuple[bool, str]:
"total_price": order.total_price, "total_price": order.total_price,
}, },
), ),
to=[order.user.email], to=[recipient],
from_email=f"{config.PROJECT_NAME} <{config.EMAIL_FROM}>", from_email=f"{config.PROJECT_NAME} <{config.EMAIL_FROM}>",
connection=connection, connection=connection,
) )

View file

@ -235,7 +235,7 @@ class AbstractVendor:
if not rate: if not rate:
raise RatesError(f"No rate found for {currency or self.currency} in {rates} with probider {provider}...") raise RatesError(f"No rate found for {currency or self.currency} in {rates} with probider {provider}...")
return float(round(price / rate, 2)) if rate else float(round(price, 2)) return float(round(price / rate, 2)) if rate else round(price, 2)
@staticmethod @staticmethod
def round_price_marketologically(price: float) -> float: def round_price_marketologically(price: float) -> float:

View file

@ -428,27 +428,35 @@ class BuyAsBusinessView(APIView):
Handles the "POST" request to process a business purchase. Handles the "POST" request to process a business purchase.
""" """
@method_decorator(ratelimit(key="ip", rate="2/h", block=True)) @method_decorator(ratelimit(key="ip", rate="10/h" if not settings.DEBUG else "888/h"))
def post(self, request, *_args, **kwargs): def post(self, request, *_args, **kwargs):
serializer = BuyAsBusinessOrderSerializer(data=request.data) serializer = BuyAsBusinessOrderSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
order = Order.objects.create(status="MOMENTAL") order = Order.objects.create(status="MOMENTAL")
products = [product.get("product_uuid") for product in serializer.validated_data.get("products")] products = [product.get("product_uuid") for product in serializer.validated_data.get("products")]
transaction = order.buy_without_registration( try:
products=products, transaction = order.buy_without_registration(
promocode_uuid=serializer.validated_data.get("promocode_uuid"), products=products,
customer_name=serializer.validated_data.get("business_identificator"), promocode_uuid=serializer.validated_data.get("promocode_uuid"),
customer_email=serializer.validated_data.get("business_email"), customer_name=serializer.validated_data.get("business_identificator"),
customer_phone_number=serializer.validated_data.get("business_phone_number"), customer_email=serializer.validated_data.get("business_email"),
billing_customer_address=serializer.validated_data.get("billing_business_address_uuid"), customer_phone_number=serializer.validated_data.get("business_phone_number"),
shipping_customer_address=serializer.validated_data.get("shipping_business_address_uuid"), billing_customer_address=serializer.validated_data.get("billing_business_address_uuid"),
payment_method=serializer.validated_data.get("payment_method"), shipping_customer_address=serializer.validated_data.get("shipping_business_address_uuid"),
is_business=True, payment_method=serializer.validated_data.get("payment_method"),
) is_business=True,
return Response( )
status=status.HTTP_201_CREATED, return Response(
data=TransactionProcessSerializer(transaction).data, status=status.HTTP_201_CREATED,
) data=TransactionProcessSerializer(transaction).data,
)
except Exception as e:
order.is_active = False
order.save()
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={"detail": str(e)},
)
def download_digital_asset_view(request, *args, **kwargs): def download_digital_asset_view(request, *args, **kwargs):

View file

@ -117,7 +117,6 @@ from core.utils.seo_builders import (
product_schema, product_schema,
website_schema, website_schema,
) )
from evibes.settings import DEBUG
from payments.serializers import TransactionProcessSerializer from payments.serializers import TransactionProcessSerializer
logger = logging.getLogger("django") logger = logging.getLogger("django")
@ -155,6 +154,7 @@ class EvibesViewSet(ModelViewSet):
@extend_schema_view(**ATTRIBUTE_GROUP_SCHEMA) @extend_schema_view(**ATTRIBUTE_GROUP_SCHEMA)
# noinspection PyUnusedLocal
class AttributeGroupViewSet(EvibesViewSet): class AttributeGroupViewSet(EvibesViewSet):
""" """
Represents a viewset for managing AttributeGroup objects. Represents a viewset for managing AttributeGroup objects.
@ -187,6 +187,7 @@ class AttributeGroupViewSet(EvibesViewSet):
@extend_schema_view(**ATTRIBUTE_SCHEMA) @extend_schema_view(**ATTRIBUTE_SCHEMA)
# noinspection PyUnusedLocal
class AttributeViewSet(EvibesViewSet): class AttributeViewSet(EvibesViewSet):
""" """
Handles operations related to Attribute objects within the application. Handles operations related to Attribute objects within the application.
@ -219,6 +220,7 @@ class AttributeViewSet(EvibesViewSet):
@extend_schema_view(**ATTRIBUTE_VALUE_SCHEMA) @extend_schema_view(**ATTRIBUTE_VALUE_SCHEMA)
# noinspection PyUnusedLocal
class AttributeValueViewSet(EvibesViewSet): class AttributeValueViewSet(EvibesViewSet):
""" """
A viewset for managing AttributeValue objects. A viewset for managing AttributeValue objects.
@ -247,6 +249,7 @@ class AttributeValueViewSet(EvibesViewSet):
@extend_schema_view(**CATEGORY_SCHEMA) @extend_schema_view(**CATEGORY_SCHEMA)
# noinspection PyUnusedLocal
class CategoryViewSet(EvibesViewSet): class CategoryViewSet(EvibesViewSet):
""" """
Manages views for Category-related operations. Manages views for Category-related operations.
@ -377,6 +380,7 @@ class CategoryViewSet(EvibesViewSet):
return Response(SeoSnapshotSerializer(payload).data) return Response(SeoSnapshotSerializer(payload).data)
# noinspection PyUnusedLocal
class BrandViewSet(EvibesViewSet): class BrandViewSet(EvibesViewSet):
""" """
Represents a viewset for managing Brand instances. Represents a viewset for managing Brand instances.
@ -502,6 +506,7 @@ class BrandViewSet(EvibesViewSet):
@extend_schema_view(**PRODUCT_SCHEMA) @extend_schema_view(**PRODUCT_SCHEMA)
# noinspection PyUnusedLocal
class ProductViewSet(EvibesViewSet): class ProductViewSet(EvibesViewSet):
""" """
Manages operations related to the `Product` model in the system. Manages operations related to the `Product` model in the system.
@ -635,6 +640,7 @@ class ProductViewSet(EvibesViewSet):
return Response(SeoSnapshotSerializer(payload).data) return Response(SeoSnapshotSerializer(payload).data)
# noinspection PyUnusedLocal
class VendorViewSet(EvibesViewSet): class VendorViewSet(EvibesViewSet):
""" """
Represents a viewset for managing Vendor objects. Represents a viewset for managing Vendor objects.
@ -666,6 +672,7 @@ class VendorViewSet(EvibesViewSet):
@extend_schema_view(**FEEDBACK_SCHEMA) @extend_schema_view(**FEEDBACK_SCHEMA)
# noinspection PyUnusedLocal
class FeedbackViewSet(EvibesViewSet): class FeedbackViewSet(EvibesViewSet):
""" """
Representation of a view set handling Feedback objects. Representation of a view set handling Feedback objects.
@ -704,6 +711,7 @@ class FeedbackViewSet(EvibesViewSet):
@extend_schema_view(**ORDER_SCHEMA) @extend_schema_view(**ORDER_SCHEMA)
# noinspection PyUnusedLocal
class OrderViewSet(EvibesViewSet): class OrderViewSet(EvibesViewSet):
""" """
ViewSet for managing orders and related operations. ViewSet for managing orders and related operations.
@ -798,7 +806,10 @@ class OrderViewSet(EvibesViewSet):
def current(self, request): def current(self, request):
if not request.user.is_authenticated: if not request.user.is_authenticated:
raise PermissionDenied(permission_denied_message) raise PermissionDenied(permission_denied_message)
order = Order.objects.get(user=request.user, status="PENDING") try:
order = Order.objects.get(user=request.user, status="PENDING")
except Order.DoesNotExist:
order = Order.objects.create(user=request.user)
return Response( return Response(
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
data=OrderDetailSerializer(order).data, data=OrderDetailSerializer(order).data,
@ -829,25 +840,30 @@ class OrderViewSet(EvibesViewSet):
except Order.DoesNotExist: except Order.DoesNotExist:
name = "Order" name = "Order"
return Response(status=status.HTTP_404_NOT_FOUND, data={"detail": _(f"{name} does not exist: {uuid}")}) return Response(status=status.HTTP_404_NOT_FOUND, data={"detail": _(f"{name} does not exist: {uuid}")})
except Exception as e:
return Response(status=status.HTTP_400_BAD_REQUEST, data={"detail": str(e)})
@action(detail=False, methods=["post"], url_path="buy_unregistered") @action(detail=False, methods=["post"], url_path="buy_unregistered")
@method_decorator(ratelimit(key="ip", rate="5/h" if not DEBUG else "888/h")) @method_decorator(ratelimit(key="ip", rate="10/h" if not settings.DEBUG else "888/h"))
def buy_unregistered(self, request): def buy_unregistered(self, request):
serializer = BuyUnregisteredOrderSerializer(data=request.data) serializer = BuyUnregisteredOrderSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
order = Order.objects.create(status="MOMENTAL") order = Order.objects.create(status="MOMENTAL")
products = [p["product_uuid"] for p in serializer.validated_data["products"]] products = [p["product_uuid"] for p in serializer.validated_data["products"]]
transaction = order.buy_without_registration( try:
products=products, transaction = order.buy_without_registration(
promocode_uuid=serializer.validated_data.get("promocode_uuid"), products=products,
customer_name=serializer.validated_data.get("customer_name"), promocode_uuid=serializer.validated_data.get("promocode_uuid"),
customer_email=serializer.validated_data.get("customer_email"), customer_name=serializer.validated_data.get("customer_name"),
customer_phone_number=serializer.validated_data.get("customer_phone_number"), customer_email=serializer.validated_data.get("customer_email"),
billing_customer_address=serializer.validated_data.get("billing_customer_address_uuid"), customer_phone_number=serializer.validated_data.get("customer_phone_number"),
shipping_customer_address=serializer.validated_data.get("shipping_customer_address_uuid"), billing_customer_address=serializer.validated_data.get("billing_customer_address_uuid"),
payment_method=serializer.validated_data.get("payment_method"), shipping_customer_address=serializer.validated_data.get("shipping_customer_address_uuid"),
) payment_method=serializer.validated_data.get("payment_method"),
return Response(status=status.HTTP_202_ACCEPTED, data=TransactionProcessSerializer(transaction).data) )
return Response(status=status.HTTP_201_CREATED, data=TransactionProcessSerializer(transaction).data)
except Exception as e:
return Response(status=status.HTTP_400_BAD_REQUEST, data={"detail": str(e)})
@action(detail=True, methods=["post"], url_path="add_order_product") @action(detail=True, methods=["post"], url_path="add_order_product")
def add_order_product(self, request, **kwargs): def add_order_product(self, request, **kwargs):
@ -921,6 +937,7 @@ class OrderViewSet(EvibesViewSet):
@extend_schema_view(**ORDER_PRODUCT_SCHEMA) @extend_schema_view(**ORDER_PRODUCT_SCHEMA)
# noinspection PyUnusedLocal
class OrderProductViewSet(EvibesViewSet): class OrderProductViewSet(EvibesViewSet):
""" """
Provides a viewset for managing OrderProduct entities. Provides a viewset for managing OrderProduct entities.
@ -993,6 +1010,7 @@ class OrderProductViewSet(EvibesViewSet):
return Response(status=status.HTTP_404_NOT_FOUND) return Response(status=status.HTTP_404_NOT_FOUND)
# noinspection PyUnusedLocal
class ProductImageViewSet(EvibesViewSet): class ProductImageViewSet(EvibesViewSet):
""" """
Manages operations related to Product images in the application. Manages operations related to Product images in the application.
@ -1025,6 +1043,7 @@ class ProductImageViewSet(EvibesViewSet):
} }
# noinspection PyUnusedLocal
class PromoCodeViewSet(EvibesViewSet): class PromoCodeViewSet(EvibesViewSet):
""" """
Manages the retrieval and handling of PromoCode instances through various Manages the retrieval and handling of PromoCode instances through various
@ -1064,6 +1083,7 @@ class PromoCodeViewSet(EvibesViewSet):
return qs.filter(user=user) return qs.filter(user=user)
# noinspection PyUnusedLocal
class PromotionViewSet(EvibesViewSet): class PromotionViewSet(EvibesViewSet):
""" """
Represents a view set for managing promotions. Represents a view set for managing promotions.
@ -1083,6 +1103,7 @@ class PromotionViewSet(EvibesViewSet):
} }
# noinspection PyUnusedLocal
class StockViewSet(EvibesViewSet): class StockViewSet(EvibesViewSet):
""" """
Handles operations related to Stock data in the system. Handles operations related to Stock data in the system.
@ -1115,6 +1136,7 @@ class StockViewSet(EvibesViewSet):
@extend_schema_view(**WISHLIST_SCHEMA) @extend_schema_view(**WISHLIST_SCHEMA)
# noinspection PyUnusedLocal
class WishlistViewSet(EvibesViewSet): class WishlistViewSet(EvibesViewSet):
""" """
ViewSet for managing Wishlist operations. ViewSet for managing Wishlist operations.
@ -1254,6 +1276,7 @@ class WishlistViewSet(EvibesViewSet):
@extend_schema_view(**ADDRESS_SCHEMA) @extend_schema_view(**ADDRESS_SCHEMA)
# noinspection PyUnusedLocal
class AddressViewSet(EvibesViewSet): class AddressViewSet(EvibesViewSet):
""" """
This class provides viewset functionality for managing `Address` objects. This class provides viewset functionality for managing `Address` objects.
@ -1276,7 +1299,7 @@ class AddressViewSet(EvibesViewSet):
filterset_class = AddressFilter filterset_class = AddressFilter
queryset = Address.objects.all() queryset = Address.objects.all()
serializer_class = AddressSerializer serializer_class = AddressSerializer
additional = {"create": "ALLOW"} additional = {"create": "ALLOW", "retrieve": "ALLOW"}
def get_serializer_class(self): def get_serializer_class(self):
if self.action == "create": if self.action == "create":
@ -1294,6 +1317,13 @@ class AddressViewSet(EvibesViewSet):
return Address.objects.none() return Address.objects.none()
def retrieve(self, request, **kwargs):
try:
address = Address.objects.get(uuid=kwargs.get("pk"))
return Response(status=status.HTTP_200_OK, data=self.get_serializer(address).data)
except Address.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
def create(self, request, **kwargs): def create(self, request, **kwargs):
create_serializer = AddressCreateSerializer(data=request.data, context={"request": request}) create_serializer = AddressCreateSerializer(data=request.data, context={"request": request})
create_serializer.is_valid(raise_exception=True) create_serializer.is_valid(raise_exception=True)
@ -1329,6 +1359,7 @@ class AddressViewSet(EvibesViewSet):
) )
# noinspection PyUnusedLocal
class ProductTagViewSet(EvibesViewSet): class ProductTagViewSet(EvibesViewSet):
""" """
Handles operations related to Product Tags within the application. Handles operations related to Product Tags within the application.

View file

@ -5,6 +5,7 @@ from storages.backends.ftp import FTPStorage
class AbsoluteFTPStorage(FTPStorage): # type: ignore class AbsoluteFTPStorage(FTPStorage): # type: ignore
# noinspection PyProtectedMember # noinspection PyProtectedMember
# noinspection PyUnresolvedReferences
def _get_config(self): def _get_config(self):
cfg = super()._get_config() cfg = super()._get_config()

View file

@ -1,5 +1,4 @@
import graphene import graphene
from django.db.models import QuerySet
from graphene import relay from graphene import relay
from graphene.types.generic import GenericScalar from graphene.types.generic import GenericScalar
from graphene_django import DjangoObjectType from graphene_django import DjangoObjectType
@ -32,7 +31,7 @@ class BalanceType(DjangoObjectType):
interfaces = (relay.Node,) interfaces = (relay.Node,)
filter_fields = ["is_active"] filter_fields = ["is_active"]
def resolve_transaction_set(self: Balance, info) -> QuerySet["Transaction"] | list: def resolve_transactions(self: Balance, info) -> list:
if info.context.user == self.user: if info.context.user == self.user:
# noinspection Mypy # noinspection Mypy
return self.transactions.all() or [] return self.transactions.all() or []

View file

@ -22,7 +22,11 @@ class Transaction(NiceModel):
process = JSONField(verbose_name=_("processing details"), default=dict) process = JSONField(verbose_name=_("processing details"), default=dict)
def __str__(self): def __str__(self):
return f"{self.balance.user.email} | {self.amount}" return (
f"{self.balance.user.email} | {self.amount}"
if self.balance
else f"{self.order.attributes.get('customer_email')} | {self.amount}"
)
def save(self, **kwargs): def save(self, **kwargs):
if self.amount != 0.0 and ( if self.amount != 0.0 and (

View file

@ -22,6 +22,7 @@ def create_balance_on_user_creation_signal(instance, created, **_kwargs):
def process_transaction_changes(instance, created, **_kwargs): def process_transaction_changes(instance, created, **_kwargs):
if created: if created:
try: try:
gateway = None
match instance.process.get("gateway", "default"): match instance.process.get("gateway", "default"):
case "gateway": case "gateway":
gateway = AbstractGateway() gateway = AbstractGateway()

View file

@ -20,7 +20,7 @@ msgstr "Баланс"
#: vibes_auth/admin.py:45 #: vibes_auth/admin.py:45
msgid "order" msgid "order"
msgstr "Заказать" msgstr "Заказ"
#: vibes_auth/admin.py:46 vibes_auth/graphene/object_types.py:44 #: vibes_auth/admin.py:46 vibes_auth/graphene/object_types.py:44
msgid "orders" msgid "orders"

View file

@ -30,6 +30,7 @@ from vibes_auth.utils.emailing import send_reset_password_email_task
logger = logging.getLogger("django") logger = logging.getLogger("django")
# noinspection GrazieInspection
@extend_schema_view(**USER_SCHEMA) @extend_schema_view(**USER_SCHEMA)
class UserViewSet( class UserViewSet(
mixins.CreateModelMixin, mixins.CreateModelMixin,