from decimal import Decimal from django.db import transaction from django.core.exceptions import ValidationError class DiscountApplier: """ Сервис для применения скидок к заказам. Поддерживает комбинирование скидок по combine_mode. Все операции атомарны. """ @staticmethod @transaction.atomic def apply_promo_code(order, promo_code, user=None): """ Применить промокод к заказу. Поддерживает комбинирование скидок. Args: order: Order promo_code: str user: CustomUser (применивший скидку) Returns: dict: { 'success': bool, 'discounts': [{'discount': Discount, 'amount': Decimal}, ...], 'total_amount': Decimal, 'error': str } """ from discounts.models import PromoCode, DiscountApplication from discounts.services.calculator import DiscountCalculator # Удаляем предыдущую скидку на заказ DiscountApplier._remove_order_discount_only(order) # Рассчитываем скидку result = DiscountCalculator.calculate_order_discount(order, promo_code) if result['error']: return { 'success': False, 'error': result['error'] } promo = result['promo_code'] discounts_data = result['discounts'] total_amount = result['total_amount'] # Создаем записи о применении для каждой скидки в DiscountApplication for disc_data in discounts_data: discount = disc_data['discount'] amount = disc_data['amount'] DiscountApplication.objects.create( order=order, discount=discount, promo_code=promo, target='order', base_amount=order.subtotal, discount_amount=amount, final_amount=order.subtotal - amount, customer=order.customer, applied_by=user or order.modified_by ) # Увеличиваем счетчик использований скидки discount.current_usage_count += 1 discount.save(update_fields=['current_usage_count']) # Пересчитываем total_amount (использует DiscountApplication) order.calculate_total() # Регистрируем использование промокода promo.record_usage(order.customer) return { 'success': True, 'discounts': discounts_data, 'total_amount': total_amount } @staticmethod @transaction.atomic def apply_auto_discounts(order, user=None): """ Применить автоматические скидки к заказу и позициям. Поддерживает комбинирование скидок. Args: order: Order user: CustomUser Returns: dict: { 'order_discounts': [...], 'item_discounts': [...], 'total_discount': Decimal } """ from discounts.models import Discount, DiscountApplication from discounts.services.calculator import DiscountCalculator result = { 'order_discounts': [], 'item_discounts': [], 'total_discount': Decimal('0') } # 1. Применяем скидки на заказ (может быть несколько) order_result = DiscountCalculator.calculate_order_discount(order) if order_result['discounts'] and not order_result['error']: total_order_amount = order_result['total_amount'] # Создаем записи о применении для всех скидок for disc_data in order_result['discounts']: discount = disc_data['discount'] amount = disc_data['amount'] DiscountApplication.objects.create( order=order, discount=discount, target='order', base_amount=order.subtotal, discount_amount=amount, final_amount=order.subtotal - amount, customer=order.customer, applied_by=user ) # Увеличиваем счетчик discount.current_usage_count += 1 discount.save(update_fields=['current_usage_count']) result['order_discounts'] = order_result['discounts'] result['total_discount'] += total_order_amount # 2. Применяем скидки на позиции available_product_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_product_discounts ) if item_result['discounts']: total_item_amount = item_result['total_amount'] # Создаем записи о применении для всех скидок base_amount = item.price * item.quantity for disc_data in item_result['discounts']: discount = disc_data['discount'] amount = disc_data['amount'] DiscountApplication.objects.create( order=order, order_item=item, discount=discount, target='order_item', base_amount=base_amount, discount_amount=amount, final_amount=base_amount - amount, customer=order.customer, applied_by=user ) # Увеличиваем счетчик discount.current_usage_count += 1 discount.save(update_fields=['current_usage_count']) result['item_discounts'].append({ 'item': item, 'discounts': item_result['discounts'], 'total_amount': total_item_amount }) result['total_discount'] += total_item_amount # Пересчитываем итоговую сумму order.calculate_total() return result @staticmethod @transaction.atomic def remove_discount_from_order(order): """ Удалить скидку с заказа. Args: order: Order """ # Удаляем записи о применении from discounts.models import DiscountApplication DiscountApplication.objects.filter(order=order).delete() # Пересчитываем order.calculate_total() @staticmethod @transaction.atomic def apply_manual_discount(order, discount, user=None): """ Применить скидку вручную к заказу. Args: order: Order discount: Discount user: CustomUser (применивший скидку) Returns: dict: { 'success': bool, 'discount_amount': Decimal, 'error': str } """ from discounts.models import DiscountApplication # Проверяем scope скидки if discount.scope != 'order': return {'success': False, 'error': 'Эта скидка не применяется к заказу'} # Проверяем мин. сумму if discount.min_order_amount and order.subtotal < discount.min_order_amount: return {'success': False, 'error': f'Мин. сумма заказа: {discount.min_order_amount} руб.'} # Удаляем предыдущую скидку на заказ DiscountApplier._remove_order_discount_only(order) # Рассчитываем сумму discount_amount = discount.calculate_discount_amount(Decimal(order.subtotal)) # Создаем запись о применении 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']) # Пересчитываем total_amount order.calculate_total() return { 'success': True, 'discount_amount': discount_amount } @staticmethod def _remove_order_discount_only(order): """ Удалить только скидку с заказа (не трогая позиции). Args: order: Order """ from discounts.models import DiscountApplication # Удаляем записи о применении скидок к заказу DiscountApplication.objects.filter(order=order, target='order').delete() # Пересчитываем (order.discount_amount теперь свойство, берущее из DiscountApplication) order.calculate_total()