Рефакторинг системы кошелька клиентов
Основные изменения: - Переход от денормализованного поля wallet_balance к вычисляемому балансу - Баланс теперь вычисляется как SUM(signed_amount) транзакций - Добавлено кеширование баланса для производительности (5 минут) - Новая модель WalletTransaction с полем signed_amount (может быть +/-) - WalletService для всех операций с кошельком (deposit, spend, adjustment) - Защита от отрицательного баланса и race conditions через select_for_update - Добавлен balance_after в каждую транзакцию для аудита - Обновлены миграции для переноса данных из старой схемы Улучшения безопасности: - Атомарные транзакции для всех операций с балансом - Блокировка строк при модификации баланса - Валидация недостаточности средств - Обязательное описание для корректировок баланса UI/UX изменения: - Обновлён вывод баланса кошелька в деталях клиента - Добавлена история транзакций с типами и описаниями - Цветовая индикация положительных транзакций (зелёный) Техническая документация: - Добавлены docstrings для всех методов WalletService - Комментарии к критичным участкам кода - Примеры использования в docstrings
This commit is contained in:
@@ -2,7 +2,7 @@ from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from phonenumber_field.formfields import PhoneNumberField
|
||||
from phonenumber_field.widgets import PhoneNumberPrefixWidget
|
||||
from .models import Customer
|
||||
from .models import Customer, ContactChannel
|
||||
|
||||
class CustomerForm(forms.ModelForm):
|
||||
phone = PhoneNumberField(
|
||||
@@ -35,43 +35,17 @@ class CustomerForm(forms.ModelForm):
|
||||
field.widget.attrs.update({'class': 'form-control'})
|
||||
|
||||
def clean_email(self):
|
||||
"""Проверяет уникальность email при создании/редактировании"""
|
||||
"""Нормализует пустые значения email в None"""
|
||||
email = self.cleaned_data.get('email')
|
||||
|
||||
# Нормализуем пустые значения в None (Django best practice для nullable полей)
|
||||
if not email:
|
||||
return None
|
||||
|
||||
# Проверяем уникальность
|
||||
queryset = Customer.objects.filter(email=email)
|
||||
|
||||
# При редактировании исключаем текущий экземпляр
|
||||
if self.instance and self.instance.pk:
|
||||
queryset = queryset.exclude(pk=self.instance.pk)
|
||||
|
||||
if queryset.exists():
|
||||
raise ValidationError('Клиент с таким email уже существует.')
|
||||
|
||||
return email
|
||||
|
||||
def clean_phone(self):
|
||||
"""Проверяет уникальность телефона при создании/редактировании"""
|
||||
"""Нормализует пустые значения телефона в None"""
|
||||
phone = self.cleaned_data.get('phone')
|
||||
|
||||
# Нормализуем пустые значения в None (Django best practice для nullable полей)
|
||||
if not phone:
|
||||
return None
|
||||
|
||||
# Проверяем уникальность
|
||||
queryset = Customer.objects.filter(phone=phone)
|
||||
|
||||
# При редактировании исключаем текущий экземпляр
|
||||
if self.instance and self.instance.pk:
|
||||
queryset = queryset.exclude(pk=self.instance.pk)
|
||||
|
||||
if queryset.exists():
|
||||
raise ValidationError('Клиент с таким номером телефона уже существует.')
|
||||
|
||||
return phone
|
||||
|
||||
def clean(self):
|
||||
@@ -85,4 +59,35 @@ class CustomerForm(forms.ModelForm):
|
||||
'Он необходим для корректной работы системы и создается автоматически.'
|
||||
)
|
||||
|
||||
return cleaned_data
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class ContactChannelForm(forms.ModelForm):
|
||||
"""Форма для добавления/редактирования канала связи"""
|
||||
|
||||
class Meta:
|
||||
model = ContactChannel
|
||||
fields = ['channel_type', 'value', 'is_primary', 'notes']
|
||||
widgets = {
|
||||
'channel_type': forms.Select(attrs={'class': 'form-select'}),
|
||||
'value': forms.TextInput(attrs={'class': 'form-control', 'placeholder': '@username, номер и т.д.'}),
|
||||
'notes': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Личный аккаунт, рабочий...'}),
|
||||
'is_primary': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
}
|
||||
|
||||
def clean_value(self):
|
||||
value = self.cleaned_data.get('value', '').strip()
|
||||
channel_type = self.cleaned_data.get('channel_type')
|
||||
|
||||
if not value:
|
||||
raise ValidationError('Значение не может быть пустым')
|
||||
|
||||
# Проверка уникальности комбинации channel_type + value
|
||||
qs = ContactChannel.objects.filter(channel_type=channel_type, value=value)
|
||||
if self.instance.pk:
|
||||
qs = qs.exclude(pk=self.instance.pk)
|
||||
if qs.exists():
|
||||
type_display = dict(ContactChannel.CHANNEL_TYPES).get(channel_type, channel_type)
|
||||
raise ValidationError(f'Такой {type_display} уже существует у другого клиента')
|
||||
|
||||
return value
|
||||
Reference in New Issue
Block a user