Refator payment provider, deal with cancellations

This commit is contained in:
Raphael Michel
2019-09-19 10:06:26 +02:00
parent e099fad0ca
commit 346f215c50
8 changed files with 99 additions and 85 deletions

View File

@@ -458,11 +458,15 @@ class Order(LockModel, LoggedModel):
positions = list(
self.positions.all().annotate(
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk')))
).select_related('item')
).select_related('item').prefetch_related('issued_gift_cards')
)
cancelable = all([op.item.allow_cancel and not op.has_checkin for op in positions])
if not cancelable or not positions:
return False
for op in positions:
for gc in op.issued_gift_cards.all():
if gc.value != op.price:
return False
if self.user_cancel_deadline and now() > self.user_cancel_deadline:
return False
if self.status == Order.STATUS_PENDING:

View File

@@ -7,6 +7,7 @@ from typing import Any, Dict, Union
import pytz
from django import forms
from django.conf import settings
from django.contrib import messages
from django.core.exceptions import ImproperlyConfigured
from django.db import transaction
from django.dispatch import receiver
@@ -21,8 +22,8 @@ from i18nfield.strings import LazyI18nString
from pretix.base.forms import PlaceholderValidator
from pretix.base.models import (
CartPosition, Event, InvoiceAddress, Order, OrderPayment, OrderRefund,
Quota,
CartPosition, Event, GiftCard, InvoiceAddress, Order, OrderPayment,
OrderRefund, Quota,
)
from pretix.base.reldate import RelativeDateField, RelativeDateWrapper
from pretix.base.settings import SettingsSandbox
@@ -30,6 +31,7 @@ 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.money import DecimalTextInput
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.views import get_cart_total
from pretix.presale.views.cart import cart_session, get_or_create_cart_id
@@ -890,12 +892,11 @@ class OffsettingProvider(BasePaymentProvider):
class GiftCardPayment(BasePaymentProvider):
is_enabled = True
identifier = "giftcard"
verbose_name = _("Gift card")
def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool:
return False
return super().is_allowed(request, total) and self.event.organizer.has_gift_cards
def order_change_allowed(self, order: Order) -> bool:
return False
@@ -903,6 +904,9 @@ class GiftCardPayment(BasePaymentProvider):
def execute_payment(self, request: HttpRequest, payment: OrderPayment) -> str:
raise PaymentException("Invalid state, should never occur.")
def payment_form_render(self, request: HttpRequest, total: Decimal) -> str:
return get_template('pretixcontrol/giftcards/checkout.html').render({})
def payment_control_render(self, request, payment) -> str:
from .models import GiftCard
@@ -933,6 +937,42 @@ class GiftCardPayment(BasePaymentProvider):
def payment_refund_supported(self, payment: OrderPayment) -> bool:
return True
def checkout_prepare(self, request: HttpRequest, cart: Dict[str, Any]) -> Union[bool, str, None]:
cs = cart_session(request)
try:
gc = self.event.organizer.accepted_gift_cards.get(
secret=request.POST.get("giftcard")
)
if gc.currency != self.event.currency:
messages.error(request, _("This gift card does not support this currency."))
return
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]
remainder = cart['total'] - gc.value
if remainder >= Decimal('0.00'):
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)
except GiftCard.DoesNotExist:
messages.error(request, _("This gift card is not known."))
except GiftCard.MultipleObjectsReturned:
messages.error(request, _("This gift card can not be redeemed since its code is not unique. Please contact the organizer of this event."))
@transaction.atomic()
def execute_refund(self, refund: OrderRefund):
from .models import GiftCard

View File

@@ -329,6 +329,15 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
if position.voucher:
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
for position in order.positions.all():
for gc in position.issued_gift_cards.all():
if gc.value < position.price:
raise OrderError(_('This order can not be canceled since the gift card {card} purchased in this order has already been redeemed.').format(
card=gc.secret
))
else:
gc.transactions.create(value=-position.price, order=order)
order.log_action('pretix.event.order.canceled', user=user, auth=api_token or oauth_application or device,
data={'cancellation_fee': cancellation_fee})
@@ -1659,9 +1668,16 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
@receiver(order_paid, dispatch_uid="pretixbase_order_paid_giftcards")
@transaction.atomic()
def signal_listener_issue_giftcards(sender: Event, order: Order, **kwargs):
any_giftcards = False
for p in order.positions.all():
if p.item.issue_giftcard:
gc = sender.organizer.issued_gift_cards.create(
currency=sender.currency, issued_in=p
)
gc.transactions.create(value=p.price, order=order)
any_giftcards = True
p.secret = gc.secret
p.save(update_fields=['secret'])
if any_giftcards:
tickets.invalidate_cache.apply_async(kwargs={'event': sender.pk, 'order': order.pk})

View File

@@ -133,6 +133,10 @@ DEFAULTS = {
'default': 'True',
'type': bool
},
'payment_giftcard__enabled': {
'default': 'True',
'type': bool
},
'payment_term_accept_late': {
'default': 'True',
'type': bool