OverIQ.com

Django PayPal Subscriptions with Django-PayPal

Last updated on July 27, 2020


PayPal Standard with Subscription

There are many online services which allows users to access their content based on monthly or yearly subscription. The process of creation a subscription works almost exactly like PayPal Buy Now button. The only major difference is in the parameters that you sent to PayPal.

Note: This post is continuation of Django PayPal Integration with Django-Paypal. So, if you haven't read this post, I suggest you to do it first, then come back here. Also, we are using the same source files we used in that post.

The following table list some common subscription parameters.

Parameter Description
cmd (required) It tells PayPal what type of action to take. This parameter must be present in every form that submits data to PayPal. To create the subscription form you must set this parameter to _xclick-subscriptions. Other possible values are _xclick (for Buy Now button), _donations (for donation button)\, _cart (for PayPal Shopping Cart button) etc. If you don't specify cmd parameter while instantiating PayPalPaymentsForm\, then a default value of _xclick will be assigned to it.
a3 (required) Subscription Amount.
p3 (required) Length of the regular billing cycle. It must be a number.
t3 (required) Units of the regular billing cycle. The possible values are D (for days), W (for weeks), M (for months), Y (for year). The p3 and t3 parameter go hand in hand. And at first they might appear confusing. So, here are some examples, p3=1 and t3='M' represents a monthly billing cycle, p3=1 and t3='Y' represents a yearly billing cycle, and p3=6 and t3='M' represents a 6-month billing cycle.
src (optional) Recurring payments. The possible values are 0 and 1. If set to 1, payment will recur at the end of the current billing cycle. The 0 indicates that the payment don't recur. The default value is 0.
sra (optional) Reattempt on failure. The possible values are 0 and 1. If set to 1 and recurring payment fails, then PayPal will attempt to collect the payments two or more times before canceling the subscription.
srt (optional) It specifies the number of times that subscription payment to recur. If this parameter is not specified then the payment will recur at regular rate until the subscription is canceled.
no_note (required) If set to 1, customers will not be prompted to enter a note with the subscription.
subscr_id (optional) PayPal generated unique id for the subscriber.

Creating Subscription Form #

Open forms.py file and add the code to create subscription form as follows:

simple_ecommerce/django_project/ecommerce_app/forms.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#...
class CheckoutForm(forms.ModelForm):
    #...

subscription_options = [
    ('1-month', '1-Month subscription ($10 USD/Mon)'),
    ('6-month', '6-Month subscription Save $10 ($50 USD/Mon)'),
    ('1-year', '1-Year subscription Save $30 ($90 USD/Mon)'),
]


class SubscriptionForm(forms.Form):
    plans = forms.ChoiceField(choices=subscription_options)

In views.py file create a view function to display the form.

simple_ecommerce/django_project/ecommerce_app/views.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#...
from .models import Product, Order, LineItem
from .forms import CartForm, CheckoutForm, SubscriptionForm
from . import cart

#...

@csrf_exempt
def payment_canceled(request):
    return render(request, 'ecommerce_app/payment_cancelled.html')


def subscription(request):
    if request.method == 'POST':
        f = SubscriptionForm(request.POST)
        if f.is_valid():
            request.session['subscription_plan'] = request.POST.get('plans')
            return redirect('process_subscription')
    else:
        f = SubscriptionForm()
    return render(request, 'ecommerce_app/subscription_form.html', locals()

When user submits the subscription form, we store the selected plan in the session (line 17) and then redirect the user to the URL named process_subscription (which we haven't yet created).

Create a template named subscription_form.html in the templates directory of the ecommerce_app directory and add the following code to it.

simple_ecommerce/django_project/ecommerce_app/templates/ecommerce_app/subscription_form.html

1
2
3
4
5
6
7
8
9
{% extends "ecommerce_app/base.html" %}
{% block title %}Subscribe{% endblock %}
{% block content %}
    <form action="" method="post">
        {% csrf_token %}
        {{ f }}
        <input type="submit" value="Subscribe">
    </form>
{% endblock %}

Next, add a new URL pattern named subscription in ecommerce_app's urls.py file as follows:

simple_ecommerce/django_project/ecommerce_app/urls.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#...

urlpatterns = [
    #...
    path('checkout/', views.checkout, name='checkout'),
    path('process-payment/', views.process_payment, name='process_payment'),
    path('payment-done/', views.payment_done, name='payment_done'),
    path('payment-cancelled/', views.payment_canceled, name='payment_cancelled'),
    path('subscribe/', views.subscription, name='subscription'),    
]

Start the server if not already running and visit http://localhost:8000/subscribe/. You should see our subscription form like this:

The subscription form is almost ready. Let's now code a view function process the subscription form. In the views.py file, add process_subscription() view towards the end of the file as follows:

simple_ecommerce/django_project/ecommerce_app/views.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#...

def subscription(request):
    #...

def process_subscription(request):

    subscription_plan = request.session.get('subscription_plan')
    host = request.get_host()

    if subscription_plan == '1-month':
        price = "10"
        billing_cycle = 1
        billing_cycle_unit = "M"
    elif subscription_plan == '6-month':
        price = "50"
        billing_cycle = 6
        billing_cycle_unit = "M"
    else:
        price = "90"
        billing_cycle = 1
        billing_cycle_unit = "Y"


    paypal_dict  = {
        "cmd": "_xclick-subscriptions",
        'business': settings.PAYPAL_RECEIVER_EMAIL,
        "a3": price,  # monthly price
        "p3": billing_cycle,  # duration of each unit (depends on unit)
        "t3": billing_cycle_unit,  # duration unit ("M for Month")
        "src": "1",  # make payments recur
        "sra": "1",  # reattempt payment on payment error
        "no_note": "1",  # remove extra notes (optional)
        'item_name': 'Content subscription',
        'custom': 1,     # custom data, pass something meaningful here
        'currency_code': 'USD',
        'notify_url': 'http://{}{}'.format(host,
                                           reverse('paypal-ipn')),
        'return_url': 'http://{}{}'.format(host,
                                           reverse('payment:done')),
        'cancel_return': 'http://{}{}'.format(host,
                                              reverse('payment:canceled')),
    }

    form = PayPalPaymentsForm(initial=paypal_dict, button_type="subscribe")
    return render(request, 'payment/process_subscription.html', locals())

Here is how this view works:

  1. In line 8, we fetch the subscription plan stored in the session using request.session dictionary.
  2. In line 9, we access the host using the get_host() method of the Request object
  3. In lines 11-22, we set subscription price, billing cycle and billing cycle unit based upon the subscription plan the user has selected.
  4. In line 25-43, we construct a dictionary containing parameters that will be sent to PayPal
  5. By default, PayPalPaymentsForm creates a Buy Now button. To create a subscription button we pass button_type="subscribe" (line 45).
  6. Finally, In line 46, we render the the template using the render() function.

Create a template named process_subscription.html in ecommerce_app's templates directory with the following code:

simple_ecommerce/django_project/ecommerce_app/templates/ecommerce_app/process_subscription.html

1
2
3
4
5
6
{% extends "ecommerce_app/base.html" %}
{% block title %}Pay using PayPal{% endblock %}
{% block content %}
    <h4>Subscribe using PayPal</h4>
    {{ form.render }}
{% endblock %}

Finally, add a new URL pattern named process_subscription to ecommerce_app's urls.py file as follows:

simple_ecommerce/django_project/ecommerce_app/urls.py

1
2
3
4
5
6
#...
urlpatterns = [
    #...
    path('subscribe/', views.subscription, name='subscription'),
    path('process_subscription/', views.process_subscription, name='process_subscription'),
]

Now, open your browser and visit the subscription page (i.e http://localhost:8000/subscribe/).

Select any subscription plan and click the submit button. You should now see process subscription page like this:

Click the subscribe button and you will be taken to PayPal checkout page:

Here you can see the name of the subscription and amount you will be billed. To proceed click PayPal checkout button and you will be redirected to PayPal login page.

Enter buyer email address and password (from sandbox account) and click Log In. In the next screen PayPal will ask you to select the payment method.

Once you have selected the payment method click the Continue button. At this point, you can also cancel the transaction by clicking cancel return link at the bottom of the page.

Assuming you clicked on the Continue button, the next screen should look like this:

Confirm the subscription by clicking Agree & Pay.

After successful payment, you will be redirected to the following page:

To return to the seller site click on "Return to Merchant" button at the bottom and PayPal will redirect you to the URL specified in the return_url parameter.

Acting on Signals #

When a customer uses PayPal Buy Now button to purchase an item, PayPal sends a single IPN to the application. We can use the payment_status attribute of PayPalIPN object to to determine whether the payment was successful or not. The transaction type (txn_type) of this kind of IPN is web_accept.

Things are little bit more involved with PayPal subscription as there are several different types of IPNs that PayPal can send during the life-cycle of a subscription.

When customer signs up for a subscription for the first time PayPal sends the 2 different IPNs to the merchant. The transaction type (txn_type) of these two IPNs are as follows:

  1. subscr_signup
  2. subscr_payment

subscr_signup - This IPN is sent only the first time user sign up for a subscription. It doesn't fire in any event later.

subscr_payment - This IPN is sent every time a recurring payment is received including when the customer signs up for the first time.

Note that the order in which you receive these two IPNs may vary, so you shouldn't depend on it.

You can use subscr_signup IPN to activate the customer account or to send a welcome note. Similarly, subscr_payment IPN can be used to send the link to access the premium content or let the customer know that the payment was successful.

Here are some other types of IPN that are sent during the life-cycle of a subscription.

IPN (txn_type) Description
subscr_cancel This IPN is sent when a subscription is canceled either by customer or merchant.
subscr_modify This IPN is sent when a customer modifies the subscription.
subscr_eot This IPN is sent when the subscription ends.
subscr_failed This IPN is sent when subscription payment fails.

You can use these IPNs to perform any desired action that makes sense in the context of your application. For example, in the event of a failed subscription payment (subscr_failed IPN), you can send an email to customer like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Hello User

We couldn't process your payment for this month.
It looks like your card is being declined.
To resolve, the issue we suggest you to check your payment info.
You may also need to contact the bank to get more information on the issue.

Happy to help.

Customer Support

Now you know different types of IPNs that are sent in the case of a subscription. Let's update our receiver function to act upon these IPNs accordingly.

simple_ecommerce/django_project/ecommerce_app/signals.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#...
from paypal.standard.ipn.signals import valid_ipn_received
from django.dispatch import receiver
from paypal.standard.models import ST_PP_COMPLETED
from django.core.mail import EmailMessage
from django.contrib.auth.models import User
from datetime import datetime


@receiver(valid_ipn_received)
def ipn_receiver(sender, **kwargs):
    ipn_obj = sender

    # check for Buy Now IPN
    if ipn_obj.txn_type == 'web_accept':

        if ipn_obj.payment_status == ST_PP_COMPLETED:
            # payment was successful
            print('great!')
            order = get_object_or_404(Order, id=ipn_obj.invoice)

            if order.get_total_cost() == ipn_obj.mc_gross:
                # mark the order as paid
                order.paid = True
                order.save()

    # check for subscription signup IPN
    elif ipn_obj.txn_type == "subscr_signup":

        # get user id and activate the account
        id = ipn_obj.custom
        user = User.objects.get(id=id)
        user.active = True
        user.save()

        subject = 'Sign Up Complete'

        message = 'Thanks for signing up!'

        email = EmailMessage(subject,
                             message,
                             'admin@myshop.com',
                             [user.email])

        email.send()

    # check for subscription payment IPN
    elif ipn_obj.txn_type == "subscr_payment":

        # get user id and extend the subscription
        id = ipn_obj.custom
        user = User.objects.get(id=id)
        # user.extend()  # extend the subscription

        subject = 'Your Invoice for {} is available'.format(
            datetime.strftime(datetime.now(), "%b %Y"))

        message = 'Thanks for using our service. The balance was automatically ' \
                  'charged to your credit card.'

        email = EmailMessage(subject,
                             message,
                             'admin@myshop.com',
                             [user.email])

        email.send()

    # check for failed subscription payment IPN
    elif ipn_obj.txn_type == "subscr_failed":
        pass

    # check for subscription cancellation IPN
    elif ipn_obj.txn_type == "subscr_cancel":
        pass

Keep in mind that you will not receive an IPN from PayPal until you make your application publicaly accessible on the internet. We have already seen how to do that with ngrok in post Django PayPal Integration with Django-Paypal.

Start the Django development server again and bind the ngrok to port 8000 (or whatever port your Django development server is listening to)

Visit http://0c0211d3.ngrok.io/subscribe/ (replace it with your ngrok URL) and sign up for a subscription again.

After successful payment, in the shell running the server you will you get the following output:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: Sign Up Complete
From: admin@myshop.com
To: admin@gmail.com
Date: Mon, 05 Nov 2018 11:15:11 -0000
Message-ID: <20181105111511.30653.9495@pp-desktop>

Thanks for signing up!
-------------------------------------------------------------------------------

Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: Your Invoice for Nov 2018 is available
From: admin@myshop.com
To: admin@gmail.com
Date: Mon, 05 Nov 2018 11:15:25 -0000
Message-ID: <20181105111525.30653.51529@pp-desktop>

Thanks for using our service. The balance was automatically charged to your credit card.
-------------------------------------------------------------------------------

Note: The two IPNs may not come back to back. Expect some delay.

Now, visit PayPal IPN list page (i.e http://localhost:8000/admin/ipn/paypalipn/) and you should see two IPNs (one of type subscr_signup and other type subscr_payment) as follows:

Just as with PayPal Buy Now button, you can pass the same pass-through variables with subscriptions. In fact, we are already passing a pass-through variable called custom.

The important thing to note is that the pass-through variables will be included with the IPNs on the recurring billing cycle in the future.