Merge branch 'main' into storefront-nuxt
This commit is contained in:
commit
949e077942
24 changed files with 204 additions and 79 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
2
core/vendors/__init__.py
vendored
2
core/vendors/__init__.py
vendored
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 []
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue