Защита от переплаты кошельком и улучшение отображения транзакций
Изменения в UI (order_form.html): - Добавлен data-code к опциям способов оплаты для идентификации метода кошелька - ID для селекта способа оплаты (payment-method-select) и поля суммы (payment-amount-input) - Динамическое ограничение max на поле суммы платежа при выборе кошелька - Подсказка 'Макс: X руб.' отображается только для оплаты кошельком - Для внешних методов (карта, наличные) ограничение отсутствует — переплата допустима Логика JS: - При выборе метода с code == 'account_balance' устанавливается max = order.amount_due - Для остальных методов max удаляется — оператор может внести сумму больше остатка - Переплата по внешним методам корректно зачисляется в кошелёк через WalletService.add_overpayment Серверная защита (transaction_service.py): - В TransactionService.create_payment добавлена проверка: если payment_method.code == 'account_balance' и amount > order.amount_due — ValidationError - Сообщение: 'Сумма оплаты из кошелька (X руб.) не может превышать остаток к оплате (Y руб.)' - Защита от обхода UI через API или прямой вызов Улучшение отображения (order_form.html, order_detail.html): - Для возврата в кошелёк (transaction_type == 'refund' и code == 'account_balance') показываем 'на баланс счёта' вместо названия метода - История становится понятнее: '−750,00 руб. Возврат 29.11.2025 на баланс счёта' Сценарий: - Кошелёк клиента 500 руб., заказ 65 руб. - Оператор выбирает оплату из кошелька — поле суммы ограничено 65 руб. - Попытка ввести 500 заблокирована UI и серверной валидацией - Для внешней оплаты (карта онлайн) можно внести 500 руб. — остаток 435 автоматически зачислится в кошелёк как переплата Цель: - Исключить путаницу в истории транзакций при оплате кошельком - Разграничить поведение: кошелёк строго ограничен, внешние методы допускают переплату - Обеспечить прозрачность движения средств для операторов
This commit is contained in:
0
myproject/orders/models/payment.py
Normal file
0
myproject/orders/models/payment.py
Normal file
@@ -54,6 +54,13 @@ class TransactionService:
|
|||||||
except PaymentMethod.DoesNotExist:
|
except PaymentMethod.DoesNotExist:
|
||||||
raise ValueError(f'Способ оплаты "{payment_method}" не найден')
|
raise ValueError(f'Способ оплаты "{payment_method}" не найден')
|
||||||
|
|
||||||
|
# Ограничение для кошелька: нельзя оплатить больше чем к оплате
|
||||||
|
if payment_method.code == 'account_balance' and amount > order.amount_due:
|
||||||
|
raise ValidationError(
|
||||||
|
f'Сумма оплаты из кошелька ({amount} руб.) не может превышать '
|
||||||
|
f'остаток к оплате ({order.amount_due} руб.)'
|
||||||
|
)
|
||||||
|
|
||||||
# Создаём транзакцию
|
# Создаём транзакцию
|
||||||
txn = Transaction.objects.create(
|
txn = Transaction.objects.create(
|
||||||
order=order,
|
order=order,
|
||||||
|
|||||||
@@ -358,7 +358,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
{{ transaction.transaction_date|date:"d.m.Y H:i" }}<br>
|
{{ transaction.transaction_date|date:"d.m.Y H:i" }}<br>
|
||||||
|
{% if transaction.transaction_type == 'refund' and transaction.payment_method.code == 'account_balance' %}
|
||||||
|
Возврат на баланс счёта
|
||||||
|
{% else %}
|
||||||
{{ transaction.payment_method.name }}
|
{{ transaction.payment_method.name }}
|
||||||
|
{% endif %}
|
||||||
{% if transaction.notes or transaction.reason %}
|
{% if transaction.notes or transaction.reason %}
|
||||||
<br><em>{{ transaction.notes|default:transaction.reason }}</em>
|
<br><em>{{ transaction.notes|default:transaction.reason }}</em>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -671,7 +671,13 @@
|
|||||||
{{ transaction.transaction_date|date:"d.m.Y H:i" }}
|
{{ transaction.transaction_date|date:"d.m.Y H:i" }}
|
||||||
</small>
|
</small>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ transaction.payment_method.name }}</td>
|
<td>
|
||||||
|
{% if transaction.transaction_type == 'refund' and transaction.payment_method.code == 'account_balance' %}
|
||||||
|
Возврат на баланс счёта
|
||||||
|
{% else %}
|
||||||
|
{{ transaction.payment_method.name }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
<strong class="{% if transaction.transaction_type == 'payment' %}text-success{% else %}text-danger{% endif %}">
|
<strong class="{% if transaction.transaction_type == 'payment' %}text-success{% else %}text-danger{% endif %}">
|
||||||
{% if transaction.transaction_type == 'refund' %}−{% else %}+{% endif %}{{ transaction.amount|floatformat:2 }}
|
{% if transaction.transaction_type == 'refund' %}−{% else %}+{% endif %}{{ transaction.amount|floatformat:2 }}
|
||||||
@@ -727,12 +733,12 @@
|
|||||||
<div class="row align-items-end">
|
<div class="row align-items-end">
|
||||||
<div class="col-12 mb-2">
|
<div class="col-12 mb-2">
|
||||||
<label class="form-label fw-bold">Способ оплаты <span class="text-danger">*</span></label>
|
<label class="form-label fw-bold">Способ оплаты <span class="text-danger">*</span></label>
|
||||||
<select name="payment_method" class="form-select" required>
|
<select name="payment_method" class="form-select" id="payment-method-select" required>
|
||||||
<option value="">Выберите способ...</option>
|
<option value="">Выберите способ...</option>
|
||||||
{% load orders_tags %}
|
{% load orders_tags %}
|
||||||
{% get_payment_methods as payment_methods %}
|
{% get_payment_methods as payment_methods %}
|
||||||
{% for pm in payment_methods %}
|
{% for pm in payment_methods %}
|
||||||
<option value="{{ pm.id }}">{{ pm.name }}{% if pm.code == 'account_balance' %} (кошелёк клиента){% endif %}</option>
|
<option value="{{ pm.id }}" data-code="{{ pm.code }}">{{ pm.name }}{% if pm.code == 'account_balance' %} (кошелёк клиента){% endif %}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -740,10 +746,11 @@
|
|||||||
<label class="form-label fw-bold">Сумма <span class="text-danger">*</span></label>
|
<label class="form-label fw-bold">Сумма <span class="text-danger">*</span></label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="number" name="amount" step="0.01" min="0.01"
|
<input type="number" name="amount" step="0.01" min="0.01"
|
||||||
class="form-control" placeholder="0.00"
|
class="form-control" placeholder="0.00" id="payment-amount-input"
|
||||||
{% if order.amount_due > 0 %}value="{{ order.amount_due|unlocalize }}"{% endif %} required>
|
{% if order.amount_due > 0 %}value="{{ order.amount_due|unlocalize }}"{% endif %} required>
|
||||||
<span class="input-group-text">руб.</span>
|
<span class="input-group-text">руб.</span>
|
||||||
</div>
|
</div>
|
||||||
|
<small class="text-muted" id="payment-limit-hint" style="display:none;">Макс: <span id="payment-max-value"></span> руб.</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 mb-2">
|
<div class="col-md-6 mb-2">
|
||||||
<label class="form-label fw-bold">Остаток</label>
|
<label class="form-label fw-bold">Остаток</label>
|
||||||
@@ -887,6 +894,36 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Ограничение оплаты из кошелька
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const paymentMethodSelect = document.getElementById('payment-method-select');
|
||||||
|
const paymentAmountInput = document.getElementById('payment-amount-input');
|
||||||
|
const paymentLimitHint = document.getElementById('payment-limit-hint');
|
||||||
|
const paymentMaxValue = document.getElementById('payment-max-value');
|
||||||
|
|
||||||
|
if (paymentMethodSelect && paymentAmountInput) {
|
||||||
|
const amountDue = {{ order.amount_due|default:0|unlocalize }};
|
||||||
|
|
||||||
|
paymentMethodSelect.addEventListener('change', function() {
|
||||||
|
const selectedOption = this.options[this.selectedIndex];
|
||||||
|
const paymentCode = selectedOption ? selectedOption.getAttribute('data-code') : null;
|
||||||
|
|
||||||
|
if (paymentCode === 'account_balance') {
|
||||||
|
// Кошелёк: ограничиваем максимумом
|
||||||
|
paymentAmountInput.setAttribute('max', amountDue);
|
||||||
|
paymentMaxValue.textContent = amountDue.toFixed(2);
|
||||||
|
paymentLimitHint.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
// Внешние методы: снимаем ограничение
|
||||||
|
paymentAmountInput.removeAttribute('max');
|
||||||
|
paymentLimitHint.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Глобально определяем initOrderItemSelect2
|
// Глобально определяем initOrderItemSelect2
|
||||||
window.initOrderItemSelect2 = function(element) {
|
window.initOrderItemSelect2 = function(element) {
|
||||||
|
|||||||
Reference in New Issue
Block a user