""" Сервис для работы с кошельком клиента. Обрабатывает пополнения, списания и корректировки баланса. """ from decimal import Decimal, ROUND_HALF_UP from django.db import transaction # Константа для округления до 2 знаков QUANTIZE_2D = Decimal('0.01') def _quantize(value): """Округление до 2 знаков после запятой""" if isinstance(value, (int, float)): value = Decimal(str(value)) return value.quantize(QUANTIZE_2D, rounding=ROUND_HALF_UP) class WalletService: """ Сервис для управления кошельком клиента. Все операции атомарны и блокируют запись клиента для избежания race conditions. """ @staticmethod @transaction.atomic def add_overpayment(order, user): """ Обработка переплаты по заказу. Переносит излишек в кошелёк клиента и нормализует amount_paid заказа. Args: order: Заказ с переплатой user: Пользователь, инициировавший операцию Returns: Decimal: Сумма переплаты или None, если переплаты нет """ from customers.models import Customer, WalletTransaction overpayment = order.amount_paid - order.total_amount if overpayment <= 0: return None # Блокируем запись клиента для обновления customer = Customer.objects.select_for_update().get(pk=order.customer_id) # Округляем переплату до 2 знаков overpayment = _quantize(overpayment) # Увеличиваем баланс кошелька customer.wallet_balance = _quantize(customer.wallet_balance + overpayment) customer.save(update_fields=['wallet_balance']) # Создаём транзакцию для аудита WalletTransaction.objects.create( customer=customer, amount=overpayment, transaction_type='deposit', order=order, description=f'Переплата по заказу #{order.order_number}', created_by=user ) # Нормализуем amount_paid заказа до total_amount order.amount_paid = order.total_amount order.save(update_fields=['amount_paid']) return overpayment @staticmethod @transaction.atomic def pay_with_wallet(order, amount, user): """ Оплата заказа из кошелька клиента. Списывает средства с кошелька и создаёт платёж в заказе. Args: order: Заказ для оплаты amount: Запрашиваемая сумма для списания user: Пользователь, инициировавший операцию Returns: Decimal: Фактически списанная сумма или None """ from customers.models import Customer, WalletTransaction from orders.models import Payment, PaymentMethod # Округляем запрошенную сумму amount = _quantize(amount) if amount <= 0: return None # Блокируем запись клиента customer = Customer.objects.select_for_update().get(pk=order.customer_id) # Остаток к оплате по заказу amount_due = order.total_amount - order.amount_paid # Определяем фактическую сумму списания (минимум из трёх) usable_amount = min(amount, customer.wallet_balance, amount_due) usable_amount = _quantize(usable_amount) if usable_amount <= 0: return None # Получаем способ оплаты "С баланса счёта" try: payment_method = PaymentMethod.objects.get(code='account_balance') except PaymentMethod.DoesNotExist: raise ValueError( 'Способ оплаты "account_balance" не найден. ' 'Запустите команду create_payment_methods.' ) # Создаём платёж в заказе Payment.objects.create( order=order, amount=usable_amount, payment_method=payment_method, created_by=user, notes='Оплата из кошелька клиента' ) # Уменьшаем баланс кошелька customer.wallet_balance = _quantize(customer.wallet_balance - usable_amount) customer.save(update_fields=['wallet_balance']) # Создаём транзакцию для аудита WalletTransaction.objects.create( customer=customer, amount=usable_amount, transaction_type='spend', order=order, description=f'Оплата заказа #{order.order_number} из кошелька', created_by=user ) return usable_amount @staticmethod @transaction.atomic def adjust_balance(customer_id, amount, description, user): """ Корректировка баланса кошелька администратором. Может быть как положительной (пополнение), так и отрицательной (списание). Args: customer_id: ID клиента amount: Сумма корректировки (может быть отрицательной) description: Обязательное описание причины корректировки user: Пользователь, выполнивший корректировку Returns: WalletTransaction: Созданная транзакция """ from customers.models import Customer, WalletTransaction if not description or not description.strip(): raise ValueError('Описание обязательно для корректировки баланса') amount = _quantize(amount) if amount == 0: raise ValueError('Сумма корректировки не может быть нулевой') # Блокируем запись клиента customer = Customer.objects.select_for_update().get(pk=customer_id) # Применяем корректировку new_balance = _quantize(customer.wallet_balance + amount) # Проверяем, что баланс не уйдёт в минус if new_balance < 0: raise ValueError( f'Корректировка приведёт к отрицательному балансу ' f'({new_balance} руб.). Операция отклонена.' ) customer.wallet_balance = new_balance customer.save(update_fields=['wallet_balance']) # Создаём транзакцию txn = WalletTransaction.objects.create( customer=customer, amount=abs(amount), transaction_type='adjustment', order=None, description=description, created_by=user ) return txn