Files
octopus/myproject/orders/models/transaction.py

175 lines
6.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 django.db import models
from accounts.models import CustomUser
from decimal import Decimal
from django.db import transaction
from django.core.exceptions import ValidationError
class Transaction(models.Model):
"""
Финансовая транзакция по заказу.
Хранит историю всех платежей и возвратов.
Транзакция может быть:
- payment: клиент платит (увеличивает amount_paid)
- refund: возврат клиенту (уменьшает amount_paid)
Поддерживает:
- Смешанную оплату (несколько транзакций разными способами)
- Частичные возвраты (любая сумма)
- Полную историю движения денег
"""
TRANSACTION_TYPE_CHOICES = [
('payment', 'Платёж'),
('refund', 'Возврат'),
]
order = models.ForeignKey(
'Order',
on_delete=models.CASCADE,
related_name='transactions',
verbose_name="Заказ"
)
transaction_type = models.CharField(
max_length=20,
choices=TRANSACTION_TYPE_CHOICES,
default='payment',
verbose_name="Тип транзакции"
)
amount = models.DecimalField(
max_digits=10,
decimal_places=2,
verbose_name="Сумма",
help_text="Всегда положительная. Для возврата используется transaction_type='refund'"
)
payment_method = models.ForeignKey(
'PaymentMethod',
on_delete=models.PROTECT,
related_name='transactions',
verbose_name="Способ оплаты/возврата"
)
# Для возвратов - опциональная связь с исходным платежом
related_payment = models.ForeignKey(
'self',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='refunds',
verbose_name="Связанный платёж",
help_text="Для возвратов - на какой платёж ссылается этот возврат"
)
transaction_date = models.DateTimeField(
auto_now_add=True,
verbose_name="Дата и время транзакции"
)
created_by = models.ForeignKey(
CustomUser,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='transactions_created',
verbose_name="Создал"
)
notes = models.TextField(
blank=True,
null=True,
verbose_name="Примечания"
)
reason = models.CharField(
max_length=255,
blank=True,
null=True,
verbose_name="Причина",
help_text="Причина возврата или особенности платежа"
)
class Meta:
verbose_name = "Транзакция"
verbose_name_plural = "Транзакции"
ordering = ['-transaction_date']
indexes = [
models.Index(fields=['order', '-transaction_date']),
models.Index(fields=['transaction_type']),
models.Index(fields=['payment_method']),
models.Index(fields=['transaction_date']),
]
def __str__(self):
type_name = self.get_transaction_type_display()
return f"{type_name} {self.amount} руб. по заказу #{self.order.order_number}"
def clean(self):
"""Валидация транзакции"""
super().clean()
# Сумма всегда положительная
if self.amount <= 0:
raise ValidationError({'amount': 'Сумма должна быть положительной'})
# Для возврата related_payment должен быть payment
if self.transaction_type == 'refund' and self.related_payment:
if self.related_payment.transaction_type != 'payment':
raise ValidationError({
'related_payment': 'Связанная транзакция должна быть платежом'
})
def save(self, *args, **kwargs):
"""При сохранении обновляем баланс заказа и обрабатываем кошелёк"""
is_new = self.pk is None
with transaction.atomic():
super().save(*args, **kwargs)
# Пересчитываем баланс заказа
self.order.recalculate_amount_paid()
# Обработка кошелька только для новых транзакций
if is_new and self.payment_method.code == 'account_balance':
from customers.models import Customer, WalletTransaction
# Блокируем запись клиента
customer = Customer.objects.select_for_update().get(pk=self.order.customer_id)
if self.transaction_type == 'payment':
# Списание из кошелька
if customer.wallet_balance < self.amount:
raise ValidationError(
f'Недостаточно средств в кошельке '
f'(доступно {customer.wallet_balance} руб.)'
)
customer.wallet_balance = (customer.wallet_balance - self.amount).quantize(Decimal('0.01'))
customer.save(update_fields=['wallet_balance'])
WalletTransaction.objects.create(
customer=customer,
amount=self.amount,
transaction_type='spend',
order=self.order,
description=f'Оплата из кошелька по заказу #{self.order.order_number}',
created_by=self.created_by
)
elif self.transaction_type == 'refund':
# Возврат в кошелёк
customer.wallet_balance = (customer.wallet_balance + self.amount).quantize(Decimal('0.01'))
customer.save(update_fields=['wallet_balance'])
WalletTransaction.objects.create(
customer=customer,
amount=self.amount,
transaction_type='deposit',
order=self.order,
description=f'Возврат в кошелёк по заказу #{self.order.order_number}',
created_by=self.created_by
)