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.services.wallet_service import WalletService if self.transaction_type == 'payment': # Списание из кошелька WalletService.create_wallet_spend( order=self.order, amount=self.amount, user=self.created_by ) elif self.transaction_type == 'refund': # Возврат в кошелёк WalletService.create_wallet_deposit( order=self.order, amount=self.amount, user=self.created_by )