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:
2026-01-11 00:30:14 +03:00
parent 27cb9ba09d
commit 241625eba7
14 changed files with 1524 additions and 0 deletions

View File

@@ -0,0 +1,110 @@
from django.db import models
class DiscountApplication(models.Model):
"""
История применения скидок к заказам и позициям.
Используется для аналитики и отчётов.
"""
DISCOUNT_TARGET_CHOICES = [
('order', 'Заказ'),
('order_item', 'Позиция заказа'),
]
order = models.ForeignKey(
'orders.Order',
on_delete=models.CASCADE,
related_name='discount_applications',
verbose_name="Заказ"
)
order_item = models.ForeignKey(
'orders.OrderItem',
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='discount_applications',
verbose_name="Позиция заказа"
)
discount = models.ForeignKey(
'Discount',
on_delete=models.SET_NULL,
null=True,
related_name='applications',
verbose_name="Скидка"
)
promo_code = models.ForeignKey(
'PromoCode',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='applications',
verbose_name="Промокод"
)
target = models.CharField(
max_length=20,
choices=DISCOUNT_TARGET_CHOICES,
verbose_name="Объект применения"
)
base_amount = models.DecimalField(
max_digits=10,
decimal_places=2,
verbose_name="Базовая сумма"
)
discount_amount = models.DecimalField(
max_digits=10,
decimal_places=2,
verbose_name="Сумма скидки"
)
final_amount = models.DecimalField(
max_digits=10,
decimal_places=2,
verbose_name="Итоговая сумма"
)
customer = models.ForeignKey(
'customers.Customer',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='discount_applications',
verbose_name="Клиент"
)
applied_at = models.DateTimeField(
auto_now_add=True,
verbose_name="Дата применения"
)
applied_by = models.ForeignKey(
'accounts.CustomUser',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='applied_discounts',
verbose_name="Применен пользователем"
)
class Meta:
verbose_name = "Применение скидки"
verbose_name_plural = "Применения скидок"
indexes = [
models.Index(fields=['order']),
models.Index(fields=['discount']),
models.Index(fields=['promo_code']),
models.Index(fields=['customer']),
models.Index(fields=['applied_at']),
]
def __str__(self):
target_info = f"Заказ #{self.order.order_number}"
if self.order_item:
target_info += f", {self.order_item.item_name_snapshot}"
return f"{self.discount.name} -> {target_info} (-{self.discount_amount})"