Payment provider API: Add method cancel_payment

This commit is contained in:
Raphael Michel
2019-11-14 10:39:54 +01:00
parent b876293453
commit 339d7f06ed
7 changed files with 168 additions and 47 deletions

View File

@@ -112,6 +112,8 @@ The provider class
.. automethod:: shred_payment_info .. automethod:: shred_payment_info
.. automethod:: cancel_payment
.. autoattribute:: is_implicit .. autoattribute:: is_implicit
.. autoattribute:: is_meta .. autoattribute:: is_meta

View File

@@ -173,9 +173,26 @@ class OrderViewSet(viewsets.ModelViewSet):
amount=ps amount=ps
) )
except OrderPayment.DoesNotExist: except OrderPayment.DoesNotExist:
order.payments.filter(state__in=(OrderPayment.PAYMENT_STATE_PENDING, for p in order.payments.filter(state__in=(OrderPayment.PAYMENT_STATE_PENDING,
OrderPayment.PAYMENT_STATE_CREATED)) \ OrderPayment.PAYMENT_STATE_CREATED)):
.update(state=OrderPayment.PAYMENT_STATE_CANCELED) try:
with transaction.atomic():
p.payment_provider.cancel_payment(p)
order.log_action('pretix.event.order.payment.canceled', {
'local_id': p.local_id,
'provider': p.provider,
}, user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
except PaymentException as e:
order.log_action(
'pretix.event.order.payment.canceled.failed',
{
'local_id': p.local_id,
'provider': p.provider,
'error': str(e)
},
user=self.request.user if self.request.user.is_authenticated else None,
auth=self.request.auth
)
p = order.payments.create( p = order.payments.create(
state=OrderPayment.PAYMENT_STATE_CREATED, state=OrderPayment.PAYMENT_STATE_CREATED,
provider='manual', provider='manual',
@@ -896,13 +913,16 @@ class PaymentViewSet(viewsets.ReadOnlyModelViewSet):
if payment.state not in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED): if payment.state not in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED):
return Response({'detail': 'Invalid state of payment'}, status=status.HTTP_400_BAD_REQUEST) return Response({'detail': 'Invalid state of payment'}, status=status.HTTP_400_BAD_REQUEST)
with transaction.atomic(): try:
payment.state = OrderPayment.PAYMENT_STATE_CANCELED with transaction.atomic():
payment.save() payment.payment_provider.cancel_payment(payment)
payment.order.log_action('pretix.event.order.payment.canceled', { payment.order.log_action('pretix.event.order.payment.canceled', {
'local_id': payment.local_id, 'local_id': payment.local_id,
'provider': payment.provider, 'provider': payment.provider,
}, user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth) }, user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
except PaymentException as e:
return Response({'detail': 'External error: {}'.format(str(e))},
status=status.HTTP_400_BAD_REQUEST)
return self.retrieve(request, [], **kwargs) return self.retrieve(request, [], **kwargs)

View File

@@ -654,6 +654,17 @@ class BasePaymentProvider:
""" """
return False return False
def cancel_payment(self, payment: OrderPayment):
"""
Will be called to cancel a payment. The default implementation just sets the payment state to canceled,
but in some cases you might want to notify an external provider.
On success, you should set ``payment.state = OrderPayment.PAYMENT_STATE_CANCELED`` (or call the super method).
On failure, you should raise a PaymentException.
"""
payment.state = OrderPayment.PAYMENT_STATE_CANCELED
payment.save()
def execute_refund(self, refund: OrderRefund): def execute_refund(self, refund: OrderRefund):
""" """
Will be called to execute an refund. Note that refunds have an amount property and can be partial. Will be called to execute an refund. Note that refunds have an amount property and can be partial.

View File

@@ -1183,20 +1183,49 @@ class OrderChangeManager:
self.order.status = Order.STATUS_PAID self.order.status = Order.STATUS_PAID
self.order.save() self.order.save()
elif self.open_payment: elif self.open_payment:
self.open_payment.state = OrderPayment.PAYMENT_STATE_CANCELED try:
self.open_payment.save() with transaction.atomic():
self.order.log_action('pretix.event.order.payment.canceled', { self.open_payment.payment_provider.cancel_payment(self.open_payment)
'local_id': self.open_payment.local_id, self.order.log_action(
'provider': self.open_payment.provider, 'pretix.event.order.payment.canceled',
}, user=self.user, auth=self.auth) {
'local_id': self.open_payment.local_id,
'provider': self.open_payment.provider,
},
user=self.user,
auth=self.auth
)
except PaymentException as e:
self.order.log_action(
'pretix.event.order.payment.canceled.failed',
{
'local_id': self.open_payment.local_id,
'provider': self.open_payment.provider,
'error': str(e)
},
user=self.user,
auth=self.auth
)
elif self.order.status in (Order.STATUS_PENDING, Order.STATUS_EXPIRED) and self._totaldiff > 0: elif self.order.status in (Order.STATUS_PENDING, Order.STATUS_EXPIRED) and self._totaldiff > 0:
if self.open_payment: if self.open_payment:
self.open_payment.state = OrderPayment.PAYMENT_STATE_CANCELED try:
self.open_payment.save() with transaction.atomic():
self.order.log_action('pretix.event.order.payment.canceled', { self.open_payment.payment_provider.cancel_payment(self.open_payment)
'local_id': self.open_payment.local_id, self.order.log_action('pretix.event.order.payment.canceled', {
'provider': self.open_payment.provider, 'local_id': self.open_payment.local_id,
}, user=self.user, auth=self.auth) 'provider': self.open_payment.provider,
}, user=self.user, auth=self.auth)
except PaymentException as e:
self.order.log_action(
'pretix.event.order.payment.canceled.failed',
{
'local_id': self.open_payment.local_id,
'provider': self.open_payment.provider,
'error': str(e)
},
user=self.user,
auth=self.auth,
)
def _check_paid_to_free(self): def _check_paid_to_free(self):
if self.order.total == 0 and (self._totaldiff < 0 or (self.split_order and self.split_order.total > 0)) and not self.order.require_approval: if self.order.total == 0 and (self._totaldiff < 0 or (self.split_order and self.split_order.total > 0)) and not self.order.require_approval:
@@ -1726,8 +1755,22 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
if open_payment and open_payment.state in (OrderPayment.PAYMENT_STATE_PENDING, if open_payment and open_payment.state in (OrderPayment.PAYMENT_STATE_PENDING,
OrderPayment.PAYMENT_STATE_CREATED): OrderPayment.PAYMENT_STATE_CREATED):
open_payment.state = OrderPayment.PAYMENT_STATE_CANCELED try:
open_payment.save(update_fields=['state']) with transaction.atomic():
open_payment.payment_provider.cancel_payment(open_payment)
order.log_action('pretix.event.order.payment.canceled', {
'local_id': open_payment.local_id,
'provider': open_payment.provider,
})
except PaymentException as e:
order.log_action(
'pretix.event.order.payment.canceled.failed',
{
'local_id': open_payment.local_id,
'provider': open_payment.provider,
'error': str(e)
},
)
order.total = (order.positions.aggregate(sum=Sum('price'))['sum'] or 0) + (order.fees.aggregate(sum=Sum('value'))['sum'] or 0) order.total = (order.positions.aggregate(sum=Sum('price'))['sum'] or 0) + (order.fees.aggregate(sum=Sum('value'))['sum'] or 0)
order.save(update_fields=['total']) order.save(update_fields=['total'])

View File

@@ -225,6 +225,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.order.email.resend': _('An email with a link to the order detail page has been resent to the user.'), 'pretix.event.order.email.resend': _('An email with a link to the order detail page has been resent to the user.'),
'pretix.event.order.payment.confirmed': _('Payment {local_id} has been confirmed.'), 'pretix.event.order.payment.confirmed': _('Payment {local_id} has been confirmed.'),
'pretix.event.order.payment.canceled': _('Payment {local_id} has been canceled.'), 'pretix.event.order.payment.canceled': _('Payment {local_id} has been canceled.'),
'pretix.event.order.payment.canceled.failed': _('Cancelling payment {local_id} has failed.'),
'pretix.event.order.payment.started': _('Payment {local_id} has been started.'), 'pretix.event.order.payment.started': _('Payment {local_id} has been started.'),
'pretix.event.order.payment.failed': _('Payment {local_id} has failed.'), 'pretix.event.order.payment.failed': _('Payment {local_id} has failed.'),
'pretix.event.order.quotaexceeded': _('The order could not be marked as paid: {message}'), 'pretix.event.order.quotaexceeded': _('The order could not be marked as paid: {message}'),

View File

@@ -480,14 +480,26 @@ class OrderPaymentCancel(OrderView):
def post(self, *args, **kwargs): def post(self, *args, **kwargs):
if self.payment.state in (OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING): if self.payment.state in (OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING):
with transaction.atomic(): try:
self.payment.state = OrderPayment.PAYMENT_STATE_CANCELED with transaction.atomic():
self.payment.save() self.payment.payment_provider.cancel_payment(self.payment)
self.order.log_action('pretix.event.order.payment.canceled', { self.order.log_action('pretix.event.order.payment.canceled', {
'local_id': self.payment.local_id, 'local_id': self.payment.local_id,
'provider': self.payment.provider, 'provider': self.payment.provider,
}, user=self.request.user) }, user=self.request.user if self.request.user.is_authenticated else None)
messages.success(self.request, _('This payment has been canceled.')) except PaymentException as e:
self.order.log_action(
'pretix.event.order.payment.canceled.failed',
{
'local_id': self.payment.local_id,
'provider': self.payment.provider,
'error': str(e)
},
user=self.request.user if self.request.user.is_authenticated else None,
)
messages.error(self.request, str(e))
else:
messages.success(self.request, _('This payment has been canceled.'))
else: else:
messages.error(self.request, _('This payment can not be canceled at the moment.')) messages.error(self.request, _('This payment can not be canceled at the moment.'))
return redirect(self.get_order_url()) return redirect(self.get_order_url())
@@ -859,9 +871,25 @@ class OrderTransition(OrderView):
amount=ps amount=ps
) )
except OrderPayment.DoesNotExist: except OrderPayment.DoesNotExist:
self.order.payments.filter(state__in=(OrderPayment.PAYMENT_STATE_PENDING, for p in self.order.payments.filter(state__in=(OrderPayment.PAYMENT_STATE_PENDING,
OrderPayment.PAYMENT_STATE_CREATED)) \ OrderPayment.PAYMENT_STATE_CREATED)):
.update(state=OrderPayment.PAYMENT_STATE_CANCELED) try:
with transaction.atomic():
p.payment_provider.cancel_payment(p)
self.order.log_action('pretix.event.order.payment.canceled', {
'local_id': p.local_id,
'provider': p.provider,
}, user=self.request.user if self.request.user.is_authenticated else None)
except PaymentException as e:
self.order.log_action(
'pretix.event.order.payment.canceled.failed',
{
'local_id': p.local_id,
'provider': p.provider,
'error': str(e)
},
user=self.request.user if self.request.user.is_authenticated else None,
)
p = self.order.payments.create( p = self.order.payments.create(
state=OrderPayment.PAYMENT_STATE_CREATED, state=OrderPayment.PAYMENT_STATE_CREATED,
provider='manual', provider='manual',

View File

@@ -12,6 +12,7 @@ from django_scopes import scope, scopes_disabled
from pretix.base.email import get_email_context from pretix.base.email import get_email_context
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.models import Event, Order, OrderPayment, Organizer, Quota from pretix.base.models import Event, Order, OrderPayment, Organizer, Quota
from pretix.base.payment import PaymentException
from pretix.base.services.locking import LockTimeoutException from pretix.base.services.locking import LockTimeoutException
from pretix.base.services.mail import SendMailException from pretix.base.services.mail import SendMailException
from pretix.base.services.orders import change_payment_provider from pretix.base.services.orders import change_payment_provider
@@ -38,6 +39,30 @@ def notify_incomplete_payment(o: Order):
logger.exception('Reminder email could not be sent') logger.exception('Reminder email could not be sent')
def cancel_old_payments(order):
for p in order.payments.filter(
state__in=(OrderPayment.PAYMENT_STATE_PENDING,
OrderPayment.PAYMENT_STATE_CREATED),
provider='banktransfer',
):
try:
with transaction.atomic():
p.payment_provider.cancel_payment(p)
order.log_action('pretix.event.order.payment.canceled', {
'local_id': p.local_id,
'provider': p.provider,
})
except PaymentException as e:
order.log_action(
'pretix.event.order.payment.canceled.failed',
{
'local_id': p.local_id,
'provider': p.provider,
'error': str(e)
},
)
@transaction.atomic @transaction.atomic
def _handle_transaction(trans: BankTransaction, code: str, event: Event=None, organizer: Organizer=None, def _handle_transaction(trans: BankTransaction, code: str, event: Event=None, organizer: Organizer=None,
slug: str=None): slug: str=None):
@@ -109,22 +134,13 @@ def _handle_transaction(trans: BankTransaction, code: str, event: Event=None, or
p.confirm() p.confirm()
except Quota.QuotaExceededException: except Quota.QuotaExceededException:
trans.state = BankTransaction.STATE_VALID trans.state = BankTransaction.STATE_VALID
trans.order.payments.filter( cancel_old_payments(trans.order)
provider='banktransfer',
state__in=(OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING),
).update(state=OrderPayment.PAYMENT_STATE_CANCELED)
except SendMailException: except SendMailException:
trans.state = BankTransaction.STATE_VALID trans.state = BankTransaction.STATE_VALID
trans.order.payments.filter( cancel_old_payments(trans.order)
provider='banktransfer',
state__in=(OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING),
).update(state=OrderPayment.PAYMENT_STATE_CANCELED)
else: else:
trans.state = BankTransaction.STATE_VALID trans.state = BankTransaction.STATE_VALID
trans.order.payments.filter( cancel_old_payments(trans.order)
provider='banktransfer',
state__in=(OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING),
).update(state=OrderPayment.PAYMENT_STATE_CANCELED)
o = trans.order o = trans.order
o.refresh_from_db() o.refresh_from_db()