Masonite Billing

Requires

  • Masonite 2.0.0+

Installation

Installing Masonite Billing is simple. We just need a new configuration file, 2 new migrations and extend our User model.

Pip Install

$ pip install masonite-billing

Add the Service Provider

Simply add the Masonite Billing Service Provider to your providers list:

from billing.providers import BillingProvider

PROVIDERS = [
    ...

    # Third Party Providers
    BillingProvider,

    # Application Providers
    ...
]

This will add a new install:billing command to craft. Just run:

$ craft install:billing

This will create a new configuration file in config/billing.py

Configuration File

All billing information will be located in the config/billing.py file and has 2 important constants:

...

DRIVER = 'stripe'

...

DRIVERS = {
    'stripe': {
        'client': os.getenv('STRIPE_CLIENT'),
        'secret': os.getenv('STRIPE_SECRET'),
        'currency': 'usd',
    }
}

The DRIVER is the processor that Masonite billing will use and the DRIVERS constant is the configuration setting for the driver.

Although there is the DRIVER constant, Masonite Billing currently only supports Stripe. Other drivers like Braintree will be supported in later releases which should be backwards compatible.

Migrations

We'll need to create 2 new migrations: one to add columns to the users table and one migration to create a new subscriptions table. Just create these migration files with craft and copy and paste the migration into those files and migrate them.

Let's first add 2 new columns to our users table.

$ craft migration add_subscription_info_to_users --table users

Now just add this column to the migration file:

with self.schema.table('users') as table:
    table.string('customer_id').nullable()
    table.string('plan_id').nullable()

Now let's add a new subscriptions table.

$ craft migration create_subscriptions_table --create subscriptions
with self.schema.create('subscriptions') as table:
    table.increments('id')
    table.integer('user_id').unsigned()
    table.string('plan')
    table.string('plan_id')
    table.string('plan_name')
    table.timestamp('trial_ends_at').nullable()
    table.timestamp('ends_at').nullable()
    table.timestamps()

Now just migrate the new migrations:

$ craft migrate

Stripe Authentication Keys

Just add your Stripe public and secret keys to your .env file:

STRIPE_CLIENT=pk_Njsd993hdc...
STRIPE_SECRET=sk_sjs8yd78H8...

Billable Model

Masonite Billing consists of a model that should be inherited by whatever model you want to add subscription billing information to. In this example here, we will focus on adding the billing integration to our User model.

from config.database import Model
from billing.models.Billable import Billable

class User(Model, Billable):
    __fillable__ = ['name', 'email', 'password']
    __auth__ = 'email'

Once that is added we will now have a plethora of methods we can use to subscribe and charge our user.

Read more about how to handle subscription and payment information in the Usage documentation.

Getting Started

Below you will notice we are using a tok_amex token, you may use this token for testing purposes but this token in production should be the token returned when processing your stripe form.

It's also important to note that the subscription records in your database are never deleted but are updated instead. So canceling a subscription does not delete that subscription from your database but only sets when the subscription ended. This is good if you want to dump the data into an email campaign tool to try and get back any lost customers.

Subscribing to Plans

To subscribe a user to plans, we can use the subscribe method like so:

user.subscribe('masonite-test', 'tok_amex')

This method retuns a string of the subscription token such as sub_j8sy7dbdns8d7sd..

If you try to subscribe a user to a plan and the plan does not exist in Stripe then Masonite will throw a billing.exceptions.PlanNotFound exception.

Checking Subscriptions

If you want to check if the user is subscribed you have a few options:

You may check if the user is subscribed to any plan:

user.is_subscribed()

or you can check if the user is subscribed to a specific plan:

user.is_subscribed('masonite-test')

You may also check if a user was subscribed but their plan has expired:

user.was_subscribed()

or you can obviously check if the user was subscribed to a specific plan:

user.was_subscribed('masonite-test')

Getting The Plan Name

If you need to get the name of the plan the user is on you can do so using the plan() method:

user.plan() # returns Masonite Test

This will return the name of the plan in Stripe, not the plan ID. For example, our plan ID might be masonite-test but the plan name could be "Awesome Plan."

Trialing

If the plan you are subscribing a user to has a trial set inside Stripe then the user will be automatically enrolled in a trial for that time. We can check if the user is on a trial using the on_trial() method. In our examples here, the masonite-test plan has a 30 day free trial set inside Stripe.

For example:

user.subscribe('masonite-test') # plan has a 30 day trial
user.on_trial() # returns True

We may also want to skip the trial and charge the user immediately for the plan:

user.skip_trial().subscribe('masonite-test') # plan has a 30 day trial
user.on_trial() # returns False

We can also specify the amount of days the user should be on a trial for when subscribing them to a plan:

user.trial(days=4).subscribe('masonite-test') # plan now has a 4 day trial
user.on_trial() # returns True

Swapping Plans

We can swap subscription plans at anytime using the swap() method:

user.subscribe('masonite-test') # plan has a 30 day trial
user.is_subscribed('masonite-test') # returns True
user.on_trial() # returns True

user.swap('new-plan') # returns True

user.is_subscribed('masonite-test') # returns False
user.is_subscribed('new-plan') # returns True

Canceling

If you want to cancel a user's subscription we can use the cancel() method.

This will cancel the users plan but continue the subscription until the period ends.

user.subscribe('masonite-test')
user.is_subscribed() # returns True
user.cancel() # returns True
user.is_subscribed() # returns True

Notice here that the last is_subscribed() method still returned True even after we canceled. The user will continue the subscription until the period ends. For example if it is a 1 month subscription and the user canceled 1 week in, Masonite Billing will continue the subscription for the remaining 3 weeks.

If you wish to cancel the user immediately we can specify the now=Trueparameter:

user.subscribe('masonite-test')
user.is_subscribed() # returns True
user.cancel(now=True) # returns True
user.is_subscribed() # now returns False

The period between canceling a subscriptions and the period running out can be caught using the user.is_canceled() method:

user.subscribe('masonite-test')
user.cancel() # returns True
user.is_canceled() # returns True

Again this will only return true if the user has an active subscription but has chosen to cancel it. If the subscription is canceled immediately, this will return False.

user.subscribe('masonite-test')
user.cancel(now=True) # returns True
user.is_canceled() # returns False

Resuming Plans

If you don't cancel a plan immediately, there will be a period between when the user canceled and when the plan can be resumed. We can use the resume() method here:

user.subscribe('masonite-test')

user.cancel() # returns True
user.is_canceled() # returns True

user.resume() # returns True
user.is_canceled() # returns False

Updating Card Information

Sometimes a user will want to switch or update their card information. We can use the card() method and pass a token to it:

user.card('tok_amex') # updates the users card

Charging users

If you want to make one off transactions for customers you can do so easily using the charge() method:

You can charge a card by passing a token:

user.charge(999, token='tok_amex') # charges the users card

You can also charge a customer directly by leaving out the token which will charge whatever card the customer has on file.

user.charge(999) # charges the customer

You can also add a description and metadata (as a dictionary) for the charge:

user.charge(999, description='Charges for Flowers', metadata={'flower_type': 'tulips, roses'})

Coupons

You will first need to setup coupons in Stripe.

Once you setup a coupon you can use coupons on both charges and subscriptions:

user.coupon('black-friday').subscribe('masonite-plan')

You can also pass in an integer to deduct an amount:

user.coupon(500).charge(1000)

This will deduct 5 dollars frin the 10 dollars you are charging the user.

You can also make a coupon for a certain percentage reduction:

user.coupon(.25).charge(1000)

This will deduct 25 percent off.

Webhooks

Webhooks allow your application to interact directly with stripe when certain actions happen such as when a user's subscription has expired. You can use these webhooks to catch any events emitted.

Getting Started

Masonite Billing also allows you to tie into Stripe webhooks. If the subscription is cancelled in Stripe, Stripe will send your server a webhook and Masonite Billing will update the database accordingly.

Be sure to setup the correct url in the the Stripe dashboard inside your Webhook Settings. The url to setup should be http://your-domain.com/stripe/webhook or if you're testing with Ngrok it should be something like http://684b1285.ngrok.io/stripe/webhook.

If you go the Ngrok route be sure to read the Testing With Ngrok section

Webhook Controller

We can simply put the webhook controller in our routes/web.py file just like any other controller but it can point to the packages controller:

...
Post().route('/stripe/webhook', '/billing.controllers.WebhookController@handle')
...

This will send all Stripe traffic to the server and handle any hooks accordingly.

Testing With Ngrok

Most developers use Ngrok for testing incoming webhooks. Ngrok is a freemium HTTP tunneling service that allows extrernal requests to tunnel into your localhost and port environment. It's exellent for testing things like this as well as other things like OAuth.

If you use Ngrok you will get a subdomain like: http://684b1285.ngrok.io . Because this is a subdomain, Masonite needs to know which subdomain to support so you're route will have to be:

...
Post().domain('684b1285').route('/stripe/webhook', '/billing.controllers.WebhookController@handle')
...

in order to catch the Ngrok subdomain. This setup will allow you to send test webhooks from Stripe to your server.

You can read more about subdomains in the Subdomain Routing section of the Routing documentation.

Csrf Protection

External incoming API calls typically will not be able to be CSRF protected because they will not know the specific token connected to a request. We will need to put an exception to the /stripe/webhook route:

class CsrfMiddleware: 
  ...

  exempt = [
    '/stripe/webhook'
  ]

Creating Custom Hooks

Currently the webhook controller only handles when a subscription is canceled but can handle any hook you like. If you need to create a custom hook you can inherit from the WebhookController and add the needed controller methods:

from billing.controllers import WebhookController

class CustomWebhookController(WebhookController):

    def handle_resource_event(self):
        return 'Webhook Handled'

Routes

You'll also have to specify a new location of your controller which should now be located in your normal controllers directory:

...
Post().route('/stripe/webhook', 'CustomWebhookController@handle'),
...

Events

Events are specific types of webhooks that Stripe will send out when certain actions occur.

You can see a list of stripe events here: https://stripe.com/docs/api#event_types​

You'll notice that we have a handle_resource_event method. The WebhookController will take all incoming webhook events into the handle method and dispatch them to other methods.

How it does this is simply takes the event lets say the charge.dispute.created Stripe event (see the link above for a list of more events) and parses it into a string that looks like:

handle_charge_dispute_created

You'll notice we just replaced the . with _ and prefixed a handle to it. This looks like a method call. If we simply create a method now:

from billing.controllers import WebhookController

class CustomWebhookController(WebhookController):

    def handle_charge_dispute_created(self):
        return 'Webhook Handled'

This method will be called whenever we receive a webhook for that event. If no method is found for the incoming webhook then the server will return a Webhook Not Supported string which you may see while development testing your Stripe webhooks.

Last updated