Package: django-stripe-billing
django-stripe-billingdjango-stripe-billingis a Django app to manage Stripe billing plans.InstallationAdd "django-stripe-billing" to yourrequirements.txtYou may now need to install it withpip install -r requirements.txt.Add "billing" to your INSTALLED_APPS setting like this:INSTALLED_APPS = [
    ...
    'billing',
    ...
]Include the billing URLconf in your project urls.py like this:path('billing/', include('billing.urls')),OPTIONAL: Use celery for webhook processing:pip install celeryand add it to requirements. If you don't install celery, it will process webhooks synchronously.Set theDjango settings.Runpython manage.py migrateto create the billing models.Runpython manage.py billing_init, which will create Customer objects for existing Users. If you don't do this, you may run into errors.Add this to your user admin file:import billing.admin
...
class UserAdmin(DefaultUserAdmin):
...
    inlines = [billing.admin.CustomerAdminInline, billing.admin.StripeEventAdminInline]Once you haveconfigured Stripe, create the desired billingPlans in the Django admin.Add thebilling.mixins.BillingMixinto 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 ConfigurationIn your Stripe dashboard, youmustconfigure it to cancel a customer's subscription if all retries for a payment fail.Do not allow the Customer to update their email address in Customer Portal.Do not allow the Customer to update billing information (other than payment method) or view invoice history.Update the branding of Checkout/Portal to match the branding of your site.In your Stripe dashboard, set up a product (with an optional statement descriptor), and set up a price for that product.In the Stripe dashboard, the following webhooks should be set to point tohttps://production.url/billing/stripe/webhook/:customer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedDjango SettingsBILLING_STRIPE_API_KEY: The Stripe API key.RequiredIf this is a real Stripe API key, it should be set in an environment variable.You may use the wordmockfor 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.RequiredThe Stripe customer metadata will store this in theapplicationkey.BILLING_CHECKOUT_SUCCESS_URLRequiredWhere Stripe Checkout should redirect on success.This view should parse Django messages.Must be an absolute URL or begin with a/.BILLING_CHECKOUT_CANCEL_URLRequiredWhere Stripe Checkout should redirect on cancel.This view should parse Django messages.Must be an absolute URL or begin with a/.BILLING_PORTAL_RETURN_URLOptionalWhere Stripe Customer Portal should redirect back to when done.If not provided, areturn_urlattribute must be specified in thePOSTwhen creating a Customer Portal.BILLING_STRIPE_WH_SECRETOptionalIf set, this should be in an environment variable.If this is set, Stripe webhook processing will verify the webhook signature for authenticity.UsagePOSTtobilling:create_checkout_sessionto create a Stripe Checkout Session.You must include 2 kwargs of a slugified plan nameslugand the planpk.We require both to enhance the privacy of private paid plans.POSTtobilling:create_portal_sessionto create a Stripe Billing Portal Session.Form data must containreturn_urlwhich is the URL to go back to once the Customer is done with the Portal. If this is omitted, it defaults to theLOGIN_REDIRECT_URL.ABillingMixinis available inbilling.mixins.BillingMixin. This defines aget_context_data(self, **kwargs)method that returns the following context:billing_enabledis a convenience check for whether billing is enabled.stripe_session_urlfor the form button to take you to the Stripe Checkout/Portal for thePAID_PUBLICplan.stripe_session_button_texttext for the button describing what it will do.billing_state_notedescribes basic info about the Customer's current subscription status.current_planthe the instance of the Customer's Plan.current_plan.nameandcurrent_plan.display_priceare useful if you want to display those things to the user.stripe_session_typeis eithercheckoutorportalor None (if it's not showing a Stripe url at all).To doPAID_PRIVATEplans, justPOSTto the appropriatebilling:create_checkout_sessionURL, which stays private because you need to pass both the slug and the pk.Things to KnowThe 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 stripecustomer_idto theCustomerthe first time theUserrequests to create a subscription. Before that, theuser.customer.customer_idwill benull.All paid plans must have a Stripeprice_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 DevelopmentRunning the Test Suitepython3 -m venv .venvsource .venv/bin/activatepip install -r requirements.txtpy.testSimulating WebhooksInstall the Stripe CLI. It's simple on Linux, just extract thetar.gzfile and put the file in yourPATH.Create the file in~/.config/stripe/config.tomlwith 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>"Runstripe listen --forward-to localhost:8000/billing/stripe/webhook/If you want to re-send an event:stripe events resend evt_<evtid>Creating migrationsTo create a migration, runDJANGO_SETTINGS_MODULE=billing.tests.settings django-admin makemigrations.Deleting Test DataFrom 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 paidPlaninstances to reflect the newprice_ids.Architecture and ModelsThere are six models in this application:Limit,Plan,PlanLimit,Customer,StripeSubscription, andStripeEvent. We'll focus on the first five, asStripeSubscriptionis supposed to be a faithful representation of a Subscription on Stripe, andStripeEventis for webhook processing.Limit, Plan, and PlanLimit ModelsTheLimitmodel 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 aLimitnamedMax Emailsto limit how many emails a user can send.There are 1 or morePlansthat have a many-to-many relationship withLimitthrough thePlanLimitmodel.
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 theLimitnamedMax Emails. In the through model,PlanLimityou set the value of theLimitfor thatPlanin anInlineAdmin.So far, so good. But what if yourPlanforgets to set one of theLimits? What's the value of theLimitfor thatPlan? For that reason, eachLimitalso defines adefaultvalue that is used if aPlanhasn't set
that particularLimit.Planscan be one of four types:free_default,free_private,paid_public,paid_private.There must at all times be exactly onefree_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.Afree_privateplan is a plan that you can assign staff to have free access at a paid level or with some higher than normal limits.Apaid_publicplan must have a correspondingprice_idin Stripe.Apaid_privateplan must have a correspondingprice_idin Stripe.free_defaultversusLimitdefaults. A source of confusion can be what is the difference between the limit values configured in thefree_default Planand the defaults set on theLimitinstances themselves? TheLimitdefaults attach whenanyPlandoes not define a value forthat particularLimit. There has to be some value for aLimitin, say, a paidPlaneven when thatPlandoes not specifically define theLimit.Thefree_default Planis simply anotherPlanthatmay or may notdefineLimits. If it does not, then functionally there is no difference between the two since the plan will fall back to the defaults set on theLimits. If it does specifically define values forLimits, 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 reasonLimitshave defaults is because there is no simple way to enforce that aPlanwill have a many-to-many relationship with every singleLimitdefined in the database. If we could enforce that easily, there would be no need for defaults on theLimitinstances themsleves.There must always be one and only onefree_default Plan. It's created in a data migration and this condition is enforced via a database constraint.Apaid_publicplan is subscribable by all users.Apaid_privateplan is subscribable by a user only by given access to a specific link to do so.Customer ModelFor non-paid plans, theCustomermodel is pretty straightforward. The only attributes of real significance is the linkedPlan, thecustomer_id, which is generated by Stripe the first time a User subscribes, andcurrent_period_end.current_period_endis when theCustomer'sPlanwill end if not renewed. After this time, theCustomerfalls back to thefree_defaultPlan. If aCustomer'sPlanis of typefree_default, theCustomercannot have acurrent_period_endsince that wouldn't make any sense, i.e., what would theCustomerfall back to.EveryUsermust have a relatedCustomer. If theUserdoes not have aCustomer, it will automatically create one on save.For paidPlans, 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 ModelStripe's Subscription model can have astatusof:incomplete,incomplete_expired,active,past_due,canceled,trialing, orunpaid.incompletemeans 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 getsincomplete_expiredwhich is functionally the same ascanceled. 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_dueoccurs when a recurring payment fails. The payment is retried according to settings in the Stripe dashboard. Once Stripe gives up, the status changes tocanceled.We don't usetrialing, which is useful if you want to have trials where the customer puts in their credit card before the trial. We don't useunpaid, which is an alternative way of handling permanent recurring payment failures instead of making the statuscancelled.Back to Our Customer ModelThere is a field on Customer calledpayment_statethat is a function of the Stripe subscription state.There is a property on Customer calledstatethat 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 inbilling.models. This can be improved and should probably operate more like a state machine.Possible Future EnhancementsMultiple paid plans. Will need to write tests to upgrade/downgrade plans and those should be their own endpoints probably.Grace periods for expired paymentsTrial periodsCoupons 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#emailsInterstitial 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.ReleasesTo publish a new release, push a git tag.