Django-subscription is an application for handling PayPal-based pay
subscriptions. Module does not handle explicit permissions; instead,
subscribed user are automatically added to predefined groups using
django.contrib.auth application. It needs django-paypal application
available at http://github.com/johnboxall/django-paypal/ for handling
payments.
Copy or symlink subscription/ directory on Python path (setup.py
script for automated installation will be supplied later on). Module
contents are available in the subscription module.
In order to use application, add subscription to INSTALLED_APPS in
Django project settings.py file.
In project’s settings.py file SUBSCRIPTION_PAYPAL_SETTINGS
should be set to a dictionary with default PayPal button settings,
as described in django-paypal documentation. At least the business
key should be set to your PayPal business e-mail.
SUBSCRIPTION_PAYPAL_FORM can be set to a form class pathname as
string to use custom PayPal payment button class. Default is
‘paypal.standard.forms.PayPalPaymentsForm’. To use PayPal encrypted
buttons or shared secrets, specify needed django-paypal settings and
set an appropriate class here.
SUBSCRIPTION_GRACE_PERIOD is an integer and it specifies number of
days after individual subscription expiry on which account is
actually treated as expired. Default is 2 days. Intent of this
setting is that recurring payments take place e.g. monthly, so
either on last day of subscription period, or even on first day
after it; this way we avoind unintentionally locking out user
account.
Two models defined by the application are available in the
subscription.models module.
Main model used by the application is Subscription. It
represents a single subscription available for users. Subscription
has following fields:
name- short namedescription- longer descriptionprice- subscription pricerecurrence_period- PayPal subscription recurrence period (used only ifrecurrence_unitis notNone)recurrence_unit- in what units is recurrence period expressed:- D for days
- W for weeks
- M for months
- Y for years
- None (NULL) for one-time (non-recurring) payment
group- one to one relation todjango.contrib.auth.models.Group. Subscription is identified by the group.
price_per_day()- returns estimate subscription price per day, as a float. This value is used to give user that upgrades subscription a rebate for unused part of month. Value is only an estimate: average length of month (30.4368 days) and year (365.2425 days) are used.get_pricing_display()- return pretty pricing info for display as a string.
This model instances define a user’s subscription. Model has following fields:
user- one-to-one relation toauth.Usermodel, primary key;subscription- foreign key relation toSubscriptionmodel, specifies kind of subscriptionuseris subscribed to;expires- expiry date (if null, subscription never expires)active- boolean, True if subscription is activecancelled- boolean, True if subscription was cancelled
Fields active and cancelled are used for implementing the
subscription change flow (see later). Every UserSubscription
starts with both active and cancelled set to False. When
PayPal subscription is confirmed, active is set to True. When
any other PayPal subscription for the same user is confirmed,
active is set to False (because active is set to True for
this other subscription, in other UserSubscription instance). When
subscription is cancelled at PayPal, cancelled is set to True.
When UserSubscription is cancelled and not active, it is deleted.
When UserSubscription has expired and is cancelled, it is deleted.
Transition graph of these state bits can be found in
file:docs/usersubscription-states.dot.png (GraphViz source in
file:docs/usersubscription-states.dot).
Class field grace_timedelta is provided (read-only) and contains
effective value of SUBSCRIPTION_GRACE_PERIOD setting as
datetime.timedelta object.
user_is_group_member()- returns true ifuseris member ofsubscription.group;expired()- returns true if there is more thanSUBSCRIPTION_GRACE_PERIODdays afterexpiresdate;valid()- returns true if:expired()is false anduser_is_group_member()is false, orexpired()is true anduser_is_group_member()is true;
unsubscribe()- removesubscription.groupfromuser’s groupssubscribe()- addsubscription.grouptouser’s groups (called automatically on PayPal one-time payment and subscription start);fix()- if notvalid(), callunsubscribe()orsubscribe();extend(timedelta=None)- extendexpiresfield by provideddatetime.timedelta, or bysubscription’s recurrence period (called automatically on PayPal subscription payments);try_change(subscription)- sendschange_checksignal to test whether change fromself.subscriptionto Subscription object supplied insubscriptionparameter is possible. Returns list of reasons why upgrade is denied; if list is empty, upgrade is allowed.
Convenience function subscription.models.unsubscribe_expired()
is also provided. It loops over all expired UserSubscription
instances and calls unsubscribe() method. It is intended to be
called automatically from cron, django-cron, or on some event.
Alternatively, fix() can be called on events related to
user, e.g. on user login.
Transaction model is mostly read-only and is used to view
subscription-related events in the admin panel. It has following
fields:
timestamp- date and time of eventsubscription- foreign key ofSubscriptionmodel that event was related touser- foreign key ofdjango.contrib.auth.models.Usermodel that event was related toipn- foreign key ofpaypal.standard.ipn.models.PayPalIPNmodel identifying payment callback related to eventevent- type of event, one of:- new usersubscription
- one-time payment
- subscription payment
- unexpected payment
- payment flagged
- deactivated
- activated
- unexpected subscription
- remove subscription
- cancel subscription
- unexpected cancel
- modify subscription
- subscription expired
The “unexpected” events are ones that could not be related to any specific user/subscription pair.
amount- amount (mc_gross) ofipncomment- site admin’s comment, only field intended to be modified.
In admin panel’s Transaction object list, fields subscription,
user, ipn are links to related modes instance’s admin forms.
On subscription-related events, the application sends signals that
project code can connect to and do some site-specific things (e.g.
send a nice e-mail to user). Signals are available in
subscription.signals package. All signals have Subscription
instance (or, in extreme cases with event signal, None) as
sender, and have arguments ipn
(paypal.standard.ipn.models.PayPalIPN model instance), user
(django.contrib.auth.models.User instance), subscription
(Subscription instance or None, same as sender),
usersubscription (UserSubscription instance). Signals are:
signed_up- user signed up for one-time payment,subscribed- user subscribedunsubscribed- user unsubscribed from PayPal (usersubscriptionis a deleted object ifusersubscription.activeis True)paid- payment received from a subscriptionevent- other strange event, does not receiveusersubscriptionargument (there is no meaningfulUserSubscriptionobject) and receives additionaleventargument, which may beunexpected_paymentflaggedunexpected_subscriptionunexpected_cancel
subscription_modify
Signal change_check is a hook for verification of subscription
change. Sender is UserSubscription object with user’s current
subscription, additional parameter subscription provides
subscription to change to. If subscription change is possible,
listener should return None, otherwise it should return a string
describing reason that will be displayed to user.
Views are available in subscription.views module
subscription_listlists available subscription usingsubscription/subscription_list.htmltemplatesubscription_detailpresents details of the selected subscription (login is required for this view) along with PayPal button for subscription or upgrade.
Module subscription.urls configures default urls for module. This
are:
- root URL displays
subscription_listview - id (numeric ID) displays
subscription_detailview for Subscription with ID id paypal/is PayPal IPN URLdone/displayssubscription/subscription_done.htmltemplate and is where successful PayPal transactions for initial subscription are redirectedchange-done/displayssubscription/subscription_change_done.htmltemplate and is where successful PayPal transactions for subscription change are redirectedcancel/displayssubscription/subscription_cancel.htmltemplate and is where cancelled PayPal transactions are redirected
Templates subscription/subscription_done.html and
subscription/subscription_cancel.html receive no context.
Template subscription/subscription_change_dane.html receives
cancel_url parameter, which is URL to PayPal list of transactions
with site’s merchant account, making it easier to cancel the old
subscription.
Template subscription/subscription_list.html receives
object_list variable which is a list of Subscription objects.
Template subscription/subscription_detail.html receives:
objectvariable which is aSubscriptionobject,usersubscriptionvariable, which is current user’s activeUserSubscriptioninstance (may be used to tell apart initial subscription from subscription change/upgrade, or to display current subscription’s expiry date),change_denied_reasons, which is a list of reasons that subscription change/upgrade is denied; if false (empty list orNoneif user is not subscribed), change or signup is allowed,formvariable which is a PayPal form for theobject, ifchange_denied_reasonsis false,cancel_url, which is URL to PayPal list of transactions with site’s merchant account, making it easier to cancel the old subscription.
Most complex flow in this app is when user wants to change (upgrade) current subscription. For subscriptions we are using PayPal standard subscriptions API. This means, we get three kinds of asynchronous IPN notifications:
- subscr_signup when user signs up for new subscription,
- subscr_payment on every single payment,
- subscr_cancel when user or merchant cancels subscription (or subscr_eot when time-limited subscription runs out; we treat subscr_eot exactly as subscr_cancel).
When user signs up, we get subscr_signup and subscr_payment for first payment, in random order. There is no support for changing running subscription, so user needs to sign up for new subscription and cancel old one.
Events for subscriptions are handled this way:
- subscr_payment finds UserSubscription object for User and Subscription ID specified in the IPN. If UserSubscription is not found, new one is created, which becomes inactive. Found or new UserSubscription object is extended for the next billing period.
- subscr_signup finds UserSubscription object for User and Subscription ID specified in the IPN. If UserSubscription is not found, new one is created. Found or created UserSubscription is set to active, User is added to subscription’s group; if user has another UserSubscription, they are made inactive and user is removed from these Subscription groups. In effect, on signup the new subscription becomes user’s only active one, and its group only subscription-related group to which user belongs.
- subscr_cancel finds relevant UserSubscription object. If it is inactive (which means subscription change), removes user from its subscription’s group, and deletes the UserSubscription. If it is active, does nothing, so user can use up rest of current billing period.
So, signup flow is:
- user clicks in PayPal subscribe button displayed on subscription detail page and subscribes at PayPal,
- subscr_payment extends the UserSubscription,
- subscr_signup makes the UserSubscription active and uncancelled and adds user to group,
- whichever of those got called first, creates the UserSubscription.
Cancel flow is:
- user cancels subscription at PayPal,
- UserSubscription is active, so it is marked cancelled, kept and stays valid until expiry.
Subscription change flow is:
- If user is allowed to change subscription, subscription detail page displays PayPal subscribe button,
- user clicks subscribe button and signs up for new subscription at PayPal,
- landing page after PayPal transaction displays link to PayPal transaction list which user can use to cancel old subscription at PayPal,
- user cancels old subscription at PayPal;
- whichever of subscr_payment or subscr_signup gets called first, creates new, inactive, uncancelled UserSubscription instance,
- subscr_payment extends new UserSubscription instance for next billing period,
- subscr_signup deactivates all active UserSubscriptions and removes user from group; then, activates and uncancels new UserSubscription and adds user to its subscription’s group,
- subscr_cancel (which gets called after previous two, because user needs some time to click through the PayPal forms) finds inactive UserSubscription, ensures that user is really not member of group, and deletes the UserSubscription object.
If user makes a mistake and cancels new subscription instead of the
old one, new subscription goes through “Cancel flow” above, does not
get deleted, so user has chance to fix things at PayPal. Project
should add signals.unsubscribed handler that would detect such
situation (if usersubscription parameter is active, and user has
inactive UserSubscription objects, cancel was probably a mistake)
and notify user of his mistake.
Example usage and templates are available as django-saas-kit
project at http://github.com/CrowdSense/django-saas-kit/
- There is no
setup.pyscript for automated installation. - No support for PayPal PDT; PDT has only presentational value (IPN needs to be received anyway, and PDT should be used only to display transaction details to user on after transaction landing page), so support for it has been intentionally omitted.
- Single payments for subscription, including possibility of pay-as-you-go scheme
This project is dual-licensed on terms of MIT (MIT-LICENSE.txt) and GPL (GPL-LICENSE.txt) licenses.