Skip to main content

Django billing plans and payments with Stripe

Project description

django-stripe-billing

django-stripe-billing is a Django app to manage Stripe billing plans.

Installation

  1. Add "django-stripe-billing" to your requirements.txt
  2. You may now need to install it with pip install -r requirements.txt.
  3. Add "billing" to your INSTALLED_APPS setting like this:
    INSTALLED_APPS = [
        ...
        'billing',
        ...
    ]
    
  4. Include the billing URLconf in your project urls.py like this:
        path('billing/', include('billing.urls')), 
    
  5. OPTIONAL: Use celery for webhook processing: pip install celery and add it to requirements. If you don't install celery, it will process webhooks synchronously.
  6. Set the Django settings.
  7. Run python manage.py migrate to create the billing models.
  8. Run python manage.py billing_init, which will create Customer objects for existing Users. If you don't do this, you may run into errors.
  9. Add this to your user admin file:
    import billing.admin
    ...
    class UserAdmin(DefaultUserAdmin):
    ...
        inlines = [billing.admin.CustomerAdminInline, billing.admin.StripeEventAdminInline]
    
  10. Once you have configured Stripe, create the desired billing Plans in the Django admin.
  11. Add the billing.mixins.BillingMixin to the view where a user might manage their billing (e.g. a "Settings" view).
  • There must be at least one Paid billing plan to use this Mixin.

Stripe Configuration

  1. In your Stripe dashboard, you must configure it to cancel a customer's subscription if all retries for a payment fail.
  2. Do not allow the Customer to update their email address in Customer Portal.
  3. Do not allow the Customer to update billing information (other than payment method) or view invoice history.
  4. Update the branding of Checkout/Portal to match the branding of your site.
  5. In your Stripe dashboard, set up a product (with an optional statement descriptor), and set up a price for that product.
  6. In the Stripe dashboard, the following webhooks should be set to point to https://production.url/billing/stripe/webhook/:
    • customer.subscription.created
    • customer.subscription.updated
    • customer.subscription.deleted

Django Settings

  • BILLING_STRIPE_API_KEY: The Stripe API key.
    • Required
    • If this is a real Stripe API key, it should be set in an environment variable.
    • You may use the word mock for a mocked Stripe client. This can be useful in local development. You can't use Checkout/Portal, but any other function that calls out to stripe will just call a mock.
    • You must use a real test environment Stripe API key if using Stripe Checkout / Portal while developing locally.
    • Obviously, only use a live environment Stripe API key in production.
  • BILLING_APPLICATION_NAME: The name of the application.
    • Required
    • The Stripe customer metadata will store this in the application key.
  • BILLING_CHECKOUT_SUCCESS_URL
    • Required
    • Where Stripe Checkout should redirect on success.
    • This view should parse Django messages.
    • Must be an absolute URL or begin with a /.
  • BILLING_CHECKOUT_CANCEL_URL
    • Required
    • Where Stripe Checkout should redirect on cancel.
    • This view should parse Django messages.
    • Must be an absolute URL or begin with a /.
  • BILLING_PORTAL_RETURN_URL
    • Optional
    • Where Stripe Customer Portal should redirect back to when done.
    • If not provided, a return_url attribute must be specified in the POST when creating a Customer Portal.
  • BILLING_STRIPE_WH_SECRET
    • Optional
    • If set, this should be in an environment variable.
    • If this is set, Stripe webhook processing will verify the webhook signature for authenticity.

Usage

  • POST to billing:create_checkout_session to create a Stripe Checkout Session.
    • You must include 2 kwargs of a slugified plan name slug and the plan pk.
    • We require both to enhance the privacy of private paid plans.
  • POST to billing:create_portal_session to create a Stripe Billing Portal Session.
    • Form data must contain return_url which is the URL to go back to once the Customer is done with the Portal. If this is omitted, it defaults to the LOGIN_REDIRECT_URL.
  • A BillingMixin is available in billing.mixins.BillingMixin. This defines a get_context_data(self, **kwargs) method that returns the following context:
    • billing_enabled is a convenience check for whether billing is enabled.
    • stripe_session_url for the form button to take you to the Stripe Checkout/Portal for the PAID_PUBLIC plan.
    • stripe_session_button_text text for the button describing what it will do.
    • billing_state_note describes basic info about the Customer's current subscription status.
    • current_plan the the instance of the Customer's Plan. current_plan.name and current_plan.display_price are useful if you want to display those things to the user.
    • stripe_session_type is either checkout or portal or None (if it's not showing a Stripe url at all).
  • To do PAID_PRIVATE plans, just POST to the appropriate billing:create_checkout_session URL, which stays private because you need to pass both the slug and the pk.

Things to Know

  • The app should automatically create a Default Free plan during installation.
  • Users should have a first name, last name and email.
  • Deleting a User or setting User.is_active to false will cancel any active Stripe subscriptions.
  • Updating a User's first name, last name or email will update it on Stripe.
  • The app will assign a stripe customer_id to the Customer the first time the User requests to create a subscription. Before that, the user.customer.customer_id will be null.
  • All paid plans must have a Stripe price_id.
  • The app will automaticaly create a free_default plan the first time its needed if one doesn't exist and it will default to whatever defaults are specified in the Limits. You can modify the plan or even delete it, but there must always be 1 free_default plan and if there is not, the app will create it the next time it needs it.
  • A user with a paid plan that has expired will drop to the limits set in the free_default plan.
  • A user with a free private (i.e. staff) plan that has expired will drop to the limits set in the free_default plan. A user with a free private plan where there is no current_period_end set will be treated as NO expiration date on the plan and will continue to enjoy the free private plan indefinitely.
  • Incoming webhooks from Stripe are treated as authoritative, so if you have someone on a private free plan, for example, and a Stripe webhook for a paid plan comes in, it will use the latter. So be sure to make changes in Stripe to match whatever changes you make to a Customer in the admin.

Local Development

Running the Test Suite

  1. python3 -m venv .venv
  2. source .venv/bin/activate
  3. pip install -r requirements.txt
  4. py.test

Simulating Webhooks

  1. Install the Stripe CLI. It's simple on Linux, just extract the tar.gz file and put the file in your PATH.
  2. Create the file in ~/.config/stripe/config.toml with this format:
  [default]
    device_name = "<choose a name>"
    test_mode_api_key = "<test environment secret or restricted key>"
    test_mode_publishable_key = "<test environment publishable key>"
  1. Run stripe listen --forward-to localhost:8000/billing/stripe/webhook/
  2. If you want to re-send an event: stripe events resend evt_<evtid>

Creating migrations

To create a migration, run DJANGO_SETTINGS_MODULE=billing.tests.settings django-admin makemigrations.

Deleting Test Data

From time to time, you may want to delete all Stripe test data via the dashboard. If you do that, your API keys should remain the same and won't need to be updated. But you will need to create a product and price in the Stripe dashboard and update any paid Plan instances to reflect the new price_ids.

Architecture and Models

There are six models in this application: Limit, Plan, PlanLimit, Customer, StripeSubscription, and StripeEvent. We'll focus on the first five, as StripeSubscription is supposed to be a faithful representation of a Subscription on Stripe, and StripeEvent is for webhook processing.

Limit, Plan, and PlanLimit Models

The Limit model defines the specific features of your application that are regulated by billing. For example, if you can send emails via your application, you might have a Limit named Max Emails to limit how many emails a user can send.

There are 1 or more Plans that have a many-to-many relationship with Limit through the PlanLimit model. It's through those relationships you set the limits for the various plans. For example, if you have a free plan that can send 1 email per day and paid plan that can send 5 emails per day, each of those plans would have a M2M relationship with the Limit named Max Emails. In the through model, PlanLimit you set the value of the Limit for that Plan in an InlineAdmin.

So far, so good. But what if your Plan forgets to set one of the Limits? What's the value of the Limit for that Plan? For that reason, each Limit also defines a default value that is used if a Plan hasn't set that particular Limit.

Plans can be one of four types: free_default, free_private, paid_public, paid_private.

  • There must at all times be exactly one free_default Plan. This is the plan that a user defaults to when they create an account. Or if their credit card doesn't go through. It's the 'fallback' plan when no other plan has been selected. If you have a free tier, it would be sensible to configure it as this plan. This plan must be free and does not interface with Stripe.
  • A free_private plan is a plan that you can assign staff to have free access at a paid level or with some higher than normal limits.
  • A paid_public plan must have a corresponding price_id in Stripe.
  • A paid_private plan must have a corresponding price_id in Stripe.

free_default versus Limit defaults. A source of confusion can be what is the difference between the limit values configured in the free_default Plan and the defaults set on the Limit instances themselves? The Limit defaults attach when any Plan does not define a value for that particular Limit. There has to be some value for a Limit in, say, a paid Plan even when that Plan does not specifically define the Limit.

The free_default Plan is simply another Plan that may or may not define Limits. If it does not, then functionally there is no difference between the two since the plan will fall back to the defaults set on the Limits. If it does specifically define values for Limits, those values as defined become what a user falls back to when their credit card expires or they cancel their paid subscription.

Practically speaking, the real reason Limits have defaults is because there is no simple way to enforce that a Plan will have a many-to-many relationship with every single Limit defined in the database. If we could enforce that easily, there would be no need for defaults on the Limit instances themsleves.

There must always be one and only one free_default Plan. It's created in a data migration and this condition is enforced via a database constraint.

A paid_public plan is subscribable by all users.

A paid_private plan is subscribable by a user only by given access to a specific link to do so.

Customer Model

For non-paid plans, the Customer model is pretty straightforward. The only attributes of real significance is the linked Plan, the customer_id, which is generated by Stripe the first time a User subscribes, and current_period_end.

current_period_end is when the Customer's Plan will end if not renewed. After this time, the Customer falls back to the free_default Plan. If a Customer's Plan is of type free_default, the Customer cannot have a current_period_end since that wouldn't make any sense, i.e., what would the Customer fall back to.

Every User must have a related Customer. If the User does not have a Customer, it will automatically create one on save.

For paid Plans, things are a little more complicated. Before we dive into it, first a brief primer on Stripe's subscription model.

Primer on Stripe's Subscription Model

Stripe's Subscription model can have a status of: incomplete, incomplete_expired, active, past_due, canceled, trialing, or unpaid.

incomplete means a Customer's credit card was attached to them and a subscription was created but the card was declined. The Customer has 23 hours to fix it and if they don't, the subscription gets incomplete_expired which is functionally the same as canceled. I.e., no invoices will be created or paid in those states. We don't use this because we just ignore incomplete subscriptions and let them expire. A user creates a fresh one in the Checkout if they come back.

past_due occurs when a recurring payment fails. The payment is retried according to settings in the Stripe dashboard. Once Stripe gives up, the status changes to canceled.

We don't use trialing, which is useful if you want to have trials where the customer puts in their credit card before the trial. We don't use unpaid, which is an alternative way of handling permanent recurring payment failures instead of making the status cancelled.

Back to Our Customer Model

There is a field on Customer called payment_state that is a function of the Stripe subscription state.

There is a property on Customer called state that is calculated from all the other attributes on Customer. These can be used for easy representation of Customer state on the frontend.

You can see what they are in billing.models. This can be improved and should probably operate more like a state machine.

Possible Future Enhancements

  • Multiple paid plans. Will need to write tests to upgrade/downgrade plans and those should be their own endpoints probably.
  • Grace periods for expired payments
  • Trial periods
  • Coupons for friends
  • "When a subscription changes to past_due, your webhook script could email you about the problem so you can reach out to the customer, or the script could email the customer directly, asking them to update their payment details." Although maybe we could rely on this: https://stripe.com/docs/billing/subscriptions/overview#emails
  • Interstitial pages to wait for webhooks. Imagine you do the checkout flow then it redirects you to a spinner page that polls an endpoint for where to go next. If the webhook hasn't been received redirect back for 3 seconds. If it has, redirect to the success page.
  • Move away from Portal. Maybe wouldn't be terrible since all the heavy lifting happens in webhook processing. The interstitial pages would be useful here.

Releases

To publish a new release, push a git tag.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django-stripe-billing-0.5.1.tar.gz (40.1 kB view hashes)

Uploaded Source

Built Distribution

django_stripe_billing-0.5.1-py3-none-any.whl (42.2 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page