Laravel Cashier
- Введение
- Настройка Stripe
- Настройка Braintree
- Подписки
- Триал период
- Обработка Stripe Webhooks
- Обработка Braintree Webhooks
- Одиночные оплаты
- Инвойсы
Введение
Laravel Cashier предоставляет простой и выразительный интерфейс для работы с платными подписками при помощи сервисов Stripe и Braintree. Он предоставляет готовые методы для работы, которые вы, возможно, писали раньше вручную. В дополнение к базовому управлению подписками, Cashier поддерживает купоны, смену подписок, цену подписок, отмену подписок на определённый период и даже создание PDF отчётов по инвойсам.
Настройка Stripe
Composer
Для начала добавьте пакет Cashier в ваш composer.json
и выполните команду composer update
:
"laravel/cashier": "~6.0"
Сервис-провайдер
Дальше зарегистрируйте сервис-провайдер Laravel\Cashier\CashierServiceProvider
в вашем файле конфигурации app
.
Миграции
Перед использованием Cashier вам также необходимо подготовить базу данных. Требуется добавить несколько колонок в таблицу users
и создать новую subscriptions
, которая будет хранить информацию о подписках:
Schema::table('users', function ($table) {
$table->string('stripe_id')->nullable();
$table->string('card_brand')->nullable();
$table->string('card_last_four')->nullable();
$table->timestamp('trial_ends_at')->nullable();
});
Schema::create('subscriptions', function ($table) {
$table->increments('id');
$table->integer('user_id');
$table->string('name');
$table->string('stripe_id');
$table->string('stripe_plan');
$table->integer('quantity');
$table->timestamp('trial_ends_at')->nullable();
$table->timestamp('ends_at')->nullable();
$table->timestamps();
});
После того, как миграция создана, просто запустите Artisan команду migrate
.
Настройка модели
Дальше добавьте трейт Billable
в вашу модель пользователя:
use Laravel\Cashier\Billable;
class User extends Authenticatable
{
use Billable;
}
Ключ Stripe
Далее, добавьте ваш ключ Stripe в конфигурационный файл services.php
:
'stripe' => [
'model' => App\User::class,
'secret' => env('STRIPE_SECRET'),
],
Настройка Braintree
Предостережение о Braintree
Для большинства операций реализации функций Cashier для сервисов Stripe и Braintree одинаковы. Оба сервиса обеспечивают функционал платных подписок через пластиковые карты, но Braintree так же поддерживает платежи через PayPal. Однако, Braintree не поддерживает некоторые функции, которые есть в Stripe. Вы должны учитывать следующие пункты при выборе между Stripe и Braintree:
- Braintree поддерживает PayPal, а Stripe - нет.
- Braintree не поддерживает
increment
иdecrement
методы для подписок. Это ограничение Braintree, а не Cashier. - Braintree не поддерживает скидки в процентах. Это ограничение Braintree, а не Cashier.
Composer
Для начала необходимо добавить пакет Braintree для Cashier в ваш файл composer.json
и запустить команду composer update
:
"laravel/cashier-braintree": "~1.0"
Сервис-провайдер
Дальше зарегистрируйте сервис-провайдер Laravel\Cashier\CashierServiceProvider
в вашем файле конфигурации app
.
Plan Credit Coupon
Before using Cashier with Braintree, you will need to define a plan-credit
discount in your Braintree control panel. This discount will be used to properly prorate subscriptions that change from yearly to monthly billing, or from monthly to yearly billing. The discount amount configured in the Braintree control panel can be any value you wish, as Cashier will simply override the defined amount with our own custom amount each time we apply the coupon.
Миграции
Перед использованием Cashier вам также необходимо подготовить базу данных. Требуется добавить несколько колонок в таблицу users
и создать новую subscriptions
, которая будет хранить информацию о подписках:
Schema::table('users', function ($table) {
$table->string('braintree_id')->nullable();
$table->string('paypal_email')->nullable();
$table->string('card_brand')->nullable();
$table->string('card_last_four')->nullable();
$table->timestamp('trial_ends_at')->nullable();
});
Schema::create('subscriptions', function ($table) {
$table->increments('id');
$table->integer('user_id');
$table->string('name');
$table->string('braintree_id');
$table->string('braintree_plan');
$table->integer('quantity');
$table->timestamp('trial_ends_at')->nullable();
$table->timestamp('ends_at')->nullable();
$table->timestamps();
});
После того, как миграция создана, просто запустите Artisan команду migrate
.
Настройка модели
Далее добавьте трейт Billable
в вашу модель пользователя:
use Laravel\Cashier\Billable;
class User extends Authenticatable
{
use Billable;
}
Сервис-провайдер
Так же вам требуется настроить следующие опции в файле конфигурации services.php
:
'braintree' => [
'model' => App\User::class,
'environment' => env('BRAINTREE_ENV'),
'merchant_id' => env('BRAINTREE_MERCHANT_ID'),
'public_key' => env('BRAINTREE_PUBLIC_KEY'),
'private_key' => env('BRAINTREE_PRIVATE_KEY'),
],
Далее требуется добавить следующие вызовы Braintree SDK в метод boot
сервис-провайдера AppServiceProvider
:
\Braintree_Configuration::environment(env('BRAINTREE_ENV'));
\Braintree_Configuration::merchantId(env('BRAINTREE_MERCHANT_ID'));
\Braintree_Configuration::publicKey(env('BRAINTREE_PUBLIC_KEY'));
\Braintree_Configuration::privateKey(env('BRAINTREE_PRIVATE_KEY'));
Подписки
Создание подписок
Для создания подписки сначала необходимо получить экземпляр вашей модели App\User
. После того, как вы его получили, можете использовать метод newSubscription
для создания модели подписки:
$user = User::find(1);
$user->newSubscription('main', 'monthly')->create($creditCardToken);
Первый аргумент переданный в метод newSubscription
должен быть именем подписки. Если ваше приложение предлагает только одну подписку, вы можете назвать её main
или primary
. Второй аргумент - специфичный для Stripe / Braintree план, на который идет подписка. Это значение должно совпадать с идентификатором плана на Stripe или Braintree.
Метод create
автоматически создаст подписку и обновит вашу базу данных платёжной информацией пользователя. Если у вас настроен пробный период подписок в Stripe, то эта информация также попадёт в базу данных.
Дополнительная информация о пользователе
Если вы хотите указать дополнительную информацию о пользователе при создании подписки, это можно сделать при помощи второго аргумента метода create
:
$user->newSubscription('main', 'monthly')->create($creditCardToken, [
'email' => $email,
]);
Чтобы узнать больше о дополнительных полях обратитесь к документации Stripe или Braintree.
Купоны
Если вы хотите применить купон при создании подписки, можете использовать метод withCoupon
:
$user->newSubscription('main', 'monthly')
->withCoupon('code')
->create($creditCardToken);
Проверка статуса подписки
После того, как пользователь оформил подписку на ваше приложение, вы легко можете проверить статус подписки различными способами. Метод subscribed
возвратит true
, если подписка пользователя активна или в пробном периоде:
if ($user->subscribed('main')) {
//
}
Этот метод является хорошим кандидатом на посредника роутов, так как вы можете ограничивать доступ к конкретным роутам и контроллерам в зависимости от статуса подписки:
public function handle($request, Closure $next)
{
if ($request->user() && ! $request->user()->subscribed('main')) {
// This user is not a paying customer...
return redirect('billing');
}
return $next($request);
}
Если вы хотите определить, использует ли пользователь пробный период, воспользуйтесь методом onTrial
. Он полезен для отображения предупреждений пользователю о том, что у него пробный период подписки:
if ($user->subscription('main')->onTrial()) {
//
}
Метод subscribedToPlan
позволяет определить, подписан ли пользователь на определённый план подписки Stripe / Braintree. В данном примере мы определяем, что main
подписка пользователя использует план monthly
:
if ($user->subscribedToPlan('monthly', 'main')) {
//
}
Определение отменённых подписок
Для того, чтобы проверить, была ли у пользователя активная подписка, которую он отменил, вы можете воспользоваться методом cancelled
:
if ($user->subscription('main')->cancelled()) {
//
}
Вы также можете определить, отменил ли пользователь свою подписку до конца срока её окончания. Например, пользователь отменил подписку 5-го марта, но она активна до 10-го марта. Тем не менее, подписка всё ещё будет считаться активной при проверке методом subscribed
. Определить отменённую подписку до окончания срока её действия можно при помощи метода onGracePeriod
:
if ($user->subscription('main')->onGracePeriod()) {
//
}
Смена планов подписок
После того, как пользователь оформил подписку в вашем приложении, он может захотеть сменить план подписки. Для этого используйте метод swap
. Например, вы можете легко сменить подписку пользователя на premium
:
$user = App\User::find(1);
$user->subscription('main')->swap('provider-plan-id');
If the user is on trial, the trial period will be maintained. Also, if a "quantity" exists for the subscription, that quantity will also be maintained. When swapping plans, you may also use the prorate
method to indicate that the charges should be pro-rated. In addition, you may use the swapAndInvoice
method to immediately invoice the user for the plan change:
$user->subscription('main')->swap('provider-plan-id');
Если требуется сменить план и пропустить триал период, можно воспользоваться методом skipTrial
:
$user->subscription('main')
->skipTrial()
->swap('provider-plan-id');
Количество подписки
Note: Количество подписки поддерживается только Stripe для Cashier. Braintree не имеет аналога "количества".
Иногда подписка измеряется "количеством". Например, ваше приложение может стоить по 10$ в месяц за каждого пользователя аккаунта. Для удобного увеличения или уменьшения количества подписки используйте методы increment
и decrement
:
$user = User::find(1);
$user->subscription('main')->incrementQuantity();
// Add five to the subscription's current quantity...
$user->subscription('main')->incrementQuantity(5);
$user->subscription('main')->decrementQuantity();
// Subtract five to the subscription's current quantity...
$user->subscription('main')->decrementQuantity(5);
В качестве альтернативы вы можете указать точное значение количество, используя метод updateQuantity
:
$user->subscription('main')->updateQuantity(10);
Для получения подробной информации о количестве подписки обратитесь к документации Stripe.
Налоги на подписку
С Cashier очень просто отправлять значение налога tax_percent
в Stripe / Braintree. Для этого реализуйте метод taxPercentage
в вашей модели пользователя, который должен возвращать число от 0 до 100 с максимум двумя знаками после запятой:
public function taxPercentage() {
return 20;
}
This enables you to apply a tax rate on a model-by-model basis, which may be helpful for a user base that spans multiple countries.
Отмена подписки
Для отмены подписки вызовите метод cancel
у подписки пользователя:
$user->subscription('main')->cancel();
Когда подписка отменяется, Cashier автоматически устанавливает поле ends_at
в вашей базе данных. Это поле используется методом subscribed
для возвращения false
. Например, если пользователь отменил подписку 1-го марта, но она активна до 5-го марта, метод subscribed
всё ещё будет возвращать true
до 5-го марта.
Для проверки того, что пользователь отменил свою подписку и находится на "grace period" используйте метод onGracePeriod
:
if ($user->subscription('main')->onGracePeriod()) {
//
}
Восстановление подписки
Если пользователь отменил свою подписку и хочет восстановить её, используйте метод resume
. Пользователь должен всё еще быть на grace period что бы возобновить подписку:
$user->subscription('main')->resume();
Если пользователь сначала отменил подписку, а потом возобновил её, до окончания grace period, оплата не будет снята сразу. Вместо этого, его подписка будет просто реактивирована и оплата будет производиться по графику, который применялся до отмены подписки.
Триал период
С вводом данных карты
Если вы хотите предоставить пользователям триал период после ввода данных пластиковой карты, вы можете использовать метод trialDays
при создании подписки:
$user = User::find(1);
$user->newSubscription('main', 'monthly')
->trialDays(10)
->create($creditCardToken);
Этот метод создает подписку с триал периодом, который записывается в базу и отправляется в Stripe / Braintree, информируя их о дате, до которой не должна сниматься оплата.
Примечание: С карты пользователя будут сняты деньги, если он не отменит подписку до окончания триала, поэтому, вы должны уведомлять его о дате окончания бесплатного периода.
Вы можете определить, что пользователь находится на триал периоде используя метод onTrial
модели пользователя или подписки. Ниже приведены два примера:
if ($user->onTrial('main')) {
//
}
if ($user->subscription('main')->onTrial()) {
//
}
Без ввода данных карты
Если вы хотите предоставить пользователям триал период без ввода данных пластиковой карты, вы можете просто установить свойство trial_ends_at
модели пользователя в дату окончания его окончания. Примерно так будет выглядеть создание пользователя:
$user = User::create([
// Populate other user properties...
'trial_ends_at' => Carbon::now()->addDays(10),
]);
Такой тип триала внутри Cashier называется "общий триал" так как он не присоединяется ни к какой подписке. Метод onTrial
модели User
вернет true
если текущая дата меньше установленной trial_ends_at
:
if ($user->onTrial()) {
// User is within their trial period...
}
Вы так же можете использовать метод onGenericTrial
если требуется проверить находится ли пользователь на "общем" триал периоде и не имеет действительной подписки:
if ($user->onGenericTrial()) {
// User is within their "generic" trial period...
}
Когда вы готовы создать настоящую подписку для пользователя, используйте метод newSubscription
:
$user = User::find(1);
$user->newSubscription('main', 'monthly')->create($creditCardToken);
Обработка Stripe Webhooks
Ошибка при оплате подписки
Что, если кредитка пользователя стала неактивной? Не беспокойтесь — Cashier включает в себя контроллер хуков, который автоматически отключит подписку за вас. Просто укажите путь к контроллеру в роутах:
Route::post(
'stripe/webhook',
'\Laravel\Cashier\Http\Controllers\WebhookController@handleWebhook'
);
Это всё! Ошибки оплаты будут обработаны контроллером. Он отменит подписку, когда Stripe определит, что оплата невозможна (обычно это происходит после 3-х попыток). Не забудьте: вам нужно настроить URI для хуков в вашей панели Stripe.
Хуки Stripe должны обходить стороной CSRF проверку Laravel, поэтому не забудьте добавить URI в исключения посредника VerifyCsrfToken
или разместите роут вне действия посредника web
:
protected $except = [
'stripe/*',
];
Другие Webhooks
Если вам требуется обрабатывать другие вебхуки Stripe просто расширьте контроллер WebhookController
. Имя метода должно соответствовать правилам наименования, применяемым в Cashier, в частности, оно должно начинаться с handle
и соответствовать имени вебхука Stripe записанному в стиле "camel case". Например, если требуется обработать вебхук invoice.payment_succeeded
, метод контроллера должен иметь имя handleInvoicePaymentSucceeded
.
<?php
namespace App\Http\Controllers;
use Laravel\Cashier\Http\Controllers\WebhookController as BaseController;
class WebhookController extends BaseController
{
/**
* Handle a Stripe webhook.
*
* @param array $payload
* @return Response
*/
public function handleInvoicePaymentSucceeded($payload)
{
// Handle The Event
}
}
Обработка Braintree Webhooks
Ошибка при оплате подписки
Что, если кредитка пользователя стала неактивной? Не беспокойтесь — Cashier включает в себя контроллер хуков, который автоматически отключит подписку за вас. Просто укажите путь к контроллеру в роутах:
Route::post(
'braintree/webhook',
'\Laravel\Cashier\Http\Controllers\WebhookController@handleWebhook'
);
Это всё! Ошибки оплаты будут обработаны контроллером. Он отменит подписку, когда Braintree определит, что оплата невозможна (обычно это происходит после 3-х попыток). Не забудьте: вам нужно настроить URI для хуков в вашей панели Braintree.
Хуки Braintree должны обходить стороной CSRF проверку Laravel, поэтому не забудьте добавить URI в исключения посредника VerifyCsrfToken
или разместите роут вне действия посредника web
:
protected $except = [
'braintree/*',
];
Другие Webhooks
Если вам требуется обрабатывать другие вебхуки Braintree просто расширьте контроллер WebhookController
. Имя метода должно соответствовать правилам наименования, применяемым в Braintree, в частности, оно должно начинаться с handle
и соответствовать имени вебхука Braintree записанному в стиле "camel case". Например, если требуется обработать вебхук dispute_opened
, метод контроллера должен иметь имя handleDisputeOpened
.
<?php
namespace App\Http\Controllers;
use Braintree\WebhookNotification;
use Laravel\Cashier\Http\Controllers\WebhookController as BaseController;
class WebhookController extends BaseController
{
/**
* Handle a Braintree webhook.
*
* @param WebhookNotification $webhook
* @return Response
*/
public function handleDisputeOpened(WebhookNotification $notification)
{
// Handle The Event
}
}
Одиночные оплаты
Простая оплата
Примечание: Когда вы используете Stripe, методcharge
принимает сумму, которую вы хотите снять в наименьшем номинале используемой валюты. Однако, при использовании Braintree вы должны передавать в методcharge
сумму в долларах:
Если вы хотите снять деньги за подписку только один раз, можете воспользоваться методом charge
.
$user->charge(100);
Метод charge
принимает вторым аргументом массив, позволяющий добавлять дополнительные опции:
// Stripe принимает сумму в центах...
$user->charge(100);
// Braintree принимает сумму в долларах...
$user->charge(1);
Метод charge
принимает массив в качестве второго аргумента, позволяя вам передавать параметры в Stripe / Braintree:
$user->charge(100, [
'custom_option' => $value,
]);
Метод charge
выбросит исключение, если оплата не прошла. Если оплата прошла успешно, метод вернет полный ответ от Stripe / Braintree:
try {
$response = $user->charge(100);
} catch (Exception $e) {
//
}
Оплата с выставлением счета
Иногда может потребоваться произвести одноразовую оплату, но с выставление счета (инвойса), для которого вы можете предоставить возможность пользователю получить PDF. Метод invoiceFor
позволит сделать это. Например, давайте выставим счет на 5.00$ за "One Time Fee":
// Stripe принимает сумму в центах...
$user->invoiceFor('One Time Fee', 500);
// Braintree принимает сумму в долларах...
$user->invoiceFor('One Time Fee', 5);
Счет будет оплачен автоматически с карты пользователя. Метод invoiceFor
так же принимает массив аргументов, позволяя вам передавать параметры в Stripe / Braintree:
$user->invoiceFor('One Time Fee', 500, [
'custom-option' => $value,
]);
Примечание: Метод invoiceFor
создаст инвойс в Stripe который будет повторять попытку оплаты, в случае неудачи. Если вы не хотите повторять попытки оплаты, вам потребуется отменить счет через Stripe API после первой неудачной попытки.
Инвойсы
Вы можете легко получить все инвойсы пользователя, используя метод invoices
:
$invoices = $user->invoices();
При отображении инвойсов пользователю вы также можете использовать вспомогательные методы для получения дополнительной информации. Например, вы можете отобразить список инвойсов таблицей с возможностью скачивания каждого из них:
<table>
@foreach ($invoices as $invoice)
<tr>
<td>{{ $invoice->date()->toFormattedDateString() }}</td>
<td>{{ $invoice->total() }}</td>
<td><a href="/user/invoice/{{ $invoice->id }}">Download</a></td>
</tr>
@endforeach
</table>
Создание PDF инвойса
Для генерации PDF для инвойсов вам потребуется установить библиотеку dompdf
:
composer require dompdf/dompdf
Из роута или контроллера вызовите метод downloadInvoice
для создания PDF и «отдачи» его пользователю. Этот метод автоматически создаёт соответствующий HTTP ответ с заголовками для начала скачивания в браузере:
Route::get('user/invoice/{invoice}', function ($invoiceId) {
return Auth::user()->downloadInvoice($invoiceId, [
'vendor' => 'Your Company',
'product' => 'Your Product',
]);
});