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:

1
2
3
4
5
6
7
8
9
from django.dispatch import receiver

from pretix.base.signals import register_payment_providers


@receiver(register_payment_providers, dispatch_uid="payment_paypal")
def register_payment_provider(sender, **kwargs):
    from .payment import Paypal
    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 its get and set 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.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.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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@property
def settings_form_fields(self):
    return OrderedDict(
        list(super().settings_form_fields.items()) + [
            ('bank_details',
             forms.CharField(
                 widget=forms.Textarea,
                 label=_('Bank account details'),
                 required=False
             ))
        ]
    )

Warning

It is highly discouraged to alter the _enabled field of the default implementation.

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: django.http.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: django.http.request.HttpRequest, total: decimal.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 _total_max and _total_min requirements to be met. It also checks the _restrict_countries 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 a TypeError on first try.

BasePaymentProvider.payment_form_render(request: django.http.request.HttpRequest, total: decimal.Decimal) → 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 checkout_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.

BasePaymentProvider.payment_form(request: django.http.request.HttpRequest) → django.forms.forms.Form

This is called by the default implementation of checkout_form_render() to obtain the form that is displayed to the user during the checkout process. The default implementation constructs the form using checkout_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 checkout_form(). It should return an object similar to settings_form_fields.

The default implementation returns an empty dictionary.

BasePaymentProvider.payment_is_valid_session(request: django.http.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: django.http.request.HttpRequest, cart: typing.Dict[str, typing.Any]) → typing.Union[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.

Parameters:cart

This dictionary contains at least the following keys:

positions:
A list of CartPosition objects that are annotated with the special attributes count and total 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) → 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.

This is an abstract method, you must override this!

BasePaymentProvider.execute_payment(request: django.http.request.HttpRequest, payment: pretix.base.models.orders.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.

If the payment is completed, you should call payment.confirm(). Please note that this might raise a Quota.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:
  • order – The order object
  • payment – An OrderPayment instance
BasePaymentProvider.calculate_fee(price: decimal.Decimal) → 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: pretix.base.models.orders.Order) → 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
BasePaymentProvider.payment_pending_render(request: django.http.request.HttpRequest, payment: pretix.base.models.orders.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 start 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: pretix.base.models.orders.Order) → 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.

BasePaymentProvider.order_change_allowed(order: pretix.base.models.orders.Order) → 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 _total_max, _total_min and _restricted_countries settings.

Parameters:order – The order object
BasePaymentProvider.payment_prepare(request: django.http.request.HttpRequest, payment: pretix.base.models.orders.OrderPayment) → typing.Union[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 an Order 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: django.http.request.HttpRequest, payment: pretix.base.models.orders.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 the verbose name of the payment provider.

Parameters:order – The order object
BasePaymentProvider.payment_refund_supported(payment: pretix.base.models.orders.OrderPayment) → bool

Will be called to check if the provider supports automatic refunding for this payment.

BasePaymentProvider.payment_partial_refund_supported(payment: pretix.base.models.orders.OrderPayment) → bool

Will be called to check if the provider supports automatic partial refunding for this payment.

BasePaymentProvider.execute_refund(refund: pretix.base.models.orders.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.shred_payment_info(obj: typing.Union[pretix.base.models.orders.OrderPayment, pretix.base.models.orders.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.is_implicit

Returns whether or whether not this payment provider is an “implicit” payment provider that will always and unconditionally be used if is_allowed() returns True and does not require any input. This is intended to be used by the FreePaymentProvider, which skips the payment choice page. By default, this returns False. Please do not set this if you don’t know exactly what you are doing.

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.

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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@login_required
def success(request):
    pid = request.GET.get('paymentId')
    payer = request.GET.get('PayerID')
    # We stored some information in the session in checkout_prepare(),
    # let's compare the new information to double-check that this is about
    # the same payment
    if pid == request.session['payment_paypal_id']:
        # Save the new information to the user's session
        request.session['payment_paypal_payer'] = payer
        try:
            # Redirect back to the confirm page. We chose to save the
            # event ID in the user's session. We could also put this
            # information into a URL parameter.
            event = Event.objects.current.get(identity=request.session['payment_paypal_event'])
            return redirect(reverse('presale:event.checkout.confirm', kwargs={
                'event': event.slug,
                'organizer': event.organizer.slug,
            }))
        except Event.DoesNotExist:
            pass  # TODO: Display error message
    else:
        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.