159 lines
6.2 KiB
Python
159 lines
6.2 KiB
Python
"""
|
||
Сервис для работы с кошельком клиента.
|
||
Обрабатывает пополнения, списания и корректировки баланса.
|
||
"""
|
||
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 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
|