Создано новое Django приложение для управления скидками: Модели: - BaseDiscount: абстрактный базовый класс с общими полями - Discount: основная модель скидки (процент/фикс, на заказ/товар/категорию) - PromoCode: промокоды для активации скидок - DiscountApplication: история применения скидок Сервисы: - DiscountCalculator: расчёт скидок для корзины и заказов - DiscountApplier: применение скидок к заказам (атомарно) - DiscountValidator: валидация промокодов и условий Админ-панель: - DiscountAdmin: управление скидками - PromoCodeAdmin: управление промокодами - DiscountApplicationAdmin: история применения (только чтение) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
229 lines
7.7 KiB
Python
229 lines
7.7 KiB
Python
from decimal import Decimal
|
||
from django.db import transaction
|
||
from django.core.exceptions import ValidationError
|
||
|
||
|
||
class DiscountApplier:
|
||
"""
|
||
Сервис для применения скидок к заказам.
|
||
Все операции атомарны.
|
||
"""
|
||
|
||
@staticmethod
|
||
@transaction.atomic
|
||
def apply_promo_code(order, promo_code, user=None):
|
||
"""
|
||
Применить промокод к заказу.
|
||
|
||
Args:
|
||
order: Order
|
||
promo_code: str
|
||
user: CustomUser (применивший скидку)
|
||
|
||
Returns:
|
||
dict: {
|
||
'success': bool,
|
||
'discount': Discount,
|
||
'discount_amount': Decimal,
|
||
'error': str
|
||
}
|
||
"""
|
||
from discounts.models import PromoCode, DiscountApplication
|
||
from discounts.services.calculator import DiscountCalculator
|
||
|
||
# Удаляем предыдущую скидку на заказ
|
||
if order.applied_promo_code:
|
||
DiscountApplier._remove_order_discount_only(order)
|
||
|
||
# Рассчитываем скидку
|
||
result = DiscountCalculator.calculate_order_discount(
|
||
order, promo_code
|
||
)
|
||
|
||
if result['error']:
|
||
return {
|
||
'success': False,
|
||
'error': result['error']
|
||
}
|
||
|
||
discount = result['discount']
|
||
promo = result['promo_code']
|
||
discount_amount = result['discount_amount']
|
||
|
||
# Применяем к заказу
|
||
order.applied_discount = discount
|
||
order.applied_promo_code = promo.code
|
||
order.discount_amount = discount_amount
|
||
order.save(update_fields=['applied_discount', 'applied_promo_code', 'discount_amount'])
|
||
|
||
# Пересчитываем total_amount
|
||
order.calculate_total()
|
||
|
||
# Регистрируем использование промокода
|
||
promo.record_usage(order.customer)
|
||
|
||
# Создаем запись о применении
|
||
DiscountApplication.objects.create(
|
||
order=order,
|
||
discount=discount,
|
||
promo_code=promo,
|
||
target='order',
|
||
base_amount=order.subtotal,
|
||
discount_amount=discount_amount,
|
||
final_amount=order.subtotal - discount_amount,
|
||
customer=order.customer,
|
||
applied_by=user or order.modified_by
|
||
)
|
||
|
||
# Увеличиваем счетчик использований скидки
|
||
discount.current_usage_count += 1
|
||
discount.save(update_fields=['current_usage_count'])
|
||
|
||
return {
|
||
'success': True,
|
||
'discount': discount,
|
||
'discount_amount': discount_amount
|
||
}
|
||
|
||
@staticmethod
|
||
@transaction.atomic
|
||
def apply_auto_discounts(order, user=None):
|
||
"""
|
||
Применить автоматические скидки к заказу и позициям.
|
||
|
||
Args:
|
||
order: Order
|
||
user: CustomUser
|
||
|
||
Returns:
|
||
dict: {
|
||
'order_discount': {...},
|
||
'item_discounts': [...],
|
||
'total_discount': Decimal
|
||
}
|
||
"""
|
||
from discounts.models import Discount, DiscountApplication
|
||
from discounts.services.calculator import DiscountCalculator
|
||
|
||
result = {
|
||
'order_discount': None,
|
||
'item_discounts': [],
|
||
'total_discount': Decimal('0')
|
||
}
|
||
|
||
# 1. Применяем скидку на заказ (если есть)
|
||
order_result = DiscountCalculator.calculate_order_discount(order)
|
||
if order_result['discount'] and not order_result['error']:
|
||
discount = order_result['discount']
|
||
discount_amount = order_result['discount_amount']
|
||
|
||
order.applied_discount = discount
|
||
order.discount_amount = discount_amount
|
||
order.save(update_fields=['applied_discount', 'discount_amount'])
|
||
|
||
# Создаем запись о применении
|
||
DiscountApplication.objects.create(
|
||
order=order,
|
||
discount=discount,
|
||
target='order',
|
||
base_amount=order.subtotal,
|
||
discount_amount=discount_amount,
|
||
final_amount=order.subtotal - discount_amount,
|
||
customer=order.customer,
|
||
applied_by=user
|
||
)
|
||
|
||
# Увеличиваем счетчик
|
||
discount.current_usage_count += 1
|
||
discount.save(update_fields=['current_usage_count'])
|
||
|
||
result['order_discount'] = {
|
||
'discount': discount,
|
||
'discount_amount': discount_amount
|
||
}
|
||
result['total_discount'] += discount_amount
|
||
|
||
# 2. Применяем скидки на позиции
|
||
available_discounts = list(DiscountCalculator.get_available_discounts(
|
||
scope='product',
|
||
auto_only=True
|
||
))
|
||
|
||
for item in order.items.all():
|
||
item_result = DiscountCalculator.calculate_item_discount(
|
||
item, available_discounts
|
||
)
|
||
|
||
if item_result['discount']:
|
||
discount = item_result['discount']
|
||
discount_amount = item_result['discount_amount']
|
||
|
||
item.applied_discount = discount
|
||
item.discount_amount = discount_amount
|
||
item.save(update_fields=['applied_discount', 'discount_amount'])
|
||
|
||
# Создаем запись о применении
|
||
DiscountApplication.objects.create(
|
||
order=order,
|
||
order_item=item,
|
||
discount=discount,
|
||
target='order_item',
|
||
base_amount=item.price * item.quantity,
|
||
discount_amount=discount_amount,
|
||
final_amount=item.get_total_price(),
|
||
customer=order.customer,
|
||
applied_by=user
|
||
)
|
||
|
||
# Увеличиваем счетчик
|
||
discount.current_usage_count += 1
|
||
discount.save(update_fields=['current_usage_count'])
|
||
|
||
result['item_discounts'].append({
|
||
'item': item,
|
||
'discount': discount,
|
||
'discount_amount': discount_amount
|
||
})
|
||
result['total_discount'] += discount_amount
|
||
|
||
# Пересчитываем итоговую сумму
|
||
order.calculate_total()
|
||
|
||
return result
|
||
|
||
@staticmethod
|
||
@transaction.atomic
|
||
def remove_discount_from_order(order):
|
||
"""
|
||
Удалить скидку с заказа.
|
||
|
||
Args:
|
||
order: Order
|
||
"""
|
||
DiscountApplier._remove_order_discount_only(order)
|
||
|
||
# Удаляем скидки с позиций
|
||
order.items.update(
|
||
applied_discount=None,
|
||
discount_amount=Decimal('0')
|
||
)
|
||
|
||
# Удаляем записи о применении (опционально - для истории можно оставить)
|
||
# DiscountApplication.objects.filter(order=order).delete()
|
||
|
||
# Пересчитываем
|
||
order.calculate_total()
|
||
|
||
@staticmethod
|
||
def _remove_order_discount_only(order):
|
||
"""
|
||
Удалить только скидку с заказа (не трогая позиции).
|
||
|
||
Args:
|
||
order: Order
|
||
"""
|
||
order.applied_discount = None
|
||
order.applied_promo_code = None
|
||
order.discount_amount = Decimal('0')
|
||
order.save(update_fields=['applied_discount', 'applied_promo_code', 'discount_amount'])
|