# План реализации системы личного счета клиента ## Обзор Реализация системы личного счета для клиентов цветочного магазина с поддержкой резервирования средств, смешанной оплаты, автоматического пополнения при переплате и полной историей транзакций. ## Ключевые бизнес-требования 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 %}
Баланс клиента
{% endif %} ``` ### 6.2 Валидация при выборе оплаты со счета **Файл**: `myproject/orders/static/orders/js/payment_validation.js` Добавить JS-валидацию: ```javascript // Проверка достаточности средств при выборе оплаты со счета function validateAccountBalance(paymentMethodCode, amount, availableBalance, allowNegative, creditLimit) { if (paymentMethodCode === 'account_balance') { if (amount > availableBalance && !allowNegative) { alert('Недостаточно средств на счете клиента!'); return false; } if (allowNegative && creditLimit > 0) { let potentialBalance = availableBalance - amount; if (Math.abs(potentialBalance) > creditLimit) { alert('Превышен лимит кредита клиента!'); return false; } } } return true; } ``` --- ## 7. Миграции ### Последовательность миграций: 1. **Добавить поля баланса в Customer**: ```bash python manage.py makemigrations customers --name add_account_balance_fields ``` 2. **Создать модель AccountTransaction**: ```bash python manage.py makemigrations customers --name create_account_transaction_model ``` 3. **Создать индексы и ограничения**: ```bash python manage.py makemigrations customers --name add_balance_constraints ``` 4. **Инициализация данных** (data migration): ```python def initialize_customer_balances(apps, schema_editor): Customer = apps.get_model('customers', 'Customer') Customer.objects.all().update( account_balance=0, available_balance=0, reserved_balance=0, allow_negative_balance=False, negative_balance_limit=0 ) ``` 5. **Добавить способ оплаты**: ```bash python manage.py create_payment_methods ``` --- ## 8. Обеспечение целостности данных ### 8.1 Транзакции и блокировки - Все операции в `@transaction.atomic` - Использование `select_for_update()` для блокировки записи клиента - Проверка статуса транзакции перед обработкой ### 8.2 Ограничения БД ```python # В миграции models.CheckConstraint( check=models.Q(reserved_balance__gte=0), name='reserved_balance_non_negative' ) models.CheckConstraint( check=models.Q(account_balance__gte=models.F('reserved_balance') * -1), name='available_balance_consistency' ) ``` ### 8.3 Предотвращение дублирования - Проверка `status='active'` перед обработкой резервирования - Связь `related_transaction` для отслеживания цепочки операций - Валидация перед созданием транзакции --- ## 9. Типы транзакций: Подробное описание ### DEPOSIT (Пополнение) - **Когда**: Администратор вручную пополняет счет - **Эффект**: `account_balance ↑`, `available_balance ↑` - **Статус**: `completed` ### AUTO_DEPOSIT (Авто-пополнение) - **Когда**: `order.amount_paid > order.total_amount` - **Эффект**: `account_balance ↑`, `available_balance ↑` - **Статус**: `completed` ### RESERVATION (Резервирование) - **Когда**: Создание заказа с оплатой со счета - **Эффект**: `available_balance ↓`, `reserved_balance ↑` - **Статус**: `active` → меняется при charge/release ### CHARGE (Списание) - **Когда**: Заказ выполнен (`is_positive_end=True`) - **Эффект**: `account_balance ↓`, `reserved_balance ↓` - **Статус**: `completed` ### RESERVATION_RELEASE (Снятие резерва) - **Когда**: Заказ отменен (`is_negative_end=True`) - **Эффект**: `available_balance ↑`, `reserved_balance ↓` - **Статус**: `completed` ### REFUND (Возврат) - **Когда**: Администратор принимает решение о возврате - **Эффект**: `account_balance ↑`, `available_balance ↑` - **Статус**: `completed` ### ADJUSTMENT (Корректировка) - **Когда**: Ручная корректировка администратором - **Эффект**: `account_balance ±`, `available_balance ±` - **Статус**: `completed` --- ## 10. Сценарии использования ### Сценарий 1: Заказ с полной оплатой со счета 1. Клиент создает заказ на 540 руб. 2. На балансе 1000 руб. 3. Выбирается способ оплаты "С баланса счета" 4. **Создается RESERVATION** на 540 руб.: `available_balance: 1000→460`, `reserved_balance: 0→540` 5. При выполнении заказа создается **CHARGE**: `account_balance: 1000→460`, `reserved_balance: 540→0` ### Сценарий 2: Смешанная оплата 1. Заказ на 540 руб. 2. На балансе 300 руб. 3. Создается Payment со счета на 300 руб. → **RESERVATION** 300 руб. 4. Создается Payment наличными на 240 руб. 5. При выполнении → **CHARGE** 300 руб. со счета ### Сценарий 3: Переплата с авто-пополнением 1. Заказ на 540 руб. 2. Клиент платит наличными 1000 руб. 3. `order.amount_paid = 1000`, `order.total_amount = 540` 4. Система создает **AUTO_DEPOSIT** на 460 руб. 5. Баланс клиента увеличивается на 460 руб. ### Сценарий 4: Отмена заказа 1. Заказ на 540 руб. с резервированием 2. `reserved_balance = 540`, `available_balance = 460` 3. Заказ меняет статус на "Отменен" (`is_negative_end=True`) 4. Сигнал создает **RESERVATION_RELEASE** 5. `available_balance: 460→1000`, `reserved_balance: 540→0` ### Сценарий 5: Кредит доверенного клиента 1. У клиента баланс 0 руб., но `allow_negative_balance=True` 2. Заказ на 540 руб. 3. Создается **RESERVATION** на 540 руб. 4. `account_balance: 0→0`, `available_balance: 0→-540`, `reserved_balance: 0→540` 5. При выполнении **CHARGE**: `account_balance: 0→-540` --- ## 11. Тестирование ### Unit Tests **Файл**: `myproject/customers/tests/test_account_balance_service.py` Тесты: - `test_deposit_increases_balance` - `test_reserve_decreases_available` - `test_charge_decreases_account_balance` - `test_release_increases_available` - `test_overpayment_creates_auto_deposit` - `test_negative_balance_validation` - `test_credit_limit_enforcement` - `test_concurrent_operations` ### Integration Tests **Файл**: `myproject/orders/tests/test_order_with_account_balance.py` Тесты: - `test_order_with_account_payment` - `test_mixed_payment_scenario` - `test_order_completion_charges_balance` - `test_order_cancellation_releases_reservation` - `test_overpayment_auto_deposit` --- ## 12. Критические файлы для реализации 1. **`myproject/customers/models.py`** - Добавить поля баланса в Customer - Создать модель AccountTransaction 2. **`myproject/customers/services/account_balance_service.py`** (НОВЫЙ) - Все методы управления балансом 3. **`myproject/orders/models/payment.py`** - Модифицировать `save()` для обработки оплаты со счета 4. **`myproject/orders/signals.py`** (НОВЫЙ) - Обработка изменения статуса заказа 5. **`myproject/customers/admin.py`** - Расширить CustomerAdmin - Создать AccountTransactionAdmin 6. **`myproject/customers/admin_views.py`** (НОВЫЙ) - Views для пополнения/возврата/корректировки 7. **`myproject/customers/forms.py`** (НОВЫЙ) - Формы для операций с балансом 8. **`myproject/orders/management/commands/create_payment_methods.py`** - Добавить способ оплаты 'account_balance' 9. **`myproject/orders/apps.py`** - Подключить сигналы --- ## 13. Последовательность реализации ### Фаза 1: Модели и миграции (основа) 1. Добавить поля в Customer 2. Создать AccountTransaction 3. Создать миграции 4. Инициализировать данные ### Фаза 2: Бизнес-логика (ядро) 1. Создать AccountBalanceService со всеми методами 2. Покрыть unit-тестами ### Фаза 3: Интеграция с заказами (связывание) 1. Модифицировать Payment.save() 2. Создать signals.py 3. Добавить способ оплаты 4. Покрыть integration-тестами ### Фаза 4: Административный интерфейс (управление) 1. Расширить CustomerAdmin 2. Создать AccountTransactionAdmin 3. Создать формы и views для операций 4. Настроить URLs ### Фаза 5: UI/UX улучшения (удобство) 1. Отображение баланса в форме заказа 2. JS-валидация при оплате 3. Виджеты истории транзакций ### Фаза 6: Тестирование и документация (качество) 1. Полное покрытие тестами 2. Ручное тестирование сценариев 3. Документация для администраторов --- ## 14. Безопасность и права доступа - Только `staff_member_required` для admin views - Транзакции нельзя удалять (`has_delete_permission = False`) - Транзакции нельзя создавать вручную (`has_add_permission = False`) - Все операции требуют `created_by` (аудит) - Mandatory `description` для adjustment --- ## Заключение Данная архитектура обеспечивает: - ✅ Полную историю операций (аудит) - ✅ Атомарность операций (транзакции БД) - ✅ Защиту от race conditions (блокировки) - ✅ Гибкость (смешанная оплата, кредит) - ✅ Интеграцию с существующей системой - ✅ Простоту управления (admin interface) - ✅ Безопасность (только администраторы) Решение готово к production-использованию после прохождения всех фаз тестирования.