# План реализации системы личного счета клиента ## Обзор Реализация системы личного счета для клиентов цветочного магазина с поддержкой резервирования средств, смешанной оплаты, автоматического пополнения при переплате и полной историей транзакций. ## Ключевые бизнес-требования 1. **Баланс счета**: У каждого клиента есть личный счет (может быть положительным или отрицательным) 2. **Пополнение**: Вручную администратором или автоматически при переплате заказа 3. **Кредитование**: Разрешен отрицательный баланс для доверенных клиентов 4. **История операций**: Полный аудит всех операций со счетом 5. **Смешанная оплата**: Можно комбинировать с другими способами оплаты 6. **Резервирование**: При создании заказа средства резервируются, при завершении списываются 7. **Управление**: Только администраторы/менеджеры имеют доступ --- ## 1. Изменения в базе данных ### 1.1 Расширение модели Customer **Файл**: `myproject/customers/models.py` Добавить поля для управления балансом: ```python # Поля баланса account_balance = models.DecimalField( max_digits=10, decimal_places=2, default=0, verbose_name="Баланс счета", help_text="Текущий баланс лицевого счета клиента" ) available_balance = models.DecimalField( max_digits=10, decimal_places=2, default=0, verbose_name="Доступный баланс", help_text="Баланс за вычетом зарезервированных средств" ) reserved_balance = models.DecimalField( max_digits=10, decimal_places=2, default=0, verbose_name="Зарезервировано", help_text="Сумма, зарезервированная под активные заказы" ) allow_negative_balance = models.BooleanField( default=False, verbose_name="Разрешить отрицательный баланс", help_text="Позволяет клиенту уходить в минус" ) negative_balance_limit = models.DecimalField( max_digits=10, decimal_places=2, default=0, verbose_name="Лимит кредита", help_text="Максимальная сумма отрицательного баланса (0 = без лимита)" ) ``` **Взаимосвязь полей**: - `account_balance` = Общий баланс клиента - `reserved_balance` = Сумма, зарезервированная под заказы - `available_balance` = `account_balance` - `reserved_balance` ### 1.2 Новая модель AccountTransaction **Файл**: `myproject/customers/models.py` (или отдельный файл в models/) Модель для хранения истории всех операций со счетом: ```python class AccountTransaction(models.Model): """ Транзакция по лицевому счету клиента. """ TRANSACTION_TYPE_CHOICES = [ ('deposit', 'Пополнение вручную'), ('auto_deposit', 'Авто-пополнение (переплата)'), ('reservation', 'Резервирование'), ('reservation_release', 'Снятие резерва'), ('charge', 'Списание за заказ'), ('refund', 'Возврат средств'), ('adjustment', 'Корректировка баланса'), ] STATUS_CHOICES = [ ('active', 'Активна'), ('completed', 'Завершена'), ('cancelled', 'Отменена'), ] customer = models.ForeignKey( 'Customer', on_delete=models.PROTECT, related_name='account_transactions' ) transaction_type = models.CharField(max_length=30, choices=TRANSACTION_TYPE_CHOICES) amount = models.DecimalField(max_digits=10, decimal_places=2) balance_before = models.DecimalField(max_digits=10, decimal_places=2) balance_after = models.DecimalField(max_digits=10, decimal_places=2) order = models.ForeignKey('orders.Order', null=True, blank=True, on_delete=models.PROTECT) payment = models.ForeignKey('orders.Payment', null=True, blank=True, on_delete=models.SET_NULL) related_transaction = models.ForeignKey('self', null=True, blank=True, on_delete=models.SET_NULL) description = models.TextField() notes = models.TextField(blank=True) status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='completed') created_at = models.DateTimeField(auto_now_add=True) created_by = models.ForeignKey('accounts.CustomUser', null=True, blank=True, on_delete=models.SET_NULL) class Meta: ordering = ['-created_at'] indexes = [ models.Index(fields=['customer', '-created_at']), models.Index(fields=['transaction_type']), models.Index(fields=['order']), models.Index(fields=['status']), ] ``` ### 1.3 Новый способ оплаты Добавить в команду `create_payment_methods.py`: ```python { 'code': 'account_balance', 'name': 'С баланса счета', 'description': 'Списание с личного счета клиента', 'is_system': True, 'order': 0 # Первый в списке } ``` --- ## 2. Бизнес-логика: AccountBalanceService **Новый файл**: `myproject/customers/services/account_balance_service.py` Создать сервис с методами: ### Основные методы: 1. **`deposit(customer, amount, description, user, notes)`** - Пополнение счета вручную администратором - Увеличивает `account_balance` и `available_balance` - Создает транзакцию типа `deposit` 2. **`auto_deposit_from_overpayment(order, overpayment_amount, user)`** - Автоматическое пополнение при переплате - Вызывается когда `order.amount_paid > order.total_amount` - Создает транзакцию типа `auto_deposit` 3. **`reserve_balance(customer, order, amount, user)`** - Резервирование средств при создании заказа с оплатой со счета - Проверяет достаточность средств (с учетом кредита) - Уменьшает `available_balance`, увеличивает `reserved_balance` - Создает транзакцию типа `reservation` со статусом `active` 4. **`charge_reserved_balance(reservation_transaction, user)`** - Списание зарезервированных средств при завершении заказа - Уменьшает `account_balance` и `reserved_balance` - Обновляет статус резервирования на `completed` - Создает транзакцию типа `charge` 5. **`release_reservation(reservation_transaction, user)`** - Снятие резервирования при отмене заказа - Увеличивает `available_balance`, уменьшает `reserved_balance` - Обновляет статус резервирования на `cancelled` - Создает транзакцию типа `reservation_release` 6. **`refund(customer, amount, order, description, user, notes)`** - Возврат средств на счет - Используется при индивидуальных решениях по возвратам - Создает транзакцию типа `refund` 7. **`adjustment(customer, amount, description, user, notes)`** - Корректировка баланса администратором - Может быть положительной или отрицательной - Требует обязательное описание ### Ключевые особенности реализации: - Все методы используют `@transaction.atomic` для атомарности - `select_for_update()` для блокировки записи клиента при изменении - Проверка лимитов кредита перед резервированием - Запись `balance_before` и `balance_after` для аудита --- ## 3. Интеграция с существующей системой платежей ### 3.1 Модификация Payment.save() **Файл**: `myproject/orders/models/payment.py` В методе `save()` добавить логику: ```python def save(self, *args, **kwargs): is_new = self.pk is None super().save(*args, **kwargs) # Пересчитываем сумму оплаты self.order.amount_paid = sum(p.amount for p in self.order.payments.all()) # Обработка оплаты с баланса счета if self.payment_method.code == 'account_balance' and is_new: from customers.services.account_balance_service import AccountBalanceService AccountBalanceService.reserve_balance( customer=self.order.customer, order=self.order, amount=self.amount, user=self.created_by ) self.order.update_payment_status() # Проверка переплаты if self.order.amount_paid > self.order.total_amount: overpayment = self.order.amount_paid - self.order.total_amount from customers.services.account_balance_service import AccountBalanceService AccountBalanceService.auto_deposit_from_overpayment( order=self.order, overpayment_amount=overpayment, user=self.created_by ) ``` ### 3.2 Обработка изменения статуса заказа **Новый файл**: `myproject/orders/signals.py` Создать сигналы для автоматической обработки: ```python from django.db.models.signals import post_save from django.dispatch import receiver from .models import Order @receiver(post_save, sender=Order) def handle_order_status_change(sender, instance, created, **kwargs): """Обработка изменения статуса заказа""" if created or not instance.status: return from customers.models import AccountTransaction from customers.services.account_balance_service import AccountBalanceService # Заказ выполнен успешно - списываем if instance.status.is_positive_end: reservations = AccountTransaction.objects.filter( order=instance, transaction_type='reservation', status='active' ) for reservation in reservations: AccountBalanceService.charge_reserved_balance( reservation_transaction=reservation, user=instance.modified_by ) # Заказ отменен - снимаем резерв elif instance.status.is_negative_end: reservations = AccountTransaction.objects.filter( order=instance, transaction_type='reservation', status='active' ) for reservation in reservations: AccountBalanceService.release_reservation( reservation_transaction=reservation, user=instance.modified_by ) ``` Подключить сигналы в `myproject/orders/apps.py`: ```python class OrdersConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'orders' def ready(self): import orders.signals # noqa ``` --- ## 4. Административный интерфейс ### 4.1 Расширение CustomerAdmin **Файл**: `myproject/customers/admin.py` Изменения: 1. **Добавить поля баланса в list_display**: ```python list_display = ( 'full_name', 'email', 'phone', 'account_balance_colored', # новое 'available_balance_display', # новое 'reserved_balance_display', # новое 'total_spent', 'is_system_customer', 'created_at' ) ``` 2. **Добавить фильтр по кредиту**: ```python list_filter = ( IsSystemCustomerFilter, 'allow_negative_balance', # новое 'created_at' ) ``` 3. **Добавить секцию баланса в fieldsets**: ```python ('Баланс лицевого счета', { 'fields': ( 'account_balance', 'available_balance', 'reserved_balance', 'allow_negative_balance', 'negative_balance_limit', ), 'classes': ('wide',), }), ``` 4. **Добавить inline для транзакций**: ```python class AccountTransactionInline(admin.TabularInline): model = AccountTransaction extra = 0 can_delete = False readonly_fields = [...] inlines = [AccountTransactionInline] ``` 5. **Добавить actions**: ```python actions = [ 'add_deposit', 'add_refund', 'add_adjustment', 'enable_negative_balance', ] ``` ### 4.2 Новый AccountTransactionAdmin **Файл**: `myproject/customers/admin.py` Создать отдельную админку для просмотра всех транзакций: ```python @admin.register(AccountTransaction) class AccountTransactionAdmin(admin.ModelAdmin): list_display = [ 'created_at', 'customer_link', 'transaction_type', 'amount_colored', 'balance_after', 'order_link', 'status' ] list_filter = ['transaction_type', 'status', 'created_at'] search_fields = ['customer__name', 'customer__email', 'description'] readonly_fields = [все поля] def has_add_permission(self, request): return False # Только через сервис def has_delete_permission(self, request, obj=None): return False # Аудит, нельзя удалять ``` ### 4.3 Кастомные views для операций **Новый файл**: `myproject/customers/admin_views.py` Создать views для: - Пополнения баланса (`/admin/customers/deposit/`) - Возврата средств (`/admin/customers/refund/`) - Корректировки (`/admin/customers/adjustment/`) **Новый файл**: `myproject/customers/admin_urls.py` ```python from django.urls import path from . import admin_views urlpatterns = [ path('deposit/', admin_views.deposit_view, name='customer_deposit'), path('refund/', admin_views.refund_view, name='customer_refund'), path('adjustment/', admin_views.adjustment_view, name='customer_adjustment'), ] ``` Подключить в основной `urls.py`. --- ## 5. Формы для операций **Новый файл**: `myproject/customers/forms.py` Создать формы: ```python class DepositForm(forms.Form): """Форма пополнения баланса""" customer = forms.ModelChoiceField(queryset=Customer.objects.all()) amount = forms.DecimalField(min_value=0.01, max_digits=10, decimal_places=2) description = forms.CharField(widget=forms.Textarea) notes = forms.CharField(widget=forms.Textarea, required=False) class RefundForm(forms.Form): """Форма возврата средств""" customer = forms.ModelChoiceField(queryset=Customer.objects.all()) amount = forms.DecimalField(min_value=0.01, max_digits=10, decimal_places=2) order = forms.ModelChoiceField(queryset=Order.objects.all(), required=False) description = forms.CharField(widget=forms.Textarea) notes = forms.CharField(widget=forms.Textarea, required=False) class AdjustmentForm(forms.Form): """Форма корректировки баланса""" customer = forms.ModelChoiceField(queryset=Customer.objects.all()) amount = forms.DecimalField(max_digits=10, decimal_places=2) # может быть отрицательным description = forms.CharField(widget=forms.Textarea) notes = forms.CharField(widget=forms.Textarea, required=False) ``` --- ## 6. UI/UX улучшения ### 6.1 Отображение баланса в форме заказа **Файл**: `myproject/orders/templates/orders/order_form.html` Добавить блок с информацией о балансе клиента: ```html {% if order.customer %}