Porting a payment provider from pretix 1.x to pretix 2.x¶
In pretix 2.x, we changed large parts of the payment provider API. This documentation details the changes we made and shows you how you can make an existing pretix 1.x payment provider compatible with pretix 2.x
Conceptual overview¶
In pretix 1.x, an order was always directly connected to a payment provider for the full life of an order. As long as an order was unpaid, this could still be changed in some cases, but once an order was paid, no changes to the payment provider were possible any more. Additionally, the internal state of orders allowed orders only to be fully paid or not paid at all. This leads to a couple of consequences:
Payment-related functions (like “execute payment” or “do a refund”) always operated on full orders.
Changing the total of an order was basically impossible once an order was paid, since there was no concept of partial payments or partial refunds.
Payment provider plugins needed to take complicated steps to detect cases that require human intervention, like e.g.
An order has expired, no quota is left to revive it, but a payment has been received
A payment has been received for a canceled order
A payment has been received for an order that has already been paid with a different payment method
An external payment service notified us of a refund/dispute
We noticed that we copied and repeated large portions of code in all our official payment provider plugins, just to deal with some of these cases.
Sometimes, there is the need to mark an order as refunded within pretix, without automatically triggering a refund with an external API. Every payment method needed to implement a user interface for this independently.
If a refund was not possible automatically, there was no way user to track which payments actually have been refunded manually and which are still left to do.
When the payment with one payment provider failed and the user changed to a different payment provider, all information about the first payment was lost from the order object and could only be retrieved from order log data, which also made it hard to design a data shredder API to get rid of this data.
In pretix 2.x, we introduced two new models, OrderPayment
and
OrderRefund
. Each instance of these is connected to an order and
represents one single attempt to pay or refund a specific amount of money. Each one of these has an individual state,
can individually fail or succeed, and carries an amount variable that can differ from the order total.
This has the following advantages:
The system can now detect orders that are over- or underpaid, independent of the payment providers in use.
Therefore, we can now allow partial payments, partial refunds, and changing paid orders, and automatically detect the cases listed above and notify the user.
Payment providers now interact with those payment and refund objects more than with orders.
Your to-do list¶
Payment processing¶
The method
BasePaymentProvider.order_pending_render
has been removed and replaced by a newBasePaymentProvider.payment_pending_render(request, payment)
method that is passed anOrderPayment
object instead of anOrder
.The method
BasePaymentProvider.payment_form_render
now receives a newtotal
parameter.The method
BasePaymentProvider.payment_perform
has been removed and replaced by a new methodBasePaymentProvider.execute_payment(request, payment)
that is passed anOrderPayment
object instead of anOrder
.The function
pretix.base.services.mark_order_paid
has been removed, instead callpayment.confirm()
on a pendingOrderPayment
object. If no further payments are required for this order, this will also mark the order as paid automatically. Note thatpayment.confirm()
can still throw aQuotaExceededException
, however it will still mark the payment as complete (not the order!), so you should catch this exception and inform the user, but not abort the transaction.A new property
BasePaymentProvider.abort_pending_allowed
has been introduced. Only if set, the user will be able to retry a payment or switch the payment method when the order currently has a payment object in state"pending"
. This replacesBasePaymentProvider.order_can_retry
, which no longer exists.The methods
BasePaymentProvider.retry_prepare
andBasePaymentProvider.order_prepare
have both been replaced by a new methodBasePaymentProvider.payment_prepare(request, payment)
that is passed anOrderPayment
object instead of anOrder
. Keep in mind that this payment object might have an amount property that differs from the order total, if the order is already partially paid.The method
BasePaymentProvider.order_paid_render
has been removed.The method
BasePaymentProvider.order_control_render
has been removed and replaced by a new methodBasePaymentProvider.payment_control_render(request, payment)
that is passed anOrderPayment
object instead of anOrder
.There’s no need to manually deal with excess payments or duplicate payments anymore, just setting the
OrderPayment
methods to the correct state will do the job.
Creating refunds¶
The methods
BasePaymentProvider.order_control_refund_render
andBasePaymentProvider.order_control_refund_perform
have been removed.Two new boolean methods
BasePaymentProvider.payment_refund_supported(payment)
andBasePaymentProvider.payment_partial_refund_supported(payment)
have been introduced. They should be set to returnTrue
if and only if the payment API allows to automatically transfer the money back to the customer.A new method
BasePaymentProvider.execute_refund(refund)
has been introduced. This method is called using aOrderRefund
object in"created"
state and is expected to transfer the money back and confirm success with callingrefund.done()
. This will only ever be called if eitherBasePaymentProvider.payment_refund_supported(payment)
orBasePaymentProvider.payment_partial_refund_supported(payment)
returnTrue
.
Processing external refunds¶
If e.g. a webhook API notifies you that a payment has been disputed or refunded with the external API, you are expected to call
OrderPayment.create_external_refund(self, amount, execution_date, info='{}')
on this payment. This will create and return an appropriateOrderRefund
object and send out a notification. However, it will not mark the order as refunded, but will ask the event organizer for a decision.
Data shredders¶
The method
BasePaymentProvider.shred_payment_info
is no longer passed an order, but instead either anOrderPayment
or anOrderRefund
.