""" Сервис для работы с кошельком клиента. Обрабатывает пополнения, списания и корректировки баланса. """ 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): """ Оплата заказа из кошелька клиента. Создаёт транзакцию. Списание из кошелька происходит автоматически в Transaction.save(). Args: order: Заказ для оплаты amount: Запрашиваемая сумма для списания user: Пользователь, инициировавший операцию Returns: Decimal: Фактически списанная сумма или None """ from customers.models import Customer from orders.services.transaction_service import TransactionService # Округляем запрошенную сумму 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 # Создаём транзакцию # Transaction.save() автоматически спишет из кошелька и создаст WalletTransaction TransactionService.create_payment( order=order, amount=usable_amount, payment_method='account_balance', user=user, notes='Оплата из кошелька клиента' ) return usable_amount @staticmethod @transaction.atomic def refund_wallet_payment(order, amount, user): """ Возврат средств в кошелёк. Используется для создания транзакции возврата с кошельком. Args: order: Заказ, по которому был платёж amount: Сумма возврата user: Пользователь, инициировавший возврат Returns: Decimal: Возвращённая сумма """ from orders.services.transaction_service import TransactionService amount = _quantize(amount) if amount <= 0: return None # Создаём транзакцию возврата # Transaction.save() автоматически вернёт в кошелёк и создаст WalletTransaction TransactionService.create_refund( order=order, amount=amount, payment_method='account_balance', user=user, reason='Возврат в кошелёк' ) return 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