diff --git a/README.md b/README.md index 38a682c7..11ed606f 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ before running installment scripts ### 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 diff --git a/core/crm/amo/gateway.py b/core/crm/amo/gateway.py index 19ee2585..b9093e90 100644 --- a/core/crm/amo/gateway.py +++ b/core/crm/amo/gateway.py @@ -3,11 +3,13 @@ import traceback from typing import Optional import requests +from constance import config from django.core.cache import cache from django.db import transaction from core.crm.exceptions import CRMException from core.models import CustomerRelationshipManagementProvider, Order, OrderCrmLink +from core.utils import is_status_code_success logger = logging.getLogger("django") @@ -25,7 +27,7 @@ class AmoCRM: logger.warning("Multiple AMO CRM providers found") 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_secret = self.instance.authentication.get("client_secret") @@ -51,6 +53,7 @@ class AmoCRM: payload = { "client_id": self.client_id, "client_secret": self.client_secret, + "redirect_uri": f"https://api.{config.BASE_DOMAIN}/", } if self.refresh_token: payload["grant_type"] = "refresh_token" @@ -59,7 +62,9 @@ class AmoCRM: payload["grant_type"] = "authorization_code" payload["code"] = self.authorization_code 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() self.access_token = data["access_token"] cache.set("amo_refresh_token", data["refresh_token"], 604800) @@ -81,16 +86,32 @@ class AmoCRM: return payload def _get_customer_name(self, order: Order) -> str: - if not order.attributes.get("business_identificator"): - return order.user.get_full_name() or ( - f"{order.attributes.get('customer_name')} | " - f"{order.attributes.get('customer_phone_number') or order.attributes.get('customer_email')}" + if type(order.attributes) is not dict: + raise ValueError("order.attributes must be a dict") + + 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: - business_identificator = order.attributes.get("business_identificator") r = requests.get( f"https://api-fns.ru/api/egr?req={business_identificator}&key={self.fns_api_key}", timeout=15 ) + r.raise_for_status() body = r.json() except requests.exceptions.RequestException as rex: logger.error(f"Unable to get company info with FNS: {rex}") @@ -137,6 +158,8 @@ class AmoCRM: body = r.json() return body.get("_embedded", {}).get("contacts", [{}])[0].get("id", None) + return None + else: return None @@ -175,5 +198,5 @@ class AmoCRM: if link.order.status == new_status: return - link.order.status = self.STATUS_MAP.get(new_status) + link.order.status = self.STATUS_MAP[new_status] link.order.save(update_fields=["status"]) diff --git a/core/filters.py b/core/filters.py index 3ac968a1..44edfe79 100644 --- a/core/filters.py +++ b/core/filters.py @@ -9,7 +9,6 @@ from django.db.models import ( Case, Exists, FloatField, - IntegerField, Max, OuterRef, Prefetch, @@ -61,6 +60,7 @@ class CaseInsensitiveListFilter(BaseInFilter, CharFilter): return qs +# noinspection PyUnusedLocal class ProductFilter(FilterSet): search = CharFilter(field_name="name", method="search_products", label=_("Search")) uuid = UUIDFilter(field_name="uuid", lookup_expr="exact", label=_("UUID")) @@ -95,7 +95,6 @@ class ProductFilter(FilterSet): ("price_order", "price"), ("sku", "sku"), ("?", "random"), - ("personal_order_only", "personal_order_only"), ), initial="uuid", ) @@ -153,7 +152,7 @@ class ProductFilter(FilterSet): if not value: 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) @@ -280,21 +279,21 @@ class ProductFilter(FilterSet): qs = qs.annotate( has_stock=Max( Case( - When(stocks__quantity__gt=0, then=Value(1)), - default=Value(0), - output_field=IntegerField(), + When(stocks__quantity__gt=0, then=Value(True)), + default=Value(False), + output_field=BooleanField(), ) ), has_price=Max( Case( - When(stocks__price__gt=0, then=Value(1)), - default=Value(0), - output_field=IntegerField(), + When(stocks__price__gt=0, then=Value(True)), + default=Value(False), + output_field=BooleanField(), ) ), ).annotate( - personal_order_only=Case( - When(has_stock=0, has_price=1, then=Value(True)), + personal_orders_only=Case( + When(has_stock=False, has_price=False, then=Value(True)), default=Value(False), output_field=BooleanField(), ) @@ -311,10 +310,11 @@ class ProductFilter(FilterSet): key = "?" mapped_requested.append(key) continue + if key == "personal_orders_only": + continue 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 = (["personal_order_only"] if not has_personal_in_request else []) + mapped_requested + final_ordering = mapped_requested + ["personal_orders_only"] if final_ordering: qs = qs.order_by(*final_ordering) @@ -396,6 +396,7 @@ class WishlistFilter(FilterSet): fields = ["uuid", "user_email", "user", "order_by"] +# noinspection PyUnusedLocal class CategoryFilter(FilterSet): search = CharFilter(field_name="name", method="search_categories", label=_("Search")) uuid = UUIDFilter(field_name="uuid", lookup_expr="exact") @@ -429,7 +430,7 @@ class CategoryFilter(FilterSet): if not value: 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) @@ -522,6 +523,7 @@ class CategoryFilter(FilterSet): return queryset.filter(parent__uuid=uuid_val) +# noinspection PyUnusedLocal class BrandFilter(FilterSet): search = CharFilter(field_name="name", method="search_brands", label=_("Search")) uuid = UUIDFilter(field_name="uuid", lookup_expr="exact") @@ -547,7 +549,7 @@ class BrandFilter(FilterSet): if not value: 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) diff --git a/core/graphene/mutations.py b/core/graphene/mutations.py index 55583cba..2abe75cf 100644 --- a/core/graphene/mutations.py +++ b/core/graphene/mutations.py @@ -546,6 +546,7 @@ class FeedbackProductAction(BaseMutation): order_product = OrderProduct.objects.get(uuid=order_product_uuid) if user != order_product.order.user: raise PermissionDenied(permission_denied_message) + feedback = None match action: case "add": feedback = order_product.do_feedback(comment=comment, rating=rating, action="add") diff --git a/core/graphene/object_types.py b/core/graphene/object_types.py index 36b57be7..b80fdcc9 100644 --- a/core/graphene/object_types.py +++ b/core/graphene/object_types.py @@ -1,8 +1,9 @@ import logging +from typing import Any from constance import config 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.utils.translation import gettext_lazy as _ from graphene import ( @@ -55,7 +56,6 @@ from core.utils.seo_builders import ( website_schema, ) from payments.graphene.object_types import TransactionType -from payments.models import Transaction logger = logging.getLogger("django") @@ -464,19 +464,19 @@ class OrderType(DjangoObjectType): ) description = _("orders") - def resolve_total_price(self: Order, _info): + def resolve_total_price(self: Order, _info) -> float: return self.total_price - def resolve_total_quantity(self: Order, _info): + def resolve_total_quantity(self: Order, _info) -> int: return self.total_quantity - def resolve_notifications(self: Order, _info): + def resolve_notifications(self: Order, _info) -> dict[str, Any]: return camelize(self.notifications) - def resolve_attributes(self: Order, _info): + def resolve_attributes(self: Order, _info) -> dict[str, Any]: 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: return self.payments_transactions.all() return None diff --git a/core/graphene/schema.py b/core/graphene/schema.py index 046f695d..5382cb66 100644 --- a/core/graphene/schema.py +++ b/core/graphene/schema.py @@ -1,6 +1,7 @@ import logging from django.core.cache import cache +from django.db.models import Max, Case, When, Value, IntegerField, BooleanField from django.utils import timezone from django.core.exceptions import PermissionDenied from graphene import Field, List, ObjectType, Schema @@ -148,7 +149,7 @@ class Query(ObjectType): product = Product.objects.get(uuid=kwargs["uuid"]) if product.is_active and product.brand.is_active and product.category.is_active: info.context.user.add_to_recently_viewed(product.uuid) - return ( + base_qs = ( Product.objects.all().select_related("brand", "category").prefetch_related("images", "stocks") if info.context.user.has_perm("core.view_product") else Product.objects.filter( @@ -162,6 +163,35 @@ class Query(ObjectType): .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 def resolve_orders(_parent, info, **kwargs): orders = Order.objects diff --git a/core/locale/ru_RU/LC_MESSAGES/django.mo b/core/locale/ru_RU/LC_MESSAGES/django.mo index a4b0facc..4bf7a31f 100644 Binary files a/core/locale/ru_RU/LC_MESSAGES/django.mo and b/core/locale/ru_RU/LC_MESSAGES/django.mo differ diff --git a/core/locale/ru_RU/LC_MESSAGES/django.po b/core/locale/ru_RU/LC_MESSAGES/django.po index e5bb3e2e..2e934903 100644 --- a/core/locale/ru_RU/LC_MESSAGES/django.po +++ b/core/locale/ru_RU/LC_MESSAGES/django.po @@ -2133,7 +2133,7 @@ msgstr "человекочитаемый идентификатор" #: core/models.py:1261 msgid "order" -msgstr "Заказать" +msgstr "Заказ" #: core/models.py:1282 msgid "a user must have only one pending order at a time" diff --git a/core/migrations/0038_backfill_product_sku.py b/core/migrations/0038_backfill_product_sku.py index 428056bb..b10511d3 100644 --- a/core/migrations/0038_backfill_product_sku.py +++ b/core/migrations/0038_backfill_product_sku.py @@ -9,6 +9,7 @@ def generate_unique_sku(make_candidate, taken): return c +# noinspection PyUnusedLocal def backfill_sku(apps, schema_editor): Product = apps.get_model("core", "Product") 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] +# noinspection PyUnusedLocal def noop(apps, schema_editor): pass diff --git a/core/models.py b/core/models.py index e5e2303f..281d484f 100644 --- a/core/models.py +++ b/core/models.py @@ -541,7 +541,7 @@ class Product(ExportModelOperationsMixin("product"), NiceModel): # type: ignore default=generate_human_readable_id, ) - objects: ProductManager = ProductManager() + objects = ProductManager() class Meta: verbose_name = _("product") @@ -599,7 +599,7 @@ class Product(ExportModelOperationsMixin("product"), NiceModel): # type: ignore @property 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] @@ -1266,6 +1266,10 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi @property def is_business(self) -> bool: + if type(self.attributes) is not dict: + self.attributes = {} + self.save() + return False with suppress(Exception): 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")) @@ -1306,7 +1310,7 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi def add_product( self, product_uuid=None, - attributes: list | None = None, + attributes: list | dict | None = None, update_quantity=True, ): if attributes is None: @@ -1776,7 +1780,10 @@ class OrderProduct(ExportModelOperationsMixin("order_product"), NiceModel): # t def download_url(self: Self) -> str: if self.product and self.product.stocks: 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 "" 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 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) else: raise ValueError(_("you cannot feedback an order which is not received")) @@ -1827,7 +1834,7 @@ class CustomerRelationshipManagementProvider(ExportModelOperationsMixin("crm_pro 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") crm = ForeignKey(to=CustomerRelationshipManagementProvider, on_delete=PROTECT, related_name="order_links") crm_lead_id = CharField(max_length=30, unique=True, db_index=True) diff --git a/core/serializers/detail.py b/core/serializers/detail.py index a027ad56..fb8baaca 100644 --- a/core/serializers/detail.py +++ b/core/serializers/detail.py @@ -106,7 +106,7 @@ class CategoryDetailSerializer(ModelSerializer): filterable_results = [] 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 filterable_results.append( { diff --git a/core/serializers/utility.py b/core/serializers/utility.py index ff68a104..ec855c61 100644 --- a/core/serializers/utility.py +++ b/core/serializers/utility.py @@ -62,7 +62,7 @@ class AddressCreateSerializer(ModelSerializer): write_only=True, 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) class Meta: @@ -177,6 +177,7 @@ class BuyUnregisteredOrderSerializer(Serializer): class BuyAsBusinessOrderSerializer(Serializer): products = AddOrderProductSerializer(many=True, required=True) business_identificator = CharField(required=True) + promocode_uuid = UUIDField(required=False) business_email = CharField(required=True) business_phone_number = CharField(required=True) billing_business_address_uuid = CharField(required=False) diff --git a/core/utils/__init__.py b/core/utils/__init__.py index 53d1f447..6b904606 100644 --- a/core/utils/__init__.py +++ b/core/utils/__init__.py @@ -255,3 +255,7 @@ def generate_human_readable_token() -> str: str: A 20-character random token. """ return "".join([secrets.choice(CROCKFORD) for _ in range(20)]) + + +def is_status_code_success(status_code: int) -> bool: + return 200 <= status_code < 300 diff --git a/core/utils/emailing.py b/core/utils/emailing.py index 375b052d..6eb1a50a 100644 --- a/core/utils/emailing.py +++ b/core/utils/emailing.py @@ -51,10 +51,20 @@ def send_order_created_email(order_pk: str) -> tuple[bool, str]: except Order.DoesNotExist: 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)]): 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() connection = mail.get_connection() @@ -71,7 +81,7 @@ def send_order_created_email(order_pk: str) -> tuple[bool, str]: "total_price": order.total_price, }, ), - to=[order.user.email], + to=[recipient], from_email=f"{config.PROJECT_NAME} <{config.EMAIL_FROM}>", connection=connection, ) diff --git a/core/vendors/__init__.py b/core/vendors/__init__.py index 634369fc..8ec7137d 100644 --- a/core/vendors/__init__.py +++ b/core/vendors/__init__.py @@ -235,7 +235,7 @@ class AbstractVendor: if not rate: 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 def round_price_marketologically(price: float) -> float: diff --git a/core/views.py b/core/views.py index 18a9c7af..5083f47c 100644 --- a/core/views.py +++ b/core/views.py @@ -428,27 +428,35 @@ class BuyAsBusinessView(APIView): 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): serializer = BuyAsBusinessOrderSerializer(data=request.data) serializer.is_valid(raise_exception=True) order = Order.objects.create(status="MOMENTAL") products = [product.get("product_uuid") for product in serializer.validated_data.get("products")] - transaction = order.buy_without_registration( - products=products, - promocode_uuid=serializer.validated_data.get("promocode_uuid"), - customer_name=serializer.validated_data.get("business_identificator"), - customer_email=serializer.validated_data.get("business_email"), - customer_phone_number=serializer.validated_data.get("business_phone_number"), - billing_customer_address=serializer.validated_data.get("billing_business_address_uuid"), - shipping_customer_address=serializer.validated_data.get("shipping_business_address_uuid"), - payment_method=serializer.validated_data.get("payment_method"), - is_business=True, - ) - return Response( - status=status.HTTP_201_CREATED, - data=TransactionProcessSerializer(transaction).data, - ) + try: + transaction = order.buy_without_registration( + products=products, + promocode_uuid=serializer.validated_data.get("promocode_uuid"), + customer_name=serializer.validated_data.get("business_identificator"), + customer_email=serializer.validated_data.get("business_email"), + customer_phone_number=serializer.validated_data.get("business_phone_number"), + billing_customer_address=serializer.validated_data.get("billing_business_address_uuid"), + shipping_customer_address=serializer.validated_data.get("shipping_business_address_uuid"), + payment_method=serializer.validated_data.get("payment_method"), + is_business=True, + ) + return Response( + 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): diff --git a/core/viewsets.py b/core/viewsets.py index 5b3b79b9..ae737b05 100644 --- a/core/viewsets.py +++ b/core/viewsets.py @@ -117,7 +117,6 @@ from core.utils.seo_builders import ( product_schema, website_schema, ) -from evibes.settings import DEBUG from payments.serializers import TransactionProcessSerializer logger = logging.getLogger("django") @@ -155,6 +154,7 @@ class EvibesViewSet(ModelViewSet): @extend_schema_view(**ATTRIBUTE_GROUP_SCHEMA) +# noinspection PyUnusedLocal class AttributeGroupViewSet(EvibesViewSet): """ Represents a viewset for managing AttributeGroup objects. @@ -187,6 +187,7 @@ class AttributeGroupViewSet(EvibesViewSet): @extend_schema_view(**ATTRIBUTE_SCHEMA) +# noinspection PyUnusedLocal class AttributeViewSet(EvibesViewSet): """ Handles operations related to Attribute objects within the application. @@ -219,6 +220,7 @@ class AttributeViewSet(EvibesViewSet): @extend_schema_view(**ATTRIBUTE_VALUE_SCHEMA) +# noinspection PyUnusedLocal class AttributeValueViewSet(EvibesViewSet): """ A viewset for managing AttributeValue objects. @@ -247,6 +249,7 @@ class AttributeValueViewSet(EvibesViewSet): @extend_schema_view(**CATEGORY_SCHEMA) +# noinspection PyUnusedLocal class CategoryViewSet(EvibesViewSet): """ Manages views for Category-related operations. @@ -377,6 +380,7 @@ class CategoryViewSet(EvibesViewSet): return Response(SeoSnapshotSerializer(payload).data) +# noinspection PyUnusedLocal class BrandViewSet(EvibesViewSet): """ Represents a viewset for managing Brand instances. @@ -502,6 +506,7 @@ class BrandViewSet(EvibesViewSet): @extend_schema_view(**PRODUCT_SCHEMA) +# noinspection PyUnusedLocal class ProductViewSet(EvibesViewSet): """ Manages operations related to the `Product` model in the system. @@ -635,6 +640,7 @@ class ProductViewSet(EvibesViewSet): return Response(SeoSnapshotSerializer(payload).data) +# noinspection PyUnusedLocal class VendorViewSet(EvibesViewSet): """ Represents a viewset for managing Vendor objects. @@ -666,6 +672,7 @@ class VendorViewSet(EvibesViewSet): @extend_schema_view(**FEEDBACK_SCHEMA) +# noinspection PyUnusedLocal class FeedbackViewSet(EvibesViewSet): """ Representation of a view set handling Feedback objects. @@ -704,6 +711,7 @@ class FeedbackViewSet(EvibesViewSet): @extend_schema_view(**ORDER_SCHEMA) +# noinspection PyUnusedLocal class OrderViewSet(EvibesViewSet): """ ViewSet for managing orders and related operations. @@ -798,7 +806,10 @@ class OrderViewSet(EvibesViewSet): def current(self, request): if not request.user.is_authenticated: 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( status=status.HTTP_200_OK, data=OrderDetailSerializer(order).data, @@ -829,25 +840,30 @@ class OrderViewSet(EvibesViewSet): except Order.DoesNotExist: name = "Order" 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") - @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): serializer = BuyUnregisteredOrderSerializer(data=request.data) serializer.is_valid(raise_exception=True) order = Order.objects.create(status="MOMENTAL") products = [p["product_uuid"] for p in serializer.validated_data["products"]] - transaction = order.buy_without_registration( - products=products, - promocode_uuid=serializer.validated_data.get("promocode_uuid"), - customer_name=serializer.validated_data.get("customer_name"), - customer_email=serializer.validated_data.get("customer_email"), - customer_phone_number=serializer.validated_data.get("customer_phone_number"), - billing_customer_address=serializer.validated_data.get("billing_customer_address_uuid"), - 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) + try: + transaction = order.buy_without_registration( + products=products, + promocode_uuid=serializer.validated_data.get("promocode_uuid"), + customer_name=serializer.validated_data.get("customer_name"), + customer_email=serializer.validated_data.get("customer_email"), + customer_phone_number=serializer.validated_data.get("customer_phone_number"), + billing_customer_address=serializer.validated_data.get("billing_customer_address_uuid"), + shipping_customer_address=serializer.validated_data.get("shipping_customer_address_uuid"), + payment_method=serializer.validated_data.get("payment_method"), + ) + 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") def add_order_product(self, request, **kwargs): @@ -921,6 +937,7 @@ class OrderViewSet(EvibesViewSet): @extend_schema_view(**ORDER_PRODUCT_SCHEMA) +# noinspection PyUnusedLocal class OrderProductViewSet(EvibesViewSet): """ Provides a viewset for managing OrderProduct entities. @@ -993,6 +1010,7 @@ class OrderProductViewSet(EvibesViewSet): return Response(status=status.HTTP_404_NOT_FOUND) +# noinspection PyUnusedLocal class ProductImageViewSet(EvibesViewSet): """ Manages operations related to Product images in the application. @@ -1025,6 +1043,7 @@ class ProductImageViewSet(EvibesViewSet): } +# noinspection PyUnusedLocal class PromoCodeViewSet(EvibesViewSet): """ Manages the retrieval and handling of PromoCode instances through various @@ -1064,6 +1083,7 @@ class PromoCodeViewSet(EvibesViewSet): return qs.filter(user=user) +# noinspection PyUnusedLocal class PromotionViewSet(EvibesViewSet): """ Represents a view set for managing promotions. @@ -1083,6 +1103,7 @@ class PromotionViewSet(EvibesViewSet): } +# noinspection PyUnusedLocal class StockViewSet(EvibesViewSet): """ Handles operations related to Stock data in the system. @@ -1115,6 +1136,7 @@ class StockViewSet(EvibesViewSet): @extend_schema_view(**WISHLIST_SCHEMA) +# noinspection PyUnusedLocal class WishlistViewSet(EvibesViewSet): """ ViewSet for managing Wishlist operations. @@ -1254,6 +1276,7 @@ class WishlistViewSet(EvibesViewSet): @extend_schema_view(**ADDRESS_SCHEMA) +# noinspection PyUnusedLocal class AddressViewSet(EvibesViewSet): """ This class provides viewset functionality for managing `Address` objects. @@ -1276,7 +1299,7 @@ class AddressViewSet(EvibesViewSet): filterset_class = AddressFilter queryset = Address.objects.all() serializer_class = AddressSerializer - additional = {"create": "ALLOW"} + additional = {"create": "ALLOW", "retrieve": "ALLOW"} def get_serializer_class(self): if self.action == "create": @@ -1294,6 +1317,13 @@ class AddressViewSet(EvibesViewSet): 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): create_serializer = AddressCreateSerializer(data=request.data, context={"request": request}) create_serializer.is_valid(raise_exception=True) @@ -1329,6 +1359,7 @@ class AddressViewSet(EvibesViewSet): ) +# noinspection PyUnusedLocal class ProductTagViewSet(EvibesViewSet): """ Handles operations related to Product Tags within the application. diff --git a/evibes/ftpstorage.py b/evibes/ftpstorage.py index 7e87e0f6..4e3ebc39 100644 --- a/evibes/ftpstorage.py +++ b/evibes/ftpstorage.py @@ -5,6 +5,7 @@ from storages.backends.ftp import FTPStorage class AbsoluteFTPStorage(FTPStorage): # type: ignore # noinspection PyProtectedMember + # noinspection PyUnresolvedReferences def _get_config(self): cfg = super()._get_config() diff --git a/payments/graphene/object_types.py b/payments/graphene/object_types.py index c39091a3..c704dd80 100644 --- a/payments/graphene/object_types.py +++ b/payments/graphene/object_types.py @@ -1,5 +1,4 @@ import graphene -from django.db.models import QuerySet from graphene import relay from graphene.types.generic import GenericScalar from graphene_django import DjangoObjectType @@ -32,7 +31,7 @@ class BalanceType(DjangoObjectType): interfaces = (relay.Node,) 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: # noinspection Mypy return self.transactions.all() or [] diff --git a/payments/models.py b/payments/models.py index 6678715b..77d30c70 100644 --- a/payments/models.py +++ b/payments/models.py @@ -22,7 +22,11 @@ class Transaction(NiceModel): process = JSONField(verbose_name=_("processing details"), default=dict) 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): if self.amount != 0.0 and ( diff --git a/payments/signals.py b/payments/signals.py index 9e9078b1..922de735 100644 --- a/payments/signals.py +++ b/payments/signals.py @@ -22,6 +22,7 @@ def create_balance_on_user_creation_signal(instance, created, **_kwargs): def process_transaction_changes(instance, created, **_kwargs): if created: try: + gateway = None match instance.process.get("gateway", "default"): case "gateway": gateway = AbstractGateway() diff --git a/vibes_auth/locale/ru_RU/LC_MESSAGES/django.mo b/vibes_auth/locale/ru_RU/LC_MESSAGES/django.mo index 914d76fb..4d3253d2 100644 Binary files a/vibes_auth/locale/ru_RU/LC_MESSAGES/django.mo and b/vibes_auth/locale/ru_RU/LC_MESSAGES/django.mo differ diff --git a/vibes_auth/locale/ru_RU/LC_MESSAGES/django.po b/vibes_auth/locale/ru_RU/LC_MESSAGES/django.po index d64929b2..033459b3 100644 --- a/vibes_auth/locale/ru_RU/LC_MESSAGES/django.po +++ b/vibes_auth/locale/ru_RU/LC_MESSAGES/django.po @@ -20,7 +20,7 @@ msgstr "Баланс" #: vibes_auth/admin.py:45 msgid "order" -msgstr "Заказать" +msgstr "Заказ" #: vibes_auth/admin.py:46 vibes_auth/graphene/object_types.py:44 msgid "orders" diff --git a/vibes_auth/viewsets.py b/vibes_auth/viewsets.py index 18373a36..9e756267 100644 --- a/vibes_auth/viewsets.py +++ b/vibes_auth/viewsets.py @@ -30,6 +30,7 @@ from vibes_auth.utils.emailing import send_reset_password_email_task logger = logging.getLogger("django") +# noinspection GrazieInspection @extend_schema_view(**USER_SCHEMA) class UserViewSet( mixins.CreateModelMixin,