handle payment provider (de-)selection and partial payments

This commit is contained in:
Lukas Bockstaller
2026-09-07 11:56:53 +02:00
parent 1df04d2131
commit 7f6d645a8b
4 changed files with 69 additions and 5 deletions
+1
View File
@@ -1605,6 +1605,7 @@ def add_payment_to_cart_session(cart_session, provider, min_value: Decimal=None,
'max_value': str(max_value) if max_value is not None else None,
'info_data': info_data or {},
})
cart_session['payments_postpone'] = False
def add_payment_to_cart(request, provider, min_value: Decimal=None, max_value: Decimal=None, info_data: dict=None):
+5 -3
View File
@@ -1440,10 +1440,12 @@ class PaymentStep(CartMixin, TemplateFlowStep):
ctx['providers'] = self.provider_forms
ctx['show_fees'] = any(p['fee'] for p in self.provider_forms)
if len(self.provider_forms) == 1:
ctx['selected'] = self.provider_forms[0]['provider'].identifier
elif 'payment' in self.request.POST:
if 'payment' in self.request.POST:
ctx['selected'] = self.request.POST['payment']
elif self.cart_session.get('payments_postpone') and self._allow_postpone:
ctx['selected'] = ''
elif len(self.provider_forms) == 1:
ctx['selected'] = self.provider_forms[0]['provider'].identifier
elif self.single_use_payment:
ctx['selected'] = self.single_use_payment['provider']
else:
@@ -135,7 +135,9 @@
{% trans "Not sure yet? You can complete your order first and then select a payment method later." %}
<br>
<span class="text-muted">
{% if payment_deadline_minutes %}
{% if current_payments %}
{% trans "To do so, please first remove the payment methods you already selected above." %}
{% elif payment_deadline_minutes %}
{% blocktrans trimmed with minutes=payment_deadline_minutes %}
Your payment needs to be completed within {{ minutes }} minutes.
{% endblocktrans %}
@@ -147,7 +149,8 @@
</span>
</div>
<div class="col-md-3 col-xs-12 text-right flip">
<button name="postpone" value="on" class="btn btn-primary">
<button name="postpone" value="on" class="btn btn-primary"
{% if current_payments %}disabled{% endif %}>
{% trans "Proceed without selection" %}
</button>
</div>
+58
View File
@@ -2405,6 +2405,64 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
o = Order.objects.last()
assert not o.payments.exists()
def test_payment_postpone_cleared_on_selection(self):
self.event.settings.set('payment_banktransfer__enabled', True)
self.event.settings.payment_choice_postpone_allowed_channels = ['web']
with scopes_disabled():
CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
response = self.client.post('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), {
'postpone': 'on',
}, follow=False)
self.assertRedirects(response, '/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug),
target_status_code=200)
assert self.client.session['carts'][self.session_key].get('payments_postpone')
# The only available provider must not be preselected while the choice is postponed
response = self.client.get('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertEqual(len(doc.select('input[name="payment"]')), 1)
self.assertEqual(len(doc.select('input[name="payment"][checked]')), 0)
# Selecting a payment method takes the order out of the postponed state again
response = self.client.post('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), {
'payment': 'banktransfer',
}, follow=False)
self.assertRedirects(response, '/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug),
target_status_code=200)
assert not self.client.session['carts'][self.session_key].get('payments_postpone')
def test_payment_postpone_disabled_with_partial_payment(self):
self.event.settings.set('payment_banktransfer__enabled', True)
self.event.settings.payment_choice_postpone_allowed_channels = ['web']
gc = self.orga.issued_gift_cards.create(currency="EUR")
gc.transactions.create(value=20, acceptor=self.orga)
with scopes_disabled():
CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
response = self.client.get('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertEqual(len(doc.select('button[name="postpone"]')), 1)
self.assertEqual(len(doc.select('button[name="postpone"][disabled]')), 0)
# Apply a gift card that only covers part of the total
response = self.client.post('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), {
'payment': 'giftcard',
'payment_giftcard-code': gc.secret,
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
target_status_code=200)
# Postponing would silently drop the gift card, so it is no longer offered
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertEqual(len(doc.select('button[name="postpone"][disabled]')), 1)
def test_premature_confirm(self):
response = self.client.get('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
self.assertRedirects(response, '/%s/%s/?require_cookie=true' % (self.orga.slug, self.event.slug),