This package provides an expressive, fluent interface to a billing services gateway. It handles almost all of the boilerplate code for managing billing customers, subscriptions, and individual charges. The usage and feature set was heavily influenced by the popular Laravel Cashier package. However, it has these major differences:
- Support for more than one billing gateway (via Facades)
- Support for multiple subscriptions per customer
- Support for multiple credit cards per customer
- Support for individual charges
- Support for free subscription plans
It currently comes bundled with drivers for these billing services:
Note: Not all features are supported by every billing gateway. See the
Gateway Limitations
section below for more information.
Add this to your composer.json file, in the require object:
"mmanos/laravel-billing": "dev-master"
After that, run composer install to install the package.
Register the Mmanos\Billing\BillingServiceProvider
in your app
configuration file.
The following composer dependencies are needed for the listed billing gateways:
- Stripe:
stripe/stripe-php
- Braintree:
braintree/braintree_php
Publish the default config file to your application so you can make modifications.
$ php artisan config:publish mmanos/laravel-billing
Before using this package, we'll need to add several columns to the table that will represent your billing customer. You can use the laravel-billing:customer-table
Artisan command to create a migration to add the necessary columns.
For example, to add the columns to the users
table use:
$ php artisan laravel-billing:customer-table users
Next, we'll need to add several columns to the table that will represent your billing subscription plans. You can use the laravel-billing:subscription-table
Artisan command to create a migration to add the necessary columns.
For example, if you have a hosting company and want to allow your customers add a subscription for a website, you might use:
$ php artisan laravel-billing:subscription-table websites
Note: If you only need one subscription per customer, you may use the same table for both the customer migration and the subscription migration (eg.
users
table).
Note: You may also support multiple subscriptions per customer by adding these fields to more than one table.
Once the migration has been created, simply run the migrate
command.
Next, add the CustomerBillableTrait to your customer model definition:
use Mmanos\Billing\CustomerBillableTrait;
class User extends Eloquent
{
use CustomerBillableTrait;
}
You should also define a subscriptionmodels
method that returns all models that represent any billing subscriptions for a customer. This allows the customer model to propagate credit card changes and status changes (if the customer is deleted) to all of it's subscription models.
public function subscriptionmodels()
{
// Return an Eloquent relationship.
return $this->hasMany('Website');
// Or, return an array or collection of models.
return Website::where('user_id', $this->id)->get();
// Or, return an array of collections.
return array(
Website::where('user_id', $this->id)->get(),
Domain::where('user_id', $this->id)->get(),
);
}
Then, add the SubscriptionBillableTrait to your subscription model definition(s):
use Mmanos\Billing\SubscriptionBillableTrait;
class Website extends Eloquent
{
use SubscriptionBillableTrait;
}
You should also define a customermodel
method that returns the model representing the customer who owns this subscription. This ensures a subscription has access to the necessary customer information when interacting with the different gateway APIs.
public function customermodel()
{
// Return an Eloquent relationship.
return $this->belongsTo('User', 'user_id');
// Or, Return an Eloquent model.
return User::find($this->user_id);
}
Finally, to add the ability to look up a model based on it's gateway ID (customer ID, or subscription ID), you need to define the array of Eloquent models acting as a Customer or Subscription.
Add your customer class to the customer_models
property in this package's config file:
'customer_models' => array('User')
And add the subscription class(es) to the subscription_models
property in this package's config file:
'subscription_models' => array('Website')
These values are primarily used by the WebhookControllers to help locate the corresponding Eloquent model from the gateway's ID when a new billing event is received.
Billing customers can be created and managed separately from subscriptions or charges.
Once you have a customer model instance, you can create the customer in the billing gateway using a gateway-specific credit card token:
$user = User::find(1);
$user->billing()->withCardToken('token')->create();
If you would like to apply a coupon when creating the customer, you may use the withCoupon
method:
$user->billing()->withCardToken('token')->withCoupon('code')->create();
The billing
method will automatically update your database with the billing gateway's customer ID and other relevant billing information.
If you would like to specify additional customer details, you may do so by passing them in to the create
method:
$user->billing()->withCardToken('token')->create(array(
'email' => $email,
));
To update an existing customers primary credit card information, or to add a coupon to their existing account, you may use the update
method:
$user->billing()->withCardToken('token')->withCoupon('code')->update();
Deleting a customer will delete their account from the billing gateway and delete all existing subscriptions:
$user->billing()->delete();
You may add more than one credit card to a customer using the creditcards
method:
$card = $user->creditcards()->create('credit_card_token');
Updating an existing credit card's information (such as expiration date or billing address) is also possible:
$card->update(array(
'exp_month' => '01',
'exp_year' => '2017',
));
Or delete an existing credit card:
$card->delete();
Retrieving and working with customer credit cards is pretty straight forward:
// Get all customer credit cards.
$cards = $user->creditcards()->get();
// Get the first card for a customer.
$card = $user->creditcards()->first();
// Find a card by it's ID.
$card = $user->creditcards()->find('card_id');
echo $card->id;
echo "{$card->brand} xxxx-xxxx-xxxx-{$card->last4} Exp: {$card->exp_month}/{$card->exp_year}"
You can easily retrieve an array of a customer's invoices using the invoices
method:
$invoices = $user->invoices()->get();
Or get the most recent invoice:
$invoice = $user->invoices()->first();
Or find an invoice by it's ID:
$invoice = $user->invoices()->find('invoice_id');
To display relevant invoice information, use the invoice properties:
$invoice->id;
$invoice->date;
$invoice->amount;
$invoice->items();
//...
Use the render
method to generate a pre-formatted HTML version of the invoice:
$invoice->render();
To verify that a customer has been created in the billing gateway, use the readyForBilling
method:
if ($user->readyForBilling()) {
//
}
Once you have a subscription model instance, you can subscribe a customer to a given plan:
$user = User::find(1);
$website = Website::find(1);
$user->subscriptions('monthly')->create($website);
You may apply a coupon specifically to the subsciption using the withCoupon
method:
$user->subscriptions('monthly')->withCoupon('code')->create($website);
You may also specify a new credit card token to use with this subscription:
$user->subscriptions('monthly')->withCardToken('token')->create($website);
Or specify an existing credit card ID:
$user->subscriptions('monthly')->withCard('card_id')->create($website);
Note: If no card or card_token is specified, the default (initial) card associated with the customer will be used.
There may be times when you haven't yet created the customer or you don't have the customer model available for use when you want to create a subscription. In these cases you may subscribe to a plan directly on the subscription model using the subscription
method:
$website->subscription('monthly')->create();
This method also supports the optional withCoupon
and withCardToken
methods:
$website->subscription('monthly')->withCoupon('code')->withCardToken('token')->create();
Note: The customer will automatically be retrieved using the
customermodel
method you defined in your subscription model above.
If your application offers a free-trial with no credit-card up front, set the cardUpFront
property on your model to false
:
protected $cardUpFront = false;
On model creation, be sure to set the trial end date on the model:
$website->billing_trial_ends_at = Carbon::now()->addDays(14);
$website->save();
It should be noted that any subscription
commands executed on an on-trial subscription model will not be synced with the billing gateway if the customer is not ready for billing (created in billing system with at least one credit card). This allows the developer to perform the same create
, swap
, etc commands on a model while you wait for the customer to be created in the billing gateway.
If you have one or more pending, on-trial, subscriptions when the user does add their credit card, you can use the withSubscriptions
flag to activate all pending, non-free, subscriptions in the billing gateway:
$user->billing()->withCardToken('token')->withSubscriptions()->create();
To swap a user to a new subscription, use the swap
method:
$website->subscription('premium')->swap();
If the user is on trial, the trial will be maintained as normal. Also, if a "quantity" exists for the subscription, that quantity will also be maintained.
Sometimes subscriptions are affected by "quantity". For example, your application might charge $10 per month per user on an account. To easily increment or decrement your subscription quantity, use the increment
and decrement
methods:
$website->subscription()->increment();
// Add five to the subscription's current quantity...
$website->subscription()->increment(5);
$website->subscription()->decrement();
// Subtract five from the subscription's current quantity...
$website->subscription()->decrement(5);
To cancel a subscription, use the cancel
method:
$website->subscription()->cancel();
When a subscription is canceled, this package will automatically set the billing_subscription_ends_at
column on your database. This column is used to know when the subscribed
method should begin returning false. For example, if a customer cancels a subscription on March 1st, but the subscription was not scheduled to end until March 5th, the subscribed
method will continue to return true
until March 5th.
If a user has canceled their subscription and you wish to resume it, use the resume
method:
$website->subscription('monthly')->resume();
You may also specify a new credit card token:
$website->subscription('monthly')->withCardToken('token')->resume();
If the user cancels a subscription and then resumes that subscription before the subscription has fully expired, they may not be billed immediately, depending on the billing gateway being used.
Sometimes you want to offer a free subscription plan. You can do that easily with the isFree
flag. Any subscription flagged as free will not be synced to the billing gateway. However, you may continue to use the same subscription
commands, for ease of use, as long as you use the isFree
flag.
To create a free subscription:
$user->subscriptions('free-plan')->isFree()->create($website);
You may also swap from a paid plan to a free plan. This will cancel the subscription in the billing gateway and update the local record to reflect the free state:
$website->subscription('free-plan')->isFree()->swap();
Note: If you do not use the
isFree
flag, then it is assumed the plan is not free and the subscription will be created in the billing gateway.
Note: This package will attempt to maintain the subscription id from the billing gateway and resume the existing subscription when swapped from paid to free and then back to paid again.
To verify that a model is subscribed to your application, use the subscribed
command:
if ($website->subscribed()) {
//
}
To determine if the model has an active subscription in the billing gateway, use the billingIsActive
method. This method would not return true if the model is currently on trial with cardUpFront
set to false
or if they have canceled and are on their grace period.
if ($website->billingIsActive()) {
//
}
You may also determine if the model is still within their trial period (if applicable) using the onTrial
method:
if ($website->onTrial()) {
//
}
To determine if the model was once an active subscriber, but has canceled their subscription, you may use the canceled
method:
if ($website->canceled()) {
//
}
You may also determine if a model has canceled their subscription, but are still on their "grace period" until the subscription fully expires. For example, if a model subscription is canceled on March 5th that was scheduled to end on March 10th, the model is on their "grace period" until March 10th. Note that the subscribed
method still returns true
during this time.
if ($website->onGracePeriod()) {
//
}
The everSubscribed
method may be used to determine if the model has ever subscribed to a plan in your application:
if ($website->everSubscribed()) {
//
}
To retrieve the customer model associated with a subscription use the customer
method:
$customer = $website->customer();
You may also retrieve an array of subscriptions associated with a customer:
$subscriptions = $user->subscriptions()->get();
Creating individual charges on a customer, outside of subscriptions, is also possible.
Creating a new charge on a customer is easy:
$charge = $user->charges()->create(499);
Note: The amount of a charge is in cents.
To charge on a new credit card token, use the withCardToken
method:
$charge = $user->charges()->withCardToken('token')->create(499);
You may also specify an existing credit card to use for a charge:
$charge = $user->charges()->withCard('card_id')->create(499);
Note: If no card or card_token is specified, the default (initial) card associated with the customer will be used.
Sometimes you may want to preauthorize a charge before you capture it:
$charge = $user->charges()->create(499, array('capture' => false));
$charge->capture();
You may optionally specify the amount to capture as long as it is less than or equal to the amount preauthorized:
$charge = $user->charges()->create(499, array('capture' => false));
$charge->capture(array('amount' => 399));
Refunding a charge is also possible:
$charge->refund();
Or optionally specify an amount to refund:
$charge->refund(array('amount' => 399, 'reason' => '...'));
You may retrieve an array of all charges for a customer:
$charges = $user->charges()->get();
Or find the most recent charge for a customer:
$charge = $user->charges()->first();
Finding a charge from it's ID is also easy:
$charge = $user->charges()->find('charge_id');
Charge objects has several properties you might find useful, including: id
, created_at
, amount
, paid
, refunded
, card
, invoice_id
, and description
.
You may also access the associated invoice object (if available) from a charge:
$invoice = $charge->invoice();
This package comes bundled with a Webhook controller for each supported gateway, which can handle things such as failed or successful invoice payments, deleted subscriptions, and trial-will-end events.
To enable these events, just point a route to the appropriate gateway controller:
// Stripe.
Route::post('stripe/webhook', 'Mmanos\Billing\Gateways\Stripe\WebhookController@handleWebhook');
// Braintree.
Route::post('braintree/webhook', 'Mmanos\Billing\Gateways\Braintree\WebhookController@handleWebhook');
By default, this package does not try to delete a subscription after a certain number of failed payment attempts. Most billing gateways can do this automatically which would trigger a deleted subscription webhook event. When that happens, we will update our local model to record that change in status.
If you have additional webhook events you would like to handle, simply extend the Webhook controller and point the route to your controller.
class WebhookController extends Mmanos\Billing\Gateways\Stripe\WebhookController
{
public function handleChargeDisputeCreated($payload)
{
// Handle The Event
}
}
To make it even easier to work with billing-related events, this package will trigger several convenient events on Eloquent models that you can hook into, so you don't have to do everything in the Webhook controller.
For example, to be notified when a model's trail will end, subscribe to the trialWillEnd
model method:
Website::trialWillEnd(function ($website, $args = array()) {
Log::info('Trial will end in ' . array_get($args, 'days') . ' day(s).');
});
There are several customer-related billing events you may subscribe to: customerCreated
, customerDeleted
, creditcardAdded
, creditcardRemoved
, creditcardUpdated
, creditcardChanged
, discountAdded
, discountRemoved
, discountUpdated
, discountChanged
, invoiceCreated
, invoicePaymentSucceeded
, and invoicePaymentFailed
.
There are several subscription-related billing events you may subscribe to: billingActivated
, billingCanceled
, planSwapped
, planChanged
, subscriptionIncremented
, subscriptionDecremented
, billingResumed
, trialExtended
, trialWillEnd
, subscriptionDiscountAdded
, subscriptionDiscountRemoved
, subscriptionDiscountUpdated
, and subscriptionDiscountChanged
.
Each billing gateway provides a different API and set of functionality. That being said, not all features are supported by every billing gateway. Here is a high level breakdown of the features NOT supported by each gateway:
- Does not support multiple subscriptions on separate credit cards (though this feature is coming). The
withCard
method will be ignored when creating a new subscription and the primary customer card will be used. However, multiple credit cards are supported on charges.
- Does not support any action regarding a credit card token. So creating/updating a user with a card token is not supported. Also, adding new credit cards is not supported. You must use their transparent redirect flows to accomplish this. You would also need to manually update the model fields when finished.
- Does not support subscription quantities.
- Does not support customer-specific discounts.
- Does not support charge descriptions.
- Does not return starting/ending customer balance on invoices.
- Does not support resuming a canceled subscription. However, a new subscription will be created instead.
- Does not support modifying the trial end date for an existing subscription. However, the existing subscription will be canceled and a new one created.
This package comes bundled with a Local gateway driver to simulate talking with a real billing gateway. All data is stored in a SQLite database located in app/storage/meta/billing-local.sqlite
.
To add a subscription plan to the local driver database, use the laravel-billing:local:create-plan
Artisan command:
$ php artisan laravel-billing:local:create-plan
It will prompt you for the plan name, amount, interval, and trial period.
To add a coupon to the local driver database, use the laravel-billing:local:create-coupon
Artisan command:
$ php artisan laravel-billing:local:create-coupon
It will prompt you for the coupon code, percent_off, amount_off, and duration.
Even though this is not a real billing gateway, it is designed to simulate how a real gateway would respond. Therefore, you still need to generate a credit card token to attach to a customer or subscription.
To generate a valid token for this driver, simply JSON encode an object of fields you want to store with the credit card. For example:
var token = JSON.stringify({
last4 : $('#card-number').val().substr(-4),
exp_month : $('#exp-month').val(),
exp_year : $('#exp-year').val()
});