Fix #571 -- Partial payments and refunds

This commit is contained in:
Raphael Michel
2018-08-06 12:24:36 +02:00
parent 8e7af49206
commit 18a378976b
115 changed files with 6026 additions and 1598 deletions
+566 -17
View File
@@ -32,8 +32,8 @@ email string The customer em
locale string The locale used for communication with this customer
datetime datetime Time of order creation
expires datetime The order will expire, if it is still pending by this time
payment_date date Date of payment receipt
payment_provider string Payment provider used for this order
payment_date date **DEPRECATED AND INACCURATE** Date of payment receipt
payment_provider string **DEPRECATED AND INACCURATE** Payment provider used for this order
total money (string) Total value of this order
comment string Internal comment on this order
checkin_attention boolean If ``True``, the check-in app should show a warning
@@ -74,6 +74,8 @@ downloads list of objects List of ticket
download options.
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
└ url string Download URL
payments list of objects List of payment processes (see below)
refunds list of objects List of refund processes (see below)
last_modified datetime Last modification of this object
===================================== ========================== =======================================================
@@ -108,6 +110,11 @@ last_modified datetime Last modificati
The attributes ``order.last_modified`` as well as the corresponding filters to the resource have been added.
An endpoint for order creation as well as ``…/mark_refunded/`` has been added.
.. versionchanged:: 2.0
The ``order.payment_date`` and ``order.payment_provider`` attributes have been deprecated in favor of the new
nested ``payments`` and ``refunds`` resources, but will still be served and removed in 2.2.
.. _order-position-resource:
Order position resource
@@ -167,9 +174,45 @@ pdf_data object Data object req
The attributes ``pseudonymization_id`` and ``pdf_data`` have been added.
.. _order-payment-resource:
Order endpoints
---------------
Order payment resource
----------------------
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
local_id integer Internal ID of this payment, starts at 1 for every order
state string Payment state, one of ``created``, ``pending``, ``confirmed``, ``canceled``, ``pending``, ``failed``, or ``refunded``
amount money (string) Payment amount
created datetime Date and time of creation of this payment
payment_date datetime Date and time of completion of this payment (or ``null``)
provider string Identification string of the payment provider
===================================== ========================== =======================================================
.. _order-payment-resource:
Order refund resource
---------------------
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
local_id integer Internal ID of this payment, starts at 1 for every order
state string Payment state, one of ``created``, ``transit``, ``external``, ``canceled``, ``failed``, or ``done``
source string How this refund has been created, one of ``buyer``, ``admin``, or ``external``
amount money (string) Payment amount
created datetime Date and time of creation of this payment
payment_date datetime Date and time of completion of this payment (or ``null``)
provider string Identification string of the payment provider
===================================== ========================== =======================================================
List of all orders
------------------
.. versionchanged:: 1.15
@@ -275,7 +318,18 @@ Order endpoints
"output": "pdf",
"url": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/download/pdf/"
}
]
],
"payments": [
{
"local_id": 1,
"state": "confirmed",
"amount": "23.00",
"created": "2017-12-01T10:00:00Z",
"payment_date": "2017-12-04T12:13:12Z",
"provider": "banktransfer"
}
],
"refunds": []
}
]
}
@@ -296,6 +350,9 @@ Order endpoints
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
Fetching individual orders
--------------------------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/
Returns information on one order, identified by its order code.
@@ -390,7 +447,18 @@ Order endpoints
"output": "pdf",
"url": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/download/pdf/"
}
]
],
"payments": [
{
"local_id": 1,
"state": "confirmed",
"amount": "23.00",
"created": "2017-12-01T10:00:00Z",
"payment_date": "2017-12-04T12:13:12Z",
"provider": "banktransfer"
}
],
"refunds": []
}
:param organizer: The ``slug`` field of the organizer to fetch
@@ -401,6 +469,9 @@ Order endpoints
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order does not exist.
Order ticket download
---------------------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/download/(output)/
Download tickets for an order, identified by its order code. Depending on the chosen output, the response might
@@ -442,6 +513,9 @@ Order endpoints
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting for a few
seconds.
Creating orders
---------------
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/
Creates a new order.
@@ -487,21 +561,23 @@ Order endpoints
* ``code`` (optional)
* ``status`` (optional) Defaults to pending for non-free orders and paid for free orders. You can only set this to
``"n"`` for pending or ``"p"`` for paid. If you create a paid order, the ``order_paid`` signal will **not** be
sent out to plugins and no email will be sent. If you want that behavior, create an unpaid order and then call
the ``mark_paid`` API method.
``"n"`` for pending or ``"p"`` for paid. We will create a payment object for this order either in state ``created``
or in state ``confirmed``, depending on this value. If you create a paid order, the ``order_paid`` signal will
**not** be sent out to plugins and no email will be sent. If you want that behavior, create an unpaid order and
then call the ``mark_paid`` API method.
* ``consume_carts`` (optional) A list of cart IDs. All cart positions with these IDs will be deleted if the
order creation is successful. Any quotas that become free by this operation will be credited to your order
creation.
* ``email``
* ``locale``
* ``payment_provider`` The identifier of the payment provider set for this order. This needs to be an existing
payment provider. You should use ``"free"`` for free orders.
* ``payment_info`` (optional) You can pass a nested JSON object that will be set as the internal ``payment_info``
value of the order. How this value is handled is up to the payment provider and you should only use this if you
know the specific payment provider in detail. Please keep in mind that the payment provider will not be called
to do anything about this (i.e. if you pass a bank account to a debit provider, *no* charge will be created),
this is just informative in case you *handled the payment already*.
payment provider. You should use ``"free"`` for free orders, and we strongly advise to use ``"manual"`` for all
orders you create as paid.
* ``payment_info`` (optional) You can pass a nested JSON object that will be set as the internal ``info``
value of the payment object that will be created. How this value is handled is up to the payment provider and you
should only use this if you know the specific payment provider in detail. Please keep in mind that the payment
provider will not be called to do anything about this (i.e. if you pass a bank account to a debit provider, *no*
charge will be created), this is just informative in case you *handled the payment already*.
* ``comment`` (optional)
* ``checkin_attention`` (optional)
* ``invoice_address`` (optional)
@@ -618,6 +694,9 @@ Order endpoints
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this
order.
Order state operations
----------------------
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/mark_paid/
Marks a pending or expired order as successfully paid.
@@ -853,8 +932,8 @@ Order endpoints
:statuscode 404: The requested order does not exist.
Order position endpoints
------------------------
List of all order positions
---------------------------
.. versionchanged:: 1.15
@@ -958,6 +1037,9 @@ Order position endpoints
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
Fetching individual positions
-----------------------------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/
Returns information on one order position, identified by its internal ID.
@@ -1026,6 +1108,9 @@ Order position endpoints
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order position does not exist.
Order position ticket download
------------------------------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/download/(output)/
Download tickets for one order position, identified by its internal ID.
@@ -1067,3 +1152,467 @@ Order position endpoints
:statuscode 404: The requested order position or download provider does not exist.
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting for a few
seconds.
Order payment endpoints
-----------------------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/payments/
Returns a list of all payments for an order.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/payments/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"local_id": 1,
"state": "confirmed",
"amount": "23.00",
"created": "2017-12-01T10:00:00Z",
"payment_date": "2017-12-04T12:13:12Z",
"provider": "banktransfer"
}
]
}
:query integer page: The page number in case of a multi-page result set, default is 1
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param order: The ``code`` field of the order to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order does not exist.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/payments/(local_id)/
Returns information on one payment, identified by its order-local ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/payments/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"local_id": 1,
"state": "confirmed",
"amount": "23.00",
"created": "2017-12-01T10:00:00Z",
"payment_date": "2017-12-04T12:13:12Z",
"provider": "banktransfer"
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param code: The ``code`` field of the order to fetch
:param local_id: The ``local_id`` field of the payment to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order or payment does not exist.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/payments/(local_id)/confirm/
Marks a payment as confirmed. Only allowed in states ``pending`` and ``created``.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/payments/1/confirm/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{"force": false}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"local_id": 1,
"state": "confirmed",
...
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param code: The ``code`` field of the order to fetch
:param local_id: The ``local_id`` field of the payment to modify
:statuscode 200: no error
:statuscode 400: Invalid request or payment state
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order or payment does not exist.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/payments/(local_id)/cancel/
Marks a payment as canceled. Only allowed in states ``pending`` and ``created``.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/payments/1/cancel/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"local_id": 1,
"state": "canceled",
...
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param code: The ``code`` field of the order to fetch
:param local_id: The ``local_id`` field of the payment to modify
:statuscode 200: no error
:statuscode 400: Invalid request or payment state
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order or payment does not exist.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/payments/(local_id)/refund/
Create and execute a manual refund. Only available in ``confirmed`` state. Returns a refund resource, not
a payment resource!
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/payments/1/refund/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"amount": "23.00",
"mark_refunded": false
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"local_id": 1,
"source": "admin",
"state": "done",
...
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param code: The ``code`` field of the order to fetch
:param local_id: The ``local_id`` field of the payment to modify
:statuscode 200: no error
:statuscode 400: Invalid request, payment state, or operation not supported by the payment provider
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order or payment does not exist.
Order refund endpoints
----------------------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/refunds/
Returns a list of all refunds for an order.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/refunds/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"local_id": 1,
"state": "done",
"source": "admin",
"amount": "23.00",
"payment": 1,
"created": "2017-12-01T10:00:00Z",
"execution_date": "2017-12-04T12:13:12Z",
"provider": "banktransfer"
}
]
}
:query integer page: The page number in case of a multi-page result set, default is 1
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param order: The ``code`` field of the order to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order does not exist.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/refunds/(local_id)/
Returns information on one refund, identified by its order-local ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/refunds/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"local_id": 1,
"state": "done",
"source": "admin",
"amount": "23.00",
"payment": 1,
"created": "2017-12-01T10:00:00Z",
"execution_date": "2017-12-04T12:13:12Z",
"provider": "banktransfer"
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param code: The ``code`` field of the order to fetch
:param local_id: The ``local_id`` field of the refund to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order or refund does not exist.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/refunds/
Creates a refund manually.
.. warning:: We recommend to only use this endpoint for refunds with payment provider ``manual``. This endpoint also
does not check for mismatching amounts etc. Be careful!
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/refunds/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"state": "created",
"source": "admin",
"amount": "23.00",
"payment": 1,
"execution_date": null,
"provider": "manual"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"local_id": 1,
"state": "created",
"source": "admin",
"amount": "23.00",
"payment": 1,
"created": "2017-12-01T10:00:00Z",
"execution_date": null,
"provider": "manual"
}
:query integer page: The page number in case of a multi-page result set, default is 1
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param order: The ``code`` field of the order to fetch
:statuscode 200: no error
:statuscode 400: Invalid data supplied
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order does not exist.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/refunds/(local_id)/done/
Marks a refund as completed. Only allowed in states ``transit`` and ``created``.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/refunds/1/done/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"local_id": 1,
"state": "done",
....
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param code: The ``code`` field of the order to fetch
:param local_id: The ``local_id`` field of the refund to modify
:statuscode 200: no error
:statuscode 400: Invalid request or refund state
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order or refund does not exist.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/refunds/(local_id)/process/
Acts on an external refund, either marks the order as refunded or pending. Only allowed in state ``external``.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/refunds/1/done/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{"mark_refunded": false}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"local_id": 1,
"state": "done",
....
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param code: The ``code`` field of the order to fetch
:param local_id: The ``local_id`` field of the refund to modify
:statuscode 200: no error
:statuscode 400: Invalid request or refund state
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order or refund does not exist.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/refunds/(local_id)/cancel/
Marks a refund as canceled. Only allowed in states ``transit``, ``external``, and ``created``.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/refunds/1/cancel/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"local_id": 1,
"state": "canceled",
....
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param code: The ``code`` field of the order to fetch
:param local_id: The ``local_id`` field of the refund to modify
:statuscode 200: no error
:statuscode 400: Invalid request or refund state
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order or refund does not exist.
+2 -1
View File
@@ -10,8 +10,9 @@ Contents:
exporter
ticketoutput
payment
payment_2.0
invoice
shredder
customview
general
quality
quality
+26 -18
View File
@@ -9,6 +9,10 @@ is very similar to creating an export output.
Please read :ref:`Creating a plugin <pluginsetup>` first, if you haven't already.
.. warning:: We changed our payment provider API a lot in pretix 2.x. Our documentation page on :ref:`payment2.0`
might be insightful even if you do not have a payment provider to port, as it outlines the rationale
behind the current design.
Provider registration
---------------------
@@ -31,7 +35,7 @@ that the plugin will provide::
The provider class
------------------
.. class:: pretix.base.payment.BasePaymentProvider
.. py:class:: pretix.base.payment.BasePaymentProvider
The central object of each payment provider is the subclass of ``BasePaymentProvider``.
@@ -54,58 +58,62 @@ The provider class
This is an abstract attribute, you **must** override this!
.. autoattribute:: is_enabled
.. autoattribute:: public_name
.. automethod:: calculate_fee
.. autoattribute:: is_enabled
.. autoattribute:: settings_form_fields
.. automethod:: settings_content_render
.. automethod:: render_invoice_text
.. automethod:: is_allowed
.. automethod:: payment_form_render
.. automethod:: payment_form
.. automethod:: is_allowed
.. autoattribute:: payment_form_fields
.. automethod:: checkout_prepare
.. automethod:: payment_is_valid_session
.. automethod:: checkout_prepare
.. automethod:: checkout_confirm_render
This is an abstract method, you **must** override this!
.. automethod:: payment_perform
.. automethod:: execute_payment
.. automethod:: calculate_fee
.. automethod:: order_pending_mail_render
.. automethod:: order_pending_render
.. automethod:: payment_pending_render
This is an abstract method, you **must** override this!
.. autoattribute:: abort_pending_allowed
.. automethod:: render_invoice_text
.. automethod:: order_change_allowed
.. automethod:: order_can_retry
.. automethod:: order_prepare
.. automethod:: payment_prepare
.. automethod:: order_paid_render
.. automethod:: payment_control_render
.. automethod:: order_control_render
.. automethod:: payment_refund_supported
.. automethod:: order_control_refund_render
.. automethod:: payment_partial_refund_supported
.. automethod:: order_control_refund_perform
.. automethod:: is_implicit
.. automethod:: execute_refund
.. automethod:: shred_payment_info
.. autoattribute:: is_implicit
.. autoattribute:: is_meta
Additional views
----------------
+127
View File
@@ -0,0 +1,127 @@
.. 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 <pretix.base.models.OrderPayment>` and
:py:class:`OrderRefund <pretix.base.models.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_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``.
@@ -86,6 +86,15 @@ Carts and Orders
.. autoclass:: pretix.base.models.OrderPosition
:members:
.. autoclass:: pretix.base.models.OrderFee
:members:
.. autoclass:: pretix.base.models.OrderPayment
:members:
.. autoclass:: pretix.base.models.OrderRefund
:members:
.. autoclass:: pretix.base.models.CartPosition
:members: