Files
octopus/myproject/discounts/services/applier.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

229 lines
7.7 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.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'])