Support for external gift cards (#2912)

This commit is contained in:
Raphael Michel
2022-11-23 14:52:56 +01:00
committed by GitHub
parent d3589696d7
commit 9624b1c505
24 changed files with 1521 additions and 523 deletions

View File

@@ -33,12 +33,13 @@
# License for the specific language governing permissions and limitations under the License.
import copy
import inspect
import uuid
from collections import defaultdict
from decimal import Decimal
from django.conf import settings
from django.contrib import messages
from django.core.exceptions import ValidationError
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.core.signing import BadSignature, loads
from django.core.validators import EmailValidator
from django.db.models import F, Q
@@ -56,12 +57,14 @@ from pretix.base.models import Customer, Membership, Order
from pretix.base.models.orders import InvoiceAddress, OrderPayment
from pretix.base.models.tax import TaxedPrice, TaxRule
from pretix.base.services.cart import (
CartError, CartManager, error_messages, get_fees, set_cart_addons,
CartError, CartManager, add_payment_to_cart, error_messages, get_fees,
set_cart_addons,
)
from pretix.base.services.memberships import validate_memberships_in_order
from pretix.base.services.orders import perform_order
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import validate_cart_addons
from pretix.base.templatetags.money import money_filter
from pretix.base.templatetags.phone_format import phone_format
from pretix.base.templatetags.rich_text import rich_text_snippet
from pretix.base.views.tasks import AsyncAction
@@ -1144,53 +1147,153 @@ class PaymentStep(CartMixin, TemplateFlowStep):
})
return providers
@cached_property
def single_use_payment(self):
singleton_payments = [p for p in self.cart_session.get('payments', []) if not p.get('multi_use_supported')]
if not singleton_payments:
return None
return singleton_payments[0]
def current_payments_valid(self, amount):
singleton_payments = [p for p in self.cart_session.get('payments', []) if not p.get('multi_use_supported')]
if len(singleton_payments) > 1:
return False
matched = Decimal('0.00')
for p in self.cart_session.get('payments', []):
if p.get('min_value') and (amount - matched) < Decimal(p['min_value']):
continue
if p.get('max_value') and (amount - matched) > Decimal(p['max_value']):
matched += Decimal(p['max_value'])
else:
matched = Decimal('0.00')
return matched == Decimal('0.00'), amount - matched
def post(self, request):
self.request = request
if "remove_payment" in request.POST:
self._remove_payment(request.POST["remove_payment"])
return redirect(self.get_step_url(request))
for p in self.provider_forms:
if p['provider'].identifier == request.POST.get('payment', ''):
self.cart_session['payment'] = p['provider'].identifier
resp = p['provider'].checkout_prepare(
pprov = p['provider']
if pprov.identifier == request.POST.get('payment', ''):
if not pprov.multi_use_supported:
# Providers with multi_use_supported will call this themselves
simulated_payments = self.cart_session.get('payments', {})
simulated_payments = [p for p in simulated_payments if p.get('multi_use_supported')]
simulated_payments.append({
'provider': pprov.identifier,
'multi_use_supported': False,
'min_value': None,
'max_value': None,
'info_data': {},
})
cart = self.get_cart(payments=simulated_payments)
else:
cart = self.get_cart()
resp = pprov.checkout_prepare(
request,
self.get_cart()
cart,
)
if isinstance(resp, str):
return redirect(resp)
elif resp is True:
return redirect(self.get_next_url(request))
if pprov.multi_use_supported:
# Provider needs to call add_payment_to_cart itself, but we need to remove all previously
# selected ones that don't have multi_use supported. Otherwise, if you first select a credit
# card, then go back and switch to a gift card, you'll have both in the session and the credit
# card has preference, which is unexpected.
self.cart_session['payments'] = [p for p in self.cart_session.get('payments', []) if p.get('multi_use_supported')]
if pprov.identifier not in [p['provider'] for p in self.cart_session.get('payments', [])]:
raise ImproperlyConfigured(f'Payment provider {pprov.identifier} set multi_use_supported '
f'and returned True from payment_prepare, but did not call '
f'add_payment_to_cart')
valid, remainder = self.current_payments_valid(cart['total'])
if valid:
return redirect(self.get_next_url(request))
else:
# Show payment step again to select another method
messages.success(
request,
_("Your payment method has been applied, but {} still need to be paid. Please select "
"a payment method for the remainder.").format(
money_filter(remainder, self.event.currency)
)
)
return redirect(self.get_step_url(request))
else:
# There can only be one payment method that does not have multi_use_supported, remove all
# previous ones.
self.cart_session['payments'] = [p for p in self.cart_session.get('payments', []) if p.get('multi_use_supported')]
add_payment_to_cart(request, pprov, None, None, None)
return redirect(self.get_next_url(request))
else:
return self.render()
if self.is_completed(request, warn=False):
# All payments already accounted for, no need to select one
return redirect(self.get_next_url(request))
messages.error(self.request, _("Please select a payment method."))
return self.render()
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['current_payments'] = [p for p in self.current_selected_payments(self._total_order_value) if p.get('multi_use_supported')]
ctx['remaining'] = self._total_order_value - sum(p['payment_amount'] for p in ctx['current_payments']) + sum(p['fee'] for p in ctx['current_payments'])
ctx['providers'] = self.provider_forms
ctx['show_fees'] = any(p['fee'] for p in self.provider_forms)
ctx['selected'] = self.request.POST.get('payment', self.cart_session.get('payment', ''))
if len(self.provider_forms) == 1:
ctx['selected'] = self.provider_forms[0]['provider'].identifier
elif 'payment' in self.request.POST:
ctx['selected'] = self.request.POST['payment']
elif self.single_use_payment:
ctx['selected'] = self.single_use_payment['provider']
else:
ctx['selected'] = ''
ctx['cart'] = self.get_cart()
return ctx
@cached_property
def payment_provider(self):
return self.request.event.get_payment_providers().get(self.cart_session['payment'])
def _is_allowed(self, prov, request):
return prov.is_allowed(request, total=self._total_order_value)
def is_completed(self, request, warn=False):
self.request = request
if 'payment' not in self.cart_session or not self.payment_provider:
if not self.cart_session.get('payments'):
if warn:
messages.error(request, _('The payment information you entered was incomplete.'))
messages.error(request, _('Please select a payment method to proceed.'))
return False
if not self.payment_provider.payment_is_valid_session(request) or \
not self.payment_provider.is_enabled or \
not self._is_allowed(self.payment_provider, request):
cart = get_cart(self.request)
total = get_cart_total(self.request)
total += sum([f.value for f in get_fees(self.request.event, self.request, total, self.invoice_address,
self.cart_session.get('payments', []), cart)])
selected = self.current_selected_payments(total, warn=warn, total_includes_payment_fees=True)
if sum(p['payment_amount'] for p in selected) != total:
if warn:
messages.error(request, _('The payment information you entered was incomplete.'))
messages.error(request, _('Please select a payment method to proceed.'))
return False
if len([p for p in selected if not p['multi_use_supported']]) > 1:
raise ImproperlyConfigured('Multiple non-multi-use providers in session, should never happen')
for p in selected:
if not p['pprov'] or not p['pprov'].is_enabled or not self._is_allowed(p['pprov'], request):
self._remove_payment(p['id'])
if p['payment_amount']:
if warn:
messages.error(request, _('Please select a payment method to proceed.'))
return False
if not p['multi_use_supported'] and not p['pprov'].payment_is_valid_session(request):
if warn:
messages.error(request, _('The payment information you entered was incomplete.'))
return False
return True
def is_applicable(self, request):
@@ -1198,18 +1301,28 @@ class PaymentStep(CartMixin, TemplateFlowStep):
for cartpos in get_cart(self.request):
if cartpos.requires_approval(invoice_address=self.invoice_address):
if 'payment' in self.cart_session:
del self.cart_session['payment']
if 'payments' in self.cart_session:
del self.cart_session['payments']
return False
used_providers = {p['provider'] for p in self.cart_session.get('payments', [])}
for p in self.request.event.get_payment_providers().values():
if p.is_implicit(request) if callable(p.is_implicit) else p.is_implicit:
if self._is_allowed(p, request):
self.cart_session['payment'] = p.identifier
self.cart_session['payments'] = [
{
'id': str(uuid.uuid4()),
'provider': p.identifier,
'multi_use_supported': False,
'min_value': None,
'max_value': None,
'info_data': {},
}
]
return False
elif self.cart_session.get('payment') == p.identifier:
elif p.identifier in used_providers:
# is_allowed might have changed, e.g. after add-on selection
del self.cart_session['payment']
self.cart_session['payments'] = [p for p in self.cart_session['payments'] if p['provider'] != p.identifier]
return True
@@ -1239,9 +1352,16 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep):
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['cart'] = self.get_cart(answers=True)
if self.payment_provider:
ctx['payment'] = self.payment_provider.checkout_confirm_render(self.request)
ctx['payment_provider'] = self.payment_provider
selected_payments = self.current_selected_payments(ctx['cart']['total'], total_includes_payment_fees=True)
ctx['payments'] = []
for p in selected_payments:
if 'info_data' in inspect.signature(p['pprov'].checkout_confirm_render).parameters:
block = p['pprov'].checkout_confirm_render(self.request, info_data=p['info_data'])
else:
block = p['pprov'].checkout_confirm_render(self.request)
ctx['payments'].append((p, block))
ctx['require_approval'] = any(cp.requires_approval(invoice_address=self.invoice_address) for cp in ctx['cart']['positions'])
ctx['addr'] = self.invoice_address
ctx['confirm_messages'] = self.confirm_messages
@@ -1326,23 +1446,27 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep):
return self.do(
self.request.event.id,
payment_provider=self.payment_provider.identifier if self.payment_provider else None,
payments=self.cart_session.get('payments', []),
positions=[p.id for p in self.positions],
email=self.cart_session.get('email'),
locale=translation.get_language(),
address=self.invoice_address.pk,
meta_info=meta_info,
sales_channel=request.sales_channel.identifier,
gift_cards=self.cart_session.get('gift_cards'),
shown_total=self.cart_session.get('shown_total'),
customer=self.cart_session.get('customer'),
)
def get_success_message(self, value):
create_empty_cart_id(self.request)
if isinstance(value, dict):
for w in value.get('warnings', []):
messages.warning(self.request, w)
return None
def get_success_url(self, value):
if isinstance(value, dict):
value = value['order_id']
order = Order.objects.get(id=value)
return self.get_order_url(order)