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 @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)) # Применяем к заказу order.applied_discount = discount order.discount_amount = discount_amount order.save(update_fields=['applied_discount', 'discount_amount']) # Пересчитываем total_amount order.calculate_total() # Создаем запись о применении 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']) return { 'success': True, 'discount_amount': discount_amount } @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'])