mirror of
https://github.com/pretix/pretix.git
synced 2026-08-05 09:57:49 +00:00
Payment and confirm
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user