forked from CGM_Public/pretix_original
Support for external gift cards (#2912)
This commit is contained in:
@@ -699,8 +699,8 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
subject_attendees_template = request.event.settings.mail_subject_order_placed_attendee
|
||||
|
||||
_order_placed_email(
|
||||
request.event, order, payment.payment_provider if payment else None, email_template, subject_template,
|
||||
log_entry, invoice, payment, is_free=free_flow
|
||||
request.event, order, email_template, subject_template,
|
||||
log_entry, invoice, [payment] if payment else [], is_free=free_flow
|
||||
)
|
||||
if email_attendees:
|
||||
for p in order.positions.all():
|
||||
|
||||
@@ -320,13 +320,16 @@ def get_email_context(**kwargs):
|
||||
return ctx
|
||||
|
||||
|
||||
def _placeholder_payment(order, payment):
|
||||
if not payment:
|
||||
return None
|
||||
if 'payment' in inspect.signature(payment.payment_provider.order_pending_mail_render).parameters:
|
||||
return str(payment.payment_provider.order_pending_mail_render(order, payment))
|
||||
else:
|
||||
return str(payment.payment_provider.order_pending_mail_render(order))
|
||||
def _placeholder_payments(order, payments):
|
||||
d = []
|
||||
for payment in payments:
|
||||
if 'payment' in inspect.signature(payment.payment_provider.order_pending_mail_render).parameters:
|
||||
d.append(str(payment.payment_provider.order_pending_mail_render(order, payment)))
|
||||
else:
|
||||
d.append(str(payment.payment_provider.order_pending_mail_render(order)))
|
||||
d = [line for line in d if line.strip()]
|
||||
if d:
|
||||
return '\n\n'.join(d)
|
||||
|
||||
|
||||
def get_best_name(position_or_address, parts=False):
|
||||
@@ -617,7 +620,7 @@ def base_placeholders(sender, **kwargs):
|
||||
_('An individual text with a reason can be inserted here.'),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'payment_info', ['order', 'payment'], _placeholder_payment,
|
||||
'payment_info', ['order', 'payments'], _placeholder_payments,
|
||||
_('The amount has been charged to your card.'),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.16 on 2022-11-17 15:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0224_eventmetaproperty_filter_allowed'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='orderpayment',
|
||||
name='process_initiated',
|
||||
field=models.BooleanField(null=True),
|
||||
),
|
||||
]
|
||||
@@ -1509,6 +1509,9 @@ class OrderPayment(models.Model):
|
||||
:type info: str
|
||||
:param fee: The ``OrderFee`` object used to track the fee for this order.
|
||||
:type fee: pretix.base.models.OrderFee
|
||||
:param process_initiated: Only for internal use inside pretix.presale to check which payments have started
|
||||
the execution process.
|
||||
:type process_initiated: bool
|
||||
"""
|
||||
PAYMENT_STATE_CREATED = 'created'
|
||||
PAYMENT_STATE_PENDING = 'pending'
|
||||
@@ -1559,6 +1562,9 @@ class OrderPayment(models.Model):
|
||||
null=True, blank=True, related_name='payments', on_delete=models.SET_NULL
|
||||
)
|
||||
migrated = models.BooleanField(default=False)
|
||||
process_initiated = models.BooleanField(
|
||||
null=True # null = created before this field was introduced
|
||||
)
|
||||
|
||||
objects = ScopedManager(organizer='order__event__organizer')
|
||||
|
||||
|
||||
@@ -63,14 +63,13 @@ from pretix.base.models import (
|
||||
OrderRefund, Quota,
|
||||
)
|
||||
from pretix.base.reldate import RelativeDateField, RelativeDateWrapper
|
||||
from pretix.base.services.cart import get_fees
|
||||
from pretix.base.settings import SettingsSandbox
|
||||
from pretix.base.signals import register_payment_providers
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.base.templatetags.rich_text import rich_text
|
||||
from pretix.helpers.countries import CachedCountries
|
||||
from pretix.helpers.money import DecimalTextInput
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri, eventreverse
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
from pretix.presale.views import get_cart, get_cart_total
|
||||
from pretix.presale.views.cart import cart_session, get_or_create_cart_id
|
||||
|
||||
@@ -138,6 +137,48 @@ class BasePaymentProvider:
|
||||
"""
|
||||
return self.settings.get('_enabled', as_type=bool)
|
||||
|
||||
@property
|
||||
def multi_use_supported(self) -> bool:
|
||||
"""
|
||||
Returns whether or whether not this payment provider supports being used multiple times in the same
|
||||
checkout, or in addition to a different payment provider. This is usually only useful for payment providers
|
||||
that represent gift cards, i.e. payment methods with an upper limit per payment instrument that can usually
|
||||
be combined with other instruments.
|
||||
|
||||
If you set this property to ``True``, the behavior of how pretix interacts with your payment provider changes
|
||||
and you will need to respect the following rules:
|
||||
|
||||
- ``payment_form_render`` must not depend on session state, it must always allow a user to add a new payment.
|
||||
Editing a payment is not possible, but pretix will give users an option to delete it.
|
||||
|
||||
- Returning ``True`` from ``checkout_prepare`` is no longer enough. Instead, you must *also* call
|
||||
``pretix.base.services.cart.add_payment_to_cart(request, provider, min_value, max_value, info_data)``
|
||||
to add the payment to the session. You are still allowed to do a redirect from ``checkout_prepare`` and then
|
||||
call this function upon return.
|
||||
|
||||
- Unlike in the general case, when ``checkout_prepare`` is called, the ``cart['total']`` parameter will _not yet_
|
||||
include payment fees charged by your provider as we don't yet know the amount of the charge, so you need to
|
||||
take care of that yourself when setting your maximum amount.
|
||||
|
||||
- ``payment_is_valid_session`` will not be called during checkout, don't rely on it. If you called
|
||||
``add_payment_to_cart``, we'll trust the payment is okay and your next chance to change that will be
|
||||
``execute_payment``.
|
||||
|
||||
The changed behavior currently only affects the behavior during initial checkout (i.e. ``checkout_prepare``),
|
||||
for ``payment_prepare`` the regular behavior applies and you are expected to just modify the amount of the
|
||||
``OrderPayment`` object if you need to.
|
||||
"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def execute_payment_needs_user(self) -> bool:
|
||||
"""
|
||||
Set this to ``True`` if your ``execute_payment`` function needs to be triggered by a user request, i.e. either
|
||||
needs the ``request`` object or might require a browser redirect. If this is ``False``, you will not receive
|
||||
a ``request`` and may not redirect since execute_payment might be called server-side.
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def test_mode_message(self) -> str:
|
||||
"""
|
||||
@@ -574,7 +615,7 @@ class BasePaymentProvider:
|
||||
ctx = {'request': request, 'form': form}
|
||||
return template.render(ctx)
|
||||
|
||||
def checkout_confirm_render(self, request, order: Order=None) -> str:
|
||||
def checkout_confirm_render(self, request, order: Order=None, info_data: dict=None) -> str:
|
||||
"""
|
||||
If the user has successfully filled in their payment data, they will be redirected
|
||||
to a confirmation page which lists all details of their order for a final review.
|
||||
@@ -584,7 +625,9 @@ class BasePaymentProvider:
|
||||
In most cases, this should include a short summary of the user's input and
|
||||
a short explanation on how the payment process will continue.
|
||||
|
||||
:param request: The current HTTP request.
|
||||
:param order: Only set when this is a change to a new payment method for an existing order.
|
||||
:param info_data: The ``info_data`` dictionary you set during ``add_payment_to_cart`` (only filled if ``multi_use_supported`` is set)
|
||||
"""
|
||||
raise NotImplementedError() # NOQA
|
||||
|
||||
@@ -618,6 +661,10 @@ class BasePaymentProvider:
|
||||
.. IMPORTANT:: If this is called, the user has not yet confirmed their order.
|
||||
You may NOT do anything which actually moves money.
|
||||
|
||||
Note: The behavior of this method changes significantly when you set
|
||||
``multi_use_supported``. Please refer to the ``multi_use_supported`` documentation
|
||||
for more information.
|
||||
|
||||
:param cart: This dictionary contains at least the following keys:
|
||||
|
||||
positions:
|
||||
@@ -657,9 +704,9 @@ class BasePaymentProvider:
|
||||
You will be passed an :py:class:`pretix.base.models.OrderPayment` object that contains
|
||||
the amount of money that should be paid.
|
||||
|
||||
If you need any special behavior, you can return a string
|
||||
containing the URL the user will be redirected to. If you are done with your process
|
||||
you should return the user to the order's detail page.
|
||||
If you need any special behavior, you can return a string containing the URL the user will be redirected to.
|
||||
If you are done with your process you should return the user to the order's detail page. Redirection is not
|
||||
allowed if you set ``execute_payment_needs_user`` to ``True``.
|
||||
|
||||
If the payment is completed, you should call ``payment.confirm()``. Please note that this might
|
||||
raise a ``Quota.QuotaExceededException`` if (and only if) the payment term of this order is over and
|
||||
@@ -671,7 +718,7 @@ class BasePaymentProvider:
|
||||
|
||||
On errors, you should raise a ``PaymentException``.
|
||||
|
||||
:param order: The order object
|
||||
:param request: A HTTP request, except if ``execute_payment_needs_user`` is ``False``
|
||||
:param payment: An ``OrderPayment`` instance
|
||||
"""
|
||||
return None
|
||||
@@ -905,6 +952,7 @@ class FreeOrderProvider(BasePaymentProvider):
|
||||
is_implicit = True
|
||||
is_enabled = True
|
||||
identifier = "free"
|
||||
execute_payment_needs_user = False
|
||||
|
||||
def checkout_confirm_render(self, request: HttpRequest) -> str:
|
||||
return _("No payment is required as this order only includes products which are free of charge.")
|
||||
@@ -991,6 +1039,7 @@ class BoxOfficeProvider(BasePaymentProvider):
|
||||
class ManualPayment(BasePaymentProvider):
|
||||
identifier = 'manual'
|
||||
verbose_name = _('Manual payment')
|
||||
execute_payment_needs_user = False
|
||||
|
||||
@property
|
||||
def test_mode_message(self):
|
||||
@@ -1133,6 +1182,8 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
identifier = "giftcard"
|
||||
verbose_name = _("Gift card")
|
||||
priority = 10
|
||||
multi_use_supported = True
|
||||
execute_payment_needs_user = False
|
||||
|
||||
@property
|
||||
def settings_form_fields(self):
|
||||
@@ -1158,8 +1209,10 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
def payment_form_render(self, request: HttpRequest, total: Decimal) -> str:
|
||||
return get_template('pretixcontrol/giftcards/checkout.html').render({})
|
||||
|
||||
def checkout_confirm_render(self, request) -> str:
|
||||
return get_template('pretixcontrol/giftcards/checkout_confirm.html').render({})
|
||||
def checkout_confirm_render(self, request, order=None, info_data=None) -> str:
|
||||
return get_template('pretixcontrol/giftcards/checkout_confirm.html').render({
|
||||
'info_data': info_data,
|
||||
})
|
||||
|
||||
def refund_control_render(self, request, refund) -> str:
|
||||
from .models import GiftCard
|
||||
@@ -1213,6 +1266,8 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
return True
|
||||
|
||||
def checkout_prepare(self, request: HttpRequest, cart: Dict[str, Any]) -> Union[bool, str, None]:
|
||||
from pretix.base.services.cart import add_payment_to_cart
|
||||
|
||||
for p in get_cart(request):
|
||||
if p.item.issue_giftcard:
|
||||
messages.error(request, _("You cannot pay with gift cards when buying a gift card."))
|
||||
@@ -1221,7 +1276,7 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
cs = cart_session(request)
|
||||
try:
|
||||
gc = self.event.organizer.accepted_gift_cards.get(
|
||||
secret=request.POST.get("giftcard")
|
||||
secret=request.POST.get("giftcard").strip()
|
||||
)
|
||||
if gc.currency != self.event.currency:
|
||||
messages.error(request, _("This gift card does not support this currency."))
|
||||
@@ -1238,34 +1293,22 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
if gc.value <= Decimal("0.00"):
|
||||
messages.error(request, _("All credit on this gift card has been used."))
|
||||
return
|
||||
if 'gift_cards' not in cs:
|
||||
cs['gift_cards'] = []
|
||||
elif gc.pk in cs['gift_cards']:
|
||||
messages.error(request, _("This gift card is already used for your payment."))
|
||||
return
|
||||
cs['gift_cards'] = cs['gift_cards'] + [gc.pk]
|
||||
|
||||
total = sum(p.total for p in cart['positions'])
|
||||
# Recompute fees. Some plugins, e.g. pretix-servicefees, change their fee schedule if a gift card is
|
||||
# applied.
|
||||
fees = get_fees(
|
||||
self.event, request, total, cart['invoice_address'], cs.get('payment'),
|
||||
cart['raw']
|
||||
for p in cs.get('payments', []):
|
||||
if p['provider'] == self.identifier and p['info_data']['gift_card'] == gc.pk:
|
||||
messages.error(request, _("This gift card is already used for your payment."))
|
||||
return
|
||||
|
||||
add_payment_to_cart(
|
||||
request,
|
||||
self,
|
||||
max_value=gc.value,
|
||||
info_data={
|
||||
'gift_card': gc.pk,
|
||||
'gift_card_secret': gc.secret,
|
||||
}
|
||||
)
|
||||
total += sum([f.value for f in fees])
|
||||
remainder = total
|
||||
if remainder > Decimal('0.00'):
|
||||
del cs['payment']
|
||||
messages.success(request, _("Your gift card has been applied, but {} still need to be paid. Please select a payment method.").format(
|
||||
money_filter(remainder, self.event.currency)
|
||||
))
|
||||
else:
|
||||
messages.success(request, _("Your gift card has been applied."))
|
||||
|
||||
kwargs = {'step': 'payment'}
|
||||
if request.resolver_match and 'cart_namespace' in request.resolver_match.kwargs:
|
||||
kwargs['cart_namespace'] = request.resolver_match.kwargs['cart_namespace']
|
||||
return eventreverse(self.event, 'presale:event.checkout', kwargs=kwargs)
|
||||
return True
|
||||
except GiftCard.DoesNotExist:
|
||||
if self.event.vouchers.filter(code__iexact=request.POST.get("giftcard")).exists():
|
||||
messages.warning(request, _("You entered a voucher instead of a gift card. Vouchers can only be entered on the first page of the shop below "
|
||||
@@ -1283,7 +1326,7 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
|
||||
try:
|
||||
gc = self.event.organizer.accepted_gift_cards.get(
|
||||
secret=request.POST.get("giftcard")
|
||||
secret=request.POST.get("giftcard").strip()
|
||||
)
|
||||
if gc.currency != self.event.currency:
|
||||
messages.error(request, _("This gift card does not support this currency."))
|
||||
@@ -1302,6 +1345,7 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
return
|
||||
payment.info_data = {
|
||||
'gift_card': gc.pk,
|
||||
'gift_card_secret': gc.secret,
|
||||
'retry': True
|
||||
}
|
||||
payment.amount = min(payment.amount, gc.value)
|
||||
@@ -1309,7 +1353,7 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
|
||||
return True
|
||||
except GiftCard.DoesNotExist:
|
||||
if self.event.vouchers.filter(code__iexact=request.POST.get("giftcard")).exists():
|
||||
if self.event.vouchers.filter(code__iexact=request.POST.get("giftcard").strip()).exists():
|
||||
messages.warning(request, _("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:
|
||||
@@ -1318,36 +1362,45 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
messages.error(request, _("This gift card can not be redeemed since its code is not unique. Please contact the organizer of this event."))
|
||||
|
||||
def execute_payment(self, request: HttpRequest, payment: OrderPayment) -> str:
|
||||
# This method will only be called when retrying payments, e.g. after a payment_prepare call. It is not called
|
||||
# during the order creation phase because this payment provider is a special case.
|
||||
for p in payment.order.positions.all(): # noqa - just a safeguard
|
||||
for p in payment.order.positions.all():
|
||||
if p.item.issue_giftcard:
|
||||
raise PaymentException(_("You cannot pay with gift cards when buying a gift card."))
|
||||
|
||||
gcpk = payment.info_data.get('gift_card')
|
||||
if not gcpk or not payment.info_data.get('retry'):
|
||||
if not gcpk:
|
||||
raise PaymentException("Invalid state, should never occur.")
|
||||
with transaction.atomic():
|
||||
gc = GiftCard.objects.select_for_update().get(pk=gcpk)
|
||||
if gc.currency != self.event.currency: # noqa - just a safeguard
|
||||
raise PaymentException(_("This gift card does not support this currency."))
|
||||
if not gc.accepted_by(self.event.organizer): # noqa - just a safeguard
|
||||
raise PaymentException(_("This gift card is not accepted by this event organizer."))
|
||||
if payment.amount > gc.value: # noqa - just a safeguard
|
||||
raise PaymentException(_("This gift card was used in the meantime. Please try again."))
|
||||
if gc.expires and gc.expires < now(): # noqa - just a safeguard
|
||||
messages.error(request, _("This gift card is no longer valid."))
|
||||
return
|
||||
trans = gc.transactions.create(
|
||||
value=-1 * payment.amount,
|
||||
order=payment.order,
|
||||
payment=payment
|
||||
)
|
||||
payment.info_data = {
|
||||
'gift_card': gc.pk,
|
||||
'transaction_id': trans.pk,
|
||||
}
|
||||
payment.confirm()
|
||||
try:
|
||||
with transaction.atomic():
|
||||
try:
|
||||
gc = GiftCard.objects.select_for_update().get(pk=gcpk)
|
||||
except GiftCard.DoesNotExist:
|
||||
raise PaymentException(_("This gift card does not support this currency."))
|
||||
if gc.currency != self.event.currency: # noqa - just a safeguard
|
||||
raise PaymentException(_("This gift card does not support this currency."))
|
||||
if not gc.accepted_by(self.event.organizer):
|
||||
raise PaymentException(_("This gift card is not accepted by this event organizer."))
|
||||
if payment.amount > gc.value:
|
||||
raise PaymentException(_("This gift card was used in the meantime. Please try again."))
|
||||
if gc.testmode and not payment.order.testmode:
|
||||
raise PaymentException(_("This gift card can only be used in test mode."))
|
||||
if not gc.testmode and payment.order.testmode:
|
||||
raise PaymentException(_("Only test gift cards can be used in test mode."))
|
||||
if gc.expires and gc.expires < now():
|
||||
raise PaymentException(_("This gift card is no longer valid."))
|
||||
|
||||
trans = gc.transactions.create(
|
||||
value=-1 * payment.amount,
|
||||
order=payment.order,
|
||||
payment=payment
|
||||
)
|
||||
payment.info_data = {
|
||||
'gift_card': gc.pk,
|
||||
'transaction_id': trans.pk,
|
||||
}
|
||||
payment.confirm()
|
||||
except PaymentException as e:
|
||||
payment.fail(info={'error': str(e)})
|
||||
raise e
|
||||
|
||||
def payment_is_valid_session(self, request: HttpRequest) -> bool:
|
||||
return True
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
# 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 uuid
|
||||
from collections import Counter, defaultdict, namedtuple
|
||||
from datetime import datetime, time, timedelta
|
||||
from decimal import Decimal
|
||||
@@ -1265,44 +1265,71 @@ class CartManager:
|
||||
raise CartError(err)
|
||||
|
||||
|
||||
def get_fees(event, request, total, invoice_address, provider, positions):
|
||||
def add_payment_to_cart(request, provider, min_value: Decimal=None, max_value: Decimal=None, info_data: dict=None):
|
||||
"""
|
||||
:param request: The current HTTP request context.
|
||||
:param provider: The instance of your payment provider.
|
||||
:param min_value: The minimum value this payment instrument supports, or ``None`` for unlimited.
|
||||
:param max_value: The maximum value this payment instrument supports, or ``None`` for unlimited. Highly discouraged
|
||||
to use for payment providers which charge a payment fee, as this can be very user-unfriendly if
|
||||
users need a second payment method just for the payment fee of the first method.
|
||||
:param info_data: A dictionary of information that will be passed through to the ``OrderPayment.info_data`` attribute.
|
||||
:return:
|
||||
"""
|
||||
from pretix.presale.views.cart import cart_session
|
||||
|
||||
cs = cart_session(request)
|
||||
cs.setdefault('payments', [])
|
||||
|
||||
cs['payments'].append({
|
||||
'id': str(uuid.uuid4()),
|
||||
'provider': provider.identifier,
|
||||
'multi_use_supported': provider.multi_use_supported,
|
||||
'min_value': str(min_value) if min_value is not None else None,
|
||||
'max_value': str(max_value) if max_value is not None else None,
|
||||
'info_data': info_data or {},
|
||||
})
|
||||
|
||||
|
||||
def get_fees(event, request, total, invoice_address, payments, positions):
|
||||
if payments and not isinstance(payments, list):
|
||||
raise TypeError("payments must now be a list")
|
||||
|
||||
fees = []
|
||||
for recv, resp in fee_calculation_for_cart.send(sender=event, request=request, invoice_address=invoice_address,
|
||||
total=total, positions=positions):
|
||||
total=total, positions=positions, payment_requests=payments):
|
||||
if resp:
|
||||
fees += resp
|
||||
|
||||
total = total + sum(f.value for f in fees)
|
||||
|
||||
cs = cart_session(request)
|
||||
if cs.get('gift_cards'):
|
||||
gcs = cs['gift_cards']
|
||||
gc_qs = event.organizer.accepted_gift_cards.filter(pk__in=cs.get('gift_cards'), currency=event.currency)
|
||||
for gc in gc_qs:
|
||||
if gc.testmode != event.testmode:
|
||||
gcs.remove(gc.pk)
|
||||
if total != 0 and payments:
|
||||
total_remaining = total
|
||||
for p in 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']):
|
||||
continue
|
||||
fval = Decimal(gc.value) # TODO: don't require an extra query
|
||||
fval = min(fval, total)
|
||||
if fval > 0:
|
||||
total -= fval
|
||||
fees.append(OrderFee(
|
||||
fee_type=OrderFee.FEE_TYPE_GIFTCARD,
|
||||
internal_type='giftcard',
|
||||
description=gc.secret,
|
||||
value=-1 * fval,
|
||||
tax_rate=Decimal('0.00'),
|
||||
tax_value=Decimal('0.00'),
|
||||
tax_rule=TaxRule.zero()
|
||||
))
|
||||
cs['gift_cards'] = gcs
|
||||
|
||||
if provider and total != 0:
|
||||
provider = event.get_payment_providers().get(provider)
|
||||
if provider:
|
||||
payment_fee = provider.calculate_fee(total)
|
||||
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:
|
||||
continue
|
||||
|
||||
payment_fee = pprov.calculate_fee(to_pay)
|
||||
total_remaining += payment_fee
|
||||
to_pay += payment_fee
|
||||
|
||||
if p.get('max_value') and to_pay > Decimal(p['max_value']):
|
||||
to_pay = min(to_pay, Decimal(p['max_value']))
|
||||
|
||||
total_remaining -= to_pay
|
||||
|
||||
if payment_fee:
|
||||
payment_fee_tax_rule = event.settings.tax_rate_default or TaxRule.zero()
|
||||
|
||||
@@ -74,7 +74,7 @@ from pretix.base.models.orders import (
|
||||
)
|
||||
from pretix.base.models.organizer import TeamAPIToken
|
||||
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
|
||||
from pretix.base.payment import BasePaymentProvider, PaymentException
|
||||
from pretix.base.payment import PaymentException
|
||||
from pretix.base.reldate import RelativeDateWrapper
|
||||
from pretix.base.secrets import assign_ticket_secret
|
||||
from pretix.base.services import tickets
|
||||
@@ -793,68 +793,75 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
raise OrderError(err, errargs)
|
||||
|
||||
|
||||
def _get_fees(positions: List[CartPosition], payment_provider: BasePaymentProvider, address: InvoiceAddress,
|
||||
meta_info: dict, event: Event, gift_cards: List[GiftCard]):
|
||||
def _get_fees(positions: List[CartPosition], payment_requests: List[dict], address: InvoiceAddress,
|
||||
meta_info: dict, event: Event, require_approval=False):
|
||||
fees = []
|
||||
total = sum([c.price for c in positions])
|
||||
|
||||
for recv, resp in order_fee_calculation.send(sender=event, invoice_address=address, total=total,
|
||||
gift_cards = [] # for backwards compatibility
|
||||
for p in payment_requests:
|
||||
if p['provider'] == 'giftcard':
|
||||
gift_cards.append(GiftCard.objects.get(pk=p['info_data']['gift_card']))
|
||||
|
||||
for recv, resp in order_fee_calculation.send(sender=event, invoice_address=address, total=total, payment_requests=payment_requests,
|
||||
meta_info=meta_info, positions=positions, gift_cards=gift_cards):
|
||||
if resp:
|
||||
fees += resp
|
||||
total += sum(f.value for f in fees)
|
||||
|
||||
gift_card_values = {}
|
||||
for gc in gift_cards:
|
||||
fval = Decimal(gc.value) # TODO: don't require an extra query
|
||||
fval = min(fval, total)
|
||||
if fval > 0:
|
||||
total -= fval
|
||||
gift_card_values[gc] = fval
|
||||
total_remaining = total
|
||||
for p in payment_requests:
|
||||
# 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']):
|
||||
p['payment_amount'] = Decimal('0.00')
|
||||
continue
|
||||
|
||||
if payment_provider:
|
||||
payment_fee = payment_provider.calculate_fee(total)
|
||||
else:
|
||||
payment_fee = 0
|
||||
pf = None
|
||||
if payment_fee:
|
||||
pf = OrderFee(fee_type=OrderFee.FEE_TYPE_PAYMENT, value=payment_fee,
|
||||
internal_type=payment_provider.identifier)
|
||||
fees.append(pf)
|
||||
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']))
|
||||
|
||||
return fees, pf, gift_card_values
|
||||
payment_fee = p['pprov'].calculate_fee(to_pay)
|
||||
total_remaining += payment_fee
|
||||
to_pay += payment_fee
|
||||
|
||||
if p.get('max_value') and to_pay > Decimal(p['max_value']):
|
||||
to_pay = min(to_pay, Decimal(p['max_value']))
|
||||
|
||||
total_remaining -= to_pay
|
||||
|
||||
p['payment_amount'] = to_pay
|
||||
if payment_fee:
|
||||
pf = OrderFee(fee_type=OrderFee.FEE_TYPE_PAYMENT, value=payment_fee,
|
||||
internal_type=p['pprov'].identifier)
|
||||
fees.append(pf)
|
||||
p['fee'] = pf
|
||||
|
||||
if total_remaining != Decimal('0.00') and not require_approval:
|
||||
raise OrderError(_("The selected payment methods do not cover the total balance."))
|
||||
|
||||
return fees
|
||||
|
||||
|
||||
def _create_order(event: Event, email: str, positions: List[CartPosition], now_dt: datetime,
|
||||
payment_provider: BasePaymentProvider, locale: str=None, address: InvoiceAddress=None,
|
||||
meta_info: dict=None, sales_channel: str='web', gift_cards: list=None, shown_total=None,
|
||||
payment_requests: List[dict], locale: str=None, address: InvoiceAddress=None,
|
||||
meta_info: dict=None, sales_channel: str='web', shown_total=None,
|
||||
customer=None):
|
||||
p = None
|
||||
payments = []
|
||||
sales_channel = get_all_sales_channels()[sales_channel]
|
||||
|
||||
with transaction.atomic():
|
||||
checked_gift_cards = []
|
||||
if gift_cards:
|
||||
gc_qs = GiftCard.objects.select_for_update().filter(pk__in=gift_cards)
|
||||
for gc in gc_qs:
|
||||
if gc.currency != event.currency:
|
||||
raise OrderError(_("This gift card does not support this currency."))
|
||||
if gc.testmode and not event.testmode:
|
||||
raise OrderError(_("This gift card can only be used in test mode."))
|
||||
if not gc.testmode and event.testmode:
|
||||
raise OrderError(_("Only test gift cards can be used in test mode."))
|
||||
if not gc.accepted_by(event.organizer):
|
||||
raise OrderError(_("This gift card is not accepted by this event organizer."))
|
||||
checked_gift_cards.append(gc)
|
||||
if checked_gift_cards and any(c.item.issue_giftcard for c in positions):
|
||||
raise OrderError(_("You cannot pay with gift cards when buying a gift card."))
|
||||
|
||||
try:
|
||||
validate_memberships_in_order(customer, positions, event, lock=True, testmode=event.testmode)
|
||||
except ValidationError as e:
|
||||
raise OrderError(e.message)
|
||||
|
||||
fees, pf, gift_card_values = _get_fees(positions, payment_provider, address, meta_info, event, checked_gift_cards)
|
||||
require_approval = any(p.requires_approval(invoice_address=address) for p in positions)
|
||||
fees = _get_fees(positions, payment_requests, address, meta_info, event, require_approval=require_approval)
|
||||
total = pending_sum = sum([c.price for c in positions]) + sum([c.value for c in fees])
|
||||
|
||||
order = Order(
|
||||
@@ -867,7 +874,7 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
|
||||
total=total,
|
||||
testmode=True if sales_channel.testmode_supported and event.testmode else False,
|
||||
meta_info=json.dumps(meta_info or {}),
|
||||
require_approval=any(p.requires_approval(invoice_address=address) for p in positions),
|
||||
require_approval=require_approval,
|
||||
sales_channel=sales_channel.identifier,
|
||||
customer=customer,
|
||||
)
|
||||
@@ -891,28 +898,11 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
|
||||
fee.tax_rule = None # TODO: deprecate
|
||||
fee.save()
|
||||
|
||||
for gc, val in gift_card_values.items():
|
||||
p = order.payments.create(
|
||||
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
|
||||
provider='giftcard',
|
||||
amount=val,
|
||||
fee=pf
|
||||
)
|
||||
trans = gc.transactions.create(
|
||||
value=-1 * val,
|
||||
order=order,
|
||||
payment=p
|
||||
)
|
||||
p.info_data = {
|
||||
'gift_card': gc.pk,
|
||||
'transaction_id': trans.pk,
|
||||
}
|
||||
p.save()
|
||||
pending_sum -= val
|
||||
|
||||
# Safety check: Is the amount we're now going to charge the same amount the user has been shown when they
|
||||
# pressed "Confirm purchase"? If not, we should better warn the user and show the confirmation page again.
|
||||
# The only *known* case where this happens is if a gift card is used in two concurrent sessions.
|
||||
# We used to have a *known* case where this happened is if a gift card is used in two concurrent sessions,
|
||||
# but this is now a payment error instead. So currently this code branch is usually only triggered by bugs
|
||||
# in other places (e.g. tax calculation).
|
||||
if shown_total is not None:
|
||||
if Decimal(shown_total) != pending_sum:
|
||||
raise OrderError(
|
||||
@@ -921,13 +911,17 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
|
||||
'check the prices below and try again.')
|
||||
)
|
||||
|
||||
if payment_provider and not order.require_approval:
|
||||
p = order.payments.create(
|
||||
state=OrderPayment.PAYMENT_STATE_CREATED,
|
||||
provider=payment_provider.identifier,
|
||||
amount=pending_sum,
|
||||
fee=pf
|
||||
)
|
||||
if payment_requests and not order.require_approval:
|
||||
for p in payment_requests:
|
||||
if not p.get('multi_use_supported') or p['payment_amount'] > Decimal('0.00'):
|
||||
payments.append(order.payments.create(
|
||||
state=OrderPayment.PAYMENT_STATE_CREATED,
|
||||
provider=p['provider'],
|
||||
amount=p['payment_amount'],
|
||||
fee=p.get('fee'),
|
||||
info=json.dumps(p['info_data']),
|
||||
process_initiated=False,
|
||||
))
|
||||
|
||||
orderpositions = OrderPosition.transform_cart_positions(positions, order)
|
||||
order.create_transactions(positions=orderpositions, fees=fees, is_new=True)
|
||||
@@ -939,12 +933,12 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
|
||||
order.log_action('pretix.event.order.consent', data={'msg': msg})
|
||||
|
||||
order_placed.send(event, order=order)
|
||||
return order, p
|
||||
return order, payments
|
||||
|
||||
|
||||
def _order_placed_email(event: Event, order: Order, pprov: BasePaymentProvider, email_template, subject_template,
|
||||
log_entry: str, invoice, payment: OrderPayment, is_free=False):
|
||||
email_context = get_email_context(event=event, order=order, payment=payment if pprov else None)
|
||||
def _order_placed_email(event: Event, order: Order, email_template, subject_template,
|
||||
log_entry: str, invoice, payments: List[OrderPayment], is_free=False):
|
||||
email_context = get_email_context(event=event, order=order, payments=payments)
|
||||
try:
|
||||
order.send_mail(
|
||||
subject_template, email_template, email_context,
|
||||
@@ -979,15 +973,13 @@ def _order_placed_email_attendee(event: Event, order: Order, position: OrderPosi
|
||||
logger.exception('Order received email could not be sent to attendee')
|
||||
|
||||
|
||||
def _perform_order(event: Event, payment_provider: str, position_ids: List[str],
|
||||
def _perform_order(event: Event, payment_requests: List[dict], position_ids: List[str],
|
||||
email: str, locale: str, address: int, meta_info: dict=None, sales_channel: str='web',
|
||||
gift_cards: list=None, shown_total=None, customer=None):
|
||||
if payment_provider:
|
||||
pprov = event.get_payment_providers().get(payment_provider)
|
||||
if not pprov:
|
||||
shown_total=None, customer=None):
|
||||
for p in payment_requests:
|
||||
p['pprov'] = event.get_payment_providers(cached=True)[p['provider']]
|
||||
if not p['pprov']:
|
||||
raise OrderError(error_messages['internal'])
|
||||
else:
|
||||
pprov = None
|
||||
|
||||
if customer:
|
||||
customer = event.organizer.customers.get(pk=customer)
|
||||
@@ -1017,8 +1009,17 @@ def _perform_order(event: Event, payment_provider: str, position_ids: List[str],
|
||||
id__in=position_ids, event=event
|
||||
)
|
||||
|
||||
validate_order.send(event, payment_provider=pprov, email=email, positions=positions, locale=locale,
|
||||
invoice_address=addr, meta_info=meta_info, customer=customer)
|
||||
validate_order.send(
|
||||
event,
|
||||
payment_provider=payment_requests[0]['provider'] if payment_requests else None, # only for backwards compatibility
|
||||
payments=payment_requests,
|
||||
email=email,
|
||||
positions=positions,
|
||||
locale=locale,
|
||||
invoice_address=addr,
|
||||
meta_info=meta_info,
|
||||
customer=customer,
|
||||
)
|
||||
|
||||
lockfn = NoLockManager
|
||||
locked = False
|
||||
@@ -1038,21 +1039,28 @@ def _perform_order(event: Event, payment_provider: str, position_ids: List[str],
|
||||
if len(position_ids) != len(positions):
|
||||
raise OrderError(error_messages['internal'])
|
||||
_check_positions(event, now_dt, positions, address=addr, sales_channel=sales_channel, customer=customer)
|
||||
order, payment = _create_order(event, email, positions, now_dt, pprov,
|
||||
locale=locale, address=addr, meta_info=meta_info, sales_channel=sales_channel,
|
||||
gift_cards=gift_cards, shown_total=shown_total, customer=customer)
|
||||
order, payment_objs = _create_order(event, email, positions, now_dt, payment_requests,
|
||||
locale=locale, address=addr, meta_info=meta_info, sales_channel=sales_channel,
|
||||
shown_total=shown_total, customer=customer)
|
||||
|
||||
free_order_flow = payment and payment_provider == 'free' and order.pending_sum == Decimal('0.00') and not order.require_approval
|
||||
free_order_flow = (
|
||||
payment_objs and
|
||||
any(p['provider'] == 'free' for p in payment_requests) and
|
||||
order.pending_sum == Decimal('0.00') and
|
||||
not order.require_approval
|
||||
)
|
||||
if free_order_flow:
|
||||
try:
|
||||
payment.confirm(send_mail=False, lock=not locked)
|
||||
for p in payment_objs:
|
||||
if p.provider == 'free':
|
||||
p.confirm(send_mail=False, lock=not locked)
|
||||
except Quota.QuotaExceededException:
|
||||
pass
|
||||
|
||||
invoice = order.invoices.last() # Might be generated by plugin already
|
||||
if not invoice and invoice_qualified(order):
|
||||
if event.settings.get('invoice_generate') == 'True' or (
|
||||
event.settings.get('invoice_generate') == 'paid' and payment.payment_provider.requires_invoice_immediately):
|
||||
event.settings.get('invoice_generate') == 'paid' and any(p['pprov'].requires_invoice_immediately for p in payment_requests)):
|
||||
invoice = generate_invoice(
|
||||
order,
|
||||
trigger_pdf=not event.settings.invoice_email_attachment or not order.email
|
||||
@@ -1084,7 +1092,7 @@ def _perform_order(event: Event, payment_provider: str, position_ids: List[str],
|
||||
subject_attendees_template = event.settings.mail_subject_order_placed_attendee
|
||||
|
||||
if sales_channel in event.settings.mail_sales_channel_placed_paid:
|
||||
_order_placed_email(event, order, pprov, email_template, subject_template, log_entry, invoice, payment,
|
||||
_order_placed_email(event, order, email_template, subject_template, log_entry, invoice, payment_objs,
|
||||
is_free=free_order_flow)
|
||||
if email_attendees:
|
||||
for p in order.positions.all():
|
||||
@@ -1092,7 +1100,33 @@ def _perform_order(event: Event, payment_provider: str, position_ids: List[str],
|
||||
_order_placed_email_attendee(event, order, p, email_attendees_template, subject_attendees_template, log_entry,
|
||||
is_free=free_order_flow)
|
||||
|
||||
return order.id
|
||||
warnings = []
|
||||
any_failed = False
|
||||
for p in payment_objs:
|
||||
if not p.payment_provider.execute_payment_needs_user:
|
||||
try:
|
||||
p.process_initiated = True
|
||||
p.save(update_fields=['process_initiated'])
|
||||
resp = p.payment_provider.execute_payment(None, p)
|
||||
if isinstance(resp, str):
|
||||
logger.warning('Payment provider returned URL from execute_payment even though execute_payment_needs_user is not set')
|
||||
except PaymentException as e:
|
||||
warnings.append(str(e))
|
||||
any_failed = True
|
||||
except Exception:
|
||||
logger.exception('Error during payment attempt')
|
||||
|
||||
if any_failed:
|
||||
# Cancel all other payments because their amount might be wrong now.
|
||||
for p in payment_objs:
|
||||
if p.state == OrderPayment.PAYMENT_STATE_CREATED:
|
||||
p.state = OrderPayment.PAYMENT_STATE_CANCELED
|
||||
p.save(update_fields=['state'])
|
||||
|
||||
return {
|
||||
'order_id': order.id,
|
||||
'warnings': warnings,
|
||||
}
|
||||
|
||||
|
||||
@receiver(signal=periodic_task)
|
||||
@@ -2394,14 +2428,14 @@ class OrderChangeManager:
|
||||
|
||||
|
||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
|
||||
def perform_order(self, event: Event, payment_provider: str, positions: List[str],
|
||||
def perform_order(self, event: Event, payments: List[dict], positions: List[str],
|
||||
email: str=None, locale: str=None, address: int=None, meta_info: dict=None,
|
||||
sales_channel: str='web', gift_cards: list=None, shown_total=None, customer=None):
|
||||
sales_channel: str='web', shown_total=None, customer=None):
|
||||
with language(locale):
|
||||
try:
|
||||
try:
|
||||
return _perform_order(event, payment_provider, positions, email, locale, address, meta_info,
|
||||
sales_channel, gift_cards, shown_total, customer)
|
||||
return _perform_order(event, payments, positions, email, locale, address, meta_info,
|
||||
sales_channel, shown_total, customer)
|
||||
except LockTimeoutException:
|
||||
self.retry()
|
||||
except (MaxRetriesExceededError, LockTimeoutException):
|
||||
|
||||
@@ -307,7 +307,7 @@ The ``sender`` keyword argument will contain an organizer.
|
||||
validate_order = EventPluginSignal(
|
||||
)
|
||||
"""
|
||||
Arguments: ``payment_provider``, ``positions``, ``email``, ``locale``, ``invoice_address``,
|
||||
Arguments: ``payments``, ``positions``, ``email``, ``locale``, ``invoice_address``,
|
||||
``meta_info``, ``customer``
|
||||
|
||||
This signal is sent out when the user tries to confirm the order, before we actually create
|
||||
@@ -316,6 +316,9 @@ but you can raise an OrderError with an appropriate exception message if you lik
|
||||
the order. We strongly discourage making changes to the order here.
|
||||
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
|
||||
**DEPRECTATION:** Stop listening to the ``payment_provider`` attribute, it will be removed
|
||||
in the future, as the ``payments`` attribute gives more information.
|
||||
"""
|
||||
|
||||
validate_cart = EventPluginSignal()
|
||||
@@ -564,7 +567,7 @@ an OrderedDict of (setting name, form field).
|
||||
|
||||
order_fee_calculation = EventPluginSignal()
|
||||
"""
|
||||
Arguments: ``positions``, ``invoice_address``, ``meta_info``, ``total``, ``gift_cards``
|
||||
Arguments: ``positions``, ``invoice_address``, ``meta_info``, ``total``, ``gift_cards``, ``payment_requests``
|
||||
|
||||
This signals allows you to add fees to an order while it is being created. You are expected to
|
||||
return a list of ``OrderFee`` objects that are not yet saved to the database
|
||||
@@ -574,8 +577,10 @@ As with all plugin signals, the ``sender`` keyword argument will contain the eve
|
||||
argument will contain the cart positions and ``invoice_address`` the invoice address (useful for
|
||||
tax calculation). The argument ``meta_info`` contains the order's meta dictionary. The ``total``
|
||||
keyword argument will contain the total cart sum without any fees. You should not rely on this
|
||||
``total`` value for fee calculations as other fees might interfere. The ``gift_cards`` argument lists
|
||||
the gift cards in use.
|
||||
``total`` value for fee calculations as other fees might interfere. The ``gift_cards`` argument
|
||||
lists the gift cards in use.
|
||||
|
||||
**DEPRECTATION:** Stop listening to the ``gift_cards`` attribute, it will be removed in the future.
|
||||
"""
|
||||
|
||||
order_fee_type_name = EventPluginSignal()
|
||||
|
||||
@@ -1171,8 +1171,8 @@ class MailSettingsForm(SettingsForm):
|
||||
widget=I18nTextarea,
|
||||
)
|
||||
base_context = {
|
||||
'mail_text_order_placed': ['event', 'order', 'payment'],
|
||||
'mail_subject_order_placed': ['event', 'order', 'payment'],
|
||||
'mail_text_order_placed': ['event', 'order', 'payments'],
|
||||
'mail_subject_order_placed': ['event', 'order', 'payments'],
|
||||
'mail_text_order_placed_attendee': ['event', 'order', 'position'],
|
||||
'mail_subject_order_placed_attendee': ['event', 'order', 'position'],
|
||||
'mail_text_order_placed_require_approval': ['event', 'order'],
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{% load i18n %}
|
||||
|
||||
<p>
|
||||
{% blocktrans %}
|
||||
Your gift card will be used to pay for this order. If the credit on the gift card is lower than the order total, you will be able to pay the
|
||||
{% blocktrans trimmed with card=info_data.gift_card_secret %}
|
||||
Your gift card {{ card }} will be used to pay for this order. If the credit on the gift card is lower than the order total, you will be able to pay the
|
||||
difference with a different payment method. If the credit is higher than the order total, you will be able to re-use the gift card in the future.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
@@ -495,7 +495,7 @@ class PaypalMethod(BasePaymentProvider):
|
||||
def abort_pending_allowed(self):
|
||||
return False
|
||||
|
||||
def _create_paypal_order(self, request, payment=None, cart=None):
|
||||
def _create_paypal_order(self, request, payment=None, cart_total=None):
|
||||
self.init_api()
|
||||
kwargs = {}
|
||||
if request.resolver_match and 'cart_namespace' in request.resolver_match.kwargs:
|
||||
@@ -510,7 +510,7 @@ class PaypalMethod(BasePaymentProvider):
|
||||
else:
|
||||
payee = {}
|
||||
|
||||
if payment and not cart:
|
||||
if payment and not cart_total:
|
||||
value = self.format_price(payment.amount)
|
||||
currency = payment.order.event.currency
|
||||
description = '{prefix}{orderstring}{postfix}'.format(
|
||||
@@ -528,8 +528,8 @@ class PaypalMethod(BasePaymentProvider):
|
||||
postfix=' {}'.format(self.settings.postfix) if self.settings.postfix else ''
|
||||
)
|
||||
request.session['payment_paypal_payment'] = payment.pk
|
||||
elif cart and not payment:
|
||||
value = self.format_price(cart['cart_total'] + cart['cart_fees'] + cart['payment_fee'])
|
||||
elif cart_total and not payment:
|
||||
value = self.format_price(cart_total)
|
||||
currency = request.event.currency
|
||||
description = __('Event tickets for {event}').format(event=request.event.name)
|
||||
custom_id = '{prefix}{slug}{postfix}'.format(
|
||||
|
||||
@@ -68,6 +68,7 @@ from pretix.plugins.paypal2.client.customer.partners_merchantintegrations_get_re
|
||||
from pretix.plugins.paypal2.payment import PaypalMethod, PaypalMethod as Paypal
|
||||
from pretix.plugins.paypal.models import ReferencedPayPalObject
|
||||
from pretix.presale.views import get_cart, get_cart_total
|
||||
from pretix.presale.views.cart import cart_session
|
||||
|
||||
logger = logging.getLogger('pretix.plugins.paypal2')
|
||||
|
||||
@@ -142,26 +143,27 @@ class XHRView(View):
|
||||
else:
|
||||
fee = prov.calculate_fee(order.pending_sum)
|
||||
|
||||
cart = {
|
||||
'positions': order.positions,
|
||||
'cart_total': order.pending_sum,
|
||||
'cart_fees': Decimal('0.00'),
|
||||
'payment_fee': fee,
|
||||
}
|
||||
cart_total = order.pending_sum + fee
|
||||
else:
|
||||
cart_total = get_cart_total(request)
|
||||
cart_fees = Decimal('0.00')
|
||||
for fee in get_fees(request.event, request, cart_total, None, None, get_cart(request)):
|
||||
cart_fees += fee.value
|
||||
cart_payments = cart_session(request).get('payments', [])
|
||||
for fee in get_fees(request.event, request, cart_total, None, cart_payments, get_cart(request)):
|
||||
cart_total += fee.value
|
||||
|
||||
cart = {
|
||||
'positions': get_cart(request),
|
||||
'cart_total': cart_total,
|
||||
'cart_fees': cart_fees,
|
||||
'payment_fee': prov.calculate_fee(cart_total + cart_fees),
|
||||
}
|
||||
total_remaining = cart_total
|
||||
for p in cart_session(request).get('payments', []):
|
||||
if p['provider'] != 'paypal':
|
||||
if p.get('min_value') and total_remaining < Decimal(p['min_value']):
|
||||
continue
|
||||
|
||||
paypal_order = prov._create_paypal_order(request, None, cart)
|
||||
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']))
|
||||
total_remaining -= to_pay
|
||||
|
||||
cart_total = total_remaining
|
||||
|
||||
paypal_order = prov._create_paypal_order(request, None, cart_total)
|
||||
r = JsonResponse(paypal_order.dict() if paypal_order else {})
|
||||
r._csp_ignore = True
|
||||
return r
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -184,9 +184,10 @@ You will receive the request triggering the order creation as the ``request`` ke
|
||||
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
checkout_confirm_page_content = EventPluginSignal()
|
||||
"""
|
||||
Arguments: 'request'
|
||||
Arguments: ``request``
|
||||
|
||||
This signals allows you to add HTML content to the confirmation page that is presented at the
|
||||
end of the checkout process, just before the order is being created.
|
||||
@@ -197,7 +198,7 @@ argument will contain the request object.
|
||||
|
||||
fee_calculation_for_cart = EventPluginSignal()
|
||||
"""
|
||||
Arguments: 'request', 'invoice_address', 'total', 'positions'
|
||||
Arguments: ``request``, ``invoice_address``, ``total``, ``positions``, ``payment_requetss``
|
||||
|
||||
This signals allows you to add fees to a cart. You are expected to return a list of ``OrderFee``
|
||||
objects that are not yet saved to the database (because there is no order yet).
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "pretixpresale/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load money %}
|
||||
{% load eventurl %}
|
||||
{% load eventsignal %}
|
||||
{% block title %}{% trans "Review order" %}{% endblock %}
|
||||
@@ -37,7 +38,7 @@
|
||||
{% include "pretixpresale/event/fragment_cart.html" with cart=cart event=request.event editable=False %}
|
||||
</div>
|
||||
</div>
|
||||
{% if payment_provider %}
|
||||
{% if payments %}
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">
|
||||
{% if payment_provider.identifier != "free" %}
|
||||
@@ -52,9 +53,25 @@
|
||||
{% trans "Payment" %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{{ payment }}
|
||||
</div>
|
||||
<ul class="list-group">
|
||||
{% for payment, rendered_block in payments %}
|
||||
<li class="list-group-item payment">
|
||||
{% if payments|length > 1 %}
|
||||
<div class="row">
|
||||
<div class="col-sm-10 col-xs-12">
|
||||
<h4>{{ payment.provider_name }}</h4>
|
||||
{{ rendered_block }}
|
||||
</div>
|
||||
<div class="col-sm-2 col-xs-12 text-right">
|
||||
<h4>{{ payment.payment_amount|money:request.event.currency }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{{ rendered_block }}
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% eventsignal event "pretix.presale.signals.checkout_confirm_page_content" request=request %}
|
||||
|
||||
@@ -4,73 +4,120 @@
|
||||
{% load bootstrap3 %}
|
||||
{% load rich_text %}
|
||||
{% block inner %}
|
||||
<p>{% trans "Please select how you want to pay." %}</p>
|
||||
{% if event.settings.payment_explanation %}
|
||||
{{ event.settings.payment_explanation|rich_text }}
|
||||
{% if current_payments %}
|
||||
<p>{% trans "You already selected the following payment methods:" %}</p>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="list-group">
|
||||
{% for p in current_payments %}
|
||||
<div class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col-xs-9">
|
||||
{{ p.provider_name }}
|
||||
</div>
|
||||
<div class="col-xs-2 text-right">
|
||||
{{ p.payment_amount|money:request.event.currency }}
|
||||
</div>
|
||||
<div class="col-xs-1 text-right">
|
||||
<button name="remove_payment" value="{{ p.id }}" title="{% trans "Remove payment" %}"
|
||||
class="btn btn-link btn-xs">
|
||||
<span class="fa fa-trash text-danger"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if remaining %}
|
||||
<div class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col-xs-9">
|
||||
<strong>{% trans "Remaining balance" %}</strong><br>
|
||||
<span class="text-muted">{% trans "Please select a payment method below." %}</span>
|
||||
</div>
|
||||
<div class="col-xs-2 text-right">
|
||||
<strong>
|
||||
{{ remaining|money:request.event.currency }}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% if remaining %}
|
||||
<p>{% trans "Please select how you want to pay the remaining balance:" %}</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p>{% trans "Please select how you want to pay." %}</p>
|
||||
{% endif %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="panel-group" id="payment_accordion">
|
||||
{% for p in providers %}
|
||||
<div class="panel panel-default" data-total="{{ p.total|floatformat:2 }}">
|
||||
<div class="accordion-radio">
|
||||
<div class="panel-heading">
|
||||
<p class="panel-title">
|
||||
{% if show_fees %}
|
||||
<strong class="pull-right flip">{% if p.fee < 0 %}-{% else %}+{% endif %} {{ p.fee|money:event.currency|cut:"-" }}</strong>
|
||||
{% endif %}
|
||||
<input type="radio" name="payment" value="{{ p.provider.identifier }}"
|
||||
title="{{ p.provider.public_name }}"
|
||||
data-parent="#payment_accordion"
|
||||
{% if selected == p.provider.identifier %}checked="checked"{% endif %}
|
||||
id="input_payment_{{ p.provider.identifier }}"
|
||||
aria-describedby="payment_{{ p.provider.identifier }}"
|
||||
data-toggle="radiocollapse" data-target="#payment_{{ p.provider.identifier }}"/>
|
||||
<label for="input_payment_{{ p.provider.identifier }}"><strong>{{ p.provider.public_name }}</strong></label>
|
||||
</p>
|
||||
{% if not current_payments or remaining %}
|
||||
{% if event.settings.payment_explanation %}
|
||||
{{ event.settings.payment_explanation|rich_text }}
|
||||
{% endif %}
|
||||
<div class="panel-group" id="payment_accordion">
|
||||
{% for p in providers %}
|
||||
<div class="panel panel-default">
|
||||
<div class="accordion-radio">
|
||||
<div class="panel-heading">
|
||||
<p class="panel-title">
|
||||
{% if show_fees %}
|
||||
<strong class="pull-right flip">{% if p.fee < 0 %}-{% else %}+{% endif %} {{ p.fee|money:event.currency|cut:"-" }}</strong>
|
||||
{% endif %}
|
||||
<input type="radio" name="payment" value="{{ p.provider.identifier }}"
|
||||
title="{{ p.provider.public_name }}"
|
||||
data-parent="#payment_accordion"
|
||||
{% if selected == p.provider.identifier %}checked="checked"{% endif %}
|
||||
id="input_payment_{{ p.provider.identifier }}"
|
||||
aria-describedby="payment_{{ p.provider.identifier }}"
|
||||
data-toggle="radiocollapse" data-target="#payment_{{ p.provider.identifier }}"/>
|
||||
<label for="input_payment_{{ p.provider.identifier }}"><strong>{{ p.provider.public_name }}</strong></label>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="payment_{{ p.provider.identifier }}"
|
||||
class="panel-collapse collapsed {% if selected == p.provider.identifier %}in{% endif %}">
|
||||
<div class="panel-body form-horizontal">
|
||||
{% if request.event.testmode %}
|
||||
{% if p.provider.test_mode_message %}
|
||||
<div class="alert alert-info">
|
||||
<p>{{ p.provider.test_mode_message }}</p>
|
||||
</div>
|
||||
{% if not request.sales_channel.testmode_supported %}
|
||||
<div class="alert alert-danger">
|
||||
<div id="payment_{{ p.provider.identifier }}"
|
||||
class="panel-collapse collapsed {% if selected == p.provider.identifier %}in{% endif %}">
|
||||
<div class="panel-body form-horizontal">
|
||||
{% if request.event.testmode %}
|
||||
{% if p.provider.test_mode_message %}
|
||||
<div class="alert alert-info">
|
||||
<p>{{ p.provider.test_mode_message }}</p>
|
||||
</div>
|
||||
{% if not request.sales_channel.testmode_supported %}
|
||||
<div class="alert alert-danger">
|
||||
<p>
|
||||
{% trans "This sales channel does not provide support for test mode." %}
|
||||
<strong>
|
||||
{% trans "If you continue, you might pay an actual order with non-existing money!" %}
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="alert alert-warning">
|
||||
<p>
|
||||
{% trans "This sales channel does not provide support for test mode." %}
|
||||
{% trans "This payment provider does not provide support for test mode." %}
|
||||
<strong>
|
||||
{% trans "If you continue, you might pay an actual order with non-existing money!" %}
|
||||
{% trans "If you continue, actual money might be transferred." %}
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="alert alert-warning">
|
||||
<p>
|
||||
{% trans "This payment provider does not provide support for test mode." %}
|
||||
<strong>
|
||||
{% trans "If you continue, actual money might be transferred." %}
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{{ p.form }}
|
||||
{{ p.form }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if not providers %}
|
||||
<p><em>{% trans "There are no payment providers enabled." %}</em></p>
|
||||
{% if not event.live %}
|
||||
<p>{% trans "Please go to the payment settings and activate one or more payment providers." %}</p>
|
||||
{% endfor %}
|
||||
{% if not providers %}
|
||||
<p><em>{% trans "There are no payment providers enabled." %}</em></p>
|
||||
{% if not event.live %}
|
||||
<p>{% trans "Please go to the payment settings and activate one or more payment providers." %}</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row checkout-button-row">
|
||||
<div class="col-md-4 col-sm-6">
|
||||
<a class="btn btn-block btn-default btn-lg"
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
# 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
|
||||
@@ -39,9 +39,11 @@ from functools import wraps
|
||||
from itertools import groupby
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.db.models import Exists, OuterRef, Prefetch, Sum
|
||||
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 language
|
||||
@@ -50,6 +52,7 @@ from pretix.base.models import (
|
||||
QuestionAnswer, QuestionOption,
|
||||
)
|
||||
from pretix.base.services.cart import get_fees
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.helpers.cookies import set_cookie_without_samesite
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
from pretix.presale.signals import question_form_fields
|
||||
@@ -103,7 +106,7 @@ class CartMixin:
|
||||
def invoice_address(self):
|
||||
return cached_invoice_address(self.request)
|
||||
|
||||
def get_cart(self, answers=False, queryset=None, order=None, downloads=False):
|
||||
def get_cart(self, answers=False, queryset=None, order=None, downloads=False, payments=None):
|
||||
if queryset is not None:
|
||||
prefetch = []
|
||||
if answers:
|
||||
@@ -196,7 +199,8 @@ class CartMixin:
|
||||
fees = order.fees.all()
|
||||
elif positions:
|
||||
fees = get_fees(
|
||||
self.request.event, self.request, total, self.invoice_address, self.cart_session.get('payment'),
|
||||
self.request.event, self.request, total, self.invoice_address,
|
||||
payments if payments is not None else self.cart_session.get('payments', []),
|
||||
cartpos
|
||||
)
|
||||
else:
|
||||
@@ -233,6 +237,57 @@ class CartMixin:
|
||||
'itemcount': sum(c.count for c in positions if not c.addon_to)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
def _remove_payment(self, payment_id):
|
||||
self.cart_session['payments'] = [p for p in self.cart_session['payments'] if p.get('id') != payment_id]
|
||||
|
||||
|
||||
def cart_exists(request):
|
||||
from pretix.presale.views.cart import get_or_create_cart_id
|
||||
@@ -334,7 +389,7 @@ def get_cart_is_free(request):
|
||||
pos = get_cart(request)
|
||||
ia = get_cart_invoice_address(request)
|
||||
total = get_cart_total(request)
|
||||
fees = get_fees(request.event, request, total, ia, cs.get('payment'), pos)
|
||||
fees = get_fees(request.event, request, total, ia, cs.get('payments', []), pos)
|
||||
request._cart_free_cache = total + sum(f.value for f in fees) == Decimal('0.00')
|
||||
return request._cart_free_cache
|
||||
|
||||
|
||||
@@ -208,6 +208,14 @@ class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TicketPageMixin,
|
||||
self.kwargs = kwargs
|
||||
if not self.order:
|
||||
raise Http404(_('Unknown order code or not authorized to access this order.'))
|
||||
if self.order.status == Order.STATUS_PENDING:
|
||||
payment_to_complete = self.order.payments.filter(state=OrderPayment.PAYMENT_STATE_CREATED, process_initiated=False).first()
|
||||
if payment_to_complete:
|
||||
return redirect(eventreverse(self.request.event, 'presale:event.order.pay.complete', kwargs={
|
||||
'order': self.order.code,
|
||||
'secret': self.order.secret,
|
||||
'payment': payment_to_complete.pk
|
||||
}))
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
@cached_property
|
||||
@@ -472,6 +480,8 @@ class OrderPaymentConfirm(EventViewMixin, OrderDetailMixin, TemplateView):
|
||||
'invoice': i.pk
|
||||
})
|
||||
messages.success(self.request, _('An invoice has been generated.'))
|
||||
self.payment.process_initiated = True
|
||||
self.payment.save(update_fields=['process_initiated'])
|
||||
resp = self.payment.payment_provider.execute_payment(request, self.payment)
|
||||
except PaymentException as e:
|
||||
messages.error(request, str(e))
|
||||
@@ -532,6 +542,8 @@ class OrderPaymentComplete(EventViewMixin, OrderDetailMixin, View):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
try:
|
||||
self.payment.process_initiated = True
|
||||
self.payment.save(update_fields=['process_initiated'])
|
||||
resp = self.payment.payment_provider.execute_payment(request, self.payment)
|
||||
except PaymentException as e:
|
||||
messages.error(request, str(e))
|
||||
|
||||
@@ -519,7 +519,17 @@ def test_use_membership(event, customer, membership, requiring_ticket):
|
||||
used_membership=membership
|
||||
)
|
||||
order = _create_order(event, email='dummy@example.org', positions=[cp1],
|
||||
now_dt=now(), payment_provider=BankTransfer(event),
|
||||
now_dt=now(),
|
||||
payment_requests=[{
|
||||
"id": "test0",
|
||||
"provider": "banktransfer",
|
||||
"max_value": None,
|
||||
"min_value": None,
|
||||
"multi_use_supported": False,
|
||||
"info_data": {},
|
||||
"payment_amount": Decimal("23.00"),
|
||||
"pprov": BankTransfer(event)
|
||||
}],
|
||||
locale='de', customer=customer)[0]
|
||||
assert order.positions.first().used_membership == membership
|
||||
|
||||
@@ -535,7 +545,15 @@ def test_use_membership_invalid(event, customer, membership, requiring_ticket):
|
||||
)
|
||||
with pytest.raises(OrderError) as excinfo:
|
||||
_perform_order(event, email='dummy@example.org', position_ids=[cp1.pk],
|
||||
payment_provider='banktransfer', address=None,
|
||||
payment_requests=[{
|
||||
"id": "test0",
|
||||
"provider": "banktransfer",
|
||||
"max_value": None,
|
||||
"min_value": None,
|
||||
"multi_use_supported": False,
|
||||
"info_data": {},
|
||||
}],
|
||||
address=None,
|
||||
locale='de', customer=customer.pk)[0]
|
||||
assert 'membership' in str(excinfo.value)
|
||||
|
||||
@@ -543,12 +561,22 @@ def test_use_membership_invalid(event, customer, membership, requiring_ticket):
|
||||
@pytest.mark.django_db
|
||||
def test_grant_when_paid_and_changed(event, customer, granting_ticket):
|
||||
cp1 = CartPosition.objects.create(
|
||||
item=granting_ticket, price=0, expires=now() + timedelta(days=1), event=event, cart_id="123",
|
||||
item=granting_ticket, price=23, expires=now() + timedelta(days=1), event=event, cart_id="123",
|
||||
)
|
||||
q = event.quotas.create(size=None, name="foo")
|
||||
q.items.add(granting_ticket)
|
||||
order = _create_order(event, email='dummy@example.org', positions=[cp1],
|
||||
now_dt=now(), payment_provider=BankTransfer(event),
|
||||
now_dt=now(),
|
||||
payment_requests=[{
|
||||
"id": "test0",
|
||||
"provider": "banktransfer",
|
||||
"max_value": None,
|
||||
"min_value": None,
|
||||
"multi_use_supported": False,
|
||||
"info_data": {},
|
||||
"pprov": BankTransfer(event),
|
||||
"payment_amount": Decimal("23.00"),
|
||||
}],
|
||||
locale='de', customer=customer)[0]
|
||||
assert not customer.memberships.exists()
|
||||
|
||||
|
||||
@@ -788,7 +788,18 @@ class VoucherTestCase(BaseQuotaTestCase):
|
||||
self.assertTrue(v.is_in_cart())
|
||||
self.assertFalse(v.is_ordered())
|
||||
|
||||
order = perform_order(event=self.event.id, payment_provider='free', positions=[cart.id])
|
||||
order = perform_order(
|
||||
event=self.event.id,
|
||||
positions=[cart.id],
|
||||
payments=[{
|
||||
"id": "test0",
|
||||
"provider": "free",
|
||||
"max_value": None,
|
||||
"min_value": None,
|
||||
"multi_use_supported": False,
|
||||
"info_data": {},
|
||||
}],
|
||||
)
|
||||
v.refresh_from_db()
|
||||
self.assertFalse(v.is_active())
|
||||
self.assertFalse(v.is_in_cart())
|
||||
@@ -797,10 +808,23 @@ class VoucherTestCase(BaseQuotaTestCase):
|
||||
# assert that the voucher cannot be reused
|
||||
cart = CartPosition.objects.create(event=self.event, item=self.item1, price=self.item1.default_price,
|
||||
expires=now() + timedelta(days=3), voucher=v)
|
||||
self.assertRaises(OrderError, perform_order, event=self.event.id, payment_provider='free', positions=[cart.id])
|
||||
self.assertRaises(
|
||||
OrderError,
|
||||
perform_order,
|
||||
event=self.event.id,
|
||||
positions=[cart.id],
|
||||
payments=[{
|
||||
"id": "test0",
|
||||
"provider": "free",
|
||||
"max_value": None,
|
||||
"min_value": None,
|
||||
"multi_use_supported": False,
|
||||
"info_data": {},
|
||||
}],
|
||||
)
|
||||
|
||||
# assert that the voucher can be re-used after cancelling the successful order
|
||||
cancel_order(order)
|
||||
cancel_order(order['order_id'])
|
||||
v.refresh_from_db()
|
||||
self.assertTrue(v.is_active())
|
||||
self.assertFalse(v.is_in_cart())
|
||||
@@ -808,7 +832,18 @@ class VoucherTestCase(BaseQuotaTestCase):
|
||||
|
||||
cart = CartPosition.objects.create(event=self.event, item=self.item1, price=self.item1.default_price,
|
||||
expires=now() + timedelta(days=3), voucher=v)
|
||||
perform_order(event=self.event.id, payment_provider='free', positions=[cart.id])
|
||||
perform_order(
|
||||
event=self.event.id,
|
||||
positions=[cart.id],
|
||||
payments=[{
|
||||
"id": "test0",
|
||||
"provider": "free",
|
||||
"max_value": None,
|
||||
"min_value": None,
|
||||
"multi_use_supported": False,
|
||||
"info_data": {},
|
||||
}],
|
||||
)
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_voucher_applicability_quota(self):
|
||||
@@ -2037,7 +2072,8 @@ class EventTest(TestCase):
|
||||
"and": [
|
||||
{"isBefore": [{"var": "now"}, {"buildTime": ["date_from"]}, None]},
|
||||
{"inList": [{"var": "product"}, {"objectList": [{"lookup": ["product", str(i1new.pk), "Text"]}]}]},
|
||||
{"inList": [{"var": "variation"}, {"objectList": [{"lookup": ["variation", str(i1new.variations.get().pk), "Text"]}]}]}
|
||||
{"inList": [{"var": "variation"},
|
||||
{"objectList": [{"lookup": ["variation", str(i1new.variations.get().pk), "Text"]}]}]}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2122,7 +2158,8 @@ class EventTest(TestCase):
|
||||
date_from=now()
|
||||
)
|
||||
q = Quota.objects.create(event=event, name='Quota', size=2)
|
||||
item = Item.objects.create(event=event, name='Early-bird ticket', default_price=0, active=True, available_until=now() - timedelta(days=1))
|
||||
item = Item.objects.create(event=event, name='Early-bird ticket', default_price=0, active=True,
|
||||
available_until=now() - timedelta(days=1))
|
||||
q.items.add(item)
|
||||
assert Event.annotated(Event.objects).first().active_quotas == []
|
||||
|
||||
|
||||
@@ -35,12 +35,14 @@ from tests.testdummy.signals import FoobazSalesChannel
|
||||
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models import (
|
||||
CartPosition, Event, InvoiceAddress, Item, Order, OrderPosition, Organizer,
|
||||
SeatingPlan,
|
||||
CartPosition, Event, GiftCard, InvoiceAddress, Item, Order, OrderPosition,
|
||||
Organizer, SeatingPlan,
|
||||
)
|
||||
from pretix.base.models.items import SubEventItem
|
||||
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
|
||||
from pretix.base.payment import FreeOrderProvider
|
||||
from pretix.base.payment import (
|
||||
FreeOrderProvider, GiftCardPayment, PaymentException,
|
||||
)
|
||||
from pretix.base.reldate import RelativeDate, RelativeDateWrapper
|
||||
from pretix.base.services.invoices import generate_invoice
|
||||
from pretix.base.services.orders import (
|
||||
@@ -76,7 +78,16 @@ def test_expiry_days(event):
|
||||
event.settings.set('payment_term_days', 5)
|
||||
event.settings.set('payment_term_weekdays', False)
|
||||
order = _create_order(event, email='dummy@example.org', positions=[],
|
||||
now_dt=today, payment_provider=FreeOrderProvider(event),
|
||||
now_dt=today,
|
||||
payment_requests=[{
|
||||
"id": "test0",
|
||||
"provider": "free",
|
||||
"max_value": None,
|
||||
"min_value": None,
|
||||
"multi_use_supported": False,
|
||||
"info_data": {},
|
||||
"pprov": FreeOrderProvider(event),
|
||||
}],
|
||||
locale='de')[0]
|
||||
assert (order.expires - today).days == 5
|
||||
|
||||
@@ -87,14 +98,32 @@ def test_expiry_weekdays(event):
|
||||
event.settings.set('payment_term_days', 5)
|
||||
event.settings.set('payment_term_weekdays', True)
|
||||
order = _create_order(event, email='dummy@example.org', positions=[],
|
||||
now_dt=today, payment_provider=FreeOrderProvider(event),
|
||||
now_dt=today,
|
||||
payment_requests=[{
|
||||
"id": "test0",
|
||||
"provider": "free",
|
||||
"max_value": None,
|
||||
"min_value": None,
|
||||
"multi_use_supported": False,
|
||||
"info_data": {},
|
||||
"pprov": FreeOrderProvider(event),
|
||||
}],
|
||||
locale='de')[0]
|
||||
assert (order.expires - today).days == 6
|
||||
assert order.expires.weekday() == 0
|
||||
|
||||
today = make_aware(datetime(2016, 9, 19, 15, 0, 0, 0))
|
||||
order = _create_order(event, email='dummy@example.org', positions=[],
|
||||
now_dt=today, payment_provider=FreeOrderProvider(event),
|
||||
now_dt=today,
|
||||
payment_requests=[{
|
||||
"id": "test0",
|
||||
"provider": "free",
|
||||
"max_value": None,
|
||||
"min_value": None,
|
||||
"multi_use_supported": False,
|
||||
"info_data": {},
|
||||
"pprov": FreeOrderProvider(event),
|
||||
}],
|
||||
locale='de')[0]
|
||||
assert (order.expires - today).days == 7
|
||||
assert order.expires.weekday() == 0
|
||||
@@ -108,7 +137,16 @@ def test_expiry_minutes(event):
|
||||
event.settings.set('payment_term_minutes', 30)
|
||||
event.settings.set('payment_term_weekdays', False)
|
||||
order = _create_order(event, email='dummy@example.org', positions=[],
|
||||
now_dt=today, payment_provider=FreeOrderProvider(event),
|
||||
now_dt=today,
|
||||
payment_requests=[{
|
||||
"id": "test0",
|
||||
"provider": "free",
|
||||
"max_value": None,
|
||||
"min_value": None,
|
||||
"multi_use_supported": False,
|
||||
"info_data": {},
|
||||
"pprov": FreeOrderProvider(event),
|
||||
}],
|
||||
locale='de')[0]
|
||||
assert (order.expires - today).days == 0
|
||||
assert (order.expires - today).seconds == 30 * 60
|
||||
@@ -121,12 +159,30 @@ def test_expiry_last(event):
|
||||
event.settings.set('payment_term_weekdays', False)
|
||||
event.settings.set('payment_term_last', now() + timedelta(days=3))
|
||||
order = _create_order(event, email='dummy@example.org', positions=[],
|
||||
now_dt=today, payment_provider=FreeOrderProvider(event),
|
||||
now_dt=today,
|
||||
payment_requests=[{
|
||||
"id": "test0",
|
||||
"provider": "free",
|
||||
"max_value": None,
|
||||
"min_value": None,
|
||||
"multi_use_supported": False,
|
||||
"info_data": {},
|
||||
"pprov": FreeOrderProvider(event),
|
||||
}],
|
||||
locale='de')[0]
|
||||
assert (order.expires - today).days == 3
|
||||
event.settings.set('payment_term_last', now() + timedelta(days=7))
|
||||
order = _create_order(event, email='dummy@example.org', positions=[],
|
||||
now_dt=today, payment_provider=FreeOrderProvider(event),
|
||||
now_dt=today,
|
||||
payment_requests=[{
|
||||
"id": "test0",
|
||||
"provider": "free",
|
||||
"max_value": None,
|
||||
"min_value": None,
|
||||
"multi_use_supported": False,
|
||||
"info_data": {},
|
||||
"pprov": FreeOrderProvider(event),
|
||||
}],
|
||||
locale='de')[0]
|
||||
assert (order.expires - today).days == 5
|
||||
|
||||
@@ -142,7 +198,16 @@ def test_expiry_last_relative(event):
|
||||
RelativeDate(days_before=2, time=None, base_date_name='date_from', minutes_before=None)
|
||||
))
|
||||
order = _create_order(event, email='dummy@example.org', positions=[],
|
||||
now_dt=today, payment_provider=FreeOrderProvider(event),
|
||||
now_dt=today,
|
||||
payment_requests=[{
|
||||
"id": "test0",
|
||||
"provider": "free",
|
||||
"max_value": None,
|
||||
"min_value": None,
|
||||
"multi_use_supported": False,
|
||||
"info_data": {},
|
||||
"pprov": FreeOrderProvider(event),
|
||||
}],
|
||||
locale='de')[0]
|
||||
assert (order.expires - today).days == 3
|
||||
|
||||
@@ -173,7 +238,16 @@ def test_expiry_last_relative_subevents(event):
|
||||
RelativeDate(days_before=2, time=None, base_date_name='date_from', minutes_before=None)
|
||||
))
|
||||
order = _create_order(event, email='dummy@example.org', positions=[cp1, cp2],
|
||||
now_dt=today, payment_provider=FreeOrderProvider(event),
|
||||
now_dt=today,
|
||||
payment_requests=[{
|
||||
"id": "test0",
|
||||
"provider": "free",
|
||||
"max_value": None,
|
||||
"min_value": None,
|
||||
"multi_use_supported": False,
|
||||
"info_data": {},
|
||||
"pprov": FreeOrderProvider(event),
|
||||
}],
|
||||
locale='de')[0]
|
||||
assert (order.expires - today).days == 6
|
||||
|
||||
@@ -185,7 +259,16 @@ def test_expiry_dst(event):
|
||||
utc = pytz.timezone('UTC')
|
||||
today = tz.localize(datetime(2016, 10, 29, 12, 0, 0)).astimezone(utc)
|
||||
order = _create_order(event, email='dummy@example.org', positions=[],
|
||||
now_dt=today, payment_provider=FreeOrderProvider(event),
|
||||
now_dt=today,
|
||||
payment_requests=[{
|
||||
"id": "test0",
|
||||
"provider": "free",
|
||||
"max_value": None,
|
||||
"min_value": None,
|
||||
"multi_use_supported": False,
|
||||
"info_data": {},
|
||||
"pprov": FreeOrderProvider(event),
|
||||
}],
|
||||
locale='de')[0]
|
||||
localex = order.expires.astimezone(tz)
|
||||
assert (localex.hour, localex.minute) == (23, 59)
|
||||
@@ -2832,7 +2915,16 @@ def test_autocheckin(clist_autocheckin, event):
|
||||
item=ticket, price=23, expires=now() + timedelta(days=1), event=event, cart_id="123"
|
||||
)
|
||||
order = _create_order(event, email='dummy@example.org', positions=[cp1],
|
||||
now_dt=today, payment_provider=FreeOrderProvider(event),
|
||||
now_dt=today,
|
||||
payment_requests=[{
|
||||
"id": "test0",
|
||||
"provider": "free",
|
||||
"max_value": None,
|
||||
"min_value": None,
|
||||
"multi_use_supported": False,
|
||||
"info_data": {},
|
||||
"pprov": FreeOrderProvider(event),
|
||||
}],
|
||||
locale='de')[0]
|
||||
assert "web" in clist_autocheckin.auto_checkin_sales_channels
|
||||
assert order.positions.first().checkins.first().auto_checked_in
|
||||
@@ -2841,7 +2933,16 @@ def test_autocheckin(clist_autocheckin, event):
|
||||
clist_autocheckin.save()
|
||||
|
||||
order = _create_order(event, email='dummy@example.org', positions=[cp1],
|
||||
now_dt=today, payment_provider=FreeOrderProvider(event),
|
||||
now_dt=today,
|
||||
payment_requests=[{
|
||||
"id": "test0",
|
||||
"provider": "free",
|
||||
"max_value": None,
|
||||
"min_value": None,
|
||||
"multi_use_supported": False,
|
||||
"info_data": {},
|
||||
"pprov": FreeOrderProvider(event),
|
||||
}],
|
||||
locale='de')[0]
|
||||
assert clist_autocheckin.auto_checkin_sales_channels == []
|
||||
assert order.positions.first().checkins.count() == 0
|
||||
@@ -2858,23 +2959,59 @@ def test_saleschannel_testmode_restriction(event):
|
||||
)
|
||||
|
||||
order = _create_order(event, email='dummy@example.org', positions=[cp1],
|
||||
now_dt=today, payment_provider=FreeOrderProvider(event),
|
||||
now_dt=today,
|
||||
payment_requests=[{
|
||||
"id": "test0",
|
||||
"provider": "free",
|
||||
"max_value": None,
|
||||
"min_value": None,
|
||||
"multi_use_supported": False,
|
||||
"info_data": {},
|
||||
"pprov": FreeOrderProvider(event),
|
||||
}],
|
||||
locale='de', sales_channel='web')[0]
|
||||
assert not order.testmode
|
||||
|
||||
order = _create_order(event, email='dummy@example.org', positions=[cp1],
|
||||
now_dt=today, payment_provider=FreeOrderProvider(event),
|
||||
now_dt=today,
|
||||
payment_requests=[{
|
||||
"id": "test0",
|
||||
"provider": "free",
|
||||
"max_value": None,
|
||||
"min_value": None,
|
||||
"multi_use_supported": False,
|
||||
"info_data": {},
|
||||
"pprov": FreeOrderProvider(event),
|
||||
}],
|
||||
locale='de', sales_channel=FoobazSalesChannel.identifier)[0]
|
||||
assert not order.testmode
|
||||
|
||||
event.testmode = True
|
||||
order = _create_order(event, email='dummy@example.org', positions=[cp1],
|
||||
now_dt=today, payment_provider=FreeOrderProvider(event),
|
||||
now_dt=today,
|
||||
payment_requests=[{
|
||||
"id": "test0",
|
||||
"provider": "free",
|
||||
"max_value": None,
|
||||
"min_value": None,
|
||||
"multi_use_supported": False,
|
||||
"info_data": {},
|
||||
"pprov": FreeOrderProvider(event),
|
||||
}],
|
||||
locale='de', sales_channel='web')[0]
|
||||
assert order.testmode
|
||||
|
||||
order = _create_order(event, email='dummy@example.org', positions=[cp1],
|
||||
now_dt=today, payment_provider=FreeOrderProvider(event),
|
||||
now_dt=today,
|
||||
payment_requests=[{
|
||||
"id": "test0",
|
||||
"provider": "free",
|
||||
"max_value": None,
|
||||
"min_value": None,
|
||||
"multi_use_supported": False,
|
||||
"info_data": {},
|
||||
"pprov": FreeOrderProvider(event),
|
||||
}],
|
||||
locale='de', sales_channel=FoobazSalesChannel.identifier)[0]
|
||||
assert not order.testmode
|
||||
|
||||
@@ -2890,13 +3027,44 @@ def test_giftcard_multiple(event):
|
||||
gc1.transactions.create(value=12)
|
||||
gc2 = event.organizer.issued_gift_cards.create(currency="EUR")
|
||||
gc2.transactions.create(value=12)
|
||||
order = _create_order(event, email='dummy@example.org', positions=[cp1],
|
||||
now_dt=now(), payment_provider=BankTransfer(event),
|
||||
locale='de', gift_cards=[gc1.pk, gc2.pk])[0]
|
||||
assert order.payments.count() == 3
|
||||
order = _create_order(
|
||||
event, email='dummy@example.org', positions=[cp1],
|
||||
now_dt=now(),
|
||||
payment_requests=[
|
||||
{
|
||||
"id": "test0",
|
||||
"provider": "giftcard",
|
||||
"max_value": "12.00",
|
||||
"min_value": None,
|
||||
"multi_use_supported": True,
|
||||
"info_data": {
|
||||
"gift_card": gc1.pk
|
||||
},
|
||||
"pprov": GiftCardPayment(event),
|
||||
},
|
||||
{
|
||||
"id": "test1",
|
||||
"provider": "giftcard",
|
||||
"max_value": "12.00",
|
||||
"min_value": None,
|
||||
"multi_use_supported": True,
|
||||
"info_data": {
|
||||
"gift_card": gc2.pk
|
||||
},
|
||||
"pprov": GiftCardPayment(event),
|
||||
},
|
||||
],
|
||||
locale='de'
|
||||
)[0]
|
||||
assert order.payments.count() == 2
|
||||
for p in order.payments.all():
|
||||
p.payment_provider.execute_payment(None, p)
|
||||
|
||||
assert order.payments.get(info__icontains=gc1.pk).amount == Decimal('12.00')
|
||||
assert order.payments.get(info__icontains=gc2.pk).amount == Decimal('11.00')
|
||||
gc1 = GiftCard.objects.get(pk=gc1.pk)
|
||||
assert gc1.value == 0
|
||||
gc2 = GiftCard.objects.get(pk=gc2.pk)
|
||||
assert gc2.value == 1
|
||||
|
||||
|
||||
@@ -2909,12 +3077,39 @@ def test_giftcard_partial(event):
|
||||
)
|
||||
gc1 = event.organizer.issued_gift_cards.create(currency="EUR")
|
||||
gc1.transactions.create(value=12)
|
||||
order = _create_order(event, email='dummy@example.org', positions=[cp1],
|
||||
now_dt=now(), payment_provider=BankTransfer(event),
|
||||
locale='de', gift_cards=[gc1.pk])[0]
|
||||
order = _create_order(
|
||||
event, email='dummy@example.org', positions=[cp1],
|
||||
now_dt=now(),
|
||||
payment_requests=[
|
||||
{
|
||||
"id": "test0",
|
||||
"provider": "giftcard",
|
||||
"max_value": "12.00",
|
||||
"min_value": None,
|
||||
"multi_use_supported": True,
|
||||
"info_data": {
|
||||
"gift_card": gc1.pk
|
||||
},
|
||||
"pprov": GiftCardPayment(event),
|
||||
},
|
||||
{
|
||||
"id": "test1",
|
||||
"provider": "banktransfer",
|
||||
"max_value": None,
|
||||
"min_value": None,
|
||||
"multi_use_supported": False,
|
||||
"info_data": {},
|
||||
"pprov": BankTransfer(event),
|
||||
},
|
||||
],
|
||||
locale='de'
|
||||
)[0]
|
||||
assert order.payments.count() == 2
|
||||
for p in order.payments.all():
|
||||
p.payment_provider.execute_payment(None, p)
|
||||
assert order.payments.get(info__icontains=gc1.pk).amount == Decimal('12.00')
|
||||
assert order.payments.get(provider='banktransfer').amount == Decimal('11.00')
|
||||
gc1 = GiftCard.objects.get(pk=gc1.pk)
|
||||
assert gc1.value == 0
|
||||
|
||||
|
||||
@@ -2929,13 +3124,40 @@ def test_giftcard_payment_fee(event):
|
||||
)
|
||||
gc1 = event.organizer.issued_gift_cards.create(currency="EUR")
|
||||
gc1.transactions.create(value=12)
|
||||
order = _create_order(event, email='dummy@example.org', positions=[cp1],
|
||||
now_dt=now(), payment_provider=BankTransfer(event),
|
||||
locale='de', gift_cards=[gc1.pk])[0]
|
||||
order = _create_order(
|
||||
event, email='dummy@example.org', positions=[cp1],
|
||||
now_dt=now(),
|
||||
payment_requests=[
|
||||
{
|
||||
"id": "test0",
|
||||
"provider": "giftcard",
|
||||
"max_value": "12.00",
|
||||
"min_value": None,
|
||||
"multi_use_supported": True,
|
||||
"info_data": {
|
||||
"gift_card": gc1.pk
|
||||
},
|
||||
"pprov": GiftCardPayment(event),
|
||||
},
|
||||
{
|
||||
"id": "test1",
|
||||
"provider": "banktransfer",
|
||||
"max_value": None,
|
||||
"min_value": None,
|
||||
"multi_use_supported": False,
|
||||
"info_data": {},
|
||||
"pprov": BankTransfer(event),
|
||||
},
|
||||
],
|
||||
locale='de'
|
||||
)[0]
|
||||
assert order.payments.count() == 2
|
||||
assert order.payments.get(info__icontains=gc1.pk).amount == Decimal('12.00')
|
||||
assert order.payments.get(provider='banktransfer').amount == Decimal('12.10')
|
||||
for p in order.payments.all():
|
||||
p.payment_provider.execute_payment(None, p)
|
||||
assert order.fees.get().value == Decimal('1.10')
|
||||
gc1 = GiftCard.objects.get(pk=gc1.pk)
|
||||
assert gc1.value == 0
|
||||
|
||||
|
||||
@@ -2948,10 +3170,36 @@ def test_giftcard_invalid_currency(event):
|
||||
)
|
||||
gc1 = event.organizer.issued_gift_cards.create(currency="USD")
|
||||
gc1.transactions.create(value=12)
|
||||
with pytest.raises(OrderError):
|
||||
_create_order(event, email='dummy@example.org', positions=[cp1],
|
||||
now_dt=now(), payment_provider=BankTransfer(event),
|
||||
locale='de', gift_cards=[gc1.pk])[0]
|
||||
_create_order(
|
||||
event, email='dummy@example.org', positions=[cp1],
|
||||
now_dt=now(),
|
||||
payment_requests=[
|
||||
{
|
||||
"id": "test0",
|
||||
"provider": "giftcard",
|
||||
"max_value": "12.00",
|
||||
"min_value": None,
|
||||
"multi_use_supported": True,
|
||||
"info_data": {
|
||||
"gift_card": gc1.pk
|
||||
},
|
||||
"pprov": GiftCardPayment(event),
|
||||
},
|
||||
{
|
||||
"id": "test1",
|
||||
"provider": "banktransfer",
|
||||
"max_value": None,
|
||||
"min_value": None,
|
||||
"multi_use_supported": False,
|
||||
"info_data": {},
|
||||
"pprov": BankTransfer(event),
|
||||
},
|
||||
],
|
||||
locale='de'
|
||||
)[0]
|
||||
with pytest.raises(PaymentException):
|
||||
for p in Order.objects.get().payments.all():
|
||||
p.payment_provider.execute_payment(None, p)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@@ -2964,10 +3212,36 @@ def test_giftcard_invalid_organizer(event):
|
||||
o2 = Organizer.objects.create(slug="foo", name="bar")
|
||||
gc1 = o2.issued_gift_cards.create(currency="EUR")
|
||||
gc1.transactions.create(value=12)
|
||||
with pytest.raises(OrderError):
|
||||
_create_order(event, email='dummy@example.org', positions=[cp1],
|
||||
now_dt=now(), payment_provider=BankTransfer(event),
|
||||
locale='de', gift_cards=[gc1.pk])[0]
|
||||
_create_order(
|
||||
event, email='dummy@example.org', positions=[cp1],
|
||||
now_dt=now(),
|
||||
payment_requests=[
|
||||
{
|
||||
"id": "test0",
|
||||
"provider": "giftcard",
|
||||
"max_value": "12.00",
|
||||
"min_value": None,
|
||||
"multi_use_supported": True,
|
||||
"info_data": {
|
||||
"gift_card": gc1.pk
|
||||
},
|
||||
"pprov": GiftCardPayment(event),
|
||||
},
|
||||
{
|
||||
"id": "test1",
|
||||
"provider": "banktransfer",
|
||||
"max_value": None,
|
||||
"min_value": None,
|
||||
"multi_use_supported": False,
|
||||
"info_data": {},
|
||||
"pprov": BankTransfer(event),
|
||||
},
|
||||
],
|
||||
locale='de'
|
||||
)[0]
|
||||
with pytest.raises(PaymentException):
|
||||
for p in Order.objects.get().payments.all():
|
||||
p.payment_provider.execute_payment(None, p)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@@ -2979,10 +3253,36 @@ def test_giftcard_test_mode_invalid(event):
|
||||
)
|
||||
gc1 = event.organizer.issued_gift_cards.create(currency="EUR", testmode=True)
|
||||
gc1.transactions.create(value=12)
|
||||
with pytest.raises(OrderError):
|
||||
_create_order(event, email='dummy@example.org', positions=[cp1],
|
||||
now_dt=now(), payment_provider=BankTransfer(event),
|
||||
locale='de', gift_cards=[gc1.pk])[0]
|
||||
_create_order(
|
||||
event, email='dummy@example.org', positions=[cp1],
|
||||
now_dt=now(),
|
||||
payment_requests=[
|
||||
{
|
||||
"id": "test0",
|
||||
"provider": "giftcard",
|
||||
"max_value": "12.00",
|
||||
"min_value": None,
|
||||
"multi_use_supported": True,
|
||||
"info_data": {
|
||||
"gift_card": gc1.pk
|
||||
},
|
||||
"pprov": GiftCardPayment(event),
|
||||
},
|
||||
{
|
||||
"id": "test1",
|
||||
"provider": "banktransfer",
|
||||
"max_value": None,
|
||||
"min_value": None,
|
||||
"multi_use_supported": False,
|
||||
"info_data": {},
|
||||
"pprov": BankTransfer(event),
|
||||
},
|
||||
],
|
||||
locale='de'
|
||||
)[0]
|
||||
with pytest.raises(PaymentException):
|
||||
for p in Order.objects.get().payments.all():
|
||||
p.payment_provider.execute_payment(None, p)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@@ -2996,10 +3296,36 @@ def test_giftcard_test_mode_event(event):
|
||||
event.save()
|
||||
gc1 = event.organizer.issued_gift_cards.create(currency="EUR", testmode=False)
|
||||
gc1.transactions.create(value=12)
|
||||
with pytest.raises(OrderError):
|
||||
_create_order(event, email='dummy@example.org', positions=[cp1],
|
||||
now_dt=now(), payment_provider=BankTransfer(event),
|
||||
locale='de', gift_cards=[gc1.pk])[0]
|
||||
_create_order(
|
||||
event, email='dummy@example.org', positions=[cp1],
|
||||
now_dt=now(),
|
||||
payment_requests=[
|
||||
{
|
||||
"id": "test0",
|
||||
"provider": "giftcard",
|
||||
"max_value": "12.00",
|
||||
"min_value": None,
|
||||
"multi_use_supported": True,
|
||||
"info_data": {
|
||||
"gift_card": gc1.pk
|
||||
},
|
||||
"pprov": GiftCardPayment(event),
|
||||
},
|
||||
{
|
||||
"id": "test1",
|
||||
"provider": "banktransfer",
|
||||
"max_value": None,
|
||||
"min_value": None,
|
||||
"multi_use_supported": False,
|
||||
"info_data": {},
|
||||
"pprov": BankTransfer(event),
|
||||
},
|
||||
],
|
||||
locale='de'
|
||||
)[0]
|
||||
with pytest.raises(PaymentException):
|
||||
for p in Order.objects.get().payments.all():
|
||||
p.payment_provider.execute_payment(None, p)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@@ -3011,10 +3337,36 @@ def test_giftcard_swap(event):
|
||||
)
|
||||
gc1 = event.organizer.issued_gift_cards.create(currency="EUR", testmode=False)
|
||||
gc1.transactions.create(value=12)
|
||||
with pytest.raises(OrderError):
|
||||
_create_order(event, email='dummy@example.org', positions=[cp1],
|
||||
now_dt=now(), payment_provider=BankTransfer(event),
|
||||
locale='de', gift_cards=[gc1.pk])[0]
|
||||
_create_order(
|
||||
event, email='dummy@example.org', positions=[cp1],
|
||||
now_dt=now(),
|
||||
payment_requests=[
|
||||
{
|
||||
"id": "test0",
|
||||
"provider": "giftcard",
|
||||
"max_value": "12.00",
|
||||
"min_value": None,
|
||||
"multi_use_supported": True,
|
||||
"info_data": {
|
||||
"gift_card": gc1.pk
|
||||
},
|
||||
"pprov": GiftCardPayment(event),
|
||||
},
|
||||
{
|
||||
"id": "test1",
|
||||
"provider": "banktransfer",
|
||||
"max_value": None,
|
||||
"min_value": None,
|
||||
"multi_use_supported": False,
|
||||
"info_data": {},
|
||||
"pprov": BankTransfer(event),
|
||||
},
|
||||
],
|
||||
locale='de'
|
||||
)[0]
|
||||
with pytest.raises(PaymentException):
|
||||
for p in Order.objects.get().payments.all():
|
||||
p.payment_provider.execute_payment(None, p)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@@ -3026,9 +3378,22 @@ def test_issue_when_paid_and_changed(event):
|
||||
)
|
||||
q = event.quotas.create(size=None, name="foo")
|
||||
q.items.add(ticket)
|
||||
order = _create_order(event, email='dummy@example.org', positions=[cp1],
|
||||
now_dt=now(), payment_provider=BankTransfer(event),
|
||||
locale='de', gift_cards=[])[0]
|
||||
order = _create_order(
|
||||
event, email='dummy@example.org', positions=[cp1],
|
||||
now_dt=now(),
|
||||
payment_requests=[
|
||||
{
|
||||
"id": "test1",
|
||||
"provider": "banktransfer",
|
||||
"max_value": None,
|
||||
"min_value": None,
|
||||
"multi_use_supported": False,
|
||||
"info_data": {},
|
||||
"pprov": BankTransfer(event),
|
||||
},
|
||||
],
|
||||
locale='de'
|
||||
)[0]
|
||||
op = order.positions.first()
|
||||
assert not op.issued_gift_cards.exists()
|
||||
order.payments.first().confirm()
|
||||
|
||||
@@ -70,6 +70,16 @@ class BundlePricesTest(TestCase):
|
||||
|
||||
self.session_key = get_cart_session_key(self.client, self.event)
|
||||
|
||||
def _manual_payment(self):
|
||||
return [{
|
||||
"id": "test1",
|
||||
"provider": "manual",
|
||||
"max_value": None,
|
||||
"min_value": None,
|
||||
"multi_use_supported": False,
|
||||
"info_data": {},
|
||||
}]
|
||||
|
||||
def test_simple_case(self):
|
||||
# Verify correct price displayed on event page
|
||||
response = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug))
|
||||
@@ -96,7 +106,7 @@ class BundlePricesTest(TestCase):
|
||||
|
||||
# Verify price is kept if cart expires and order is sent
|
||||
with scopes_disabled():
|
||||
_perform_order(self.event, 'manual', [cp1.pk, cp2.pk], 'admin@example.org', 'en', None, {}, 'web')
|
||||
_perform_order(self.event, self._manual_payment(), [cp1.pk, cp2.pk], 'admin@example.org', 'en', None, {}, 'web')
|
||||
op1 = OrderPosition.objects.get(is_bundled=False)
|
||||
op2 = OrderPosition.objects.get(is_bundled=True)
|
||||
assert op1.price == Decimal('13.00')
|
||||
@@ -141,7 +151,7 @@ class BundlePricesTest(TestCase):
|
||||
|
||||
# Verify price is kept if cart expires and order is sent
|
||||
with scopes_disabled():
|
||||
_perform_order(self.event, 'manual', [cp1.pk, cp2.pk], 'admin@example.org', 'en', None, {}, 'web')
|
||||
_perform_order(self.event, self._manual_payment(), [cp1.pk, cp2.pk], 'admin@example.org', 'en', None, {}, 'web')
|
||||
op1 = OrderPosition.objects.get(is_bundled=False)
|
||||
op2 = OrderPosition.objects.get(is_bundled=True)
|
||||
assert op1.price == Decimal('17.37')
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user