1 of 24

django-getpaid

Multi-broker payment processing for django

2 of 24

Problem to be solved

  • Generic and lightweight payment processing app easy to integrate with any other app
  • Single API to rule them all - allowing switching payment brokers (that's very djangish)
  • Using many payment brokers at once (why? why not!)
  • Production ready architecture (asynchronous calls!)
  • Multiple currencies support

3 of 24

What's already there?

Satchmo, python-payflowpro, django-authorizenet, mamona, django-paypal, django-payme and other...

None meet my requirements

starting new project django-getpaid

(heavily based on mamona)

4 of 24

Status of project

  • Code is in version 1.x which means it is production ready
  • Very detailed documentation
  • Currently supported backends:
    • Dotpay (Polish)
    • Transferuj.pl (Polish)
    • PayU.pl (Polish)
    • Przelewy24 (Polish)
    • moip (Brazilian)
    • Paymill
  • Proven successful deployments on many productions
  • Officially mentioned as third party library:
    • Transferuj.pl - https://transferuj.pl/integracja-w-sklepach.html
    • Dotpay.pl - http://www.dotpay.pl/integracja/platformy_sklepowe/

5 of 24

Code & Docs

PyPi

$ pip install django-getpaid

GitHub

$ pip install git+https://github.com/cypreess/django-getpaid.git

Documentation

https://django-getpaid.readthedocs.org/

6 of 24

Integrations steps

  1. Adding to INSTALLED_APPS
  2. GETPAID_BACKENDS settings variable
  3. GETPAID_BACKENDS_SETTINGS variable
  4. Connecting urls
  5. Registering Order model to getpaid
  6. Providing a signal listener for order total amount calculation and payment accepting
  7. Displaying payment form in your view

7 of 24

1. INSTALLED_APPS

Very straightforward:

INSTALLED_APPS += ('getpaid', )

8 of 24

2. GETPAID_BACKENDS

An iterable of backends that should be enabled:

Payment backend is identified by fully qualified Python import path, so it can be imported from any module. Backends are also django-apps that need to installed:

GETPAID_BACKENDS = ('getpaid.backends.dummy',� 'getpaid.backends.payu', )

INSTALLED_APPS += GETPAID_BACKENDS

9 of 24

3. GETPAID_BACKENDS_SETTINGS

A dict with keys being backends paths with dicts of configuration values for a given backend. E.g.

Strictly dependent on backend. Refer to docs.

GETPAID_BACKENDS_SETTINGS = {� 'getpaid.backends.payu' : {� 'pos_id' : 123456,� 'key1' : 'xxxxxxxxxxxxx',� 'key2' : 'xxxxxxxxxxxxx',� 'pos_auth_key': 'xxxxxxxxx',� 'signing' : True, # optional� },�}

10 of 24

4. urls.py

getpaid will also automatically discover and register all urls.py from enabled backends

url(r'', include('getpaid.urls')),

11 of 24

5. Order model

from django.core.urlresolvers import reverse�from django.db import models�import getpaidclass Order(models.Model):� name = models.CharField(max_length=100)� total = models.DecimalField(decimal_places=2, max_digits=8, default=0)� currency = models.CharField(max_length=3, default='EUR')� status = models.CharField(max_length=1, blank=True, default='W', choices=(('W', 'Waiting for payment'), ('P', 'Payment complete')))� def get_absolute_url(self):� return reverse('order_detail', kwargs={'pk': self.pk})� def __unicode__(self):� return self.name��getpaid.register_to_payment(Order, unique=False, related_name='payments')

12 of 24

6. Listeners (1/2)

Getpaid will send a query signal:

An example listener for it:

new_payment_query = Signal(providing_args=['order', 'payment'])

from getpaid.signals import new_payment_query

from django.dispatch import receiver

�@receiver(new_payment_query)

def new_payment_query_listener(sender, order=None,

payment=None, **kwargs):� payment.amount = order.total� payment.currency = order.currency��

13 of 24

6. Listeners (2/2)

You should also do something on successful payment. At least change order status?

Example listener:

��

payment_status_changed = Signal(providing_args=['old_status', 'new_status'])

from getpaid.signals import payment_status_changed

from django.dispatch import receiver

@receiver(payment_status_changed)�def payment_status_changed_listener(sender, instance, old_status,

new_status, **kwargs):� if old_status != 'paid' and new_status == 'paid':� # Ensures that we process order only one� instance.order.status = 'P'� instance.order.save()

14 of 24

7. Payment form (1/2)

Finally we can display a form to make payment.

Your example Order view code:

Form will filter a list of available payment backend to those supporting given currency.

from django.views.generic.detail import DetailView�from getpaid.forms import PaymentMethodForm�from getpaid_test_project.orders.models import Order��class OrderView(DetailView):� model=Order�� def get_context_data(self, **kwargs):� context = super(OrderView, self).get_context_data(**kwargs)� context['payment_form'] = PaymentMethodForm(self.object.currency, initial={'order': self.object})return context

15 of 24

7. Payment form (2/2)

and django template boilerplate:

<form action="{% url getpaid-new-payment currency=object.currency %}" method="post">� {% csrf_token %}� {{ payment_form.as_p }}� <input type="submit" value="Continue">�</form>

16 of 24

Highlights of backend design

  • Payment backend is simply django app that should provide PaymentProcessor class
  • PaymentProcessor.get_gateway_url() - most important method that should return an URL to the payment gateway (GET and POST are supported)
  • PaymentProcessor.BACKEND_ACCEPTED_CURRENCY an iterable with all currencies ISO codes that are available with given backend
  • Backend can introduce its own views (for receiving payments status)
  • Backend can have its own models (eg. storing some custom data needed for transactions)
  • Backend should provide if necessary management script with additional configuration informations

17 of 24

Example: PayU backend

$. /manage.py payu_configuration�Login to PayU configuration page and setup following links:�� * Success URL: http://example.com/getpaid.backends.payu/success/%orderId%/� https://example.com/getpaid.backends.payu/success/%orderId%/�� * Failure URL: http://example.com/getpaid.backends.payu/failure/%orderId%/� https://example.com/getpaid.backends.payu/failure/%orderId%/�� * Online URL: http://example.com/getpaid.backends.payu/online/� https://example.com/getpaid.backends.payu/online/��To change domain name please edit Sites settings. Don't forget to setup your web server to accept https connection in order to use secure links.��Request signing is ON� * Please be sure that you enabled signing payments in PayU configuration page.

18 of 24

Payment flow diagram

OrderView

(template with PaymentForm)

getpaid�NewPaymentView

PaymentProcessor�.get_gateway_url()

Payment broker�system

PaymentProcessor�SuccessView / FailureView

getpaid�FallbackView

Order

.get_absolute_url()

PaymentProcessor�OnlineView�(custom)

19 of 24

PaymentFactory model

  • Payments are stored in Payment model
  • Payment model has a real ForeignKey to registered Order model class (no contenttype)
  • This involves dynamic runtime Payment class generation via PaymentFactory
  • This is a little troublemaker (migrations, importing issues) but does not mess your database

20 of 24

Payment statuses

  • new
  • in progress
  • partially_paid
  • paid
  • failed

Other statuses are not really important from the perspective of an order processing

21 of 24

South migrations? How

Each Payment model class (and DB table) is custom for each application, therefore no common migrations can be added to django-getpaid.

But south is great and already has what we need! Use following settings variable:

Now you can make migrations by your own and store them with your project.

SOUTH_MIGRATION_MODULES = {� 'getpaid' : 'yourproject.migrations.getpaid',� 'payu' : 'yourproject.migrations.getpaid_payu',�}

22 of 24

Tricky things #1

  • Some payment brokers (PayU) require to send IP of the client that is going to create a new payment
  • Your django will probably be served behind web server or proxy
  • You need to get real client IP address (not e.g. 127.0.0.1)

class SetRemoteAddrFromForwardedFor(object):� def process_request(self, request):� try:� real_ip = request.META['HTTP_X_FORWARDED_FOR']� except KeyError:� passelse:� # HTTP_X_FORWARDED_FOR can be a comma-separated list of IPs.# Take just the first one.� real_ip = real_ip.split(",")[0]� request.META['REMOTE_ADDR'] = real_ip

23 of 24

Tricky things #2

  • If you are using SSL be aware that getpaid is making relative HTTP Redirects
  • Django automatically translates relative redirects to full http:// or https:// URL before returning response
  • When choosing a protocol it makes educated guessing, but have no chance to guess if it's running behind proxy or web server
  • Solution: your proxy should clear & set "X-Forwarded-Proto: https" HTTP header for all SSL requests and you should tell django to use it (in settings.py):

SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

24 of 24

Thank you

Contributors are welcome via github fork+pull request

https://github.com/cypreess/django-getpaid

Krzysztof Dorosz

github: https://github.com/cypreess

twitter: @krzysztofdorosz

linkedin: http://www.linkedin.com/in/krzysztofdorosz

e-mail: cypreess@gmail.com