- Введение
- Обновление Cashier
- Установка
- Настройка
- Быстрый старт
- Клиенты
- Способы оплаты
- Подписки
- Пробные периоды подписок
- Обработка вебхуков Stripe
- Одноразовые платежи
- Checkout
- Счета
- Обработка неудачных платежей
- Сильная аутентификация клиента (SCA)
- Stripe SDK
- Тестирование
#Введение
Laravel Cashier Stripe предоставляет выразительный и удобный интерфейс для работы с сервисами подписочного биллинга Stripe. Он берет на себя почти весь рутинный код, связанный с подписками, который вам не хочется писать вручную. Помимо базового управления подписками, Cashier поддерживает купоны, смену подписок, "количества" в подписках, льготные периоды отмены и даже генерацию PDF счетов.
#Обновление Cashier
При обновлении до новой версии Cashier важно внимательно ознакомиться с руководством по обновлению.
Чтобы избежать несовместимых изменений, Cashier использует фиксированную версию Stripe API. В версии Cashier 15 используется Stripe API версии 2023-10-16. Версия Stripe API будет обновляться в минорных релизах для поддержки новых функций и улучшений Stripe.
#Установка
Сначала установите пакет Cashier для Stripe с помощью менеджера пакетов Composer:
composer require laravel/cashier
После установки пакета опубликуйте миграции Cashier с помощью Artisan-команды vendor:publish:
php artisan vendor:publish --tag="cashier-migrations"
Затем выполните миграции базы данных:
php artisan migrate
Миграции Cashier добавят несколько колонок в таблицу users. Также будет создана новая таблица subscriptions для хранения подписок клиентов и таблица subscription_items для подписок с несколькими ценами.
При желании вы также можете опубликовать конфигурационный файл Cashier с помощью команды vendor:publish Artisan:
php artisan vendor:publish --tag="cashier-config"
Наконец, чтобы Cashier корректно обрабатывал все события Stripe, не забудьте настроить обработку вебхуков Cashier.
Stripe рекомендует, чтобы любые колонки, используемые для хранения идентификаторов Stripe, были чувствительны к регистру. Поэтому при использовании MySQL убедитесь, что для колонки stripe_id установлена сортировка utf8_bin. Подробнее об этом можно прочитать в документации Stripe.
#Настройка
#Модель с биллингом
Перед использованием Cashier добавьте трейд Billable в определение вашей модели с биллингом. Обычно это модель App\Models\User. Этот трейд предоставляет методы для выполнения общих задач биллинга, таких как создание подписок, применение купонов и обновление информации о способах оплаты:
use Laravel\Cashier\Billable;
class User extends Authenticatable
{
use Billable;
}
Cashier предполагает, что вашей моделью с биллингом будет класс App\Models\User, поставляемый с Laravel. Если вы хотите изменить это, вы можете указать другую модель через метод useCustomerModel. Обычно этот метод вызывается в методе boot класса AppServiceProvider:
use App\Models\Cashier\User;
use Laravel\Cashier\Cashier;
/**
* Инициализация сервисов приложения.
*/
public function boot(): void
{
Cashier::useCustomerModel(User::class);
}
Если вы используете модель, отличную от стандартной App\Models\User, предоставляемой Laravel, вам нужно опубликовать и изменить миграции Cashier, чтобы они соответствовали имени таблицы вашей альтернативной модели.
#API ключи
Далее настройте ваши Stripe API ключи в файле .env вашего приложения. Вы можете получить ключи в панели управления Stripe:
STRIPE_KEY=your-stripe-key
STRIPE_SECRET=your-stripe-secret
STRIPE_WEBHOOK_SECRET=your-stripe-webhook-secret
Убедитесь, что переменная окружения STRIPE_WEBHOOK_SECRET определена в вашем .env файле, так как она используется для проверки, что входящие вебхуки действительно от Stripe.
#Настройка валюты
В Cashier по умолчанию используется валюта доллары США (USD). Вы можете изменить валюту по умолчанию, установив переменную окружения CASHIER_CURRENCY в вашем .env файле:
CASHIER_CURRENCY=eur
Кроме настройки валюты, вы можете указать локаль для форматирования денежных значений на счетах. Внутри Cashier использует класс PHP NumberFormatter для установки локали валюты:
CASHIER_CURRENCY_LOCALE=nl_BE
Для использования локалей, отличных от en, убедитесь, что на вашем сервере установлено и настроено PHP расширение ext-intl.
#Настройка налогов
Благодаря Stripe Tax возможно автоматически рассчитывать налоги для всех счетов, создаваемых Stripe. Вы можете включить автоматический расчет налогов, вызвав метод calculateTaxes в методе boot класса App\Providers\AppServiceProvider вашего приложения:
use Laravel\Cashier\Cashier;
/**
* Инициализация сервисов приложения.
*/
public function boot(): void
{
Cashier::calculateTaxes();
}
После включения расчета налогов, все новые подписки и одноразовые счета будут автоматически получать расчет налогов.
Для корректной работы этой функции данные о клиенте, такие как имя, адрес и налоговый идентификатор, должны быть синхронизированы со Stripe. Для этого вы можете использовать методы синхронизации данных клиента и налоговых идентификаторов, предоставляемые Cashier.
Налоги не рассчитываются для одноразовых платежей и оплаты одноразовых платежей через Checkout.
#Логирование
Cashier позволяет указать канал логирования для записи критических ошибок Stripe. Вы можете задать канал логирования, определив переменную окружения CASHIER_LOGGER в вашем .env файле:
CASHIER_LOGGER=stack
Исключения, возникающие при вызовах API Stripe, будут логироваться через канал логирования по умолчанию вашего приложения.
#Использование кастомных моделей
Вы можете расширять модели, используемые внутри Cashier, определяя собственные модели и наследуя соответствующие модели Cashier:
use Laravel\Cashier\Subscription as CashierSubscription;
class Subscription extends CashierSubscription
{
// ...
}
После определения вашей модели вы можете указать Cashier использовать её через класс Laravel\Cashier\Cashier. Обычно это делается в методе boot класса App\Providers\AppServiceProvider вашего приложения:
use App\Models\Cashier\Subscription;
use App\Models\Cashier\SubscriptionItem;
/**
* Инициализация сервисов приложения.
*/
public function boot(): void
{
Cashier::useSubscriptionModel(Subscription::class);
Cashier::useSubscriptionItemModel(SubscriptionItem::class);
}
#Быстрый старт
#Продажа продуктов
Перед использованием Stripe Checkout необходимо определить продукты с фиксированными ценами в вашей панели Stripe. Также следует настроить обработку вебхуков Cashier.
Предоставление биллинга за продукты и подписки через ваше приложение может показаться сложным. Однако благодаря Cashier и Stripe Checkout вы можете легко создать современные и надежные платежные интеграции.
Для взимания оплаты с клиентов за одноразовые продукты мы используем Cashier, чтобы направить клиентов в Stripe Checkout, где они введут данные оплаты и подтвердят покупку. После оплаты через Checkout клиент будет перенаправлен на URL успеха в вашем приложении:
use Illuminate\Http\Request;
Route::get('/checkout', function (Request $request) {
$stripePriceId = 'price_deluxe_album';
$quantity = 1;
return $request->user()->checkout([$stripePriceId => $quantity], [
'success_url' => route('checkout-success'),
'cancel_url' => route('checkout-cancel'),
]);
})->name('checkout');
Route::view('checkout.success')->name('checkout-success');
Route::view('checkout.cancel')->name('checkout-cancel');
Как видно из примера выше, мы используем метод checkout из Cashier для перенаправления клиента в Stripe Checkout по заданному "идентификатору цены". В Stripe "цены" — это определённые цены для конкретных продуктов.
При необходимости метод checkout автоматически создаст клиента в Stripe и свяжет эту запись с соответствующим пользователем в базе данных вашего приложения. После завершения сессии Checkout клиент будет перенаправлен на страницу успеха или отмены, где вы можете показать ему информационное сообщение.
#Передача метаданных в Stripe Checkout
При продаже продуктов часто нужно отслеживать завершённые заказы и купленные товары через модели Cart и Order, определённые в вашем приложении. При перенаправлении клиентов в Stripe Checkout для завершения покупки может потребоваться передать идентификатор существующего заказа, чтобы связать завершённую покупку с соответствующим заказом после возврата клиента в приложение.
Для этого можно передать массив metadata в метод checkout. Представим, что при начале процесса оформления заказа в нашем приложении создаётся ожидающий Order. Модели Cart и Order в этом примере иллюстративны и не предоставляются Cashier. Вы можете реализовать их по своему усмотрению:
use App\Models\Cart;
use App\Models\Order;
use Illuminate\Http\Request;
Route::get('/cart/{cart}/checkout', function (Request $request, Cart $cart) {
$order = Order::create([
'cart_id' => $cart->id,
'price_ids' => $cart->price_ids,
'status' => 'incomplete',
]);
return $request->user()->checkout($order->price_ids, [
'success_url' => route('checkout-success').'?session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => route('checkout-cancel'),
'metadata' => ['order_id' => $order->id],
]);
})->name('checkout');
Как видно из примера, при начале оформления заказа мы передаем все связанные с корзиной/заказом идентификаторы цен Stripe в метод checkout. Конечно, ваше приложение отвечает за связывание этих элементов с "корзиной" или заказом по мере добавления клиентом. Также мы передаем ID заказа в сессию Stripe Checkout через массив metadata. Наконец, мы добавили шаблонную переменную CHECKOUT_SESSION_ID в URL успеха Checkout. Когда Stripe перенаправит клиента обратно, эта переменная автоматически заполнится ID сессии Checkout.
Далее создадим маршрут успеха Checkout. Это маршрут, на который пользователи будут перенаправлены после завершения покупки через Stripe Checkout. В этом маршруте мы можем получить ID сессии Stripe Checkout и соответствующий объект, чтобы получить наши метаданные и обновить заказ клиента:
use App\Models\Order;
use Illuminate\Http\Request;
use Laravel\Cashier\Cashier;
Route::get('/checkout/success', function (Request $request) {
$sessionId = $request->get('session_id');
if ($sessionId === null) {
return;
}
$session = Cashier::stripe()->checkout->sessions->retrieve($sessionId);
if ($session->payment_status !== 'paid') {
return;
}
$orderId = $session['metadata']['order_id'] ?? null;
$order = Order::findOrFail($orderId);
$order->update(['status' => 'completed']);
return view('checkout-success', ['order' => $order]);
})->name('checkout-success');
Для получения дополнительной информации обратитесь к документации Stripe о данных, содержащихся в объекте сессии Checkout.
#Продажа подписок
Перед использованием Stripe Checkout необходимо определить продукты с фиксированными ценами в вашей панели Stripe. Также следует настроить обработку вебхуков Cashier.
Предоставление биллинга за продукты и подписки через ваше приложение может показаться сложным. Однако благодаря Cashier и Stripe Checkout вы можете легко создать современные и надежные платежные интеграции.
Чтобы научиться продавать подписки с помощью Cashier и Stripe Checkout, рассмотрим простой пример сервиса подписки с базовым ежемесячным (price_basic_monthly) и годовым (price_basic_yearly) планом. Эти две цены могут быть объединены под продуктом "Basic" (pro_basic) в вашей панели Stripe. Кроме того, сервис может предлагать план Expert как pro_expert.
Сначала посмотрим, как клиент может подписаться на наши услуги. Представим, что клиент нажимает кнопку "подписаться" на странице тарифов для базового плана. Эта кнопка или ссылка должна вести на маршрут Laravel, который создаст сессию Stripe Checkout для выбранного плана:
use Illuminate\Http\Request;
Route::get('/subscription-checkout', function (Request $request) {
return $request->user()
->newSubscription('default', 'price_basic_monthly')
->trialDays(5)
->allowPromotionCodes()
->checkout([
'success_url' => route('your-success-route'),
'cancel_url' => route('your-cancel-route'),
]);
});
Как видно из примера, мы перенаправляем клиента в сессию Stripe Checkout, где он сможет подписаться на базовый план. После успешного оформления или отмены клиент будет возвращён на URL, который мы указали в методе checkout. Чтобы узнать, когда подписка действительно началась (так как некоторые способы оплаты требуют времени на обработку), необходимо настроить обработку вебхуков Cashier.
Теперь, когда клиенты могут оформлять подписки, нужно ограничить доступ к определённым частям приложения только для подписанных пользователей. Конечно, мы всегда можем проверить текущий статус подписки пользователя с помощью метода subscribed, предоставляемого трейтом Billable:
@if ($user->subscribed())
<p>You are subscribed.</p>
@endif
Мы также можем легко проверить, подписан ли пользователь на конкретный продукт или цену:
@if ($user->subscribedToProduct('pro_basic'))
<p>You are subscribed to our Basic product.</p>
@endif
@if ($user->subscribedToPrice('price_basic_monthly'))
<p>You are subscribed to our monthly Basic plan.</p>
@endif
#Создание middleware для проверки подписки
Для удобства вы можете создать middleware, который проверяет, подписан ли пользователь. После определения middleware его можно назначить маршруту, чтобы запретить доступ неподписанным пользователям:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class Subscribed
{
/**
* Обработка входящего запроса.
*/
public function handle(Request $request, Closure $next): Response
{
if (! $request->user()?->subscribed()) {
// Перенаправить пользователя на страницу биллинга и предложить подписаться...
return redirect('/billing');
}
return $next($request);
}
}
После определения middleware его можно назначить маршруту:
use App\Http\Middleware\Subscribed;
Route::get('/dashboard', function () {
// ...
})->middleware([Subscribed::class]);
#Позволить клиентам управлять своим тарифным планом
Клиенты могут захотеть изменить свой тарифный план на другой продукт или "уровень". Самый простой способ — направить клиентов в Портал выставления счетов Stripe, который предоставляет готовый интерфейс для скачивания счетов, обновления способа оплаты и изменения подписок.
Сначала создайте ссылку или кнопку в вашем приложении, которая ведет на маршрут Laravel, где будет инициирована сессия Портала выставления счетов:
<a href="{{ route('billing') }}">
Billing
</a>
Далее определим маршрут, который запускает сессию Портала выставления счетов Stripe и перенаправляет пользователя в портал. Метод redirectToBillingPortal принимает URL, на который пользователь будет возвращён после выхода из портала:
use Illuminate\Http\Request;
Route::get('/billing', function (Request $request) {
return $request->user()->redirectToBillingPortal(route('dashboard'));
})->middleware(['auth'])->name('billing');
Если вы настроили обработку вебхуков Cashier, он автоматически будет синхронизировать таблицы базы данных, связанные с Cashier, анализируя входящие вебхуки от Stripe. Например, когда пользователь отменяет подписку через Портал выставления счетов Stripe, Cashier получит соответствующий вебхук и отметит подписку как "отменённую" в базе данных вашего приложения.
#Клиенты
#Получение клиентов
Вы можете получить клиента по его Stripe ID с помощью метода Cashier::findBillable. Этот метод вернёт экземпляр модели с биллингом:
use Laravel\Cashier\Cashier;
$user = Cashier::findBillable($stripeId);
#Создание клиентов
Иногда нужно создать клиента Stripe без начала подписки. Это можно сделать с помощью метода createAsStripeCustomer:
$stripeCustomer = $user->createAsStripeCustomer();
После создания клиента в Stripe вы можете начать подписку позже. В метод можно передать необязательный массив $options с дополнительными параметрами создания клиента, поддерживаемыми Stripe API:
$stripeCustomer = $user->createAsStripeCustomer($options);
Если нужно получить объект клиента Stripe для модели с биллингом, используйте метод asStripeCustomer:
$stripeCustomer = $user->asStripeCustomer();
Метод createOrGetStripeCustomer используется, если вы хотите получить объект клиента Stripe для модели с биллингом, но не уверены, существует ли он уже в Stripe. Если клиента нет, метод создаст нового:
$stripeCustomer = $user->createOrGetStripeCustomer();
#Обновление клиентов
Иногда нужно обновить данные клиента Stripe напрямую. Это можно сделать с помощью метода updateStripeCustomer, который принимает массив опций обновления клиента, поддерживаемых Stripe API:
$stripeCustomer = $user->updateStripeCustomer($options);
#Балансы
Stripe позволяет зачислять или списывать средства с «баланса» клиента. Позже этот баланс будет зачислен или списан при выставлении новых счётов. Чтобы проверить общий баланс клиента, вы можете использовать метод balance, доступный на вашей модели, поддерживающей выставление счетов. Метод balance вернёт форматированную строку с представлением баланса в валюте клиента:
$balance = $user->balance();
Чтобы увеличить баланс клиента, передайте сумму в метод creditBalance. При желании можно добавить описание:
$user->creditBalance(500, 'Пополнение для премиум-клиента.');
Чтобы списать баланс клиента, передайте сумму в метод debitBalance:
$user->debitBalance(300, 'Штраф за неправильное использование.');
Метод applyBalance создаст новые транзакции баланса для клиента. Вы можете получить эти записи с помощью метода balanceTransactions, что полезно для ведения журнала начислений и списаний:
// Получить все транзакции...
$transactions = $user->balanceTransactions();
foreach ($transactions as $transaction) {
// Сумма транзакции...
$amount = $transaction->amount(); // $2.31
// Получить связанный счет, если доступен...
$invoice = $transaction->invoice();
}
#Налоговые идентификаторы
Cashier предоставляет удобный способ управления налоговыми идентификаторами клиента. Например, метод taxIds возвращает все налоговые идентификаторы, назначенные клиенту, в виде коллекции:
$taxIds = $user->taxIds();
Вы также можете получить конкретный налоговый идентификатор клиента по его идентификатору:
$taxId = $user->findTaxId('txi_belgium');
Вы можете создать новый Tax ID, указав действительный тип и значение в метод createTaxId:
$taxId = $user->createTaxId('eu_vat', 'BE0123456789');
Метод createTaxId сразу добавит VAT ID в аккаунт клиента. Проверка VAT ID также выполняется Stripe; однако это асинхронный процесс. Вы можете получать уведомления об обновлениях проверки, подписавшись на событие вебхука customer.tax_id.updated и проверяя параметр verification VAT ID. Для получения дополнительной информации о работе с вебхуками обратитесь к документации по определению обработчиков вебхуков.
Вы можете удалить Tax ID с помощью метода deleteTaxId:
$user->deleteTaxId('txi_belgium');
#Синхронизация данных клиента со Stripe
Обычно, когда пользователи вашего приложения обновляют имя, адрес электронной почты или другую информацию, которая также хранится в Stripe, вы должны уведомлять Stripe об этих изменениях. Это позволит поддерживать данные Stripe в актуальном состоянии с данными вашего приложения.
Чтобы автоматизировать это, вы можете определить слушатель события на вашей модели, для которой выставляются счёта, который реагирует на событие updated модели. Затем, внутри этого слушателя, вы можете вызвать метод syncStripeCustomerDetails у модели:
use App\Models\User;
use function Illuminate\Events\queueable;
/**
* Метод "booted" модели.
*/
protected static function booted(): void
{
static::updated(queueable(function (User $customer) {
if ($customer->hasStripeId()) {
$customer->syncStripeCustomerDetails();
}
}));
}
Теперь при каждом обновлении модели клиента её данные будут синхронизироваться со Stripe. Для удобства Cashier автоматически синхронизирует информацию клиента со Stripe при первоначальном создании клиента.
Вы можете настроить столбцы, используемые для синхронизации информации клиента со Stripe, переопределив различные методы, предоставляемые Cashier. Например, вы можете переопределить метод stripeName, чтобы указать атрибут, который будет считаться "именем" клиента при синхронизации с Stripe:
/**
* Получить имя клиента для синхронизации со Stripe.
*/
public function stripeName(): string|null
{
return $this->company_name;
}
Аналогично, вы можете переопределить методы stripeEmail, stripePhone, stripeAddress и stripePreferredLocales. Эти методы будут синхронизировать информацию с соответствующими параметрами клиента при обновлении объекта клиента Stripe. Если вы хотите полностью контролировать процесс синхронизации информации клиента, вы можете переопределить метод syncStripeCustomerDetails.
#Портал выставления счетов
Stripe предлагает удобный способ настроить портал выставления счетов, чтобы ваши клиенты могли управлять своей подпиской, способами оплаты и просматривать историю платежей. Вы можете перенаправить пользователей в портал выставления счетов, вызвав метод redirectToBillingPortal на модели с биллингом из контроллера или маршрута:
use Illuminate\Http\Request;
Route::get('/billing-portal', function (Request $request) {
return $request->user()->redirectToBillingPortal();
});
По умолчанию, когда пользователь завершит управление подпиской, он сможет вернуться на маршрут home вашего приложения через ссылку в портале Stripe. Вы можете указать пользовательский URL для возврата, передав его в метод redirectToBillingPortal:
use Illuminate\Http\Request;
Route::get('/billing-portal', function (Request $request) {
return $request->user()->redirectToBillingPortal(route('billing'));
});
Если вы хотите получить URL портала выставления счетов без генерации HTTP-редиректа, вы можете вызвать метод billingPortalUrl:
$url = $request->user()->billingPortalUrl(route('billing'));
#Способы оплаты
#Сохранение способов оплаты
Для создания подписок или выполнения одноразовых платежей через Stripe необходимо сохранить способ оплаты и получить его идентификатор из Stripe. Подход к этому зависит от того, планируете ли вы использовать способ оплаты для подписок или одноразовых платежей, поэтому рассмотрим оба варианта.
#Способы оплаты для подписок
При сохранении информации о кредитной карте клиента для последующего использования в подписке необходимо использовать API Stripe "Setup Intents" для безопасного сбора данных способа оплаты клиента. "Setup Intent" сообщает Stripe о намерении списать средства с платежного метода клиента. Трейт Billable в Cashier включает метод createSetupIntent для удобного создания нового Setup Intent. Этот метод следует вызывать из маршрута или контроллера, который отображает форму для сбора данных способа оплаты клиента:
return view('update-payment-method', [
'intent' => $user->createSetupIntent()
]);
После создания Setup Intent и передачи его в представление, вы должны прикрепить его секрет к элементу, который будет собирать способ оплаты. Например, рассмотрим форму "обновления способа оплаты":
<input id="card-holder-name" type="text">
<!-- Заглушка для Stripe Elements -->
<div id="card-element"></div>
<button id="card-button" data-secret="{{ $intent->client_secret }}">
Обновить способ оплаты
</button>
Далее библиотека Stripe.js может быть использована для прикрепления Stripe Element к форме и безопасного сбора платежных данных клиента:
<script src="https://js.stripe.com/v3/"></script>
<script>
const stripe = Stripe('stripe-public-key');
const elements = stripe.elements();
const cardElement = elements.create('card');
cardElement.mount('#card-element');
</script>
Далее карту можно проверить и получить безопасный "идентификатор способа оплаты" из Stripe с помощью метода Stripe confirmCardSetup:
const cardHolderName = document.getElementById('card-holder-name');
const cardButton = document.getElementById('card-button');
const clientSecret = cardButton.dataset.secret;
cardButton.addEventListener('click', async (e) => {
const { setupIntent, error } = await stripe.confirmCardSetup(
clientSecret, {
payment_method: {
card: cardElement,
billing_details: { name: cardHolderName.value }
}
}
);
if (error) {
// Показать пользователю "error.message"...
} else {
// Карта успешно проверена...
}
});
После проверки карты Stripe вы можете передать полученный идентификатор setupIntent.payment_method в ваше Laravel-приложение, где он может быть прикреплен к клиенту. Способ оплаты можно либо добавить как новый способ оплаты, либо использовать для обновления способа оплаты по умолчанию. Также вы можете сразу использовать идентификатор способа оплаты для создания новой подписки.
Если вы хотите узнать больше о Setup Intents и сборе платежных данных клиента, пожалуйста, ознакомьтесь с этим обзором от Stripe.
#Способы оплаты для одноразовых платежей
Конечно, при выполнении одноразового платежа с использованием способа оплаты клиента идентификатор способа оплаты используется только один раз. Из-за ограничений Stripe вы не можете использовать сохранённый способ оплаты по умолчанию клиента для одноразовых платежей. Клиент должен ввести данные способа оплаты с помощью библиотеки Stripe.js. Например, рассмотрим следующую форму:
<input id="card-holder-name" type="text">
<!-- Заглушка для Stripe Elements -->
<div id="card-element"></div>
<button id="card-button">
Оплатить
</button>
После определения такой формы библиотека Stripe.js может быть использована для прикрепления Stripe Element к форме и безопасного сбора платежных данных клиента:
<script src="https://js.stripe.com/v3/"></script>
<script>
const stripe = Stripe('stripe-public-key');
const elements = stripe.elements();
const cardElement = elements.create('card');
cardElement.mount('#card-element');
</script>
Далее карту можно проверить и получить безопасный "идентификатор способа оплаты" из Stripe с помощью метода Stripe createPaymentMethod:
const cardHolderName = document.getElementById('card-holder-name');
const cardButton = document.getElementById('card-button');
cardButton.addEventListener('click', async (e) => {
const { paymentMethod, error } = await stripe.createPaymentMethod(
'card', cardElement, {
billing_details: { name: cardHolderName.value }
}
);
if (error) {
// Показать пользователю "error.message"...
} else {
// Карта успешно проверена...
}
});
Если карта успешно проверена, вы можете передать paymentMethod.id в ваше Laravel-приложение и обработать одноразовый платеж.
#Получение способов оплаты
Метод paymentMethods на экземпляре модели с биллингом возвращает коллекцию экземпляров Laravel\Cashier\PaymentMethod:
$paymentMethods = $user->paymentMethods();
По умолчанию этот метод возвращает способы оплаты всех типов. Чтобы получить способы оплаты определённого типа, вы можете передать type в качестве аргумента метода:
$paymentMethods = $user->paymentMethods('sepa_debit');
Для получения способа оплаты по умолчанию клиента можно использовать метод defaultPaymentMethod:
$paymentMethod = $user->defaultPaymentMethod();
Вы можете получить конкретный способ оплаты, прикреплённый к модели с биллингом, используя метод findPaymentMethod:
$paymentMethod = $user->findPaymentMethod($paymentMethodId);
#Наличие способа оплаты
Чтобы определить, есть ли у модели с биллингом способ оплаты по умолчанию, вызовите метод hasDefaultPaymentMethod:
if ($user->hasDefaultPaymentMethod()) {
// ...
}
Метод hasPaymentMethod позволяет определить, есть ли у модели с биллингом хотя бы один способ оплаты:
if ($user->hasPaymentMethod()) {
// ...
}
Этот метод проверит наличие любых способов оплаты у модели. Чтобы проверить наличие способа оплаты определённого типа, передайте type в качестве аргумента:
if ($user->hasPaymentMethod('sepa_debit')) {
// ...
}
#Обновление способа оплаты по умолчанию
Метод updateDefaultPaymentMethod используется для обновления информации о способе оплаты по умолчанию клиента. Метод принимает идентификатор способа оплаты Stripe и назначает его как способ оплаты по умолчанию для выставления счетов:
$user->updateDefaultPaymentMethod($paymentMethod);
Чтобы синхронизировать информацию о способе оплаты по умолчанию с информацией в Stripe, используйте метод updateDefaultPaymentMethodFromStripe:
$user->updateDefaultPaymentMethodFromStripe();
Способ оплаты по умолчанию клиента может использоваться только для выставления счетов и создания новых подписок. Из-за ограничений Stripe он не может использоваться для одноразовых платежей.
#Добавление способов оплаты
Чтобы добавить новый способ оплаты, вызовите метод addPaymentMethod на модели с биллингом, передав идентификатор способа оплаты:
$user->addPaymentMethod($paymentMethod);
Чтобы узнать, как получить идентификаторы способов оплаты, ознакомьтесь с документацией по сохранению способов оплаты.
#Удаление способов оплаты
Чтобы удалить способ оплаты, вызовите метод delete на экземпляре Laravel\Cashier\PaymentMethod, который вы хотите удалить:
$paymentMethod->delete();
Метод deletePaymentMethod удалит конкретный способ оплаты из модели с биллингом:
$user->deletePaymentMethod('pm_visa');
Метод deletePaymentMethods удалит всю информацию о способах оплаты для модели с биллингом:
$user->deletePaymentMethods();
По умолчанию этот метод удалит способы оплаты всех типов. Чтобы удалить способы оплаты определённого типа, передайте type в качестве аргумента:
$user->deletePaymentMethods('sepa_debit');
Если у пользователя есть активная подписка, ваше приложение не должно позволять удалять способ оплаты по умолчанию.
#Подписки
Подписки позволяют настроить регулярные платежи для ваших клиентов. Подписки Stripe, управляемые Cashier, поддерживают несколько цен подписки, количество подписок, пробные периоды и многое другое.
#Создание подписок
Для создания подписки сначала получите экземпляр вашей модели с биллингом, обычно это экземпляр App\Models\User. После получения модели вы можете использовать метод newSubscription для создания подписки модели:
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$request->user()->newSubscription(
'default', 'price_monthly'
)->create($request->paymentMethodId);
// ...
});
Первый аргумент метода newSubscription — внутренний тип подписки. Если ваше приложение предлагает только одну подписку, вы можете назвать её default или primary. Этот тип подписки используется только внутри приложения и не предназначен для отображения пользователям. Кроме того, он не должен содержать пробелов и не должен изменяться после создания подписки. Второй аргумент — конкретная цена, на которую подписывается пользователь. Это значение должно соответствовать идентификатору цены в Stripe.
Метод create, который принимает идентификатор способа оплаты Stripe или объект Stripe PaymentMethod, начнёт подписку и обновит базу данных с ID клиента Stripe и другой релевантной информацией о биллинге.
Передача идентификатора способа оплаты напрямую в метод create подписки автоматически добавит его в сохранённые способы оплаты пользователя.
#Сбор регулярных платежей через электронные счета
Вместо автоматического сбора регулярных платежей вы можете настроить Stripe на отправку счета клиенту по электронной почте каждый раз, когда наступает срок платежа. Клиент сможет вручную оплатить счет после его получения. При таком способе сбора платежей клиенту не нужно заранее предоставлять способ оплаты:
$user->newSubscription('default', 'price_monthly')->createAndSendInvoice();
Время, в течение которого клиент должен оплатить счет, прежде чем его подписка будет отменена, определяется опцией days_until_due. По умолчанию это 30 дней; однако вы можете указать конкретное значение для этой опции, если хотите:
$user->newSubscription('default', 'price_monthly')->createAndSendInvoice([], [
'days_until_due' => 30
]);
#Количество
Если вы хотите указать конкретное количество для цены при создании подписки, вызовите метод quantity на билдере подписки перед созданием:
$user->newSubscription('default', 'price_monthly')
->quantity(5)
->create($paymentMethod);
#Дополнительные параметры
Если вы хотите указать дополнительные параметры клиента или подписки, поддерживаемые Stripe, вы можете передать их вторым и третьим аргументами в метод create:
$user->newSubscription('default', 'price_monthly')->create($paymentMethod, [
'email' => $email,
], [
'metadata' => ['note' => 'Некоторая дополнительная информация.'],
]);
#Купоны
Если вы хотите применить купон при создании подписки, используйте метод withCoupon:
$user->newSubscription('default', 'price_monthly')
->withCoupon('code')
->create($paymentMethod);
Или, если вы хотите применить промокод Stripe, используйте метод withPromotionCode:
$user->newSubscription('default', 'price_monthly')
->withPromotionCode('promo_code_id')
->create($paymentMethod);
Переданный ID промокода должен быть API ID промокода Stripe, а не кодом, видимым клиенту. Если вам нужно найти ID промокода по видимому клиенту коду, используйте метод findPromotionCode:
// Найти ID промокода по клиентскому коду...
$promotionCode = $user->findPromotionCode('SUMMERSALE');
// Найти активный ID промокода по клиентскому коду...
$promotionCode = $user->findActivePromotionCode('SUMMERSALE');
В приведённом примере возвращаемый объект $promotionCode является экземпляром Laravel\Cashier\PromotionCode. Этот класс оборачивает базовый объект Stripe\PromotionCode. Вы можете получить купон, связанный с промокодом, вызвав метод coupon:
$coupon = $user->findPromotionCode('SUMMERSALE')->coupon();
Экземпляр купона позволяет определить сумму скидки и тип скидки — фиксированная или процентная:
if ($coupon->isPercentage()) {
return $coupon->percentOff().'%'; // 21.5%
} else {
return $coupon->amountOff(); // $5.99
}
Вы также можете получить скидки, которые в данный момент применены к клиенту или подписке:
$discount = $billable->discount();
$discount = $subscription->discount();
Возвращаемые экземпляры Laravel\Cashier\Discount оборачивают базовый объект Stripe\Discount. Вы можете получить купон, связанный с этой скидкой, вызвав метод coupon:
$coupon = $subscription->discount()->coupon();
Если вы хотите применить новый купон или промокод к клиенту или подписке, используйте методы applyCoupon или applyPromotionCode:
$billable->applyCoupon('coupon_id');
$billable->applyPromotionCode('promotion_code_id');
$subscription->applyCoupon('coupon_id');
$subscription->applyPromotionCode('promotion_code_id');
Помните, что следует использовать API ID промокода Stripe, а не код, видимый клиенту. Одновременно к клиенту или подписке может быть применён только один купон или промокод.
Для получения дополнительной информации обратитесь к документации Stripe по купонным скидкам и промокодам.
#Добавление подписок
Если вы хотите добавить подписку клиенту, у которого уже есть способ оплаты по умолчанию, вызовите метод add на билдере подписки:
use App\Models\User;
$user = User::find(1);
$user->newSubscription('default', 'price_monthly')->add();
#Создание подписок из панели управления Stripe
Вы также можете создавать подписки непосредственно из панели управления Stripe. В этом случае Cashier синхронизирует новые подписки и присвоит им тип default. Чтобы настроить тип подписки для подписок, созданных через панель, определите обработчики событий вебхуков.
Кроме того, через панель Stripe можно создать только один тип подписки. Если ваше приложение предлагает несколько типов подписок, через панель можно добавить только один из них.
Наконец, убедитесь, что у клиента не более одной активной подписки каждого типа. Если у клиента две подписки с типом default, Cashier будет использовать только последнюю, хотя обе будут синхронизированы с базой данных вашего приложения.
#Проверка статуса подписки
После подписки клиента на ваше приложение вы можете легко проверить статус его подписки с помощью различных удобных методов. Метод subscribed возвращает true, если у клиента есть активная подписка, даже если она находится в пробном периоде. Метод subscribed принимает тип подписки в качестве первого аргумента:
if ($user->subscribed('default')) {
// ...
}
Метод subscribed отлично подходит для использования в middleware маршрутов, позволяя ограничивать доступ к маршрутам и контроллерам в зависимости от статуса подписки пользователя:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureUserIsSubscribed
{
/**
* Обработать входящий запрос.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if ($request->user() && ! $request->user()->subscribed('default')) {
// Этот пользователь не является платящим клиентом...
return redirect('billing');
}
return $next($request);
}
}
Если вы хотите определить, находится ли пользователь ещё в пробном периоде, используйте метод onTrial. Он полезен для отображения предупреждения пользователю о том, что он всё ещё в пробном периоде:
if ($user->subscription('default')->onTrial()) {
// ...
}
Метод subscribedToProduct позволяет определить, подписан ли пользователь на конкретный продукт по идентификатору продукта Stripe. В Stripe продукты — это наборы цен. В этом примере мы проверим, подписан ли пользователь с подпиской default на продукт "premium" приложения. Идентификатор продукта Stripe должен соответствовать одному из ваших продуктов в панели Stripe:
if ($user->subscribedToProduct('prod_premium', 'default')) {
// ...
}
Передав массив в метод subscribedToProduct, вы можете проверить, подписан ли пользователь с подпиской default на продукты "basic" или "premium":
if ($user->subscribedToProduct(['prod_basic', 'prod_premium'], 'default')) {
// ...
}
Метод subscribedToPrice позволяет определить, соответствует ли подписка клиента определённому ID цены:
if ($user->subscribedToPrice('price_basic_monthly', 'default')) {
// ...
}
Метод recurring позволяет определить, подписан ли пользователь в данный момент и не находится ли он в пробном периоде:
if ($user->subscription('default')->recurring()) {
// ...
}
Если у пользователя есть две подписки с одинаковым типом, метод subscription всегда вернёт последнюю подписку. Например, у пользователя может быть две записи подписок с типом default; одна из них — старая, истёкшая, а другая — текущая активная. Всегда возвращается последняя подписка, а старые сохраняются в базе для истории.
#Статус отменённой подписки
Чтобы определить, был ли пользователь когда-то активным подписчиком, но отменил подписку, используйте метод canceled:
if ($user->subscription('default')->canceled()) {
// ...
}
Вы также можете определить, отменил ли пользователь свою подписку, но всё ещё находится в «льготный период» до полного окончания подписки. Например, если пользователь отменяет подписку 5 марта, которая изначально должна была закончиться 10 марта, пользователь находится в «льготном периоде» до 10 марта. Обратите внимание, что метод subscribed в это время всё ещё возвращает true:
if ($user->subscription('default')->onGracePeriod()) {
// ...
}
Чтобы определить, отменил ли пользователь подписку и уже вышел из «льготного периода», можно использовать метод ended:
if ($user->subscription('default')->ended()) {
// ...
}
#Статусы Incomplete и Past Due
Если для подписки после создания требуется дополнительное действие по оплате, подписка будет помечена как incomplete. Статусы подписок хранятся в столбце stripe_status таблицы subscriptions базы данных Cashier.
Аналогично, если при смене цены требуется дополнительное действие по оплате, подписка будет помечена как past_due. В этих состояниях подписка не считается активной, пока клиент не подтвердит оплату. Проверить наличие неполной оплаты можно с помощью метода hasIncompletePayment на биллабельной модели или экземпляре подписки:
if ($user->hasIncompletePayment('default')) {
// ...
}
if ($user->subscription('default')->hasIncompletePayment()) {
// ...
}
Если у подписки неполная оплата, следует направить пользователя на страницу подтверждения оплаты Cashier, передав идентификатор latestPayment. Этот идентификатор можно получить с помощью метода latestPayment экземпляра подписки:
<a href="{{ route('cashier.payment', $subscription->latestPayment()->id) }}">
Please confirm your payment.
</a>
Если вы хотите, чтобы подписка считалась активной в состояниях past_due или incomplete, можно использовать методы keepPastDueSubscriptionsActive и keepIncompleteSubscriptionsActive, предоставляемые Cashier. Обычно эти методы вызываются в методе register вашего App\Providers\AppServiceProvider:
use Laravel\Cashier\Cashier;
/**
* Зарегистрировать сервисы приложения.
*/
public function register(): void
{
Cashier::keepPastDueSubscriptionsActive();
Cashier::keepIncompleteSubscriptionsActive();
}
Если подписка в состоянии incomplete, её нельзя изменить, пока оплата не подтверждена. Поэтому методы swap и updateQuantity выбросят исключение, если подписка в состоянии incomplete.
#Области подписок (Subscription Scopes)
Большинство состояний подписок доступны как query scopes, что позволяет легко выполнять запросы к базе данных для подписок в определённом состоянии:
// Получить все активные подписки...
$subscriptions = Subscription::query()->active()->get();
// Получить все отменённые подписки пользователя...
$subscriptions = $user->subscriptions()->canceled()->get();
Полный список доступных областей приведён ниже:
Subscription::query()->active();
Subscription::query()->canceled();
Subscription::query()->ended();
Subscription::query()->incomplete();
Subscription::query()->notCanceled();
Subscription::query()->notOnGracePeriod();
Subscription::query()->notOnTrial();
Subscription::query()->onGracePeriod();
Subscription::query()->onTrial();
Subscription::query()->pastDue();
Subscription::query()->recurring();
#Изменение цен
После подписки клиента на ваше приложение он может захотеть изменить цену подписки. Чтобы сменить цену, передайте идентификатор цены Stripe в метод swap. При смене цены предполагается, что пользователь хочет повторно активировать подписку, если она была отменена. Переданный идентификатор должен соответствовать цене Stripe, доступной в панели Stripe:
use App\Models\User;
$user = App\Models\User::find(1);
$user->subscription('default')->swap('price_yearly');
Если клиент находится на пробном периоде, он сохранится. Также, если для подписки задана «quantity», она будет сохранена.
Если вы хотите сменить цену и отменить текущий пробный период, можно вызвать метод skipTrial:
$user->subscription('default')
->skipTrial()
->swap('price_yearly');
Если вы хотите сменить цену и сразу выставить счёт клиенту, не дожидаясь следующего цикла оплаты, используйте метод swapAndInvoice:
$user = User::find(1);
$user->subscription('default')->swapAndInvoice('price_yearly');
#Пропорциональные расчёты (Prorations)
По умолчанию Stripe делает пропорциональные расчёты при смене цены. Метод noProrate позволяет обновить цену без пропорциональных изменений:
$user->subscription('default')->noProrate()->swap('price_yearly');
Для подробностей о пропорциональных расчётах подписок смотрите документацию Stripe.
Вызов метода noProrate перед swapAndInvoice не повлияет на пропорциональные расчёты. Счёт всегда будет выставлен.
#Количество подписок
Иногда подписки зависят от «quantity». Например, приложение для управления проектами может брать $10 в месяц за каждый проект. Методы incrementQuantity и decrementQuantity позволяют легко увеличить или уменьшить количество подписок:
use App\Models\User;
$user = User::find(1);
$user->subscription('default')->incrementQuantity();
// Добавить пять к текущему количеству подписки...
$user->subscription('default')->incrementQuantity(5);
$user->subscription('default')->decrementQuantity();
// Вычесть пять из текущего количества подписки...
$user->subscription('default')->decrementQuantity(5);
Также можно установить конкретное количество с помощью метода updateQuantity:
$user->subscription('default')->updateQuantity(10);
Метод noProrate позволяет обновить количество без пропорциональных изменений:
$user->subscription('default')->noProrate()->updateQuantity(10);
Для подробностей о количестве подписок смотрите документацию Stripe.
#Количество для подписок с несколькими продуктами
Если у вашей подписки несколько продуктов, передавайте ID цены, количество которой хотите изменить, вторым аргументом в методы увеличения/уменьшения количества:
$user->subscription('default')->incrementQuantity(1, 'price_chat');
#Подписки с несколькими продуктами
Подписки с несколькими продуктами позволяют назначать несколько продуктов на одну подписку. Например, у вас есть базовая подписка за $10 в месяц и дополнительный продукт «живой чат» за $15 в месяц. Информация о таких подписках хранится в таблице subscription_items базы данных Cashier.
Вы можете указать несколько продуктов для подписки, передав массив цен вторым аргументом в метод newSubscription:
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$request->user()->newSubscription('default', [
'price_monthly',
'price_chat',
])->create($request->paymentMethodId);
// ...
});
В примере выше у клиента будет две цены, привязанные к подписке default. Обе цены будут списываться по своим интервалам оплаты. При необходимости можно указать количество для каждой цены с помощью метода quantity:
$user = User::find(1);
$user->newSubscription('default', ['price_monthly', 'price_chat'])
->quantity(5, 'price_chat')
->create($paymentMethod);
Если вы хотите добавить цену к существующей подписке, вызовите метод addPrice:
$user = User::find(1);
$user->subscription('default')->addPrice('price_chat');
В примере выше новая цена будет добавлена, и клиенту выставят счёт в следующем цикле оплаты. Чтобы выставить счёт сразу, используйте метод addPriceAndInvoice:
$user->subscription('default')->addPriceAndInvoice('price_chat');
Если нужно добавить цену с определённым количеством, передайте количество вторым аргументом в методы addPrice или addPriceAndInvoice:
$user = User::find(1);
$user->subscription('default')->addPrice('price_chat', 5);
Вы можете удалить цены из подписки с помощью метода removePrice:
$user->subscription('default')->removePrice('price_chat');
Нельзя удалить последнюю цену из подписки. Вместо этого следует отменить подписку.
#Смена цен
Вы также можете менять цены в подписках с несколькими продуктами. Например, если у клиента есть подписка с ценой price_basic и дополнительным продуктом price_chat, и вы хотите обновить клиента с price_basic на price_pro:
use App\Models\User;
$user = User::find(1);
$user->subscription('default')->swap(['price_pro', 'price_chat']);
При выполнении примера выше элемент подписки с price_basic удаляется, элемент с price_chat сохраняется, а для price_pro создаётся новый элемент подписки.
Вы можете указать опции для элементов подписки, передав массив ключ-значение в метод swap. Например, чтобы указать количество для цен подписки:
$user = User::find(1);
$user->subscription('default')->swap([
'price_pro' => ['quantity' => 5],
'price_chat'
]);
Если нужно сменить одну цену в подписке, можно вызвать метод swap на самом элементе подписки. Это удобно, если нужно сохранить все метаданные других цен подписки:
$user = User::find(1);
$user->subscription('default')
->findItemOrFail('price_basic')
->swap('price_pro');
#Пропорциональные расчёты
По умолчанию Stripe делает пропорциональные расчёты при добавлении или удалении цен в подписке с несколькими продуктами. Чтобы изменить цену без пропорциональных расчётов, добавьте метод noProrate к операции с ценой:
$user->subscription('default')->noProrate()->removePrice('price_chat');
#Количество
Чтобы обновить количество для отдельных цен подписки, используйте существующие методы работы с количеством, передавая ID цены дополнительным аргументом:
$user = User::find(1);
$user->subscription('default')->incrementQuantity(5, 'price_chat');
$user->subscription('default')->decrementQuantity(3, 'price_chat');
$user->subscription('default')->updateQuantity(10, 'price_chat');
При наличии нескольких цен в подписке атрибуты stripe_price и quantity модели Subscription будут равны null. Для доступа к отдельным ценам используйте связь items модели Subscription.
#Элементы подписки
Если у подписки несколько цен, в базе данных в таблице subscription_items будет несколько элементов подписки. К ним можно получить доступ через связь items подписки:
use App\Models\User;
$user = User::find(1);
$subscriptionItem = $user->subscription('default')->items->first();
// Получить цену Stripe и количество для конкретного элемента...
$stripePrice = $subscriptionItem->stripe_price;
$quantity = $subscriptionItem->quantity;
Также можно получить конкретный элемент с помощью метода findItemOrFail:
$user = User::find(1);
$subscriptionItem = $user->subscription('default')->findItemOrFail('price_chat');
#Несколько подписок
Stripe позволяет клиентам иметь несколько подписок одновременно. Например, у вас может быть спортзал с подпиской на плавание и подпиской на силовые тренировки, каждая с разной ценой. Клиенты могут подписываться на одну или обе подписки.
При создании подписок в приложении можно указать тип подписки в методе newSubscription. Тип — это любая строка, обозначающая тип подписки, которую пользователь оформляет:
use Illuminate\Http\Request;
Route::post('/swimming/subscribe', function (Request $request) {
$request->user()->newSubscription('swimming')
->price('price_swimming_monthly')
->create($request->paymentMethodId);
// ...
});
В этом примере мы оформили месячную подписку на плавание. Позже клиент может захотеть перейти на годовую подписку. Для изменения подписки достаточно сменить цену у подписки swimming:
$user->subscription('swimming')->swap('price_swimming_yearly');
Разумеется, подписку можно полностью отменить:
$user->subscription('swimming')->cancel();
#Оплата по использованию (Metered Billing)
Оплата по использованию позволяет взимать плату с клиентов в зависимости от их использования продукта в течение расчётного периода. Например, можно брать плату за количество отправленных сообщений или писем в месяц.
Чтобы начать использовать оплату по использованию, сначала создайте в панели Stripe продукт с ценой по использованию. Затем используйте метод meteredPrice, чтобы добавить ID такой цены к подписке клиента:
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$request->user()->newSubscription('default')
->meteredPrice('price_metered')
->create($request->paymentMethodId);
// ...
});
Также можно начать подписку с оплатой по использованию через Stripe Checkout:
$checkout = Auth::user()
->newSubscription('default', [])
->meteredPrice('price_metered')
->checkout();
return view('your-checkout-view', [
'checkout' => $checkout,
]);
#Отчёт об использовании
По мере использования приложения вы будете отправлять данные об использовании в Stripe для точного выставления счёта. Чтобы увеличить количество использования в подписке с оплатой по использованию, используйте метод reportUsage:
$user = User::find(1);
$user->subscription('default')->reportUsage();
По умолчанию к периоду оплаты добавляется «usage quantity» равное 1. Можно передать конкретное количество использования для добавления в период оплаты:
$user = User::find(1);
$user->subscription('default')->reportUsage(15);
Если в подписке несколько цен, нужно использовать метод reportUsageFor, чтобы указать, для какой цены по использованию отправлять данные:
$user = User::find(1);
$user->subscription('default')->reportUsageFor('price_metered', 15);
Иногда нужно обновить ранее отправленные данные об использовании. Для этого можно передать метку времени или объект DateTimeInterface вторым параметром в reportUsage. Stripe обновит данные, отправленные в указанное время. Обновлять можно, пока дата и время находятся в текущем расчётном периоде:
$user = User::find(1);
$user->subscription('default')->reportUsage(5, $timestamp);
#Получение записей об использовании
Чтобы получить прошлые данные об использовании клиента, используйте метод usageRecords экземпляра подписки:
$user = User::find(1);
$usageRecords = $user->subscription('default')->usageRecords();
Если в подписке несколько цен, используйте метод usageRecordsFor, чтобы указать цену по использованию, для которой нужно получить данные:
$user = User::find(1);
$usageRecords = $user->subscription('default')->usageRecordsFor('price_metered');
Методы usageRecords и usageRecordsFor возвращают коллекцию с ассоциативными массивами записей об использовании. Вы можете перебрать их для отображения общего использования клиента:
@foreach ($usageRecords as $usageRecord)
- Начало периода: {{ $usageRecord['period']['start'] }}
- Конец периода: {{ $usageRecord['period']['end'] }}
- Общее использование: {{ $usageRecord['total_usage'] }}
@endforeach
Для полного описания всех возвращаемых данных об использовании и работы с постраничной навигацией Stripe смотрите официальную документацию Stripe API.
#Налоги на подписки
Вместо ручного расчёта налогов вы можете автоматически рассчитывать налоги с помощью Stripe Tax
Чтобы указать налоговые ставки, применяемые к подписке пользователя, реализуйте метод taxRates в вашей биллабельной модели и верните массив с ID налоговых ставок Stripe. Эти ставки можно определить в вашей панели Stripe:
/**
* Налоговые ставки, применяемые к подпискам клиента.
*
* @return array<int, string>
*/
public function taxRates(): array
{
return ['txr_id'];
}
Метод taxRates позволяет применять налоговые ставки индивидуально для каждого клиента, что полезно при работе с пользователями из разных стран и с разными налоговыми ставками.
Если вы предлагаете подписки с несколькими продуктами, можно определить разные налоговые ставки для каждой цены, реализовав метод priceTaxRates в биллабельной модели:
/**
* Налоговые ставки, применяемые к подпискам клиента.
*
* @return array<string, array<int, string>>
*/
public function priceTaxRates(): array
{
return [
'price_monthly' => ['txr_id'],
];
}
Метод taxRates применяется только к платежам по подпискам. Если вы используете Cashier для разовых платежей, налоговую ставку нужно указывать вручную.
#Синхронизация налоговых ставок
При изменении жёстко заданных ID налоговых ставок, возвращаемых методом taxRates, настройки налогов в существующих подписках пользователя останутся прежними. Чтобы обновить налоговые ставки в существующих подписках согласно новым значениям taxRates, вызовите метод syncTaxRates на экземпляре подписки пользователя:
$user->subscription('default')->syncTaxRates();
Это также синхронизирует налоговые ставки для элементов подписки с несколькими продуктами. Если ваше приложение предлагает такие подписки, убедитесь, что биллабельная модель реализует метод priceTaxRates описанный выше.
#Налоговые льготы
Cashier предоставляет методы isNotTaxExempt, isTaxExempt и reverseChargeApplies для определения, освобождён ли клиент от налогов. Эти методы обращаются к API Stripe для получения статуса налоговой льготы клиента:
use App\Models\User;
$user = User::find(1);
$user->isTaxExempt();
$user->isNotTaxExempt();
$user->reverseChargeApplies();
Эти методы также доступны у любого объекта Laravel\Cashier\Invoice. Однако при вызове на объекте Invoice они определяют статус льготы на момент создания счёта.
#Дата якоря подписки
По умолчанию якорь расчётного цикла — дата создания подписки или, если используется пробный период, дата окончания пробного периода. Чтобы изменить дату якоря расчётного цикла, используйте метод anchorBillingCycleOn:
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$anchor = Carbon::parse('first day of next month');
$request->user()->newSubscription('default', 'price_monthly')
->anchorBillingCycleOn($anchor->startOfDay())
->create($request->paymentMethodId);
// ...
});
Для подробностей о настройке расчётных циклов подписок смотрите документацию Stripe по расчётным циклам
#Отмена подписок
Чтобы отменить подписку, вызовите метод cancel у подписки пользователя:
$user->subscription('default')->cancel();
При отмене подписки Cashier автоматически установит значение ends_at в таблице subscriptions вашей базы данных. Этот столбец используется, чтобы метод subscribed начал возвращать false.
Например, если клиент отменяет подписку 1 марта, но она должна была закончиться 5 марта, метод subscribed будет возвращать true до 5 марта. Это сделано, потому что обычно пользователю разрешается пользоваться приложением до конца расчётного периода.
Вы можете определить, отменил ли пользователь подписку, но всё ещё находится в «льготном периоде», используя метод onGracePeriod:
if ($user->subscription('default')->onGracePeriod()) {
// ...
}
Если нужно отменить подписку немедленно, вызовите метод cancelNow у подписки пользователя:
$user->subscription('default')->cancelNow();
Если нужно отменить подписку немедленно и выставить счёт за оставшееся невыставленное использование или новые/ожидающие прорыва счета, вызовите метод cancelNowAndInvoice у подписки пользователя:
$user->subscription('default')->cancelNowAndInvoice();
Также можно отменить подписку в определённое время:
$user->subscription('default')->cancelAt(
now()->addDays(10)
);
Наконец, всегда следует отменять подписки пользователя перед удалением связанной модели пользователя:
$user->subscription('default')->cancelNow();
$user->delete();
#Возобновление подписок
Если клиент отменил подписку и вы хотите её возобновить, вызовите метод resume у подписки. Клиент должен находиться в «льготном периоде», чтобы возобновить подписку:
$user->subscription('default')->resume();
Если клиент отменил подписку и возобновил её до полного окончания, счёт выставлен не будет. Подписка просто активируется заново, и оплата будет списываться по исходному циклу.
#Пробные периоды подписок
#С предварительным указанием способа оплаты
Если вы хотите предложить клиентам пробный период, при этом сразу собирая данные способа оплаты, используйте метод trialDays при создании подписки:
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$request->user()->newSubscription('default', 'price_monthly')
->trialDays(10)
->create($request->paymentMethodId);
// ...
});
Этот метод установит дату окончания пробного периода в записи подписки в базе данных и укажет Stripe не начинать списания до этой даты. При использовании trialDays Cashier переопределит любой пробный период, заданный по умолчанию для цены в Stripe.
Если подписка клиента не будет отменена до окончания пробного периода, с него спишут плату сразу после его окончания. Поэтому обязательно уведомляйте пользователей о дате окончания пробного периода.
Метод trialUntil позволяет указать экземпляр DateTime, который задаёт дату окончания пробного периода:
use Carbon\Carbon;
$user->newSubscription('default', 'price_monthly')
->trialUntil(Carbon::now()->addDays(10))
->create($paymentMethod);
Вы можете определить, находится ли пользователь в пробном периоде, используя либо метод onTrial у экземпляра пользователя, либо метод onTrial у экземпляра подписки. Оба приведённых ниже примера эквивалентны:
if ($user->onTrial('default')) {
// ...
}
if ($user->subscription('default')->onTrial()) {
// ...
}
Вы можете использовать метод endTrial для немедленного завершения пробного периода подписки:
$user->subscription('default')->endTrial();
Чтобы определить, истёк ли существующий пробный период, вы можете использовать методы hasExpiredTrial:
if ($user->hasExpiredTrial('default')) {
// ...
}
if ($user->subscription('default')->hasExpiredTrial()) {
// ...
}
#Определение количества дней пробного периода в Stripe / Cashier
Вы можете задать количество дней пробного периода для вашей цены в панели управления Stripe или всегда передавать их явно через Cashier. Если вы решите определить количество дней пробного периода в Stripe, имейте в виду, что новые подписки, включая подписки для клиентов, которые уже имели подписку ранее, всегда будут получать пробный период, если вы явно не вызовете метод skipTrial().
#Без предварительного указания способа оплаты
Если вы хотите предложить пробный период без предварительного сбора информации о способе оплаты пользователя, вы можете установить значение столбца trial_ends_at в записи пользователя на желаемую дату окончания пробного периода. Обычно это делается при регистрации пользователя:
use App\Models\User;
$user = User::create([
// ...
'trial_ends_at' => now()->addDays(10),
]);
Обязательно добавьте приведение к дате для атрибута trial_ends_at в определении класса вашей модели с биллингом.
Cashier называет такой тип пробного периода «общим пробным периодом», так как он не привязан к какой-либо существующей подписке. Метод onTrial у экземпляра модели с биллингом вернёт true, если текущая дата не превышает значение trial_ends_at:
if ($user->onTrial()) {
// Пользователь находится в пробном периоде...
}
Когда вы будете готовы создать реальную подписку для пользователя, вы можете использовать метод newSubscription как обычно:
$user = User::find(1);
$user->newSubscription('default', 'price_monthly')->create($paymentMethod);
Чтобы получить дату окончания пробного периода пользователя, вы можете использовать метод trialEndsAt. Этот метод вернёт экземпляр Carbon, если пользователь находится в пробном периоде, или null, если нет. Вы также можете передать необязательный параметр типа подписки, если хотите получить дату окончания пробного периода для конкретной подписки, отличной от стандартной:
if ($user->onTrial()) {
$trialEndsAt = $user->trialEndsAt('main');
}
Вы также можете использовать метод onGenericTrial, если хотите узнать, что пользователь находится именно в «общем» пробном периоде и ещё не создал реальную подписку:
if ($user->onGenericTrial()) {
// Пользователь находится в «общем» пробном периоде...
}
#Продление пробного периода
Метод extendTrial позволяет продлить пробный период подписки после её создания. Если пробный период уже истёк и клиент уже оплачивает подписку, вы всё равно можете предложить ему продлённый пробный период. Время, проведённое в пробном периоде, будет вычтено из следующего счёта клиента:
use App\Models\User;
$subscription = User::find(1)->subscription('default');
// Завершить пробный период через 7 дней...
$subscription->extendTrial(
now()->addDays(7)
);
// Добавить ещё 5 дней к пробному периоду...
$subscription->extendTrial(
$subscription->trial_ends_at->addDays(5)
);
#Обработка вебхуков Stripe
Вы можете использовать Stripe CLI для тестирования вебхуков во время локальной разработки.
Stripe может уведомлять ваше приложение о различных событиях через вебхуки. По умолчанию маршрут, указывающий на контроллер вебхуков Cashier, автоматически регистрируется провайдером сервиса Cashier. Этот контроллер обрабатывает все входящие запросы вебхуков.
По умолчанию контроллер вебхуков Cashier автоматически обрабатывает отмену подписок с большим количеством неудачных платежей (как определено в настройках Stripe), обновления клиентов, удаление клиентов, обновления подписок и изменения способов оплаты; однако, как мы скоро увидим, вы можете расширить этот контроллер для обработки любых событий вебхуков Stripe по вашему желанию.
Чтобы ваше приложение могло обрабатывать вебхуки Stripe, обязательно настройте URL вебхука в панели управления Stripe. По умолчанию контроллер вебхуков Cashier отвечает на путь /stripe/webhook. Полный список всех вебхуков, которые следует включить в панели Stripe:
customer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedcustomer.updatedcustomer.deletedpayment_method.automatically_updatedinvoice.payment_action_requiredinvoice.payment_succeeded
Для удобства Cashier включает Artisan-команду cashier:webhook. Эта команда создаст вебхук в Stripe, который будет слушать все события, необходимые Cashier:
php artisan cashier:webhook
По умолчанию созданный вебхук будет указывать на URL, определённый переменной окружения APP_URL и маршрутом cashier.webhook, включённым в Cashier. Вы можете указать опцию --url при вызове команды, если хотите использовать другой URL:
php artisan cashier:webhook --url "https://example.com/stripe/webhook"
Создаваемый вебхук будет использовать версию API Stripe, совместимую с вашей версией Cashier. Если вы хотите использовать другую версию Stripe, можете указать опцию --api-version:
php artisan cashier:webhook --api-version="2019-12-03"
После создания вебхук будет сразу активен. Если вы хотите создать вебхук, но оставить его отключённым до готовности, можете указать опцию --disabled при вызове команды:
php artisan cashier:webhook --disabled
Обязательно защитите входящие запросы вебхуков Stripe с помощью встроенного в Cashier middleware проверки подписи вебхука.
#Вебхуки и защита от CSRF
Поскольку вебхуки Stripe должны обходить защиту CSRF Laravel, обязательно добавьте URI в исключения в вашем middleware App\Http\Middleware\VerifyCsrfToken или вынесите маршрут из группы middleware web:
protected $except = [
'stripe/*',
];
#Определение обработчиков событий вебхуков
Cashier автоматически обрабатывает отмену подписок из-за неудачных платежей и другие распространённые события вебхуков Stripe. Однако, если у вас есть дополнительные события вебхуков, которые вы хотите обработать, вы можете слушать следующие события, которые отправляет Cashier:
Laravel\Cashier\Events\WebhookReceivedLaravel\Cashier\Events\WebhookHandled
Оба события содержат полный полезный нагрузочный пакет вебхука Stripe. Например, если вы хотите обработать вебхук invoice.payment_succeeded, вы можете зарегистрировать listener, который будет обрабатывать это событие:
<?php
namespace App\Listeners;
use Laravel\Cashier\Events\WebhookReceived;
class StripeEventListener
{
/**
* Обработка полученных вебхуков Stripe.
*/
public function handle(WebhookReceived $event): void
{
if ($event->payload['type'] === 'invoice.payment_succeeded') {
// Обработать входящее событие...
}
}
}
После определения слушателя вы можете зарегистрировать его в EventServiceProvider вашего приложения:
<?php
namespace App\Providers;
use App\Listeners\StripeEventListener;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Laravel\Cashier\Events\WebhookReceived;
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
WebhookReceived::class => [
StripeEventListener::class,
],
];
}
#Проверка подписей вебхуков
Для защиты ваших вебхуков вы можете использовать подписи вебхуков Stripe. Для удобства Cashier автоматически включает middleware, который проверяет валидность входящих запросов вебхуков Stripe.
Чтобы включить проверку webhook, убедитесь, что переменная окружения STRIPE_WEBHOOK_SECRET установлена в файле .env вашего приложения. Значение secret вебхука можно получить в панели управления вашей учётной записью Stripe.
#Одноразовые платежи
#Простой платёж
Если вы хотите сделать одноразовый платёж с клиента, вы можете использовать метод charge у экземпляра модели с биллингом. Вам нужно будет указать идентификатор способа оплаты вторым аргументом метода charge:
use Illuminate\Http\Request;
Route::post('/purchase', function (Request $request) {
$stripeCharge = $request->user()->charge(
100, $request->paymentMethodId
);
// ...
});
Метод charge принимает массив в качестве третьего аргумента, позволяя передавать любые опции для создания платежа в Stripe. Подробнее об опциях создания платежей можно узнать в документации Stripe:
$user->charge(100, $paymentMethod, [
'custom_option' => $value,
]);
Вы также можете использовать метод charge без привязки к конкретному клиенту или пользователю. Для этого вызовите метод charge у нового экземпляра вашей модели с биллингом:
use App\Models\User;
$stripeCharge = (new User)->charge(100, $paymentMethod);
Метод charge выбросит исключение, если платёж не удастся. Если платёж успешен, метод вернёт экземпляр Laravel\Cashier\Payment:
try {
$payment = $user->charge(100, $paymentMethod);
} catch (Exception $e) {
// ...
}
Метод charge принимает сумму платежа в минимальной единице валюты, используемой вашим приложением. Например, если клиенты платят в долларах США, сумма должна указываться в центах.
#Платёж с выставлением счёта
Иногда нужно сделать одноразовый платёж и предоставить клиенту PDF-счёт. Метод invoicePrice позволяет сделать именно это. Например, выставим счёт клиенту за пять новых футболок:
$user->invoicePrice('price_tshirt', 5);
Счёт будет сразу выставлен на оплату с использованием способа оплаты по умолчанию пользователя. Метод invoicePrice также принимает массив в качестве третьего аргумента с опциями выставления счёта для позиции. Четвёртый аргумент — это массив с опциями выставления счёта для самого счёта:
$user->invoicePrice('price_tshirt', 5, [
'discounts' => [
['coupon' => 'SUMMER21SALE']
],
], [
'default_tax_rates' => ['txr_id'],
]);
Аналогично invoicePrice, вы можете использовать метод tabPrice для создания одноразового платежа за несколько позиций (до 250 позиций на счёт), добавляя их в «чек» клиента, а затем выставляя счёт. Например, выставим счёт за пять футболок и две кружки:
$user->tabPrice('price_tshirt', 5);
$user->tabPrice('price_mug', 2);
$user->invoice();
Или вы можете использовать метод invoiceFor для одноразового платежа с использованием способа оплаты по умолчанию клиента:
$user->invoiceFor('One Time Fee', 500);
Хотя метод invoiceFor доступен, рекомендуется использовать методы invoicePrice и tabPrice с заранее определёнными ценами. Это позволит получить более подробную аналитику и данные в панели Stripe по продажам на уровне каждого продукта.
Методы invoice, invoicePrice и invoiceFor создают счёт Stripe, который будет повторно пытаться провести неудачные платежи. Если вы не хотите, чтобы счёта повторяли попытки, вам нужно будет закрывать их через API Stripe после первой неудачной попытки.
#Создание Payment Intents
Вы можете создать новый платежный intent Stripe, вызвав метод pay у экземпляра модели с возможностью оплаты. Вызов этого метода создаст платежный intent, обёрнутый в экземпляр Laravel\Cashier\Payment:
use Illuminate\Http\Request;
Route::post('/pay', function (Request $request) {
$payment = $request->user()->pay(
$request->get('amount')
);
return $payment->client_secret;
});
После создания payment intent вы можете вернуть клиентский секрет на фронтенд вашего приложения, чтобы пользователь мог завершить оплату в браузере. Подробнее о построении платежных потоков с использованием Stripe payment intents смотрите в документации Stripe.
При использовании метода pay клиенту будут доступны способы оплаты, включённые по умолчанию в вашей панели Stripe. Если вы хотите разрешить только определённые способы оплаты, используйте метод payWith:
use Illuminate\Http\Request;
Route::post('/pay', function (Request $request) {
$payment = $request->user()->payWith(
$request->get('amount'), ['card', 'bancontact']
);
return $payment->client_secret;
});
Методы pay и payWith принимают сумму платежа в минимальной единице валюты, используемой вашим приложением. Например, если клиенты платят в долларах США, сумма должна указываться в центах.
#Возврат платежей
Если вам нужно вернуть платёж Stripe, вы можете использовать метод refund. Этот метод принимает ID Stripe payment intent в качестве первого аргумента:
$payment = $user->charge(100, $paymentMethodId);
$user->refund($payment->id);
#Счёта
#Получение счетов
Вы можете легко получить массив счетов модели, используя метод invoices. Метод invoices возвращает коллекцию экземпляров Laravel\Cashier\Invoice:
$invoices = $user->invoices();
Если вы хотите включить в результаты ожидающие счета, используйте метод invoicesIncludingPending:
$invoices = $user->invoicesIncludingPending();
Вы можете использовать метод findInvoice для получения конкретного счёта по его ID:
$invoice = $user->findInvoice($invoiceId);
#Отображение информации о счёте
При выводе списка счетов клиента вы можете использовать методы счёта для отображения соответствующей информации. Например, можно вывести все счета в таблице, чтобы пользователь мог легко скачать любой из них:
<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>
#Предстоящие счета
Чтобы получить предстоящий счёт для клиента, используйте метод upcomingInvoice:
$invoice = $user->upcomingInvoice();
Аналогично, если у клиента несколько подписок, вы можете получить предстоящий счёт для конкретной подписки:
$invoice = $user->subscription('default')->upcomingInvoice();
#Предварительный просмотр счетов подписки
С помощью метода previewInvoice вы можете предварительно просмотреть счёт перед изменением цены. Это позволит понять, как будет выглядеть счёт клиента после изменения цены:
$invoice = $user->subscription('default')->previewInvoice('price_yearly');
Вы можете передать массив цен в метод previewInvoice, чтобы просмотреть счёт с несколькими новыми ценами:
$invoice = $user->subscription('default')->previewInvoice(['price_yearly', 'price_metered']);
#Генерация PDF счетов
Перед генерацией PDF счетов следует установить библиотеку Dompdf через Composer — это рендерер счетов по умолчанию в Cashier:
composer require dompdf/dompdf
В маршруте или контроллере вы можете использовать метод downloadInvoice для генерации PDF-счёта для скачивания. Метод автоматически сформирует правильный HTTP-ответ для скачивания счёта:
use Illuminate\Http\Request;
Route::get('/user/invoice/{invoice}', function (Request $request, string $invoiceId) {
return $request->user()->downloadInvoice($invoiceId);
});
По умолчанию все данные в счёте берутся из информации о клиенте и счёте, хранящихся в Stripe. Имя файла формируется на основе значения app.name в конфиге. Однако вы можете настроить некоторые данные, передав массив вторым аргументом в метод downloadInvoice. Этот массив позволяет указать информацию о вашей компании и продукте:
return $request->user()->downloadInvoice($invoiceId, [
'vendor' => 'Your Company',
'product' => 'Your Product',
'street' => 'Main Str. 1',
'location' => '2000 Antwerp, Belgium',
'phone' => '+32 499 00 00 00',
'email' => 'info@example.com',
'url' => 'https://example.com',
'vendorVat' => 'BE123456789',
]);
Метод downloadInvoice также позволяет указать имя файла через третий аргумент. К имени автоматически добавится суффикс .pdf:
return $request->user()->downloadInvoice($invoiceId, [], 'my-invoice');
#Кастомный рендерер счетов
Cashier также позволяет использовать кастомный рендерер счетов. По умолчанию используется реализация DompdfInvoiceRenderer, которая использует PHP-библиотеку dompdf для генерации PDF счетов Cashier. Однако вы можете использовать любой рендерер, реализовав интерфейс Laravel\Cashier\Contracts\InvoiceRenderer. Например, вы можете генерировать PDF счёт через API стороннего сервиса:
use Illuminate\Support\Facades\Http;
use Laravel\Cashier\Contracts\InvoiceRenderer;
use Laravel\Cashier\Invoice;
class ApiInvoiceRenderer implements InvoiceRenderer
{
/**
* Сгенерировать данный счёт и вернуть сырые байты PDF.
*/
public function render(Invoice $invoice, array $data = [], array $options = []): string
{
$html = $invoice->view($data)->render();
return Http::get('https://example.com/html-to-pdf', ['html' => $html])->get()->body();
}
}
После реализации контракта рендерера счетов обновите значение конфигурации cashier.invoices.renderer в файле config/cashier.php вашего приложения. Укажите там имя класса вашей реализации рендерера.
#Checkout
Cashier Stripe также поддерживает Stripe Checkout. Stripe Checkout упрощает создание страниц оплаты, предоставляя готовую хостинговую страницу для приёма платежей.
В следующей документации описано, как начать использовать Stripe Checkout с Cashier. Чтобы узнать больше о Stripe Checkout, рекомендуем ознакомиться с официальной документацией Stripe по Checkout.
#Оплата продуктов через Checkout
Вы можете выполнить оплату существующего продукта, созданного в панели Stripe, используя метод checkout у модели с биллингом. Метод checkout инициирует новую сессию Stripe Checkout. По умолчанию требуется передать ID цены Stripe:
use Illuminate\Http\Request;
Route::get('/product-checkout', function (Request $request) {
return $request->user()->checkout('price_tshirt');
});
При необходимости вы можете указать количество продукта:
use Illuminate\Http\Request;
Route::get('/product-checkout', function (Request $request) {
return $request->user()->checkout(['price_tshirt' => 15]);
});
Когда клиент посещает этот маршрут, его перенаправляют на страницу Stripe Checkout. По умолчанию после успешной оплаты или отмены покупки пользователя перенаправляют на маршрут home, но вы можете указать свои URL для успешного и отменённого платежа через опции success_url и cancel_url:
use Illuminate\Http\Request;
Route::get('/product-checkout', function (Request $request) {
return $request->user()->checkout(['price_tshirt' => 1], [
'success_url' => route('your-success-route'),
'cancel_url' => route('your-cancel-route'),
]);
});
При определении опции success_url вы можете указать Stripe добавить ID сессии Checkout в качестве параметра строки запроса. Для этого добавьте литерал {CHECKOUT_SESSION_ID} в строку запроса success_url. Stripe заменит этот плейсхолдер на реальный ID сессии:
use Illuminate\Http\Request;
use Stripe\Checkout\Session;
use Stripe\Customer;
Route::get('/product-checkout', function (Request $request) {
return $request->user()->checkout(['price_tshirt' => 1], [
'success_url' => route('checkout-success').'?session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => route('checkout-cancel'),
]);
});
Route::get('/checkout-success', function (Request $request) {
$checkoutSession = $request->user()->stripe()->checkout->sessions->retrieve($request->get('session_id'));
return view('checkout.success', ['checkoutSession' => $checkoutSession]);
})->name('checkout-success');
#Промокоды
По умолчанию Stripe Checkout не разрешает промокоды, которые может использовать пользователь. К счастью, их легко включить для вашей страницы Checkout. Для этого вызовите метод allowPromotionCodes:
use Illuminate\Http\Request;
Route::get('/product-checkout', function (Request $request) {
return $request->user()
->allowPromotionCodes()
->checkout('price_tshirt');
});
#Одноразовые оплаты через Checkout
Вы также можете выполнить простой платёж за продукт, который не создан в вашей панели Stripe. Для этого используйте метод checkoutCharge у модели с биллингом, передав сумму, название продукта и необязательное количество. При посещении этого маршрута клиента перенаправят на страницу Stripe Checkout:
use Illuminate\Http\Request;
Route::get('/charge-checkout', function (Request $request) {
return $request->user()->checkoutCharge(1200, 'T-Shirt', 5);
});
При использовании метода checkoutCharge Stripe всегда создаёт новый продукт и цену в вашей панели Stripe. Поэтому рекомендуется создавать продукты заранее в панели Stripe и использовать метод checkout.
#Оплата подписок через Checkout
Для использования Stripe Checkout с подписками необходимо включить вебхук customer.subscription.created в вашей панели Stripe. Этот вебхук создаст запись подписки в базе данных и сохранит все связанные элементы подписки.
Вы также можете использовать Stripe Checkout для инициации подписок. После определения подписки с помощью методов билдера подписок Cashier вызовите метод checkout. При посещении этого маршрута клиента перенаправят на страницу Stripe Checkout:
use Illuminate\Http\Request;
Route::get('/subscription-checkout', function (Request $request) {
return $request->user()
->newSubscription('default', 'price_monthly')
->checkout();
});
Так же, как и при оформлении покупки продуктов, вы можете настроить URL-адреса для успешного завершения и отмены:
use Illuminate\Http\Request;
Route::get('/subscription-checkout', function (Request $request) {
return $request->user()
->newSubscription('default', 'price_monthly')
->checkout([
'success_url' => route('your-success-route'),
'cancel_url' => route('your-cancel-route'),
]);
});
Конечно, вы также можете включить промокоды для оформления подписок:
use Illuminate\Http\Request;
Route::get('/subscription-checkout', function (Request $request) {
return $request->user()
->newSubscription('default', 'price_monthly')
->allowPromotionCodes()
->checkout();
});
К сожалению, Stripe Checkout не поддерживает все варианты выставления счетов для подписок при их создании. Использование метода anchorBillingCycleOn на билдере подписки, настройка поведения прореагирования или поведения оплаты не будут иметь эффекта во время сессий Stripe Checkout. Пожалуйста, ознакомьтесь с документацией Stripe Checkout Session API, чтобы узнать, какие параметры доступны.
#Stripe Checkout и пробные периоды
Разумеется, вы можете задать пробный период при создании подписки, которая будет оформлена через Stripe Checkout:
$checkout = Auth::user()->newSubscription('default', 'price_monthly')
->trialDays(3)
->checkout();
Однако пробный период должен быть не менее 48 часов — это минимальное время пробного периода, поддерживаемое Stripe Checkout.
#Подписки и вебхуки
Помните, что Stripe и Cashier обновляют статусы подписок через вебхуки, поэтому возможно, что подписка еще не будет активна, когда клиент вернется в приложение после ввода платежных данных. Чтобы обработать такую ситуацию, вы можете вывести сообщение, информирующее пользователя о том, что его платеж или подписка находятся в ожидании.
#Сбор налоговых идентификаторов
Checkout также поддерживает сбор налогового идентификатора клиента. Чтобы включить это в сессии оформления, вызовите метод collectTaxIds при создании сессии:
$checkout = $user->collectTaxIds()->checkout('price_tshirt');
При вызове этого метода клиенту будет доступен новый флажок, позволяющий указать, что покупка осуществляется от имени компании. В этом случае у него появится возможность ввести свой налоговый идентификатор.
Если вы уже настроили автоматический сбор налогов в сервис-провайдере вашего приложения, то эта функция будет включена автоматически, и вызов метода collectTaxIds не требуется.
#Оформление покупок гостями
С помощью метода Checkout::guest вы можете инициировать сессии оформления для гостей вашего приложения, у которых нет «учетной записи»:
use Illuminate\Http\Request;
use Laravel\Cashier\Checkout;
Route::get('/product-checkout', function (Request $request) {
return Checkout::guest()->create('price_tshirt', [
'success_url' => route('your-success-route'),
'cancel_url' => route('your-cancel-route'),
]);
});
Аналогично созданию сессий оформления для существующих пользователей, вы можете использовать дополнительные методы, доступные в экземпляре Laravel\Cashier\CheckoutBuilder, чтобы настроить сессию оформления для гостя:
use Illuminate\Http\Request;
use Laravel\Cashier\Checkout;
Route::get('/product-checkout', function (Request $request) {
return Checkout::guest()
->withPromotionCode('promo-code')
->create('price_tshirt', [
'success_url' => route('your-success-route'),
'cancel_url' => route('your-cancel-route'),
]);
});
После завершения оформления гостем Stripe может отправить событие вебхука checkout.session.completed, поэтому убедитесь, что вы настроили вебхук Stripe для отправки этого события в ваше приложение. После включения вебхука в панели Stripe вы можете обработать вебхук с помощью Cashier. Объект, содержащийся в полезной нагрузке вебхука, будет checkout объектом, который вы можете проверить для выполнения заказа вашего клиента.
#Обработка неудачных платежей
Иногда платежи за подписки или одноразовые платежи могут не пройти. В таких случаях Cashier выбросит исключение Laravel\Cashier\Exceptions\IncompletePayment, информирующее вас об этом. После перехвата этого исключения у вас есть два варианта действий.
Во-первых, вы можете перенаправить клиента на специальную страницу подтверждения платежа, которая включена в Cashier. Эта страница уже имеет зарегистрированный именованный маршрут через сервис-провайдер Cashier. Таким образом, вы можете перехватить исключение IncompletePayment и перенаправить пользователя на страницу подтверждения платежа:
use Laravel\Cashier\Exceptions\IncompletePayment;
try {
$subscription = $user->newSubscription('default', 'price_monthly')
->create($paymentMethod);
} catch (IncompletePayment $exception) {
return redirect()->route(
'cashier.payment',
[$exception->payment->id, 'redirect' => route('home')]
);
}
На странице подтверждения платежа клиенту будет предложено повторно ввести данные кредитной карты и выполнить дополнительные действия, требуемые Stripe, например, подтверждение "3D Secure". После подтверждения платежа пользователь будет перенаправлен на URL, указанный в параметре redirect. При перенаправлении к URL будут добавлены переменные строки запроса message (строка) и success (целое число). В настоящее время страница платежа поддерживает следующие типы платежных методов:
- Кредитные карты
- Alipay
- Bancontact
- BECS Direct Debit
- EPS
- Giropay
- iDEAL
- SEPA Direct Debit
В качестве альтернативы вы можете позволить Stripe самостоятельно обрабатывать подтверждение платежа. В этом случае вместо перенаправления на страницу подтверждения платежа вы можете настроить автоматические письма о выставлении счетов Stripe в панели Stripe. Однако, если будет перехвачено исключение IncompletePayment, вы все равно должны уведомить пользователя, что он получит письмо с дальнейшими инструкциями по подтверждению платежа.
Исключения платежей могут быть выброшены для следующих методов: charge, invoiceFor и invoice на моделях с использованием трейта Billable. При работе с подписками методы create на SubscriptionBuilder, а также incrementAndInvoice и swapAndInvoice на моделях Subscription и SubscriptionItem могут выбрасывать исключения неполного платежа.
Определить, есть ли у существующей подписки неполный платеж, можно с помощью метода hasIncompletePayment на биллабельной модели или экземпляре подписки:
if ($user->hasIncompletePayment('default')) {
// ...
}
if ($user->subscription('default')->hasIncompletePayment()) {
// ...
}
Вы можете узнать конкретный статус неполного платежа, проверив свойство payment в экземпляре исключения:
use Laravel\Cashier\Exceptions\IncompletePayment;
try {
$user->charge(1000, 'pm_card_threeDSecure2Required');
} catch (IncompletePayment $exception) {
// Получить статус платежного намерения...
$exception->payment->status;
// Проверить конкретные условия...
if ($exception->payment->requiresPaymentMethod()) {
// ...
} elseif ($exception->payment->requiresConfirmation()) {
// ...
}
}
#Подтверждение платежей
Некоторые методы оплаты требуют дополнительных данных для подтверждения платежей. Например, методы SEPA требуют дополнительных данных "мандата" во время процесса оплаты. Вы можете передать эти данные в Cashier с помощью метода withPaymentConfirmationOptions:
$subscription->withPaymentConfirmationOptions([
'mandate_data' => '...',
])->swap('price_xxx');
Вы можете ознакомиться с документацией Stripe API, чтобы узнать все параметры, принимаемые при подтверждении платежей.
#Строгая аутентификация клиента (SCA)
Если ваш бизнес или один из ваших клиентов находится в Европе, вам необходимо соблюдать правила ЕС по строгой аутентификации клиента (SCA). Эти правила были введены в сентябре 2019 года Европейским союзом для предотвращения мошенничества с платежами. К счастью, Stripe и Cashier готовы к созданию приложений, соответствующих требованиям SCA.
Перед началом ознакомьтесь с руководством Stripe по PSD2 и SCA, а также с их документацией по новым API SCA.
#Платежи, требующие дополнительного подтверждения
Правила SCA часто требуют дополнительной проверки для подтверждения и обработки платежа. В таких случаях Cashier выбрасывает исключение Laravel\Cashier\Exceptions\IncompletePayment, информирующее вас о необходимости дополнительной проверки. Дополнительную информацию о том, как обрабатывать эти исключения, можно найти в разделе документации о обработке неудачных платежей.
Экраны подтверждения платежей, предоставляемые Stripe или Cashier, могут быть адаптированы под конкретный банк или эмитента карты и включать дополнительное подтверждение карты, временный небольшой платеж, отдельную аутентификацию устройства или другие формы проверки.
#Состояния incomplete и past_due
Когда платеж требует дополнительного подтверждения, подписка остается в состоянии incomplete или past_due, что отражается в столбце stripe_status базы данных. Cashier автоматически активирует подписку клиента, как только подтверждение платежа будет завершено и ваше приложение получит уведомление от Stripe через вебхук.
Для получения дополнительной информации о состояниях incomplete и past_due обратитесь к нашей дополнительной документации по этим состояниям.
#Уведомления о платежах вне сессии
Поскольку правила SCA требуют, чтобы клиенты время от времени подтверждали свои платежные данные даже при активной подписке, Cashier может отправлять уведомления клиенту, когда требуется подтверждение платежа вне сессии. Например, это может происходить при продлении подписки. Уведомления о платежах Cashier можно включить, установив переменную окружения CASHIER_PAYMENT_NOTIFICATION в класс уведомления. По умолчанию это уведомление отключено. Разумеется, Cashier включает класс уведомления для этой цели, но вы можете использовать собственный класс уведомления, если хотите:
CASHIER_PAYMENT_NOTIFICATION=Laravel\Cashier\Notifications\ConfirmPayment
Чтобы гарантировать доставку уведомлений о подтверждении платежей вне сессии, убедитесь, что вебхуки Stripe настроены для вашего приложения и что вебхук invoice.payment_action_required включен в панели Stripe. Кроме того, ваша модель Billable должна использовать трейт Laravel Illuminate\Notifications\Notifiable.
Уведомления будут отправляться даже тогда, когда клиенты вручную выполняют платеж, требующий дополнительного подтверждения. К сожалению, Stripe не может определить, был ли платеж выполнен вручную или «вне сессии». Однако клиент увидит сообщение «Платеж успешен», если посетит страницу платежа после подтверждения платежа. Клиент не сможет случайно подтвердить один и тот же платеж дважды и получить двойное списание.
#Stripe SDK
Многие объекты Cashier являются обертками над объектами Stripe SDK. Если вы хотите взаимодействовать с объектами Stripe напрямую, вы можете удобно получить их с помощью метода asStripe:
$stripeSubscription = $subscription->asStripeSubscription();
$stripeSubscription->application_fee_percent = 5;
$stripeSubscription->save();
Вы также можете использовать метод updateStripeSubscription для прямого обновления подписки Stripe:
$subscription->updateStripeSubscription(['application_fee_percent' => 5]);
Вы можете вызвать метод stripe у класса Cashier, если хотите использовать клиент Stripe\StripeClient напрямую. Например, вы можете использовать этот метод для доступа к экземпляру StripeClient и получения списка цен из вашего аккаунта Stripe:
use Laravel\Cashier\Cashier;
$prices = Cashier::stripe()->prices->all();
#Тестирование
При тестировании приложения, использующего Cashier, вы можете мокировать реальные HTTP-запросы к API Stripe; однако для этого потребуется частично реализовать поведение Cashier самостоятельно. Поэтому мы рекомендуем позволять вашим тестам обращаться к реальному API Stripe. Хотя это медленнее, это дает больше уверенности в том, что ваше приложение работает как ожидается, а медленные тесты можно сгруппировать в отдельную группу PHPUnit.
При тестировании помните, что у Cashier уже есть отличный набор тестов, поэтому вам следует сосредоточиться только на тестировании логики подписок и платежей вашего приложения, а не на каждом внутреннем поведении Cashier.
Для начала добавьте тестовую версию вашего секретного ключа Stripe в файл phpunit.xml:
<env name="STRIPE_SECRET" value="sk_test_<your-key>"/>
Теперь при взаимодействии с Cashier во время тестирования будут отправляться реальные API-запросы в тестовую среду Stripe. Для удобства рекомендуется заранее заполнить ваш тестовый аккаунт Stripe подписками и ценами, которые вы будете использовать в тестах.
Чтобы протестировать различные сценарии выставления счетов, такие как отказ в оплате по кредитной карте и ошибки, вы можете использовать широкий набор тестовых номеров карт и токенов, предоставляемых Stripe.