Рефакторинг: отдельные 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

@@ -571,44 +571,32 @@
<!-- Оплата (смешанная оплата) -->
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-credit-card"></i> Оплата</h5>
<button type="button" class="btn btn-sm btn-success" id="add-payment-btn">
<i class="bi bi-plus-circle"></i> Добавить платеж
</button>
</div>
<div class="card-body">
<!-- Блок кошелька клиента -->
{% if order.customer %}
<div class="alert alert-info d-flex justify-content-between align-items-center">
<div>
<strong><i class="bi bi-wallet2"></i> Кошелёк клиента:</strong>
{% if order.customer.wallet_balance > 0 %}
<span class="text-success fw-bold">{{ order.customer.wallet_balance|floatformat:2 }} руб.</span>
{% else %}
<span class="text-muted">0.00 руб.</span>
{% endif %}
<span class="ms-3">Остаток к оплате: <strong>{{ order.amount_due|floatformat:2 }} руб.</strong></span>
</div>
{% if order.customer.wallet_balance > 0 and order.amount_due > 0 %}
<div class="d-flex gap-2">
<button type="button" class="btn btn-primary btn-sm" id="apply-wallet-max-btn">
Учесть максимум
</button>
<div class="input-group" style="max-width: 280px;">
<input type="number" step="0.01" min="0" class="form-control form-control-sm" id="apply-wallet-amount-input" placeholder="Сумма из кошелька">
<button type="button" class="btn btn-outline-primary btn-sm" id="apply-wallet-amount-btn">Учесть сумму</button>
<div class="alert alert-info mb-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<strong><i class="bi bi-wallet2"></i> Кошелёк клиента:</strong>
{% if order.customer.wallet_balance > 0 %}
<span class="text-success fw-bold">{{ order.customer.wallet_balance|floatformat:2 }}&nbsp;руб.</span>
{% else %}
<span class="text-muted">0.00&nbsp;руб.</span>
{% endif %}
</div>
<div>
<strong>Остаток к оплате:</strong>
<span class="text-primary fw-bold">{{ order.amount_due|floatformat:2 }}&nbsp;руб.</span>
</div>
</div>
{% endif %}
</div>
{% endif %}
<!-- Скрытые поля для formset management -->
{{ payment_formset.management_form }}
<!-- Уже сохраненные платежи (информационно) -->
<!-- Уже сохраненные платежи -->
{% if order.pk and order.payments.exists %}
<div class="mb-4">
<h6 class="text-muted mb-3"><i class="bi bi-check-circle"></i> Проведенные платежи</h6>
@@ -628,13 +616,14 @@
<span class="text-muted">{{ payment.notes|default:"—" }}</span>
</div>
<div class="col-md-1 text-end">
<button type="button" class="btn btn-outline-danger btn-sm delete-existing-payment-btn"
data-payment-id="{{ payment.id }}"
data-payment-name="{{ payment.payment_method.name }}"
data-payment-amount="{{ payment.amount|floatformat:2 }}"
title="Удалить платеж">
<i class="bi bi-trash"></i>
</button>
<form method="post" action="{% url 'orders:payment-delete' order.order_number payment.id %}" style="display: inline;">
{% csrf_token %}
<button type="submit" class="btn btn-outline-danger btn-sm"
onclick="return confirm('Удалить платеж {{ payment.payment_method.name }} на сумму {{ payment.amount|floatformat:2 }} руб.?');"
title="Удалить платеж">
<i class="bi bi-trash"></i>
</button>
</form>
</div>
</div>
</div>
@@ -642,251 +631,64 @@
</div>
{% endif %}
<!-- Контейнер для НОВЫХ платежей -->
<div id="payments-container">
<!-- Здесь будут добавляться новые платежи -->
<!-- Форма добавления нового платежа -->
{% if order.pk %}
<div class="border rounded p-3 bg-white">
<h6 class="mb-3"><i class="bi bi-plus-circle"></i> Добавить новый платеж</h6>
<form method="post" action="{% url 'orders:payment-add' order.order_number %}" id="payment-add-form">
{% csrf_token %}
<div class="row align-items-end">
<div class="col-md-4">
<label class="form-label">Способ оплаты</label>
<select name="payment_method" class="form-select" required>
<option value="">---------</option>
{% load orders_tags %}
{% get_payment_methods as payment_methods %}
{% for pm in payment_methods %}
<option value="{{ pm.id }}">{{ pm.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-3">
<label class="form-label">Сумма</label>
<input type="number" name="amount" step="0.01" min="0.01" class="form-control" placeholder="0.00" required>
</div>
<div class="col-md-3">
<label class="form-label">Примечания</label>
<input type="text" name="notes" class="form-control" placeholder="Опционально">
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-success w-100">
<i class="bi bi-plus-lg"></i> Добавить
</button>
</div>
</div>
</form>
</div>
{% else %}
<div class="alert alert-warning mb-0">
<i class="bi bi-info-circle"></i> Сначала создайте заказ, затем вы сможете добавлять платежи.
</div>
{% endif %}
<!-- Итоговая сумма платежей -->
<div id="payments-total-section" class="border-top pt-3 mt-3 mb-3">
{% if order.pk %}
<div class="border-top pt-3 mt-3">
<div class="row align-items-center">
<div class="col">
<p class="mb-0 text-muted"><i class="bi bi-cash-stack"></i> Всего внесено:</p>
</div>
<div class="col-auto">
<h5 class="mb-0 text-success">
<span id="payments-total-value">{{ order.amount_paid|default:"0.00"|floatformat:2 }}</span>&nbsp;руб.
{{ order.amount_paid|default:"0.00"|floatformat:2 }}&nbsp;руб.
</h5>
</div>
</div>
</div>
<!-- Скрытый шаблон для новых платежей -->
<template id="empty-payment-form-template">
<div class="payment-form border rounded p-3 mb-3" data-form-index="__prefix__">
<input type="hidden" name="payments-__prefix__-id" id="id_payments-__prefix__-id" form="order-form">
<input type="checkbox" name="payments-__prefix__-DELETE" id="id_payments-__prefix__-DELETE" form="order-form" style="display: none;">
<div class="row align-items-end">
<div class="col-md-4">
<div class="mb-2">
<label class="form-label">Способ оплаты</label>
<select name="payments-__prefix__-payment_method" class="form-select" id="id_payments-__prefix__-payment_method" form="order-form">
<option value="">---------</option>
{% for pm in payment_formset.forms.0.fields.payment_method.queryset %}
<option value="{{ pm.id }}">{{ pm.name }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="col-md-3">
<div class="mb-2">
<label class="form-label">Сумма</label>
<input type="number" name="payments-__prefix__-amount" step="0.01" min="0" class="form-control" placeholder="Сумма платежа" id="id_payments-__prefix__-amount" form="order-form">
</div>
</div>
<div class="col-md-4">
<div class="mb-2">
<label class="form-label">Примечания</label>
<textarea name="payments-__prefix__-notes" class="form-control" rows="1" placeholder="Примечания к платежу (опционально)" id="id_payments-__prefix__-notes" form="order-form"></textarea>
</div>
</div>
<div class="col-md-1">
<button type="button" class="btn btn-danger btn-sm w-100 remove-payment-btn">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>
</template>
{% endif %}
</div>
</div>
<script>
(function() {
const walletBalance = parseFloat("{{ order.customer.wallet_balance|default:'0' }}".replace(',', '.')) || 0;
const amountDue = parseFloat("{{ order.amount_due|default:'0' }}".replace(',', '.')) || 0;
function addPaymentRow() {
const totalFormsInput = document.querySelector('input[name="payments-TOTAL_FORMS"]');
const idx = parseInt(totalFormsInput.value, 10);
const tpl = document.getElementById('empty-payment-form-template').innerHTML.replace(/__prefix__/g, idx);
const container = document.getElementById('payments-container');
const wrapper = document.createElement('div');
wrapper.innerHTML = tpl.trim();
container.appendChild(wrapper.firstElementChild);
totalFormsInput.value = String(idx + 1);
return container.querySelector(`.payment-form[data-form-index="${idx}"]`);
}
function selectAccountBalance(selectEl) {
if (!selectEl) return;
const options = Array.from(selectEl.options);
const target = options.find(o => o.textContent.trim() === 'С баланса счёта');
if (target) {
selectEl.value = target.value;
selectEl.dispatchEvent(new Event('change', { bubbles: true }));
}
}
function applyWallet(amount) {
if (!amount || amount <= 0) {
alert('Введите сумму больше 0.');
return;
}
if (amount > walletBalance) {
alert(`Недостаточно средств в кошельке. Доступно ${walletBalance.toFixed(2)} руб.`);
return;
}
if (amount > amountDue) {
alert(`Сумма превышает остаток к оплате (${amountDue.toFixed(2)} руб.).`);
return;
}
// Всегда добавляем новую строку платежа
const formEl = addPaymentRow();
const sel = formEl.querySelector('select[id^="id_payments-"][id$="-payment_method"]');
const amt = formEl.querySelector('input[id^="id_payments-"][id$="-amount"]');
const notes = formEl.querySelector('textarea[id^="id_payments-"][id$="-notes"]');
// Загружаем список способов оплаты
if (sel) {
fetch('/products/api/payment-methods/')
.then(response => response.json())
.then(data => {
sel.innerHTML = '<option value="">---------</option>';
data.forEach(method => {
const option = document.createElement('option');
option.value = method.id;
option.textContent = method.name;
sel.appendChild(option);
});
// После загрузки устанавливаем "С баланса счёта"
for (let i = 0; i < sel.options.length; i++) {
if (sel.options[i].textContent.trim() === 'С баланса счёта') {
sel.value = sel.options[i].value;
break;
}
}
})
.catch(error => {
console.error('Error loading payment methods:', error);
});
}
// Проставляем сумму
if (amt) {
const maxUsable = Math.min(walletBalance, amountDue);
const finalAmount = Math.min(amount, maxUsable);
amt.value = finalAmount.toFixed(2);
amt.setAttribute('max', maxUsable.toFixed(2));
}
// Небольшая подсказка в примечания
if (notes && !notes.value) {
notes.value = 'Оплата из кошелька';
}
// Добавляем обработчик удаления
const removeBtn = formEl.querySelector('.remove-payment-btn');
if (removeBtn) {
removeBtn.addEventListener('click', function() {
if (!confirm('Вы действительно хотите удалить этот платеж?')) {
return;
}
const deleteCheckbox = formEl.querySelector('input[name$="-DELETE"]');
const idField = formEl.querySelector('input[name$="-id"]');
if (idField && idField.value) {
deleteCheckbox.checked = true;
formEl.classList.add('deleted');
formEl.style.display = 'none';
} else {
formEl.remove();
}
});
}
}
// Обработчики кнопок применения кошелька
document.addEventListener('DOMContentLoaded', function() {
const applyMaxBtn = document.getElementById('apply-wallet-max-btn');
const applyAmountBtn = document.getElementById('apply-wallet-amount-btn');
const amountInput = document.getElementById('apply-wallet-amount-input');
if (applyMaxBtn) {
applyMaxBtn.addEventListener('click', function() {
const maxUsable = Math.min(walletBalance, amountDue);
applyWallet(maxUsable);
});
}
if (applyAmountBtn && amountInput) {
applyAmountBtn.addEventListener('click', function() {
const val = parseFloat((amountInput.value || '0').replace(',', '.')) || 0;
applyWallet(val);
});
}
});
// Автозаполнение при выборе "С баланса счёта"
document.getElementById('payments-container').addEventListener('change', function(e) {
const sel = e.target;
if (sel.tagName === 'SELECT') {
const label = sel.options[sel.selectedIndex]?.textContent?.trim() || '';
if (label === 'С баланса счёта') {
const wrap = sel.closest('.payment-form');
const amt = wrap.querySelector('input[id^="id_payments-"][id$="-amount"]');
if (amt) {
const maxUsable = Math.min(walletBalance, amountDue);
amt.value = maxUsable.toFixed(2);
amt.setAttribute('max', maxUsable.toFixed(2));
}
}
}
});
})();
</script>
<script>
// Обработчик удаления существующих платежей
document.addEventListener('DOMContentLoaded', function() {
const deleteButtons = document.querySelectorAll('.delete-existing-payment-btn');
deleteButtons.forEach(button => {
button.addEventListener('click', function() {
const paymentId = this.dataset.paymentId;
const paymentName = this.dataset.paymentName;
const paymentAmount = this.dataset.paymentAmount;
if (!confirm(`Удалить платеж "${paymentName}" на сумму ${paymentAmount} руб.?`)) {
return;
}
// Создаем скрытую форму для отправки
const form = document.createElement('form');
form.method = 'POST';
form.action = window.location.href;
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = 'csrfmiddlewaretoken';
csrfInput.value = csrfToken;
form.appendChild(csrfInput);
const paymentIdInput = document.createElement('input');
paymentIdInput.type = 'hidden';
paymentIdInput.name = 'delete_payment_id';
paymentIdInput.value = paymentId;
form.appendChild(paymentIdInput);
document.body.appendChild(form);
form.submit();
});
});
});
</script>
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-three-dots"></i> Дополнительно</h5>

View File

@@ -1 +1 @@
# Инициализация пакета templatetags
# Template tags package

View File

@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
from django import template
from orders.models import PaymentMethod
register = template.Library()
@register.simple_tag
def get_payment_methods():
"""
Получить список активных способов оплаты.
Использование: {% get_payment_methods as payment_methods %}
"""
return PaymentMethod.objects.filter(is_active=True).order_by('order', 'name')

View File

@@ -11,6 +11,10 @@ urlpatterns = [
path('<int:order_number>/edit/', views.order_update, name='order-update'),
path('<int:order_number>/delete/', views.order_delete, name='order-delete'),
# Payment Management
path('<int:order_number>/payments/add/', views.payment_add, name='payment-add'),
path('<int:order_number>/payments/<int:payment_id>/delete/', views.payment_delete, name='payment-delete'),
# AJAX endpoints
path('api/customer-address-history/', views.get_customer_address_history, name='api-customer-address-history'),

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"])