Files
octopus/myproject/discounts/services/validator.py
Andrey Smakotin 241625eba7 feat(discounts): добавлено приложение скидок
Создано новое Django приложение для управления скидками:

Модели:
- BaseDiscount: абстрактный базовый класс с общими полями
- Discount: основная модель скидки (процент/фикс, на заказ/товар/категорию)
- PromoCode: промокоды для активации скидок
- DiscountApplication: история применения скидок

Сервисы:
- DiscountCalculator: расчёт скидок для корзины и заказов
- DiscountApplier: применение скидок к заказам (атомарно)
- DiscountValidator: валидация промокодов и условий

Админ-панель:
- DiscountAdmin: управление скидками
- PromoCodeAdmin: управление промокодами
- DiscountApplicationAdmin: история применения (только чтение)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 00:30:14 +03:00

102 lines
3.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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