Payment and confirm

This commit is contained in:
Raphael Michel
2025-01-05 13:51:58 +01:00
parent 8f13c03245
commit ab4ecd0b6b
8 changed files with 491 additions and 131 deletions
+26
View File
@@ -722,6 +722,10 @@ class BasePaymentProvider:
"""
return ""
def storefrontapi_prepare(self, session_data, total, info):
# TODO: docstring
return True
def checkout_prepare(self, request: HttpRequest, cart: Dict[str, Any]) -> Union[bool, str]:
"""
Will be called after the user selects this provider as their payment method.
@@ -1447,6 +1451,28 @@ class GiftCardPayment(BasePaymentProvider):
}
)
def storefrontapi_prepare(self, session_data, total, info):
# todo: validate gift card not paid with gift card
try:
gc = self.event.organizer.accepted_gift_cards.get(
secret=info.get("giftcard").strip()
)
try:
self._add_giftcard_to_cart(session_data, gc)
return True
except ValidationError as e:
raise PaymentException(str(e.message))
except GiftCard.DoesNotExist:
if self.event.vouchers.filter(code__iexact=info.get("giftcard")).exists():
raise PaymentException(
_("You entered a voucher instead of a gift card. Vouchers can only be entered on the first page of the shop below "
"the product selection.")
)
else:
raise PaymentException(_("This gift card is not known."))
except GiftCard.MultipleObjectsReturned:
raise PaymentException(_("This gift card can not be redeemed since its code is not unique. Please contact the organizer of this event."))
def checkout_prepare(self, request: HttpRequest, cart: Dict[str, Any]) -> Union[bool, str, None]:
for p in get_cart(request):
if p.item.issue_giftcard:
+132
View File
@@ -0,0 +1,132 @@
import copy
import uuid
from decimal import Decimal
from django.core.exceptions import ImproperlyConfigured
from django.utils.translation import gettext as _
from pretix.base.storelogic import IncompleteError
from pretix.base.templatetags.money import money_filter
def payment_is_applicable(event, total, cart_positions, invoice_address, cart_session, request):
for cartpos in cart_positions:
if cartpos.requires_approval(invoice_address=invoice_address):
if 'payments' in cart_session:
del cart_session['payments']
return False
used_providers = {p['provider'] for p in cart_session.get('payments', [])}
for provider in event.get_payment_providers().values():
if provider.is_implicit(request) if callable(provider.is_implicit) else provider.is_implicit:
# TODO: do we need a different is_allowed for storefrontapi?
if provider.is_allowed(request, total=total):
cart_session['payments'] = [
{
'id': str(uuid.uuid4()),
'provider': provider.identifier,
'multi_use_supported': False,
'min_value': None,
'max_value': None,
'info_data': {},
}
]
return False
elif provider.identifier in used_providers:
# is_allowed might have changed, e.g. after add-on selection
cart_session['payments'] = [p for p in cart_session['payments'] if
p['provider'] != provider.identifier]
return True
def current_selected_payments(event, total, cart_session, total_includes_payment_fees=False, fail=False):
def _remove_payment(payment_id):
cart_session['payments'] = [p for p in cart_session['payments'] if p.get('id') != payment_id]
raw_payments = copy.deepcopy(cart_session.get('payments', []))
payments = []
total_remaining = total
for p in raw_payments:
# This algorithm of treating min/max values and fees needs to stay in sync between the following
# places in the code base:
# - pretix.base.services.cart.get_fees
# - pretix.base.services.orders._get_fees
# - pretix.presale.storelogic.payment.current_selected_payments
if p.get('min_value') and total_remaining < Decimal(p['min_value']):
_remove_payment(p['id'])
if fail:
raise IncompleteError(
_('Your selected payment method can only be used for a payment of at least {amount}.').format(
amount=money_filter(Decimal(p['min_value']), event.currency)
)
)
continue
to_pay = total_remaining
if p.get('max_value') and to_pay > Decimal(p['max_value']):
to_pay = min(to_pay, Decimal(p['max_value']))
pprov = event.get_payment_providers(cached=True).get(p['provider'])
if not pprov:
_remove_payment(p['id'])
continue
if not total_includes_payment_fees:
fee = pprov.calculate_fee(to_pay)
total_remaining += fee
to_pay += fee
else:
fee = Decimal('0.00')
if p.get('max_value') and to_pay > Decimal(p['max_value']):
to_pay = min(to_pay, Decimal(p['max_value']))
p['payment_amount'] = to_pay
p['provider_name'] = pprov.public_name
p['pprov'] = pprov
p['fee'] = fee
total_remaining -= to_pay
payments.append(p)
return payments
def ensure_payment_is_completed(event, total, cart_session, request):
def _remove_payment(payment_id):
cart_session['payments'] = [p for p in cart_session['payments'] if p.get('id') != payment_id]
if not cart_session.get('payments'):
raise IncompleteError(_('Please select a payment method to proceed.'))
selected = current_selected_payments(event, total, cart_session, fail=True, total_includes_payment_fees=True)
if sum(p['payment_amount'] for p in selected) != total:
raise IncompleteError(_('Please select a payment method to proceed.'))
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:
# TODO: do we need a different is_allowed for storefrontapi?
if not p['pprov'] or not p['pprov'].is_enabled or not p['pprov'].is_allowed(request, total=total):
_remove_payment(p['id'])
if p['payment_amount']:
raise IncompleteError(_('Please select a payment method to proceed.'))
if not p['multi_use_supported'] and not p['pprov'].payment_is_valid_session(request):
raise IncompleteError(_('The payment information you entered was incomplete.'))
def current_payments_valid(cart_session, amount):
singleton_payments = [p for p in 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 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
@@ -360,6 +360,9 @@ class BankTransfer(BasePaymentProvider):
}
return template.render(ctx)
def storefrontapi_prepare(self, session_data, total, info):
return True
def checkout_prepare(self, request, total):
form = self.payment_form(request)
if form.is_valid():
+26 -63
View File
@@ -33,7 +33,6 @@
# License for the specific language governing permissions and limitations under the License.
import copy
import inspect
import uuid
from decimal import Decimal
from django.conf import settings
@@ -75,6 +74,9 @@ from pretix.base.storelogic.addons import (
addons_is_applicable, addons_is_completed, get_addon_groups,
)
from pretix.base.storelogic.fields import ensure_fields_are_completed
from pretix.base.storelogic.payment import (
current_payments_valid, ensure_payment_is_completed, payment_is_applicable,
)
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
@@ -908,14 +910,15 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
return redirect_to_url(self.get_next_url(request))
def is_completed(self, request, warn=False):
self.request = request
try:
ensure_fields_are_completed(
self.request.event,
self.event,
self._positions_for_questions,
self.cart_session,
self.invoice_address,
self.all_optional,
get_cart_is_free(self.request),
get_cart_is_free(request),
)
except IncompleteError as e:
if warn:
@@ -1121,20 +1124,7 @@ class PaymentStep(CartMixin, TemplateFlowStep):
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
return current_payments_valid(self.cart_session, amount)
def post(self, request):
self.request = request
@@ -1238,6 +1228,7 @@ class PaymentStep(CartMixin, TemplateFlowStep):
def is_completed(self, request, warn=False):
if not self.cart_session.get('payments'):
# Is also in ensure_payment_is_completed, but saves us performance of cart evaluation
if warn:
messages.error(request, _('Please select a payment method to proceed.'))
return False
@@ -1250,58 +1241,30 @@ class PaymentStep(CartMixin, TemplateFlowStep):
except TaxRule.SaleNotAllowed:
# ignore for now, will fail on order creation
pass
selected = self.current_selected_payments(total, warn=warn, total_includes_payment_fees=True)
if sum(p['payment_amount'] for p in selected) != total:
try:
ensure_payment_is_completed(
self.event,
total,
self.cart_session,
self.request,
)
except IncompleteError as e:
if warn:
messages.error(request, _('Please select a payment method to proceed.'))
messages.warning(self.request, str(e))
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):
self.request = request
for cartpos in get_cart(self.request):
if cartpos.requires_approval(invoice_address=self.invoice_address):
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 provider in self.request.event.get_payment_providers().values():
if provider.is_implicit(request) if callable(provider.is_implicit) else provider.is_implicit:
if self._is_allowed(provider, request):
self.cart_session['payments'] = [
{
'id': str(uuid.uuid4()),
'provider': provider.identifier,
'multi_use_supported': False,
'min_value': None,
'max_value': None,
'info_data': {},
}
]
return False
elif provider.identifier in used_providers:
# is_allowed might have changed, e.g. after add-on selection
self.cart_session['payments'] = [p for p in self.cart_session['payments'] if p['provider'] != provider.identifier]
return True
return payment_is_applicable(
self.event,
self._total_order_value,
get_cart(request),
self.invoice_address,
self.cart_session,
request,
)
def get(self, request):
self.request.pci_dss_payment_page = True
+12 -49
View File
@@ -31,7 +31,6 @@
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
import copy
from collections import defaultdict
from datetime import datetime, timedelta
from decimal import Decimal
@@ -44,7 +43,6 @@ from django.db.models import Exists, OuterRef, Prefetch, Sum
from django.utils import translation
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django_scopes import scopes_disabled
from pretix.base.i18n import get_language_without_region
@@ -54,7 +52,8 @@ from pretix.base.models import (
QuestionAnswer, QuestionOption, TaxRule,
)
from pretix.base.services.cart import get_fees
from pretix.base.templatetags.money import money_filter
from pretix.base.storelogic import IncompleteError
from pretix.base.storelogic.payment import current_selected_payments
from pretix.helpers.cookies import set_cookie_without_samesite
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.signals import question_form_fields
@@ -256,52 +255,16 @@ class CartMixin:
}
def current_selected_payments(self, total, warn=False, total_includes_payment_fees=False):
raw_payments = copy.deepcopy(self.cart_session.get('payments', []))
payments = []
total_remaining = total
for p in raw_payments:
# This algorithm of treating min/max values and fees needs to stay in sync between the following
# places in the code base:
# - pretix.base.services.cart.get_fees
# - pretix.base.services.orders._get_fees
# - pretix.presale.views.CartMixin.current_selected_payments
if p.get('min_value') and total_remaining < Decimal(p['min_value']):
if warn:
messages.warning(
self.request,
_('Your selected payment method can only be used for a payment of at least {amount}.').format(
amount=money_filter(Decimal(p['min_value']), self.request.event.currency)
)
)
self._remove_payment(p['id'])
continue
to_pay = total_remaining
if p.get('max_value') and to_pay > Decimal(p['max_value']):
to_pay = min(to_pay, Decimal(p['max_value']))
pprov = self.request.event.get_payment_providers(cached=True).get(p['provider'])
if not pprov:
self._remove_payment(p['id'])
continue
if not total_includes_payment_fees:
fee = pprov.calculate_fee(to_pay)
total_remaining += fee
to_pay += fee
else:
fee = Decimal('0.00')
if p.get('max_value') and to_pay > Decimal(p['max_value']):
to_pay = min(to_pay, Decimal(p['max_value']))
p['payment_amount'] = to_pay
p['provider_name'] = pprov.public_name
p['pprov'] = pprov
p['fee'] = fee
total_remaining -= to_pay
payments.append(p)
return payments
try:
return current_selected_payments(
self.request.event,
total,
self.cart_session,
total_includes_payment_fees=total_includes_payment_fees,
fail=warn
)
except IncompleteError as e:
messages.warning(self.request, str(e))
def _remove_payment(self, payment_id):
self.cart_session['payments'] = [p for p in self.cart_session['payments'] if p.get('id') != payment_id]
+237 -15
View File
@@ -1,7 +1,7 @@
import logging
from celery.result import AsyncResult
from django.core.exceptions import ValidationError
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.utils import translation
from django.utils.translation import gettext as _
from rest_framework import serializers, status, viewsets
@@ -13,13 +13,19 @@ from rest_framework.reverse import reverse
from pretix.base.models import Item, ItemVariation, SubEvent, TaxRule
from pretix.base.models.orders import CartPosition, CheckoutSession, OrderFee
from pretix.base.services.cart import (
add_items_to_cart, error_messages, get_fees, set_cart_addons,
add_items_to_cart, add_payment_to_cart_session, error_messages, get_fees,
set_cart_addons,
)
from pretix.base.services.orders import perform_order
from pretix.base.storelogic.addons import get_addon_groups
from pretix.base.storelogic.fields import (
get_checkout_fields, get_position_fields,
)
from pretix.base.storelogic.payment import current_selected_payments
from pretix.base.timemachine import time_machine_now
from pretix.presale.signals import (
order_api_meta_from_request, order_meta_from_request,
)
from pretix.presale.views.cart import generate_cart_id
from pretix.storefrontapi.endpoints.event import (
CategorySerializer, ItemSerializer,
@@ -140,13 +146,9 @@ class CartPositionSerializer(MinimalCartPositionSerializer):
d = super().to_representation(instance)
fields = get_position_fields(self.context["event"], instance)
d["fields"] = FieldSerializer(
fields,
many=True,
context={**self.context, "position": instance}
fields, many=True, context={**self.context, "position": instance}
).data
d["fields_data"] = {
f.identifier: f.current_value(instance) for f in fields
}
d["fields_data"] = {f.identifier: f.current_value(instance) for f in fields}
return d
@@ -192,14 +194,28 @@ class CheckoutSessionSerializer(serializers.ModelSerializer):
fields = get_checkout_fields(self.context["event"])
d["fields"] = FieldSerializer(
fields,
many=True,
context={**self.context, "checkout": checkout}
fields, many=True, context={**self.context, "checkout": checkout}
).data
d["fields_data"] = {
f.identifier: f.current_value(checkout.session_data) for f in fields
}
payments = current_selected_payments(
self.context["event"],
total,
checkout.session_data,
total_includes_payment_fees=False,
fail=False,
)
d["payments"] = [
{
"identifier": p["pprov"].identifier,
"label": str(p["pprov"].public_name),
"payment_amount": str(p["payment_amount"]),
}
for p in payments
]
steps = get_steps(
self.context["event"],
cartpos,
@@ -307,7 +323,9 @@ class CheckoutViewSet(viewsets.ViewSet):
elif request.method == "GET":
data = [
{
"parent": MinimalCartPositionSerializer(grp["pos"], context=ctx).data,
"parent": MinimalCartPositionSerializer(
grp["pos"], context=ctx
).data,
"categories": [
{
"category": CategorySerializer(
@@ -340,14 +358,151 @@ class CheckoutViewSet(viewsets.ViewSet):
status=200,
)
def _get_total(self, cs, payments):
cartpos = cs.get_cart_positions(prefetch_questions=True)
total = sum(p.price for p in cartpos)
try:
# TODO: do we need a different get_fees for storefrontapi?
fees = get_fees(
self.request.event,
self.request,
total,
(cs.invoice_address if hasattr(cs, "invoice_address") else None),
payments=payments,
positions=cartpos,
)
except TaxRule.SaleNotAllowed:
# ignore for now, will fail on order creation
fees = []
total += sum([f.value for f in fees])
return total
@action(detail=True, methods=["GET", "POST"])
def payment(self, request, *args, **kwargs):
cs = get_object_or_404(
self.request.event.checkout_sessions, cart_id=kwargs["cart_id"]
)
if request.method == "POST":
# TODO: allow explicit removal
for provider in self.request.event.get_payment_providers().values():
if provider.identifier == request.data.get("identifier", ""):
if not provider.multi_use_supported:
# Providers with multi_use_supported will call this themselves
simulated_payments = cs.session_data.get("payments", {})
simulated_payments = [
p
for p in simulated_payments
if p.get("multi_use_supported")
]
simulated_payments.append(
{
"provider": provider.identifier,
"multi_use_supported": False,
"min_value": None,
"max_value": None,
"info_data": {},
}
)
total = self._get_total(
cs,
simulated_payments,
)
else:
total = self._get_total(
cs,
[
p
for p in cs.session_data.get("payments", [])
if p.get("multi_use_supported")
],
)
resp = provider.storefrontapi_prepare(
cs.session_data,
total,
request.data.get("info"),
)
if provider.multi_use_supported:
if resp is True:
# 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.
cs.session_data["payments"] = [
p
for p in cs.session_data.get("payments", [])
if p.get("multi_use_supported")
]
if provider.identifier not in [
p["provider"]
for p in cs.session_data.get("payments", [])
]:
raise ImproperlyConfigured(
f"Payment provider {provider.identifier} set multi_use_supported "
f"and returned True from payment_prepare, but did not call "
f"add_payment_to_cart"
)
else:
if resp is True or isinstance(resp, str):
# There can only be one payment method that does not have multi_use_supported, remove all
# previous ones.
cs.session_data["payments"] = [
p
for p in cs.session_data.get("payments", [])
if p.get("multi_use_supported")
]
add_payment_to_cart_session(
cs.session_data, provider, None, None, None
)
cs.save(update_fields=["session_data"])
return self._return_checkout_status(cs, 200)
elif request.method == "GET":
available_providers = []
total = self._get_total(
cs,
[
p
for p in cs.session_data.get("payments", [])
if p.get("multi_use_supported")
],
)
for provider in sorted(
self.request.event.get_payment_providers().values(),
key=lambda p: (-p.priority, str(p.public_name).title()),
):
# TODO: do we need a different is_allowed for storefrontapi?
if not provider.is_enabled or not provider.is_allowed(
self.request, total
):
continue
fee = provider.calculate_fee(total)
available_providers.append(
{
"identifier": provider.identifier,
"label": provider.public_name,
"fee": str(fee),
"total": str(total + fee),
}
)
return Response(
data={
"available_providers": available_providers,
},
status=200,
)
@action(detail=True, methods=["PATCH"])
def fields(self, request, *args, **kwargs):
cs = get_object_or_404(
self.request.event.checkout_sessions, cart_id=kwargs["cart_id"]
)
server_pos = {
p.pk: p for p in cs.get_cart_positions(prefetch_questions=True)
}
server_pos = {p.pk: p for p in cs.get_cart_positions(prefetch_questions=True)}
for req_pos in request.data.get("cart_positions", []):
pos = server_pos[req_pos["id"]]
fields = get_position_fields(self.request.event, pos)
@@ -398,6 +553,73 @@ class CheckoutViewSet(viewsets.ViewSet):
time_machine_now(default=None),
)
@action(detail=True, methods=["POST"])
def confirm(self, request, *args, **kwargs):
cs = get_object_or_404(
self.request.event.checkout_sessions, cart_id=kwargs["cart_id"]
)
cartpos = cs.get_cart_positions(prefetch_questions=True)
total = sum(p.price for p in cartpos)
try:
fees = get_fees(
self.request.event,
self.request,
total,
(cs.invoice_address if hasattr(cs, "invoice_address") else None),
payments=[], # todo
positions=cartpos,
)
except TaxRule.SaleNotAllowed as e:
raise ValidationError(str(e)) # todo: need better message?
total += sum([f.value for f in fees])
steps = get_steps(
request.event,
cartpos,
getattr(cs, "invoice_address", None),
cs.session_data,
total,
)
for step in steps:
applicable = step.is_applicable()
valid = not applicable or step.is_valid()
if not valid:
raise ValidationError(f"Step {step.identifier} is not valid")
# todo: confirm messages, or integrate them as fields?
meta_info = {
"contact_form_data": cs.session_data.get("contact_form_data", {}),
}
api_meta = {}
for receiver, response in order_meta_from_request.send(
sender=request.event, request=request
):
meta_info.update(response)
for receiver, response in order_api_meta_from_request.send(
sender=request.event, request=request
):
api_meta.update(response)
# todo: delete checkout session
# todo: give info about order
return self._do_async(
cs,
perform_order,
self.request.event.id,
payments=cs.session_data.get("payments", []),
positions=[p.id for p in cartpos],
email=cs.session_data.get("email"),
locale=translation.get_language(),
address=cs.invoice_address.pk if hasattr(cs, "invoice_address") else None,
meta_info=meta_info,
sales_channel=request.sales_channel.identifier,
shown_total=None,
customer=cs.customer,
override_now_dt=time_machine_now(default=None),
api_meta=api_meta,
)
@action(
detail=True,
methods=["GET"],
+3 -3
View File
@@ -89,8 +89,8 @@ class PricingField(serializers.Field):
return None
item = item_or_var if isinstance(item_or_var, Item) else item_or_var.item
suggested_price = item.suggested_price
display_price = item.display_price
suggested_price = item_or_var.suggested_price
display_price = item_or_var.display_price
if self.context.get("price_included"):
display_price = TaxedPrice(
@@ -104,7 +104,7 @@ class PricingField(serializers.Field):
if hasattr(item, "initial_price"):
# Pre-select current price for add-ons
suggested_price = item.initial_price
suggested_price = item_or_var.initial_price
return {
"display_price": {
+52 -1
View File
@@ -1,10 +1,16 @@
from collections import UserDict
from decimal import Decimal
from django.test import RequestFactory
from pretix.base.storelogic import IncompleteError
from pretix.base.storelogic.addons import (
addons_is_applicable, addons_is_completed,
)
from pretix.base.storelogic.fields import ensure_fields_are_completed
from pretix.base.storelogic.payment import (
ensure_payment_is_completed, payment_is_applicable,
)
class CheckoutStep:
@@ -58,14 +64,59 @@ class FieldsStep(CheckoutStep):
return True
class PaymentStep(CheckoutStep):
identifier = "payment"
@property
def request(self):
# TODO: find a better way to avoid this
rf = RequestFactory()
r = rf.get("/")
r.event = self.event
r.organizer = self.event.organizer
self.cart_session.setdefault("fake_request", {})
cart_id = self.cart_positions[0].cart_id
r.session = UserDict(
{
f"current_cart_event_{self.event.pk}": cart_id,
"carts": {cart_id: self.cart_session},
}
)
r.session.session_key = cart_id
return r
def is_applicable(self):
return payment_is_applicable(
self.event,
self.total,
self.cart_positions,
self.invoice_address,
self.cart_session,
self.request,
)
def is_valid(self):
try:
ensure_payment_is_completed(
self.event,
self.total,
self.cart_session,
self.request,
)
except IncompleteError:
return False
else:
return True
def get_steps(event, cart_positions, invoice_address, cart_session, total):
return [
AddonStep(event, cart_positions, invoice_address, cart_session, total),
FieldsStep(event, cart_positions, invoice_address, cart_session, total),
PaymentStep(event, cart_positions, invoice_address, cart_session, total),
# todo: cross-selling
# todo: customers
# todo: memberships
# todo: plugin signals
# todo: payment
# todo: confirmations
]