Версия фреймворка: 8.x 5.8 5.4 4.2

Laravel Cashier (Braintree)

Введение

Laravel Cashier Braintree предоставляет выразительный, свободный интерфейс к сервисам биллинга по подписке Braintree. Он обрабатывает практически весь код биллинга подписки, который вы боитесь писать. В дополнение к базовому управлению подписками, Cashier может обрабатывать купоны, изменять подписку, "количество" подписок, льготные периоды отмены и даже создавать PDF-файлы счетов-фактур.

Это документы для интеграции Cashier с Braintree. Вы можете найти документы интеграции Stripe здесь.
Если вы выполняете только разовые платежи и не предполагаете подписку, вам не следует использовать Cashier. Вместо этого используйте Braintree SDK.

Предостережения

Для многих операций Stripe и Braintree функционируют одинаково. Обе услуги обеспечивают оплату подписки с помощью кредитных карт, но Braintree также поддерживает платежи через PayPal. Тем не менее, в Braintree также не хватает некоторых функций, которые поддерживаются Stripe. Принимая решение об использовании Stripe или Braintree, вы должны помнить следующее:

  • Braintree поддерживает PayPal, а Stripe - нет.
  • Braintree не поддерживает методы increment и decment для подписок. Это ограничение Braintree, а не ограничение Cashier.
  • Braintree не поддерживает процентные скидки. Это ограничение Braintree, а не ограничение Cashier.

Установка

Для начала, установите пакет Cashier для Braintree с помощью Composer:

composer require laravel/cashier-braintree

Настройка

Купоны кредитного плана

Прежде чем использовать Cashier с Braintree, вам нужно определить скидку plan-credit на панели управления Braintree. Эта скидка будет использоваться для правильного пропорционального распределения подписок, которые изменяются от годового к ежемесячному выставлению счетов или от ежемесячного к ежегодному выставлению счетов.

Сумма скидки, настроенная в панели управления Braintree, может быть любой, какую вы пожелаете, поскольку Cashier будет переопределять определенную сумму нашей собственной индивидуальной суммой каждый раз, когда мы применяем купон. Этот купон необходим, поскольку Braintree изначально не поддерживает пропорциональную подписку на всех периодах подписки.

Миграции базы данных

Перед использованием Cashier нам нужно подготовить базу данных. Нам нужно добавить несколько столбцов в вашу таблицу users и создать новую таблицу subscription, чтобы хранить все подписки наших клиентов:

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->unsignedInteger('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

Затем добавьте трейт Billable в вашу модель:

use Laravel\Cashier\Billable;

class User extends Authenticatable
{
    use Billable;
}

Ключи API

Далее вам необходимо настроить следующие параметры в вашем файле 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(config('services.braintree.environment'));
\Braintree_Configuration::merchantId(config('services.braintree.merchant_id'));
\Braintree_Configuration::publicKey(config('services.braintree.public_key'));
\Braintree_Configuration::privateKey(config('services.braintree.private_key'));

Настройка валюты

Валюта Cashier по умолчанию - доллары США (USD). Вы можете изменить ее, вызвав метод Cashier::useCurrency из метода boot одного из ваших сервис-провайдеров. Метод useCurrency принимает два строковых параметра: валюта и символ валюты:

use Laravel\Cashier\Cashier;

Cashier::useCurrency('eur', '€');

Подписки

Создание подписок

Чтобы создать подписку, сначала получите экземпляр вашей оплачиваемой модели, которая скорее всего будет экземпляром App\User. После получения экземпляра модели вы можете использовать метод newSubscription для создания подписки модели:

$user = User::find(1);

$user->newSubscription('main', 'premium')->create($token);

Первым аргументом, передаваемым методу newSubscription, должно быть имя подписки. Если ваше приложение предлагает только одну подписку, вы можете назвать ее main или primary Второй аргумент - это конкретный план, на который подписывается пользователь. Это значение должно соответствовать идентификатору плана в Braintree.

Метод create, который принимает кредитную карту / источник токена, начнет подписку, а также обновит вашу базу данных с помощью идентификатора клиента и другой соответствующей платежной информации.

Дополнительные данные пользователя

Если вы хотите указать дополнительные сведения о клиенте, вы можете сделать это, передав их в качестве второго аргумента методу create:

$user->newSubscription('main', 'monthly')->create($token, [
    'email' => $email,
]);

Чтобы узнать больше о дополнительных полях, поддерживаемых Braintree, ознакомьтесь с соответствующей документацией Braintree.

Купоны

Если вы хотите применить купон при создании подписки, вы можете использовать метод withCoupon:

$user->newSubscription('main', 'monthly')
     ->withCoupon('code')
     ->create($token);

Проверка статуса подписки

Когда пользователь подписан на ваше приложение, вы можете легко проверить его статус подписки, используя различные удобные методы. Во-первых, метод subscribed возвращает значение true если у пользователя есть активная подписка, даже если действует пробный период:

if ($user->subscribed('main')) {
    //
}

Метод subscribed также отлично подходит для посредника роута, позволяя фильтровать доступ к роутам и контроллерам на основе статуса подписки пользователя:

public function handle($request, Closure $next)
{
    if ($request->user() && ! $request->user()->subscribed('main')) {
        // Этот пользователь не заплатил...
        return redirect('billing');
    }

    return $next($request);
}

Если вы хотите узнать, действует ли пробный период, вы можете использовать метод onTrial. Этот метод может быть полезен для отображения предупреждения пользователю о том, что у него еще действует пробный период:

if ($user->subscription('main')->onTrial()) {
    //
}

Метод subscribedToPlan может использоваться для определения того, подписан ли пользователь на данный план на основе идентификатора плана. В этом примере мы определим, активна ли подписка main на monthly план:

if ($user->subscribedToPlan('monthly', 'main')) {
    //
}

Метод recurring может использоваться для определения, подписан ли пользователь в данный момент и у него закончился пробный период:

if ($user->subscription('main')->recurring()) {
    //
}

Статус отмененной подписки

Чтобы определить, был ли пользователь когда-то активным подписчиком, но отменил свою подписку, вы можете использовать метод cancelled:

if ($user->subscription('main')->cancelled()) {
    //
}

Вы также можете определить, если пользователь отменил свою подписку, но все еще действует "льготный период", пока срок действия подписки не истечет полностью. Например, если пользователь отменяет подписку 5 марта, срок действия которой изначально истекал 10 марта, пользователь получает "льготный период" до 10 марта. Обратите внимание, что в течение этого времени метод subscribed по-прежнему возвращает значение true:

if ($user->subscription('main')->onGracePeriod()) {
    //
}

Чтобы определить, отменил ли пользователь свою подписку без "льготного периода", вы можете использовать метод ended:

if ($user->subscription('main')->ended()) {
    //
}

Изменение планов

После того, как пользователь подписался на ваше приложение, он может захотеть перейти на новый тарифный план. Чтобы перевести пользователя на новую подписку, передайте идентификатор плана методу swap:

$user = App\User::find(1);

$user->subscription('main')->swap('provider-plan-id');

Если у пользователя был пробный период, он будет сохранен. Кроме того, если для подписки существует "количество", оно также будет сохраняться.

Если вы хотите поменять планы и отменить любой пробный период, на котором в данный момент находится пользователь, вы можете использовать метод skipTrial:

$user->subscription('main')
        ->skipTrial()
        ->swap('provider-plan-id');

Налоги на подписку

Чтобы указать процент налога, который пользователь платит за подписку, внедрите метод taxPercentage в оплачиваемую модель и верните числовое значение в диапазоне от 0 до 100, не более 2 десятичных знаков.

public function taxPercentage()
{
    return 20;
}

Метод taxPercentage позволяет вам применять налоговую ставку в зависимости от модели, что может быть полезно для базы пользователей, охватывающей несколько стран и налоговые ставки.

Метод taxPercentage применяется только к плате за подписку. Если вы используете Cashier для одноразовых оплат, вам нужно будет у них вручную указать ставку налога.

Отмена подписок

Чтобы отменить подписку, вызовите метод cancel в подписке пользователя:

$user->subscription('main')->cancel();

Когда подписка отменяется, Cashier автоматически установит столбец sets_at в вашей базе данных. Этот столбец используется для того, чтобы знать, когда метод subscribed должен начать возвращать false Например, если клиент отменяет подписку 1 марта, но срок подписки запланирован до 5 марта, метод subscribed продолжит возвращать true до 5 марта.

Вы можете определить, отменил ли пользователь свою подписку, но все еще действует "льготный период", используя метод onGracePeriod:

if ($user->subscription('main')->onGracePeriod()) {
    //
}

Если вы хотите немедленно отменить подписку, вызовите метод cancelNow в подписке пользователя:

$user->subscription('main')->cancelNow();

Возобновление подписок

Если пользователь отменил свою подписку и вы хотите возобновить ее, используйте метод resume. Пользователь должен оставаться в льготном периоде для возобновления подписки:

$user->subscription('main')->resume();

Если пользователь отменяет подписку, а затем возобновляет эту подписку до того, как срок действия подписки полностью истек, счета не будут немедленно выставлены. Вместо этого их подписка будет повторно активирована, и им будет выставлен счет в прежнем цикле оплат.

Пробные подписки

С кредитной картой

Если вы хотите предложить своим клиентам пробные периоды, в то же время собирая информацию о способе оплаты заранее, вы должны использовать метод trialDays при создании своих подписок:

$user = User::find(1);

$user->newSubscription('main', 'monthly')
            ->trialDays(10)
            ->create($token);

Этот метод устанавливает дату окончания пробного периода в записи подписки в базе данных, а также дает команду Braintree не начинать выставление счетов клиенту до этой даты.

Если подписка клиента не отменена до даты окончания пробного периода, с него будет снята оплата по окончании пробного периода, поэтому вы должны обязательно уведомить своих пользователей о дате окончания пробного периода.

Вы можете определить, действует ли у пользователя пробный период, используя либо метод onTrial экземпляра пользователя, либо метод onTrial экземпляра подписки. Два примера ниже идентичны:

if ($user->onTrial('main')) {
    //
}

if ($user->subscription('main')->onTrial()) {
    //
}

Без кредитной карты

Если вы хотите предложить пробные периоды без предварительного сбора информации о способе оплаты, вы можете установить в столбце trial_ends_at записи пользователя желаемую дату окончания пробного периода. Обычно это делается при регистрации пользователя:

$user = User::create([
    // Заполните другие пользовательские свойства...
    'trial_ends_at' => now()->addDays(10),
]);
Не забудьте добавить мутатор даты для trial_ends_at в описании модели.

Cashier относится к такому типу пробных версий как "общая пробная версия", поскольку она не привязана ни к одной из существующих подписок. Метод onTrial в экземпляре User вернет true, если текущая дата не превышает значения trial_ends_at:

if ($user->onTrial()) {
    // Пользователь на протяжении пробного периода...
}

Вы также можете использовать метод onGenericTrial, если вы хотите точно знать, что пользователь находится в "общем пробном периоде" и еще не оформил фактическую подписку:

if ($user->onGenericTrial()) {
    // Пользователь находится в пределах "общего пробного периода"...
}

Когда вы будете готовы создать фактическую подписку для пользователя, вы можете использовать метод newSubscription, как обычно:

$user = User::find(1);

$user->newSubscription('main', 'monthly')->create($token);

Клиенты

Создание клиентов

Иногда вы можете захотеть создать клиента Braintree без начала подписки. Вы можете сделать это с помощью метода createAsBraintreeCustomer:

$user->createAsBraintreeCustomer();

После того, как клиент был создан в Braintree, вы можете начать подписку позже.

Карты

Обновление кредитных карт

Метод updateCard может использоваться для обновления информации о кредитной карте клиента. Этот метод принимает токен Braintree и назначает новую кредитную карту в качестве источника оплаты по умолчанию:

$user->updateCard($token);

Обработка веб-хуков

Braintree может уведомить ваше приложение о различных событиях через веб-хуки. Для обработки веб-хуков определите роут, который указывает на Cashier контроллер с веб-хуками. Этот контроллер будет обрабатывать все входящие запросы веб-хука и отправлять их в соответствующий метод контроллера:

Route::post(
    'braintree/webhook',
    '\Laravel\Cashier\Http\Controllers\WebhookController@handleWebhook'
);
После того, как вы зарегистрировали свой роут, обязательно настройте URL-адрес веб-хука в настройках панели управления Braintree.

По умолчанию этот контроллер будет автоматически обрабатывать отмену подписок, которые имеют слишком много неудачных платежей (как определено вашими настройками Braintree); однако, как будет описано дальше, вы можете расширить этот контроллер для обработки любого события веб-хука, которое вам захочется.

Веб-хуки & CSRF Защита

Поскольку веб-хукам необходимо обойти защиту CSRF Laravel, не забудьте добавить URI в исключение вашего посредника VerifyCsrfToken или разместить роут вне группы посредника web:

protected $except = [
    'braintree/*',
];

Определение обработчиков событий веб-хуков

Cashier автоматически обрабатывает отмену подписки по сбойным платежам, но если у вас есть дополнительные события веб-хуков, которые вы хотите обработать, расширьте контроллер веб-хуков. Имена ваших методов должны соответствовать ожидаемому соглашению Cashier, в частности, методы должны иметь префикс handle и имя "camel case", которое вы хотите обработать. Например, если вы хотите обработать веб-хук dispute_opened, вам следует добавить метод handleDisputeOpened в контроллер:

<?php

namespace App\Http\Controllers;

use Braintree\WebhookNotification;
use Laravel\Cashier\Http\Controllers\WebhookController as CashierController;

class WebhookController extends CashierController
{
    /**
     * Обрабатывает новый спор.
     *
     * @param  \Braintree\WebhookNotification  $webhook
     * @return \Symfony\Component\HttpFoundation\Responses
     */
    public function handleDisputeOpened(WebhookNotification $webhook)
    {
        // Handle The Webhook...
    }
}

Неудачные подписки

Что делать, если срок действия кредитной карты клиента истекает? Не беспокойтесь - Cashier включает контроллер веб-хуков, который может легко отменить подписку клиента для вас. Просто укажите маршрут до контроллера:

Route::post(
    'braintree/webhook',
    '\Laravel\Cashier\Http\Controllers\WebhookController@handleWebhook'
);

Так просто! Неудачные платежи будут захвачены и обработаны контроллером. Контроллер отменит подписку клиента, когда Braintree определит, что подписка не удалась (обычно после трех неудачных попыток оплаты). Не забудьте: вам необходимо настроить URI веб-хука в настройках панели управления Braintree.

Разовые платежи

Простой платеж

Вы должны передать полную сумму в долларах в метод charge:

Если вы хотите произвести "одноразовую" оплату с кредитной карты подписанного клиента, вы можете использовать метод charge на экземпляре оплачиваемой модели.

$user->charge(1);

Метод charge принимает массив в качестве второго аргумента, что позволяет вам передавать любые параметры, которые вы пожелаете, в создание платежа. Обратитесь к документации Braintree относительно вариантов, доступных вам при начислении платежей:

$user->charge(1, [
    'custom_option' => $value,
]);

Метод charge вызовет исключение в случае сбоя платежа. Если платеж успешен, полный ответ Braintree будет возвращен методом:

try {
    $response = $user->charge(1);
} catch (Exception $e) {
    //
}

Платеж по счету

Иногда вам может потребоваться произвести единовременную оплату, но также сгенерировать счет на оплату, чтобы вы могли предложить вашему клиенту квитанцию в формате PDF. Метод invoiceFor позволяет вам сделать это. Например, давайте выставим счет клиенту на сумму 5 долларов за "Единовременный сбор":

$user->invoiceFor('Единовременный сбор', 5);

Оплата немедленно пройдет с кредитной карты пользователя. Метод invoiceFor также принимает массив в качестве третьего аргумента. Этот массив содержит параметры выставления счета для элемента счета. Вы должны включить опцию description при вызове метода invoiceFor:

$user->invoiceFor('Единовременный сбор', 5, [
    'description' => 'описание вашего счета',
]);

Счета

Вы можете легко получить массив счетов оплачиваемой модели, используя метод invoices:

$invoices = $user->invoices();

// Включая ожидающие счета в результате...
$invoices = $user->invoicesIncludingPending();

При перечислении счетов-фактур для клиента вы можете использовать вспомогательные методы счета-фактуры для отображения соответствующей информации о счете-фактуре. Например, вы можете перечислить каждый счет в таблице, что позволит пользователю легко загрузить любой из них:

<table>
    @foreach ($invoices as $invoice)
        <tr>
            <td>{{ $invoice->date()->toFormattedDateString() }}</td>
            <td>{{ $invoice->total() }}</td>
            <td><a href="/user/invoice/{{ $invoice->id }}">Скачать</a></td>
        </tr>
    @endforeach
</table>

Генерация PDF-файлов счетов

Используя маршрут или контроллер, используйте метод downloadInvoice для генерации загрузки счета в формате PDF. Этот метод автоматически генерирует правильный HTTP-ответ для отправки загрузки в браузер:

use Illuminate\Http\Request;

Route::get('user/invoice/{invoice}', function (Request $request, $invoiceId) {
    return $request->user()->downloadInvoice($invoiceId, [
        'vendor'  => 'Your Company',
        'product' => 'Your Product',
    ]);
});