Files
octopus/myproject/customers/services/wallet_service.py
Andrey Smakotin c1351e1f49 Исправлена форма заказа: две колонки и корректная работа кнопки сохранения
- Разделен экран на две колонки: заказ слева, оплата справа
- Форма оплаты вынесена за пределы основной формы заказа (устранена проблема вложенных форм)
- Исправлен метод calculate_total() для сохранения итоговой суммы в БД
- Добавлена модель Transaction для учета платежей и возвратов
- Добавлена модель PaymentMethod для методов оплаты
- Удалена старая модель Payment, заменена на Transaction
- Добавлен TransactionService для управления транзакциями
- Обновлен интерфейс форм оплаты для правой колонки
- Кнопка 'Сохранить изменения' теперь работает корректно
2025-11-29 14:33:23 +03:00

205 lines
7.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Сервис для работы с кошельком клиента.
Обрабатывает пополнения, списания и корректировки баланса.
"""
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