The Payment class

Create a Payment class

django-payments ship an abstract payments.models.BasePayment class. Individual projects need to subclass it and implement a few methods and may include any extra payment-related fields on this model. It is also possible to add a foreign key to an existing purchase or order model.

The following instance methods are required:

BasePayment.get_failure_url() str

URL where users will be redirected after a failed payment.

Return the URL where users will be redirected after a failed attempt to complete a payment. This is usually a page explaining the situation to the user with an option to retry the payment.

Note that the URL may contain the ID of this payment, allowing the target page to show relevant contextual information.

Subclasses MUST implement this method.

BasePayment.get_success_url() str

URL where users will be redirected after a successful payment.

Return the URL where users will be redirected after a successful payment. This is usually a page showing a payment summary, though it’s application-dependant what to show on it.

Note that the URL may contain the ID of this payment, allowing the target page to show relevant contextual information.

Subclasses MUST implement this method.

BasePayment.get_purchased_items() Iterable[PurchasedItem]

Return an iterable of purchased items.

This information is sent to the payment processor when initiating the payment flow. See PurchasedItem for details.

Subclasses MUST implement this method.

Example implementation

# mypaymentapp/models.py
from decimal import Decimal

from payments import PurchasedItem
from payments.models import BasePayment

class Payment(BasePayment):

    def get_failure_url(self) -> str:
        # Return a URL where users are redirected after
        # they fail to complete a payment:
        return f"http://example.com/payments/{self.pk}/failure"

    def get_success_url(self) -> str:
        # Return a URL where users are redirected after
        # they successfully complete a payment:
        return f"http://example.com/payments/{self.pk}/success"

    def get_purchased_items(self) -> Iterable[PurchasedItem]:
        # Return items that will be included in this payment.
        yield PurchasedItem(
            name='The Hound of the Baskervilles',
            sku='BSKV',
            quantity=9,
            price=Decimal(10),
            currency='USD',
        )

Create a payment view

Write a view that will handle the payment. You can obtain a form instance by passing POST data to get_form():

# mypaymentapp/views.py
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from payments import get_payment_model, RedirectNeeded

def payment_details(request, payment_id):
    payment = get_object_or_404(get_payment_model(), id=payment_id)

    try:
        form = payment.get_form(data=request.POST or None)
    except RedirectNeeded as redirect_to:
        return redirect(str(redirect_to))

    return TemplateResponse(
        request,
        'payment.html',
        {'form': form, 'payment': payment}
    )

Note

Please note that Payment.get_form() may raise a RedirectNeeded exception. In this case, you need to redirect the user to the supplied URL.

Prepare a template that displays the form using its action and method:

<!-- templates/payment.html -->
<form action="{{ form.action }}" method="{{ form.method }}">
    {% csrf_token %}
    {{ form.as_p }}
    <p><input type="submit" value="Proceed" /></p>
</form>

Once users have completed a payment, they will be redirected to the URl returned by get_success_url() or get_failure_url().

Mutating a Payment instance

When operating Payment instances, care should be take to only save changes atomically. If a model is loaded into memory, mutated, and then saved back to the database it is possible to overwrite concurrent changes made by handling a notification from the payment processor. Keep in mind that most processors are likely implement “at least once” notification delivery.

In general, you should either:

  • Use atomic updates only specifying the relevant fields. For example, if the application-local Payment class has a custom field named discount_card_code, use BasePayment.objects.filter(pk=payment_id).update(discount_card_code="123XYZ"). This is the recommended approach.

  • Lock the database row while mutating a python instance of BasePayment (may negatively affect performance at scale).

Registering the Payment class

Once the Payment class has been implemented, it needs to be registered as the payment model for an application. This is done by adding a variable to the settings.py file. E.g.:

# A dotted path to the Payment class.
PAYMENT_MODEL = 'mypaymentapp.models.Payment'