.. highlight:: python :linenothreshold: 5 .. _`payment2.0`: 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, :py:class:`OrderPayment ` and :py:class:`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 new ``BasePaymentProvider.payment_pending_render(request, payment)`` method that is passed an ``OrderPayment`` object instead of an ``Order``. * The method ``BasePaymentProvider.payment_form_render`` now receives a new ``total`` parameter. * The method ``BasePaymentProvider.payment_perform`` has been removed and replaced by a new method ``BasePaymentProvider.execute_payment(request, payment)`` that is passed an ``OrderPayment`` object instead of an ``Order``. * The function ``pretix.base.services.mark_order_paid`` has been removed, instead call ``payment.confirm()`` on a pending ``OrderPayment`` object. If no further payments are required for this order, this will also mark the order as paid automatically. Note that ``payment.confirm()`` can still throw a ``QuotaExceededException``, 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 replaces ``BasePaymentProvider.order_can_retry``, which no longer exists. * The methods ``BasePaymentProvider.retry_prepare`` and ``BasePaymentProvider.order_prepare`` have both been replaced by a new method ``BasePaymentProvider.payment_prepare(request, payment)`` that is passed an ``OrderPayment`` object instead of an ``Order``. **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 method ``BasePaymentProvider.payment_control_render(request, payment)`` that is passed an ``OrderPayment`` object instead of an ``Order``. * 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`` and ``BasePaymentProvider.order_control_refund_perform`` have been removed. * Two new boolean methods ``BasePaymentProvider.payment_refund_supported(payment)`` and ``BasePaymentProvider.payment_partial_refund_supported(payment)`` have been introduced. They should be set to return ``True`` 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 a ``OrderRefund`` object in ``"created"`` state and is expected to transfer the money back and confirm success with calling ``refund.done()``. This will only ever be called if either ``BasePaymentProvider.payment_refund_supported(payment)`` or ``BasePaymentProvider.payment_partial_refund_supported(payment)`` return ``True``. 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 appropriate ``OrderRefund`` 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** an ``OrderPayment`` **or** an ``OrderRefund``.