Files
octopus/nested-singing-rainbow.md
Andrey Smakotin 5ead7fdd2e Реализация системы кошелька клиента для переплат
- Добавлено поле wallet_balance в модель Customer
- Создана модель WalletTransaction для истории операций
- Реализован сервис WalletService с методами:
  * add_overpayment - автоматическое зачисление переплаты
  * pay_with_wallet - оплата заказа из кошелька
  * adjust_balance - ручная корректировка баланса
- Интеграция с Payment.save() для автоматической обработки переплат
- UI для оплаты из кошелька в деталях заказа
- Отображение баланса и долга на странице клиента
- Админка с inline транзакций и запретом ручного создания
- Добавлен способ оплаты account_balance
- Миграция 0004 для customers приложения
2025-11-26 14:47:11 +03:00

772 lines
28 KiB
Markdown
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.
# План реализации системы личного счета клиента
## Обзор
Реализация системы личного счета для клиентов цветочного магазина с поддержкой резервирования средств, смешанной оплаты, автоматического пополнения при переплате и полной историей транзакций.
## Ключевые бизнес-требования
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 %}
<div class="alert alert-info">
<h5>Баланс клиента</h5>
<ul>
<li>Общий баланс: <strong>{{ order.customer.account_balance }} руб.</strong></li>
<li>Доступно: <strong>{{ order.customer.available_balance }} руб.</strong></li>
<li>Зарезервировано: <strong>{{ order.customer.reserved_balance }} руб.</strong></li>
</ul>
</div>
{% 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-использованию после прохождения всех фаз тестирования.