feat(discounts): добавлено приложение скидок
Создано новое Django приложение для управления скидками: Модели: - BaseDiscount: абстрактный базовый класс с общими полями - Discount: основная модель скидки (процент/фикс, на заказ/товар/категорию) - PromoCode: промокоды для активации скидок - DiscountApplication: история применения скидок Сервисы: - DiscountCalculator: расчёт скидок для корзины и заказов - DiscountApplier: применение скидок к заказам (атомарно) - DiscountValidator: валидация промокодов и условий Админ-панель: - DiscountAdmin: управление скидками - PromoCodeAdmin: управление промокодами - DiscountApplicationAdmin: история применения (только чтение) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
101
myproject/discounts/services/validator.py
Normal file
101
myproject/discounts/services/validator.py
Normal file
@@ -0,0 +1,101 @@
|
||||
from decimal import Decimal
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
|
||||
class DiscountValidator:
|
||||
"""
|
||||
Сервис для валидации скидок и промокодов.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def validate_promo_code(code, customer=None, order_subtotal=None):
|
||||
"""
|
||||
Валидировать промокод.
|
||||
|
||||
Args:
|
||||
code: Код промокода
|
||||
customer: Customer для проверки использований
|
||||
order_subtotal: Сумма заказа для проверки min_order_amount
|
||||
|
||||
Returns:
|
||||
tuple: (is_valid, promo_code_or_none, error_message)
|
||||
"""
|
||||
from discounts.models import PromoCode
|
||||
|
||||
if not code or not code.strip():
|
||||
return False, None, "Промокод не указан"
|
||||
|
||||
try:
|
||||
promo = PromoCode.objects.get(
|
||||
code__iexact=code.strip().upper(),
|
||||
is_active=True
|
||||
)
|
||||
except PromoCode.DoesNotExist:
|
||||
return False, None, "Промокод не найден"
|
||||
|
||||
# Проверяем валидность промокода
|
||||
is_valid, error = promo.is_valid(customer)
|
||||
if not is_valid:
|
||||
return False, None, error
|
||||
|
||||
# Проверяем мин. сумму заказа
|
||||
if order_subtotal is not None and promo.discount.min_order_amount:
|
||||
if Decimal(order_subtotal) < promo.discount.min_order_amount:
|
||||
return False, None, f"Минимальная сумма заказа: {promo.discount.min_order_amount} руб."
|
||||
|
||||
# Проверяем scope (только заказ, не товары)
|
||||
if promo.discount.scope not in ('order', 'product', 'category'):
|
||||
return False, None, "Этот тип промокода не поддерживается"
|
||||
|
||||
return True, promo, None
|
||||
|
||||
@staticmethod
|
||||
def validate_discount_for_order(discount, order):
|
||||
"""
|
||||
Проверить, можно ли применить скидку к заказу.
|
||||
|
||||
Args:
|
||||
discount: Discount
|
||||
order: Order
|
||||
|
||||
Returns:
|
||||
tuple: (is_valid, error_message)
|
||||
"""
|
||||
if not discount.is_active:
|
||||
return False, "Скидка неактивна"
|
||||
|
||||
# Проверяем даты
|
||||
if not discount.is_valid_now():
|
||||
return False, "Скидка недействительна"
|
||||
|
||||
# Проверяем мин. сумму заказа
|
||||
if discount.scope == 'order' and discount.min_order_amount:
|
||||
if order.subtotal < discount.min_order_amount:
|
||||
return False, f"Минимальная сумма заказа: {discount.min_order_amount} руб."
|
||||
|
||||
return True, None
|
||||
|
||||
@staticmethod
|
||||
def validate_auto_discount_for_cart(discount, cart_subtotal, customer=None):
|
||||
"""
|
||||
Проверить, можно ли применить автоматическую скидку к корзине.
|
||||
|
||||
Args:
|
||||
discount: Discount
|
||||
cart_subtotal: Десятичное число - сумма корзины
|
||||
customer: Customer (опционально)
|
||||
|
||||
Returns:
|
||||
bool: True если скидка применима
|
||||
"""
|
||||
if not discount.is_auto:
|
||||
return False
|
||||
|
||||
if not discount.is_valid_now():
|
||||
return False
|
||||
|
||||
if discount.scope == 'order' and discount.min_order_amount:
|
||||
if cart_subtotal < discount.min_order_amount:
|
||||
return False
|
||||
|
||||
return True
|
||||
Reference in New Issue
Block a user