From 20d5f5db21c83800722ccbce71137f81d43c3049 Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Wed, 15 Oct 2025 14:25:58 +0300 Subject: [PATCH] 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. --- core/models.py | 10 +++++-- core/tasks.py | 19 ++++++------ payments/models.py | 74 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 90 insertions(+), 13 deletions(-) diff --git a/core/models.py b/core/models.py index 1ab647c6..80ef3bf0 100644 --- a/core/models.py +++ b/core/models.py @@ -143,6 +143,13 @@ class Vendor(ExportModelOperationsMixin("vendor"), NiceModel): # type: ignore [ verbose_name=_("response file"), 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: return self.name @@ -739,9 +746,8 @@ class ProductImage(ExportModelOperationsMixin("product_image"), NiceModel): # t verbose_name=_("product image"), upload_to=get_product_uuid_as_path, ) - priority = IntegerField( + priority = PositiveIntegerField( default=1, - validators=[MinValueValidator(1)], help_text=_("determines the order in which images are displayed"), verbose_name=_("display priority"), ) diff --git a/core/tasks.py b/core/tasks.py index 3792ad66..9f417649 100644 --- a/core/tasks.py +++ b/core/tasks.py @@ -4,7 +4,7 @@ import shutil import uuid from datetime import date, timedelta from time import sleep -from typing import Any +from typing import Any, Type import requests from celery.app import shared_task @@ -14,7 +14,8 @@ from django.core.cache import cache from core.models import Product, Promotion 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 logger = get_task_logger(__name__) @@ -39,16 +40,15 @@ def update_products_task() -> tuple[bool, str]: if not update_products_task_running: 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: - vendor = vendor_class() + for vendor in vendors: try: vendor.update_stock() except VendorInactiveError: - logger.info(f"Skipping {vendor_class} due to inactivity") + logger.info(f"Skipping {vendor.__str__} due to inactivity") 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() @@ -70,10 +70,9 @@ def update_orderproducts_task() -> tuple[bool, str]: message confirming the successful execution of the task. :rtype: Tuple[bool, str] """ - vendors_classes: list[Any] = [] + vendors: list[Type[AbstractVendor]] = get_vendors_integrations() - for vendor_class in vendors_classes: - vendor = vendor_class() + for vendor in vendors: vendor.update_order_products_statuses() return True, "Success" diff --git a/payments/models.py b/payments/models.py index 77d30c70..81f9a10d 100644 --- a/payments/models.py +++ b/payments/models.py @@ -1,6 +1,18 @@ from constance import config +from django.conf import settings 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 core.abstract import NiceModel @@ -20,6 +32,7 @@ class Transaction(NiceModel): related_name="payments_transactions", ) 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): return ( @@ -67,3 +80,62 @@ class Balance(NiceModel): if self.amount != 0.0 and len(str(self.amount).split(".")[1]) > 2: self.amount = round(self.amount, 2) 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