Features: 1) Add support for linking Gateway to transactions including constraints and limits; 2) Implement new Gateway model with transactional rules and currency configurations; 3) Add integration_path field to Vendor for dynamic integrations; 4) Improve task logic to dynamically use vendor integrations for stock updates and order statuses.

Fixes: 1) Replace `IntegerField` with `PositiveIntegerField` for product image priority to enforce positive values.

Extra: 1) Optimize imports for consistent formatting; 2) Clarify logging messages in tasks; 3) Minor docstring and help text updates.
This commit is contained in:
Egor Pavlovich Gorbunov 2025-10-15 14:25:58 +03:00
parent 6fa037390c
commit 20d5f5db21
3 changed files with 90 additions and 13 deletions

View file

@ -143,6 +143,13 @@ class Vendor(ExportModelOperationsMixin("vendor"), NiceModel): # type: ignore [
verbose_name=_("response file"), verbose_name=_("response file"),
help_text=_("vendor's last processing response"), help_text=_("vendor's last processing response"),
) )
integration_path = CharField(
null=True,
blank=True,
max_length=255,
help_text=_("vendor's integration file path"),
verbose_name=_("integration path"),
)
def __str__(self) -> str: def __str__(self) -> str:
return self.name return self.name
@ -739,9 +746,8 @@ class ProductImage(ExportModelOperationsMixin("product_image"), NiceModel): # t
verbose_name=_("product image"), verbose_name=_("product image"),
upload_to=get_product_uuid_as_path, upload_to=get_product_uuid_as_path,
) )
priority = IntegerField( priority = PositiveIntegerField(
default=1, default=1,
validators=[MinValueValidator(1)],
help_text=_("determines the order in which images are displayed"), help_text=_("determines the order in which images are displayed"),
verbose_name=_("display priority"), verbose_name=_("display priority"),
) )

View file

@ -4,7 +4,7 @@ import shutil
import uuid import uuid
from datetime import date, timedelta from datetime import date, timedelta
from time import sleep from time import sleep
from typing import Any from typing import Any, Type
import requests import requests
from celery.app import shared_task from celery.app import shared_task
@ -14,7 +14,8 @@ from django.core.cache import cache
from core.models import Product, Promotion from core.models import Product, Promotion
from core.utils.caching import set_default_cache from core.utils.caching import set_default_cache
from core.vendors import VendorInactiveError, delete_stale from core.utils.vendors import get_vendors_integrations
from core.vendors import VendorInactiveError, delete_stale, AbstractVendor
from evibes.settings import MEDIA_ROOT from evibes.settings import MEDIA_ROOT
logger = get_task_logger(__name__) logger = get_task_logger(__name__)
@ -39,16 +40,15 @@ def update_products_task() -> tuple[bool, str]:
if not update_products_task_running: if not update_products_task_running:
cache.set("update_products_task_running", True, 86400) cache.set("update_products_task_running", True, 86400)
vendors_classes: list[Any] = [] vendors: list[Type[AbstractVendor]] = get_vendors_integrations()
for vendor_class in vendors_classes: for vendor in vendors:
vendor = vendor_class()
try: try:
vendor.update_stock() vendor.update_stock()
except VendorInactiveError: except VendorInactiveError:
logger.info(f"Skipping {vendor_class} due to inactivity") logger.info(f"Skipping {vendor.__str__} due to inactivity")
except Exception as e: except Exception as e:
logger.warning(f"Skipping {vendor_class} due to error: {e!s}") logger.warning(f"Skipping {vendor.__str__} due to error: {e!s}")
delete_stale() delete_stale()
@ -70,10 +70,9 @@ def update_orderproducts_task() -> tuple[bool, str]:
message confirming the successful execution of the task. message confirming the successful execution of the task.
:rtype: Tuple[bool, str] :rtype: Tuple[bool, str]
""" """
vendors_classes: list[Any] = [] vendors: list[Type[AbstractVendor]] = get_vendors_integrations()
for vendor_class in vendors_classes: for vendor in vendors:
vendor = vendor_class()
vendor.update_order_products_statuses() vendor.update_order_products_statuses()
return True, "Success" return True, "Success"

View file

@ -1,6 +1,18 @@
from constance import config from constance import config
from django.conf import settings
from django.contrib.postgres.indexes import GinIndex from django.contrib.postgres.indexes import GinIndex
from django.db.models import CASCADE, CharField, FloatField, ForeignKey, JSONField, OneToOneField, QuerySet from django.db.models import (
CASCADE,
CharField,
FloatField,
ForeignKey,
JSONField,
OneToOneField,
PositiveIntegerField,
QuerySet,
Sum,
)
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from core.abstract import NiceModel from core.abstract import NiceModel
@ -20,6 +32,7 @@ class Transaction(NiceModel):
related_name="payments_transactions", related_name="payments_transactions",
) )
process = JSONField(verbose_name=_("processing details"), default=dict) process = JSONField(verbose_name=_("processing details"), default=dict)
gateway = ForeignKey("payments.Gateway", on_delete=CASCADE, blank=True, null=True, related_name="transactions")
def __str__(self): def __str__(self):
return ( return (
@ -67,3 +80,62 @@ class Balance(NiceModel):
if self.amount != 0.0 and len(str(self.amount).split(".")[1]) > 2: if self.amount != 0.0 and len(str(self.amount).split(".")[1]) > 2:
self.amount = round(self.amount, 2) self.amount = round(self.amount, 2)
super().save(**kwargs) super().save(**kwargs)
class Gateway(NiceModel):
name = CharField(max_length=20, null=False, blank=False, verbose_name=_("name"))
default_currency = CharField(
max_length=3, null=False, blank=False, verbose_name=_("default currency"), choices=settings.CURRENCIES
)
currencies = CharField(
max_length=3,
null=False,
blank=False,
verbose_name=_("currencies"),
help_text=_(f"comma separated list of currencies supported by this gateway, choose from {settings.CURRENCIES}"),
)
integration_path = CharField(max_length=255, null=True, blank=True)
minimum_transaction_amount = FloatField(
null=False, blank=False, default=0, verbose_name=_("minimum transaction amount")
)
maximum_transaction_amount = FloatField(
null=False, blank=False, default=0, verbose_name=_("maximum transaction amount")
)
daily_limit = PositiveIntegerField(
null=False,
blank=False,
default=0,
verbose_name=_("daily limit"),
help_text=_("daily sum limit of transactions' amounts. 0 means no limit"),
)
monthly_limit = PositiveIntegerField(
null=False,
blank=False,
default=0,
verbose_name=_("monthly limit"),
help_text=_("monthly sum limit of transactions' amounts. 0 means no limit"),
)
priority = PositiveIntegerField(null=False, blank=False, default=10, verbose_name=_("priority"), unique=True)
def __str__(self):
return self.name
class Meta:
verbose_name = _("payment gateway")
verbose_name_plural = _("payment gateways")
@property
def can_be_used(self) -> bool:
today = now().date()
current_month_start = today.replace(day=1)
daily_sum = self.transactions.filter(created__date=today).aggregate(total=Sum("amount"))["total"] or 0
monthly_sum = (
self.transactions.filter(created__gte=current_month_start).aggregate(total=Sum("amount"))["total"] or 0
)
daily_ok = self.daily_limit == 0 or daily_sum < self.daily_limit
monthly_ok = self.monthly_limit == 0 or monthly_sum < self.monthly_limit
return daily_ok and monthly_ok