Writing a payment provider plugin¶
In this document, we will walk through the creation of a payment provider plugin. This is very similar to creating an export output.
Please read Creating a plugin first, if you haven’t already.
Warning
We changed our payment provider API a lot in pretix 2.x. Our documentation page on Porting a payment provider from pretix 1.x to pretix 2.x 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¶
The payment provider API does not make a lot of usage from signals, however, it
does use a signal to get a list of all available payment providers. Your plugin
should listen for this signal and return the subclass of pretix.base.payment.BasePaymentProvider
that the plugin will provide:
1from django.dispatch import receiver
2
3from pretix.base.signals import register_payment_providers
4
5
6@receiver(register_payment_providers, dispatch_uid="payment_paypal")
7def register_payment_provider(sender, **kwargs):
8 from .payment import Paypal
9 return Paypal
The provider class¶
- class pretix.base.payment.BasePaymentProvider¶
The central object of each payment provider is the subclass of
BasePaymentProvider
.- BasePaymentProvider.event¶
The default constructor sets this property to the event we are currently working for.
- BasePaymentProvider.settings¶
The default constructor sets this property to a
SettingsSandbox
object. You can use this object to store settings using itsget
andset
methods. All settings you store are transparently prefixed, so you get your very own settings namespace.
- BasePaymentProvider.identifier¶
A short and unique identifier for this payment provider. This should only contain lowercase letters and in most cases will be the same as your package name.
This is an abstract attribute, you must override this!
- BasePaymentProvider.verbose_name¶
A human-readable name for this payment provider. This should be short but self-explaining. Good examples include ‘Bank transfer’ and ‘Credit card via Stripe’.
This is an abstract attribute, you must override this!
- BasePaymentProvider.public_name¶
A human-readable name for this payment provider to be shown to the public. This should be short but self-explaining. Good examples include ‘Bank transfer’ and ‘Credit card’, but ‘Credit card via Stripe’ might be to explicit. By default, this is the same as
verbose_name
- BasePaymentProvider.confirm_button_name¶
A label for the “confirm” button on the last page before a payment is started. This is not used in the regular checkout flow, but only if the payment method is selected for an existing order later on.
- BasePaymentProvider.is_enabled¶
Returns whether or whether not this payment provider is enabled. By default, this is determined by the value of the
_enabled
setting.
- BasePaymentProvider.priority¶
Returns a priority that is used for sorting payment providers. Higher priority means higher up in the list. Default to 100. Providers with same priority are sorted alphabetically.
- BasePaymentProvider.settings_form_fields¶
When the event’s administrator visits the event configuration page, this method is called to return the configuration fields available.
It should therefore return a dictionary where the keys should be (unprefixed) settings keys and the values should be corresponding Django form fields.
The default implementation returns the appropriate fields for the
_enabled
,_fee_abs
,_fee_percent
and_availability_date
settings mentioned above.We suggest that you return an
OrderedDict
object instead of a dictionary and make use of the default implementation. Your implementation could look like this:1@property 2def settings_form_fields(self): 3 return OrderedDict( 4 list(super().settings_form_fields.items()) + [ 5 ('bank_details', 6 forms.CharField( 7 widget=forms.Textarea, 8 label=_('Bank account details'), 9 required=False 10 )) 11 ] 12 )
Warning
It is highly discouraged to alter the
_enabled
field of the default implementation.
- BasePaymentProvider.walletqueries¶
Warning
This property is considered experimental. It might change or get removed at any time without prior notice.
A list of wallet payment methods that should be dynamically joined to the public name of the payment method, if they are available to the user. The detection is made on a best effort basis with no guarantees of correctness and actual availability. Wallets that pretix can check for are exposed through
pretix.base.payment.WalletQueries
.
- BasePaymentProvider.settings_form_clean(cleaned_data)¶
Overriding this method allows you to inject custom validation into the settings form.
- Parameters:
cleaned_data – Form data as per previous validations.
- Returns:
Please return the modified cleaned_data
- BasePaymentProvider.settings_content_render(request: HttpRequest) str ¶
When the event’s administrator visits the event configuration page, this method is called. It may return HTML containing additional information that is displayed below the form fields configured in
settings_form_fields
.
- BasePaymentProvider.is_allowed(request: HttpRequest, total: Decimal = None) bool ¶
You can use this method to disable this payment provider for certain groups of users, products or other criteria. If this method returns
False
, the user will not be able to select this payment method. This will only be called during checkout, not on retrying.The default implementation checks for the
_availability_date
setting to be either unset or in the future and for the_availability_from
,_total_max
, and_total_min
requirements to be met. It also checks the_restrict_countries
and_restrict_to_sales_channels
setting.- Parameters:
total – The total value without the payment method fee, after taxes.
Changed in version 1.17.0: The
total
parameter has been added. For backwards compatibility, this method is called again without this parameter if it raises aTypeError
on first try.
- BasePaymentProvider.payment_form_render(request: HttpRequest, total: Decimal, order: Order = None) str ¶
When the user selects this provider as their preferred payment method, they will be shown the HTML you return from this method.
The default implementation will call
payment_form()
and render the returned form. If your payment method doesn’t require the user to fill out form fields, you should just return a paragraph of explanatory text.- Parameters:
order – Only set when this is a change to a new payment method for an existing order.
- BasePaymentProvider.payment_form(request: HttpRequest) Form ¶
This is called by the default implementation of
payment_form_render()
to obtain the form that is displayed to the user during the checkout process. The default implementation constructs the form usingpayment_form_fields
and sets appropriate prefixes for the form and all fields and fills the form with data form the user’s session.If you overwrite this, we strongly suggest that you inherit from
PaymentProviderForm
(from this module) that handles some nasty issues about required fields for you.
- BasePaymentProvider.payment_form_fields¶
This is used by the default implementation of
payment_form()
. It should return an object similar tosettings_form_fields
.The default implementation returns an empty dictionary.
- BasePaymentProvider.payment_is_valid_session(request: HttpRequest) bool ¶
This is called at the time the user tries to place the order. It should return
True
if the user’s session is valid and all data your payment provider requires in future steps is present.
- BasePaymentProvider.checkout_prepare(request: HttpRequest, cart: Dict[str, Any]) bool | str ¶
Will be called after the user selects this provider as their payment method. If you provided a form to the user to enter payment data, this method should at least store the user’s input into their session.
This method should return
False
if the user’s input was invalid,True
if the input was valid and the frontend should continue with default behavior or a string containing a URL if the user should be redirected somewhere else.On errors, you should use Django’s message framework to display an error message to the user (or the normal form validation error messages).
The default implementation stores the input into the form returned by
payment_form()
in the user’s session.If your payment method requires you to redirect the user to an external provider, this might be the place to do so.
Important
If this is called, the user has not yet confirmed their order. You may NOT do anything which actually moves money.
- Note: The behavior of this method changes significantly when you set
multi_use_supported
. Please refer to themulti_use_supported
documentation for more information.
- Parameters:
cart –
This dictionary contains at least the following keys:
- positions:
A list of
CartPosition
objects that are annotated with the special attributescount
andtotal
because multiple objects of the same content are grouped into one.- raw:
The raw list of
CartPosition
objects in the users cart- total:
The overall total including the fee for the payment method.
- payment_fee:
The fee for the payment method.
- BasePaymentProvider.checkout_confirm_render(request, order: Order = None, info_data: dict = None) str ¶
If the user has successfully filled in their payment data, they will be redirected to a confirmation page which lists all details of their order for a final review. This method should return the HTML which should be displayed inside the ‘Payment’ box on this page.
In most cases, this should include a short summary of the user’s input and a short explanation on how the payment process will continue.
- Parameters:
request – The current HTTP request.
order – Only set when this is a change to a new payment method for an existing order.
info_data – The
info_data
dictionary you set duringadd_payment_to_cart
(only filled ifmulti_use_supported
is set)
This is an abstract method, you must override this!
- BasePaymentProvider.execute_payment(request: HttpRequest, payment: OrderPayment) str ¶
After the user has confirmed their purchase, this method will be called to complete the payment process. This is the place to actually move the money if applicable. You will be passed an
pretix.base.models.OrderPayment
object that contains the amount of money that should be paid.If you need any special behavior, you can return a string containing the URL the user will be redirected to. If you are done with your process you should return the user to the order’s detail page. Redirection is only allowed if you set
execute_payment_needs_user
toTrue
.If the payment is completed, you should call
payment.confirm()
. Please note that this might raise aQuota.QuotaExceededException
if (and only if) the payment term of this order is over and some of the items are sold out. You should use the exception message to display a meaningful error to the user.The default implementation just returns
None
and therefore leaves the order unpaid. The user will be redirected to the order’s detail page by default.On errors, you should raise a
PaymentException
.- Parameters:
request – A HTTP request, except if
execute_payment_needs_user
isFalse
payment – An
OrderPayment
instance
- BasePaymentProvider.calculate_fee(price: Decimal) Decimal ¶
Calculate the fee for this payment provider which will be added to final price before fees (but after taxes). It should include any taxes. The default implementation makes use of the setting
_fee_abs
for an absolute fee and_fee_percent
for a percentage.- Parameters:
price – The total value without the payment method fee, after taxes.
- BasePaymentProvider.order_pending_mail_render(order: Order, payment: OrderPayment) str ¶
After the user has submitted their order, they will receive a confirmation email. You can return a string from this method if you want to add additional information to this email.
- Parameters:
order – The order object
payment – The payment object
- BasePaymentProvider.payment_pending_render(request: HttpRequest, payment: OrderPayment) str ¶
Render customer-facing instructions on how to proceed with a pending payment
- Returns:
HTML
- BasePaymentProvider.abort_pending_allowed¶
Whether or not a user can abort a payment in pending state to switch to another payment method. This returns
False
by default which is no guarantee that aborting a pending payment can never happen, it just hides the frontend button to avoid users accidentally committing double payments.
- BasePaymentProvider.render_invoice_text(order: Order, payment: OrderPayment) str ¶
This is called when an invoice for an order with this payment provider is generated. The default implementation returns the content of the _invoice_text configuration variable (an I18nString), or an empty string if unconfigured. For paid orders, the default implementation always renders a string stating that the invoice is already paid.
- BasePaymentProvider.render_invoice_stamp(order: Order, payment: OrderPayment) str ¶
This is called when an invoice for an order with this payment provider is generated. The default implementation returns “paid” if the order was already paid, and
None
otherwise. You can override this with a string, but it should be really short to make the invoice look pretty.
- BasePaymentProvider.order_change_allowed(order: Order, request: HttpRequest = None) bool ¶
Will be called to check whether it is allowed to change the payment method of an order to this one.
The default implementation checks for the
_availability_date
setting to be either unset or in the future, as well as for the_availability_from
,_total_max
,_total_min
, and_restricted_countries
settings.- Parameters:
order – The order object
- BasePaymentProvider.payment_prepare(request: HttpRequest, payment: OrderPayment) bool | str ¶
Will be called if the user retries to pay an unpaid order (after the user filled in e.g. the form returned by
payment_form()
) or if the user changes the payment method.It should return and report errors the same way as
checkout_prepare()
, but receives anOrder
object instead of a cart object.Note: The
Order
object given to this method might be different from the version stored in the database as it’s total will already contain the payment fee for the new payment method.
- BasePaymentProvider.payment_control_render(request: HttpRequest, payment: OrderPayment) str ¶
Will be called if the event administrator views the details of a payment.
It should return HTML code containing information regarding the current payment status and, if applicable, next steps.
The default implementation returns an empty string.
- Parameters:
order – The order object
- BasePaymentProvider.payment_control_render_short(payment: OrderPayment) str ¶
Will be called if the event administrator performs an action on the payment. Should return a very short version of the payment method. Usually, this should return e.g. an account identifier of the payee, but no information on status, dates, etc.
The default implementation falls back to
payment_presale_render
.- Parameters:
payment – The payment object
- BasePaymentProvider.payment_refund_supported(payment: OrderPayment) bool ¶
Will be called to check if the provider supports automatic refunding for this payment.
- BasePaymentProvider.payment_partial_refund_supported(payment: OrderPayment) bool ¶
Will be called to check if the provider supports automatic partial refunding for this payment.
- BasePaymentProvider.payment_presale_render(payment: OrderPayment) str ¶
Will be called if the ticket customer views the details of a payment. This is currently used e.g. when the customer requests a refund to show which payment method is used for the refund. This should only include very basic information about the payment, such as “VISA card …9999”, and never raw payment information.
The default implementation returns the public name of the payment provider.
- Parameters:
order – The order object
- BasePaymentProvider.execute_refund(refund: OrderRefund)¶
Will be called to execute an refund. Note that refunds have an amount property and can be partial.
This should transfer the money back (if possible). On success, you should call
refund.done()
. On failure, you should raise a PaymentException.
- BasePaymentProvider.refund_control_render(request: HttpRequest, refund: OrderRefund) str ¶
Will be called if the event administrator views the details of a refund.
It should return HTML code containing information regarding the current refund status and, if applicable, next steps.
The default implementation returns an empty string.
- Parameters:
refund – The refund object
- BasePaymentProvider.refund_control_render_short(refund: OrderRefund) str ¶
Will be called if the event administrator performs an action on the refund. Should return a very short description of the refund method. Usually, this should return e.g. an account identifier of the refund recipient, but no information on status, dates, etc.
The default implementation returns an empty string.
- Parameters:
refund – The refund object
- BasePaymentProvider.new_refund_control_form_render(request: HttpRequest, order: Order) str ¶
Render a form that will be shown to backend users when trying to create a new refund.
Usually, refunds are created from an existing payment object, e.g. if there is a credit card payment and the credit card provider returns
True
frompayment_refund_supported
, the system will automatically create anOrderRefund
and callexecute_refund
on that payment. This method can and should not be used in that situation! Instead, by implementing this method you can add a refund flow for this payment provider that starts without an existing payment. For example, even though an order was paid by credit card, it could easily be refunded by SEPA bank transfer. In that case, the SEPA bank transfer provider would implement this method and return a form that asks for the IBAN.This method should return HTML or
None
. All form fields should have a globally unique name.
- BasePaymentProvider.new_refund_control_form_process(request: HttpRequest, amount: Decimal, order: Order) OrderRefund ¶
Process a backend user’s request to initiate a new refund with an amount of
amount
fororder
.This method should parse the input provided to the form created and either raise
ValidationError
or return anOrderRefund
object increated
state that has not yet been saved to the database. The system will then callexecute_refund
on that object.
- BasePaymentProvider.api_payment_details(payment: OrderPayment)¶
Will be called to populate the
details
parameter of the payment in the REST API.- Parameters:
payment – The payment in question.
- Returns:
A serializable dictionary
- BasePaymentProvider.api_refund_details(refund: OrderRefund)¶
Will be called to populate the
details
parameter of the refund in the REST API.- Parameters:
refund – The refund in question.
- Returns:
A serializable dictionary
- BasePaymentProvider.matching_id(payment: OrderPayment)¶
Will be called to get an ID for matching this payment when comparing pretix records with records of an external source. This should return the main transaction ID for your API.
- Parameters:
payment – The payment in question.
- Returns:
A string or None
- BasePaymentProvider.refund_matching_id(refund: OrderRefund)¶
Will be called to get an ID for matching this refund when comparing pretix records with records of an external source. This should return the main transaction ID for your API.
- Parameters:
refund – The refund in question.
- Returns:
A string or None
- BasePaymentProvider.shred_payment_info(obj: OrderPayment | OrderRefund)¶
When personal data is removed from an event, this method is called to scrub payment-related data from a payment or refund. By default, it removes all info from the
info
attribute. You can override this behavior if you want to retain attributes that are not personal data on their own, i.e. a reference to a transaction in an external system. You can also override this to scrub more data, e.g. data from external sources that is saved in LogEntry objects or other places.- Parameters:
order – An order
- BasePaymentProvider.cancel_payment(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.
- BasePaymentProvider.is_implicit = <function BasePaymentProvider.is_implicit>¶
- BasePaymentProvider.is_meta¶
Returns whether or whether not this payment provider is a “meta” payment provider that only works as a settings holder for other payment providers and should never be used directly. This is a trick to implement payment gateways with multiple payment methods but unified payment settings. Take a look at the built-in stripe provider to see how this might be used. By default, this returns
False
.
- BasePaymentProvider.execute_payment_needs_user¶
Set this to
True
if yourexecute_payment
function needs to be triggered by a user request, i.e. either needs therequest
object or might require a browser redirect. If this isFalse
, you will not receive arequest
and may not redirect since execute_payment might be called server-side. You should ensure that yourexecute_payment
method has a limited execution time (i.e. by usingtimeout
for all external calls) and handles all error cases appropriately.
- BasePaymentProvider.multi_use_supported¶
Returns whether or whether not this payment provider supports being used multiple times in the same checkout, or in addition to a different payment provider. This is usually only useful for payment providers that represent gift cards, i.e. payment methods with an upper limit per payment instrument that can usually be combined with other instruments.
If you set this property to
True
, the behavior of how pretix interacts with your payment provider changes and you will need to respect the following rules:payment_form_render
must not depend on session state, it must always allow a user to add a new payment.Editing a payment is not possible, but pretix will give users an option to delete it.
Returning
True
fromcheckout_prepare
is no longer enough. Instead, you must also callpretix.base.services.cart.add_payment_to_cart(request, provider, min_value, max_value, info_data)
to add the payment to the session. You are still allowed to do a redirect fromcheckout_prepare
and then call this function upon return.Unlike in the general case, when
checkout_prepare
is called, thecart['total']
parameter will not yet include payment fees charged by your provider as we don’t yet know the amount of the charge, so you need to take care of that yourself when setting your maximum amount.payment_is_valid_session
will not be called during checkout, don’t rely on it. If you calledadd_payment_to_cart
, we’ll trust the payment is okay and your next chance to change that will beexecute_payment
.
The changed behavior currently only affects the behavior during initial checkout (i.e.
checkout_prepare
), forpayment_prepare
the regular behavior applies and you are expected to just modify the amount of theOrderPayment
object if you need to.
- BasePaymentProvider.test_mode_message¶
If this property is set to a string, this will be displayed when this payment provider is selected while the event is in test mode. You should use it to explain to your user how your plugin behaves, e.g. if it falls back to a test mode automatically as well or if actual payments will be performed.
If you do not set this (or, return
None
), pretix will show a default message warning the user that this plugin does not support test mode payments.
- BasePaymentProvider.requires_invoice_immediately¶
Return whether this payment method requires an invoice to exist for an order, even though the event is configured to only create invoices for paid orders. By default this is False, but it might be overwritten for e.g. bank transfer. execute_payment is called after the invoice is created.
Additional views¶
See also: Creating custom views.
For most simple payment providers it is more than sufficient to implement
some of the BasePaymentProvider
methods. However, in some cases
it is necessary to introduce additional views. One example is the PayPal
provider. It redirects the user to a PayPal website in the
BasePaymentProvider.checkout_prepare()
step of the checkout process
and provides PayPal with a URL to redirect back to. This URL points to a
view which looks roughly like this:
1@login_required
2def success(request):
3 pid = request.GET.get('paymentId')
4 payer = request.GET.get('PayerID')
5 # We stored some information in the session in checkout_prepare(),
6 # let's compare the new information to double-check that this is about
7 # the same payment
8 if pid == request.session['payment_paypal_id']:
9 # Save the new information to the user's session
10 request.session['payment_paypal_payer'] = payer
11 try:
12 # Redirect back to the confirm page. We chose to save the
13 # event ID in the user's session. We could also put this
14 # information into a URL parameter.
15 event = Event.objects.current.get(identity=request.session['payment_paypal_event'])
16 return redirect(reverse('presale:event.checkout.confirm', kwargs={
17 'event': event.slug,
18 'organizer': event.organizer.slug,
19 }))
20 except Event.DoesNotExist:
21 pass # TODO: Display error message
22 else:
23 pass # TODO: Display error message
If you do not want to provide a view of your own, you could even let PayPal
redirect directly back to the confirm page and handle the query parameters
inside BasePaymentProvider.checkout_is_valid_session()
. However,
because some external providers (not PayPal) force you to have a constant
redirect URL, it might be necessary to define custom views.