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:: cancel_payment
.. autoattribute:: is_implicit
.. autoattribute:: is_meta

View File

@@ -173,9 +173,26 @@ class OrderViewSet(viewsets.ModelViewSet):
amount=ps
)
except OrderPayment.DoesNotExist:
order.payments.filter(state__in=(OrderPayment.PAYMENT_STATE_PENDING,
OrderPayment.PAYMENT_STATE_CREATED)) \
.update(state=OrderPayment.PAYMENT_STATE_CANCELED)
for p in order.payments.filter(state__in=(OrderPayment.PAYMENT_STATE_PENDING,
OrderPayment.PAYMENT_STATE_CREATED)):
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(
state=OrderPayment.PAYMENT_STATE_CREATED,
provider='manual',
@@ -896,13 +913,16 @@ class PaymentViewSet(viewsets.ReadOnlyModelViewSet):
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)
with transaction.atomic():
payment.state = OrderPayment.PAYMENT_STATE_CANCELED
payment.save()
payment.order.log_action('pretix.event.order.payment.canceled', {
'local_id': payment.local_id,
'provider': payment.provider,
}, user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
try:
with transaction.atomic():
payment.payment_provider.cancel_payment(payment)
payment.order.log_action('pretix.event.order.payment.canceled', {
'local_id': payment.local_id,
'provider': payment.provider,
}, 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)

View File

@@ -654,6 +654,17 @@ class BasePaymentProvider:
"""
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):
"""
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.save()
elif self.open_payment:
self.open_payment.state = OrderPayment.PAYMENT_STATE_CANCELED
self.open_payment.save()
self.order.log_action('pretix.event.order.payment.canceled', {
'local_id': self.open_payment.local_id,
'provider': self.open_payment.provider,
}, user=self.user, auth=self.auth)
try:
with transaction.atomic():
self.open_payment.payment_provider.cancel_payment(self.open_payment)
self.order.log_action(
'pretix.event.order.payment.canceled',
{
'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:
if self.open_payment:
self.open_payment.state = OrderPayment.PAYMENT_STATE_CANCELED
self.open_payment.save()
self.order.log_action('pretix.event.order.payment.canceled', {
'local_id': self.open_payment.local_id,
'provider': self.open_payment.provider,
}, user=self.user, auth=self.auth)
try:
with transaction.atomic():
self.open_payment.payment_provider.cancel_payment(self.open_payment)
self.order.log_action('pretix.event.order.payment.canceled', {
'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,
)
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:
@@ -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,
OrderPayment.PAYMENT_STATE_CREATED):
open_payment.state = OrderPayment.PAYMENT_STATE_CANCELED
open_payment.save(update_fields=['state'])
try:
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.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.payment.confirmed': _('Payment {local_id} has been confirmed.'),
'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.failed': _('Payment {local_id} has failed.'),
'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):
if self.payment.state in (OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING):
with transaction.atomic():
self.payment.state = OrderPayment.PAYMENT_STATE_CANCELED
self.payment.save()
self.order.log_action('pretix.event.order.payment.canceled', {
'local_id': self.payment.local_id,
'provider': self.payment.provider,
}, user=self.request.user)
messages.success(self.request, _('This payment has been canceled.'))
try:
with transaction.atomic():
self.payment.payment_provider.cancel_payment(self.payment)
self.order.log_action('pretix.event.order.payment.canceled', {
'local_id': self.payment.local_id,
'provider': self.payment.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': 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:
messages.error(self.request, _('This payment can not be canceled at the moment.'))
return redirect(self.get_order_url())
@@ -859,9 +871,25 @@ class OrderTransition(OrderView):
amount=ps
)
except OrderPayment.DoesNotExist:
self.order.payments.filter(state__in=(OrderPayment.PAYMENT_STATE_PENDING,
OrderPayment.PAYMENT_STATE_CREATED)) \
.update(state=OrderPayment.PAYMENT_STATE_CANCELED)
for p in self.order.payments.filter(state__in=(OrderPayment.PAYMENT_STATE_PENDING,
OrderPayment.PAYMENT_STATE_CREATED)):
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(
state=OrderPayment.PAYMENT_STATE_CREATED,
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.i18n import language
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.mail import SendMailException
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')
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
def _handle_transaction(trans: BankTransaction, code: str, event: Event=None, organizer: Organizer=None,
slug: str=None):
@@ -109,22 +134,13 @@ def _handle_transaction(trans: BankTransaction, code: str, event: Event=None, or
p.confirm()
except Quota.QuotaExceededException:
trans.state = BankTransaction.STATE_VALID
trans.order.payments.filter(
provider='banktransfer',
state__in=(OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING),
).update(state=OrderPayment.PAYMENT_STATE_CANCELED)
cancel_old_payments(trans.order)
except SendMailException:
trans.state = BankTransaction.STATE_VALID
trans.order.payments.filter(
provider='banktransfer',
state__in=(OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING),
).update(state=OrderPayment.PAYMENT_STATE_CANCELED)
cancel_old_payments(trans.order)
else:
trans.state = BankTransaction.STATE_VALID
trans.order.payments.filter(
provider='banktransfer',
state__in=(OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING),
).update(state=OrderPayment.PAYMENT_STATE_CANCELED)
cancel_old_payments(trans.order)
o = trans.order
o.refresh_from_db()