- Добавлено поле wallet_balance в модель Customer - Создана модель WalletTransaction для истории операций - Реализован сервис WalletService с методами: * add_overpayment - автоматическое зачисление переплаты * pay_with_wallet - оплата заказа из кошелька * adjust_balance - ручная корректировка баланса - Интеграция с Payment.save() для автоматической обработки переплат - UI для оплаты из кошелька в деталях заказа - Отображение баланса и долга на странице клиента - Админка с inline транзакций и запретом ручного создания - Добавлен способ оплаты account_balance - Миграция 0004 для customers приложения
772 lines
28 KiB
Markdown
772 lines
28 KiB
Markdown
# План реализации системы личного счета клиента
|
||
|
||
## Обзор
|
||
|
||
Реализация системы личного счета для клиентов цветочного магазина с поддержкой резервирования средств, смешанной оплаты, автоматического пополнения при переплате и полной историей транзакций.
|
||
|
||
## Ключевые бизнес-требования
|
||
|
||
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-использованию после прохождения всех фаз тестирования.
|