Рефакторинг: отдельные endpoints для управления платежами (Django best practices)

ПРОБЛЕМА:
Использование PaymentFormSet для платежей было НЕПРАВИЛЬНЫМ подходом:
1. Платежи = финансовые транзакции (не должны редактироваться inline)
2. Формы валидировали существующие платежи как новые
3. Сложная логика с formset management forms
4. Конфликты валидации кошелька

РЕШЕНИЕ (Django Best Practices):
Разделили управление платежами на отдельные операции:

АРХИТЕКТУРА:
`
POST /orders/111/payments/add/          # Добавить платеж
POST /orders/111/payments/123/delete/   # Удалить платеж
`

ПРЕИМУЩЕСТВА:
 Чистая архитектура (separation of concerns)
 Платежи = неизменяемые транзакции
 Простая валидация (только для новых)
 Легко тестировать
 API-ready структура

ИЗМЕНЕНИЯ:

1. orders/views.py:
   - Убран PaymentFormSet из order_create и order_update
   - Добавлен payment_add(request, order_number)
   - Добавлен payment_delete(request, order_number, payment_id)
   - Используется простой PaymentForm вместо formset
   - Payment.save() автоматически обрабатывает:
     * Списание из кошелька
     * Обработку переплаты
     * Обновление amount_paid

2. orders/urls.py:
   - Добавлены URL patterns для payment-add и payment-delete
   - Структура: /orders/<number>/payments/add|<id>/delete/

3. orders/templates/orders/order_form.html:
   - Убран PaymentFormSet и все его скрипты (~265 строк)
   - Простая HTML форма для добавления платежа
   - Существующие платежи: read-only список с кнопками удаления
   - Каждое удаление = отдельный POST запрос
   - Для создания: показываем предупреждение вместо формы

4. orders/templatetags/orders_tags.py (NEW):
   - Template tag get_payment_methods
   - Загружает активные способы оплаты
   - Использование: {% get_payment_methods as payment_methods %}

РЕЗУЛЬТАТ:
- Код: -191 строка
- Логика: простая и понятная
- Архитектура: правильная (как в учебнике)
- Платежи: только add/delete (без edit)
- Валидация: работает корректно
- UX: чище и понятнее
This commit is contained in:
2025-11-29 02:27:50 +03:00
parent ee002d5fed
commit 84ed3a0c7d
5 changed files with 160 additions and 342 deletions

View File

@@ -8,8 +8,8 @@ from django.contrib.auth.decorators import login_required
from django.core.exceptions import ValidationError
from django.db import models
from decimal import Decimal
from .models import Order, OrderItem, Address, OrderStatus
from .forms import OrderForm, OrderItemFormSet, OrderStatusForm, PaymentFormSet
from .models import Order, OrderItem, Address, OrderStatus, Payment, PaymentMethod
from .forms import OrderForm, OrderItemFormSet, OrderStatusForm, PaymentForm
from .filters import OrderFilter
from .services.address_service import AddressService
import json
@@ -65,9 +65,8 @@ def order_create(request):
if request.method == 'POST':
form = OrderForm(request.POST)
formset = OrderItemFormSet(request.POST)
payment_formset = PaymentFormSet(request.POST, prefix='payments')
if form.is_valid() and formset.is_valid() and payment_formset.is_valid():
if form.is_valid() and formset.is_valid():
# Сохраняем форму БЕЗ commit, чтобы не вызывать reset_delivery_cost() до сохранения items
order = form.save(commit=False)
@@ -90,31 +89,12 @@ def order_create(request):
formset.instance = order
formset.save()
# Сохраняем платежи (устанавливаем created_by)
payment_formset.instance = order
unsaved_payments = payment_formset.save(commit=False)
for p in unsaved_payments:
if p.created_by_id is None:
p.created_by = request.user
p.order = order
p.save()
# Обрабатываем удалённые платежи
from customers.services.wallet_service import WalletService
for obj in payment_formset.deleted_objects:
# Если удаляем платёж из кошелька - возвращаем сумму обратно
if hasattr(obj, 'payment_method') and obj.payment_method and getattr(obj.payment_method, 'code', '') == 'account_balance':
WalletService.refund_wallet_payment(order, obj.amount, request.user)
obj.delete()
# Пересчитываем стоимость доставки если она не установлена вручную
delivery_cost = form.cleaned_data.get('delivery_cost')
if not delivery_cost or delivery_cost <= 0:
order.reset_delivery_cost()
# Пересчитываем сумму оплачено и итоговую стоимость
order.amount_paid = sum(p.amount for p in order.payments.all())
# Пересчитываем итоговую стоимость
order.calculate_total()
order.update_payment_status()
@@ -137,12 +117,10 @@ def order_create(request):
form = OrderForm(initial=initial_data)
formset = OrderItemFormSet()
payment_formset = PaymentFormSet(prefix='payments')
context = {
'form': form,
'formset': formset,
'payment_formset': payment_formset,
'preselected_customer': preselected_customer,
'title': 'Создание заказа',
'button_text': 'Создать заказ',
@@ -157,36 +135,10 @@ def order_update(request, order_number):
order = get_object_or_404(Order, order_number=order_number)
if request.method == 'POST':
# Обработка удаления существующего платежа
delete_payment_id = request.POST.get('delete_payment_id')
if delete_payment_id:
try:
from orders.models import Payment
from customers.services.wallet_service import WalletService
payment = Payment.objects.get(pk=delete_payment_id, order=order)
# Если это платеж из кошелька - возвращаем средства
if payment.payment_method and payment.payment_method.code == 'account_balance':
WalletService.refund_wallet_payment(order, payment.amount, request.user)
payment.delete()
# Пересчитываем сумму оплаты
order.amount_paid = sum(p.amount for p in order.payments.all())
order.update_payment_status()
messages.success(request, 'Платеж успешно удален.')
return redirect('orders:order-update', order_number=order.order_number)
except Payment.DoesNotExist:
messages.error(request, 'Платеж не найден.')
return redirect('orders:order-update', order_number=order.order_number)
form = OrderForm(request.POST, instance=order)
formset = OrderItemFormSet(request.POST, instance=order)
payment_formset = PaymentFormSet(request.POST, instance=order, prefix='payments')
if form.is_valid() and formset.is_valid() and payment_formset.is_valid():
if form.is_valid() and formset.is_valid():
order = form.save(commit=False)
# Обрабатываем адрес доставки
@@ -218,26 +170,7 @@ def order_update(request, order_number):
order.save()
formset.save()
# Сохраняем платежи (устанавливаем created_by)
payment_formset.instance = order
unsaved_payments = payment_formset.save(commit=False)
for p in unsaved_payments:
if p.created_by_id is None:
p.created_by = request.user
p.order = order
p.save()
# Обрабатываем удалённые платежи
from customers.services.wallet_service import WalletService
for obj in payment_formset.deleted_objects:
# Если удаляем платёж из кошелька - возвращаем сумму обратно
if hasattr(obj, 'payment_method') and obj.payment_method and getattr(obj.payment_method, 'code', '') == 'account_balance':
WalletService.refund_wallet_payment(order, obj.amount, request.user)
obj.delete()
# Пересчитываем сумму оплачено и итоговую стоимость
order.amount_paid = sum(p.amount for p in order.payments.all())
# Пересчитываем итоговую стоимость
order.calculate_total()
order.update_payment_status()
@@ -254,20 +187,15 @@ def order_update(request, order_number):
for i, item_form in enumerate(formset):
if item_form.errors:
print(f" Item form {i} errors: {item_form.errors}")
if not payment_formset.is_valid():
print(f"PaymentFormSet errors: {payment_formset.errors}")
print(f"PaymentFormSet non_form_errors: {payment_formset.non_form_errors()}")
print("=== КОНЕЦ ОШИБОК ===\n")
messages.error(request, 'Пожалуйста, исправьте ошибки в форме.')
else:
form = OrderForm(instance=order)
formset = OrderItemFormSet(instance=order)
payment_formset = PaymentFormSet(instance=order, prefix='payments')
context = {
'form': form,
'formset': formset,
'payment_formset': payment_formset,
'order': order,
'title': f'Редактирование заказа #{order.order_number}',
'button_text': 'Сохранить изменения',
@@ -293,6 +221,76 @@ def order_delete(request, order_number):
return render(request, 'orders/order_confirm_delete.html', context)
# === УПРАВЛЕНИЕ ПЛАТЕЖАМИ ===
@login_required
@require_http_methods(["POST"])
def payment_add(request, order_number):
"""
Добавление нового платежа к заказу.
Отдельный endpoint для чистоты архитектуры.
"""
order = get_object_or_404(Order, order_number=order_number)
form = PaymentForm(request.POST)
if form.is_valid():
payment = form.save(commit=False)
payment.order = order
payment.created_by = request.user
try:
# save() вызовет Payment.save() который обработает:
# - Списание из кошелька (если account_balance)
# - Обработку переплаты
# - Обновление amount_paid и payment_status
payment.save()
messages.success(
request,
f'Платеж на сумму {payment.amount} руб. '
f'({payment.payment_method.name}) успешно добавлен.'
)
except ValidationError as e:
messages.error(request, f'Ошибка при добавлении платежа: {e}')
else:
# Показываем ошибки валидации
for field, errors in form.errors.items():
for error in errors:
messages.error(request, f'{field}: {error}')
# Возвращаемся на страницу редактирования
return redirect('orders:order-update', order_number=order.order_number)
@login_required
@require_http_methods(["POST"])
def payment_delete(request, order_number, payment_id):
"""
Удаление платежа.
Возвращает средства в кошелек, если платеж был из кошелька.
"""
order = get_object_or_404(Order, order_number=order_number)
payment = get_object_or_404(Payment, pk=payment_id, order=order)
# Сохраняем данные для сообщения
payment_info = f'{payment.payment_method.name} на сумму {payment.amount} руб.'
# Если это платеж из кошелька - возвращаем средства
if payment.payment_method.code == 'account_balance':
from customers.services.wallet_service import WalletService
WalletService.refund_wallet_payment(order, payment.amount, request.user)
payment.delete()
# Пересчитываем сумму оплаты
order.amount_paid = sum(p.amount for p in order.payments.all())
order.update_payment_status()
messages.success(request, f'Платеж {payment_info} успешно удален.')
return redirect('orders:order-update', order_number=order.order_number)
# === AJAX ENDPOINTS ===
@require_http_methods(["POST"])