Skip to content

leftpudding/django-subscription

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

31 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Django Subscription

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.

1 Installation

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.

2 Settings

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.

3 Models

Two models defined by the application are available in the subscription.models module.

3.1 Subscription

Main model used by the application is Subscription. It represents a single subscription available for users. Subscription has following fields:

  • name - short name
  • description - longer description
  • price - subscription price
  • recurrence_period - PayPal subscription recurrence period (used only if recurrence_unit is not None)
  • 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 to django.contrib.auth.models.Group. Subscription is identified by the group.

3.1.1 methods

  • 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.

3.2 UserSubscription

This model instances define a user’s subscription. Model has following fields:

  • user - one-to-one relation to auth.User model, primary key;
  • subscription - foreign key relation to Subscription model, specifies kind of subscription user is subscribed to;
  • expires - expiry date (if null, subscription never expires)
  • active - boolean, True if subscription is active
  • cancelled - 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.

3.2.1 methods

  • user_is_group_member() - returns true if user is member of subscription.group;
  • expired() - returns true if there is more than SUBSCRIPTION_GRACE_PERIOD days after expires date;
  • valid() - returns true if:
    • expired() is false and user_is_group_member() is false, or
    • expired() is true and user_is_group_member() is true;
  • unsubscribe() - remove subscription.group from user’s groups
  • subscribe() - add subscription.group to user’s groups (called automatically on PayPal one-time payment and subscription start);
  • fix() - if not valid(), call unsubscribe() or subscribe();
  • extend(timedelta=None) - extend expires field by provided datetime.timedelta, or by subscription’s recurrence period (called automatically on PayPal subscription payments);
  • try_change(subscription) - sends change_check signal to test whether change from self.subscription to Subscription object supplied in subscription parameter 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.

3.3 Transaction

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 event
  • subscription - foreign key of Subscription model that event was related to
  • user - foreign key of django.contrib.auth.models.User model that event was related to
  • ipn - foreign key of paypal.standard.ipn.models.PayPalIPN model identifying payment callback related to event
  • event - 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) of ipn
  • comment - 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.

4 Signals

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 subscribed
  • unsubscribed - user unsubscribed from PayPal (usersubscription is a deleted object if usersubscription.active is True)
  • paid - payment received from a subscription
  • event - other strange event, does not receive usersubscription argument (there is no meaningful UserSubscription object) and receives additional event argument, which may be
    • unexpected_payment
    • flagged
    • unexpected_subscription
    • unexpected_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.

5 Views

Views are available in subscription.views module

  • subscription_list lists available subscription using subscription/subscription_list.html template
  • subscription_detail presents details of the selected subscription (login is required for this view) along with PayPal button for subscription or upgrade.

6 URLs

Module subscription.urls configures default urls for module. This are:

  • root URL displays subscription_list view
  • id (numeric ID) displays subscription_detail view for Subscription with ID id
  • paypal/ is PayPal IPN URL
  • done/ displays subscription/subscription_done.html template and is where successful PayPal transactions for initial subscription are redirected
  • change-done/ displays subscription/subscription_change_done.html template and is where successful PayPal transactions for subscription change are redirected
  • cancel/ displays subscription/subscription_cancel.html template and is where cancelled PayPal transactions are redirected

7 Templates

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:

  • object variable which is a Subscription object,
  • usersubscription variable, which is current user’s active UserSubscription instance (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 or None if user is not subscribed), change or signup is allowed,
  • form variable which is a PayPal form for the object, if change_denied_reasons is false,
  • cancel_url, which is URL to PayPal list of transactions with site’s merchant account, making it easier to cancel the old subscription.

8 Subscription change

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.

9 Example code

Example usage and templates are available as django-saas-kit project at http://github.com/CrowdSense/django-saas-kit/

10 Bugs and omissions

  • There is no setup.py script 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.

10.1 Plans

  • Single payments for subscription, including possibility of pay-as-you-go scheme

11 License

This project is dual-licensed on terms of MIT (MIT-LICENSE.txt) and GPL (GPL-LICENSE.txt) licenses.

About

Subscriptions based web app using django-paypal

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Python 100.0%