Laravel Cashier (Stripe)
简介
Laravel Cashier Stripe 为 Stripe's 订阅计费服务提供了一个富有表现力、流畅的界面。它可以处理几乎所有你不想手写的样板订阅计费代码。除了基本的订阅管理之外,Cashier 还可以处理优惠券、交换订阅、订阅「数量」、取消宽限期,甚至生成发票 PDF。
升级 Cashier
升级到新版本的 Cashier 时,请务必仔细查看 升级指南。
WARNING
为了防止重大更改,Cashier 使用固定的 Stripe API 版本。 Cashier 16 使用 Stripe API 版本 2025-06-30.basil。 Stripe API 版本将在次要版本中更新,以便利用新的 Stripe 功能和改进。
安装
首先,使用 Composer 包管理器安装 Stripe 的 Cashier 包:
composer require laravel/cashier安装包后,使用 vendor:publish Artisan 命令发布 Cashier 的迁移:
php artisan vendor:publish --tag="cashier-migrations"然后,迁移你的数据库:
php artisan migrateCashier 的迁移将向你的 users 表添加几列。同时还会创建一个新的 subscriptions 表来保存所有客户的订阅,并创建一个 subscription_items 表来保存具有多个价格的订阅。
如果你愿意,你还可以使用 vendor:publish Artisan 命令发布 Cashier 的配置文件:
php artisan vendor:publish --tag="cashier-config"最后,为了确保 Cashier 正确处理所有 Stripe 事件,请记住 configure Cashier's Webhook 处理。
WARNING
Stripe 建议用于存储 Stripe 标识符的任何列都应区分大小写。因此,在使用 MySQL 时,应确保将 stripe_id 列的列排序规则设置为 utf8_bin。有关这方面的更多信息可以在 Stripe 文档 中找到。
配置
可计费模型
在使用 Cashier 之前,请将 Billable 特征添加到你的计费模型定义中。通常,这将是 App\Models\User 模型。此特征提供了各种方法来允许你执行常见的计费任务,例如创建订阅、应用优惠券和更新付款方式信息:
use Laravel\Cashier\Billable;
class User extends Authenticatable
{
use Billable;
}Cashier 假设你的计费模型将是 Laravel 附带的 App\Models\User 类。如果你想更改此设置,可以通过 useCustomerModel 方法指定不同的模型。通常应在 AppServiceProvider 类的 boot 方法中调用此方法:
use App\Models\Cashier\User;
use Laravel\Cashier\Cashier;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Cashier::useCustomerModel(User::class);
}WARNING
如果你使用的模型不是 Laravel 提供的 App\Models\User 模型,则需要发布并更改提供的 Cashier migrations 以匹配你的替代模型的表名称。
API 密钥
接下来,你应该在应用程序的 .env 文件中配置 Stripe API 密钥。你可以从 Stripe 控制面板检索你的 Stripe API 密钥:
STRIPE_KEY=your-stripe-key
STRIPE_SECRET=your-stripe-secret
STRIPE_WEBHOOK_SECRET=your-stripe-webhook-secretWARNING
你应该确保在应用程序的 .env 文件中定义 STRIPE_WEBHOOK_SECRET 环境变量,因为此变量用于确保传入的 Webhook 实际上来自 Stripe。
货币配置
默认出纳货币为美元 (USD)。你可以通过在应用程序的 .env 文件中设置 CASHIER_CURRENCY 环境变量来更改默认货币:
CASHIER_CURRENCY=eur除了配置Cashier货币之外,你还可以指定在格式化货币值以在发票上显示时使用的区域设置。在内部,Cashier 使用 PHP's NumberFormatter class 来设置货币区域设置:
CASHIER_CURRENCY_LOCALE=nl_BEWARNING
为了使用 en 以外的区域设置,请确保在你的服务器上安装并配置了 ext-intl PHP 扩展。
税务配置
借助 Stripe Tax,可以自动计算 Stripe 生成的所有发票的税费。你可以通过调用应用程序 App\Providers\AppServiceProvider 类的 boot 方法中的 calculateTaxes 方法来启用自动税收计算:
use Laravel\Cashier\Cashier;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Cashier::calculateTaxes();
}启用税务计算后,任何新订阅和生成的任何一次性发票都将收到自动税务计算。
为了使此功能正常工作,你的客户的账单详细信息(例如客户的姓名、地址和税号)需要同步到 Stripe。你可以使用 Cashier 提供的 customer data synchronization 和 Tax ID 方法来完成此操作。
日志
Cashier 允许你指定记录严重 Stripe 错误时要使用的日志通道。你可以通过在应用程序的 .env 文件中定义 CASHIER_LOGGER 环境变量来指定日志通道:
CASHIER_LOGGER=stack对 Stripe 的 API 调用生成的异常将通过应用程序的默认日志通道记录。
使用自定义模型
你可以通过定义自己的模型并扩展相应的 Cashier 模型来自由扩展 Cashier 内部使用的模型:
use Laravel\Cashier\Subscription as CashierSubscription;
class Subscription extends CashierSubscription
{
// ...
}定义模型后,你可以通过 Laravel\Cashier\Cashier 类指示 Cashier 使用你的自定义模型。通常,你应该在应用程序的 App\Providers\AppServiceProvider 类的 boot 方法中告知 Cashier 有关你的自定义模型的信息:
use App\Models\Cashier\Subscription;
use App\Models\Cashier\SubscriptionItem;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Cashier::useSubscriptionModel(Subscription::class);
Cashier::useSubscriptionItemModel(SubscriptionItem::class);
}快速入门
销售产品
INFO
在使用 Stripe Checkout 之前,你应该在 Stripe 仪表板中定义具有固定价格的产品。此外,你应该configure Cashier's Webhook 处理。
通过应用程序提供产品和订阅计费可能会令人生畏。然而,借助 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', 'checkout.success')->name('checkout-success');
Route::view('/checkout/cancel', 'checkout.cancel')->name('checkout-cancel');正如你在上面的示例中看到的,我们将利用 Cashier 提供的 Checkout 方法将客户重定向到 Stripe Checkout 以获取给定的「价格标识符」。使用 Stripe 时,「价格」指的是 defined prices for specific products。
If necessary, the Checkout method will automatically create a customer in Stripe and connect that Stripe customer record to the corresponding user in your application's database. After completing the Checkout session, the customer will be redirected to a dedicated success or cancellation page where you can display an informational message to the customer.
向 Stripe Checkout 提供元数据
销售产品时,通常通过你自己的应用程序定义的 Cart 和 Order 模型来跟踪已完成的订单和购买的产品。将客户重定向到 Stripe Checkout 来完成购买时,你可能需要提供现有订单标识符,以便在客户重定向回你的应用程序时,你可以将已完成的购买与相应的订单关联起来。
为此,你可以向 Checkout 方法提供 metadata 数组。假设当用户开始结帐流程时,我们的应用程序中会创建一个待处理的 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');正如你在上面的示例中看到的,当用户开始结帐流程时,我们将向 Checkout 方法提供所有购物车/订单的关联 Stripe 价格标识符。当然,你的应用程序负责将这些商品与「购物车」或客户添加的订单相关联。我们还通过 metadata 数组向 Stripe Checkout 会话提供订单 ID。最后,我们将 CHECKOUT_SESSION_ID 模板变量添加到结帐成功路由中。当 Stripe 将客户重定向回你的应用程序时,此模板变量将自动填充结帐会话 ID。
接下来,让我们构建 Checkout 成功路线。这是用户通过 Stripe 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');有关 data contained by the Checkout session object 的更多信息,请参阅 Stripe 的文档。
销售订阅
INFO
在使用 Stripe Checkout 之前,你应该在 Stripe 仪表板中定义具有固定价格的产品。此外,你应该configure Cashier's Webhook 处理。
通过应用程序提供产品和订阅计费可能会令人生畏。然而,借助 Cashier 和 Stripe Checkout,你可以轻松构建现代、强大的支付集成。
要了解如何使用 Cashier 和 Stripe Checkout 销售订阅,我们考虑一下具有基本月度 (price_basic_monthly) 和年度 (price_basic_yearly) 计划的订阅服务的简单场景。在我们的 Stripe 仪表板中,这两个价格可以分组在「基本」产品 (pro_basic) 下。此外,我们的订阅服务可能会提供专家计划,如 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 会话,这将允许他们订阅我们的基本计划。成功结帐或取消后,客户将被重定向回我们提供给 Checkout 方法的 URL。要了解他们的订阅何时实际开始(因为某些付款方式需要几秒钟的处理时间),我们还需要 configure Cashier's Webhook 处理。
现在客户可以开始订阅,我们需要限制应用程序的某些部分,以便只有订阅的用户才能访问它们。当然,我们始终可以通过 Cashier 的 Billable 特征提供的 subscribed 方法来确定用户当前的订阅状态:
@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 来确定传入请求是否来自订阅用户。一旦定义了这个中间件,你可以轻松地将其分配给一个路由,以防止未订阅的用户访问该路由:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class Subscribed
{
/**
* Handle an incoming request.
*/
public function handle(Request $request, Closure $next): Response
{
if (! $request->user()?->subscribed()) {
// Redirect user to billing page and ask them to subscribe...
return redirect('/billing');
}
return $next($request);
}
}定义中间件后,你可以将其分配给路由:
use App\Http\Middleware\Subscribed;
Route::get('/dashboard', function () {
// ...
})->middleware([Subscribed::class]);允许客户管理计费方案
当然,客户可能希望将其订阅计划更改为其他产品或「级别」。最简单的方法是引导客户访问 Stripe 的 Customer Billing Portal,它提供托管用户界面,允许客户下载发票、更新付款方式以及更改订阅计划。
首先,在你的应用程序中定义一个链接或按钮,将用户引导至 Laravel 路由,我们将利用该路由来启动 Billing Portal 会话:
<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');INFO
只要你配置了 Cashier 的 Webhook 处理,Cashier 就会通过检查来自 Stripe 的传入 Webhook,自动保持应用程序的 Cashier 相关数据库表同步。因此,例如,当用户通过 Stripe 的客户计费门户取消订阅时,Cashier 将收到相应的 Webhook 并在应用程序的数据库中将订阅标记为「已取消」。
客户
获取客户
你可以使用 Cashier::findBillable 方法通过 Stripe ID 检索客户。此方法将返回计费模型的实例:
use Laravel\Cashier\Cashier;
$user = Cashier::findBillable($stripeId);创建客户
有时,你可能希望在不开始订阅的情况下创建 Stripe 客户。你可以使用 createAsStripeCustomer 方法来完成此操作:
$stripeCustomer = $user->createAsStripeCustomer();在 Stripe 中创建客户后,你可以稍后开始订阅。你可以提供可选的 $options 数组来传入任何其他 customer creation parameters that are supported by the Stripe API:
$stripeCustomer = $user->createAsStripeCustomer($options);如果你想返回计费模型的 Stripe 客户对象,你可以使用 asStripeCustomer 方法:
$stripeCustomer = $user->asStripeCustomer();如果你想要检索给定计费模型的 Stripe 客户对象,但不确定该计费模型是否已经是 Stripe 内的客户,则可以使用 createOrGetStripeCustomer 方法。如果 Stripe 中尚不存在,此方法将创建一个新客户:
$stripeCustomer = $user->createOrGetStripeCustomer();更新客户
有时,你可能希望直接向 Stripe 客户更新附加信息。你可以使用 updateStripeCustomer 方法来完成此操作。此方法接受 customer update options supported by the Stripe API 数组:
$stripeCustomer = $user->updateStripeCustomer($options);余额
Stripe 允许你贷记或借记客户的「余额」。稍后,该余额将记入新发票的贷方或借方。要检查客户的总余额,你可以使用你的计费型号上提供的 balance 方法。 balance 方法将返回以客户货币表示的余额的格式化字符串:
$balance = $user->balance();要记入客户的余额,你可以向 creditBalance 方法提供一个值。如果你愿意,你还可以提供描述:
$user->creditBalance(500, 'Premium customer top-up.');向 debitBalance 方法提供值将从客户的余额中扣除:
$user->debitBalance(300, 'Bad usage penalty.');applyBalance 方法将为客户创建新的客户余额交易。你可以使用 balanceTransactions 方法检索这些交易记录,这对于提供贷项和借项日志供客户查看可能很有用:
// Retrieve all transactions...
$transactions = $user->balanceTransactions();
foreach ($transactions as $transaction) {
// Transaction amount...
$amount = $transaction->amount(); // $2.31
// Retrieve the related invoice when available...
$invoice = $transaction->invoice();
}税号
Cashier 提供了一种管理客户税号的简单方法。例如,taxIds 方法可用于检索作为集合分配给客户的所有 tax IDs:
$taxIds = $user->taxIds();你还可以通过标识符检索客户的特定税号:
$taxId = $user->findTaxId('txi_belgium');你可以通过向 createTaxId 方法提供有效的 type 和值来创建新的税号:
$taxId = $user->createTaxId('eu_vat', 'BE0123456789');createTaxId 方法会立即将增值税 ID 添加到客户的帐户中。 Verification of VAT IDs is also done by Stripe;然而,这是一个异步过程。你可以通过订阅 customer.tax_id.updated Webhook 事件并检查 the VAT IDs PH2PH parameter 来收到验证更新通知。有关处理 Webhooks 的更多信息,请参阅 documentation on defining Webhook handlers。
你可以使用 deleteTaxId 方法删除税号:
$user->deleteTaxId('txi_belgium');与 Stripe 同步客户数据
通常,当你的应用程序的用户更新他们的姓名、电子邮件地址或 Stripe 也存储的其他信息时,你应该将更新通知 Stripe。通过这样做,Stripe 的信息副本将与你的应用程序同步。
要自动执行此操作,你可以在计费模型上定义一个事件侦听器,该事件侦听器对模型的 updated 事件做出反应。然后,在事件侦听器中,你可以调用模型上的 syncStripeCustomerDetails 方法:
use App\Models\User;
use function Illuminate\Events\queueable;
/**
* The "booted" method of the model.
*/
protected static function booted(): void
{
static::updated(queueable(function (User $customer) {
if ($customer->hasStripeId()) {
$customer->syncStripeCustomerDetails();
}
}));
}现在,每当客户模型更新时,其信息都会与 Stripe 同步。为方便起见,Cashier 也会在首次创建客户时自动将其信息与 Stripe 同步。
你可以通过覆盖 Cashier 提供的各种方法来自定义用于将客户信息同步到 Stripe 的列。例如,你可以重写 StripeName 方法来自定义当 Cashier 将客户信息同步到 Stripe 时应被视为客户「姓名」的属性:
/**
* Get the customer name that should be synced to Stripe.
*/
public function stripeName(): string|null
{
return $this->company_name;
}同样,你可以覆盖 StripeEmail、StripePhone(最多 20 个字符)、StripeAddress 和 StripePreferredLocales 方法。这些方法将在 updating the Stripe customer object 时将信息同步到相应的客户参数。如果你希望完全控制客户信息同步过程,你可以覆盖 syncStripeCustomerDetails 方法。
计费门户
Stripe 提供 an easy way to set up a billing portal,以便你的客户可以管理他们的订阅、付款方式并查看他们的账单历史记录。你可以通过从控制器或路由调用计费模型上的 redirectToBillingPortal 方法将用户重定向到计费门户:
use Illuminate\Http\Request;
Route::get('/billing-portal', function (Request $request) {
return $request->user()->redirectToBillingPortal();
});默认情况下,当用户完成订阅管理后,他们将能够通过 Stripe 计费门户中的链接返回到应用程序的 home 路径。你可以通过将 URL 作为参数传递给 redirectToBillingPortal 方法来提供用户应返回的自定义 URL:
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 获取其标识符。完成此操作的方式取决于你计划将该支付方式用于订阅还是单次扣款,因此我们将分别说明。
订阅的支付方式
当为订阅将来使用而存储客户信用卡信息时,必须使用 Stripe「Setup Intents」API 来安全收集客户的支付方式详情。「Setup Intent」向 Stripe 表明打算对该客户的支付方式扣款。Cashier 的 Billable trait 包含 createSetupIntent 方法,可轻松创建新的 Setup Intent。你应在将渲染收集客户支付方式表单的路由或控制器中调用此方法:
return view('update-payment-method', [
'intent' => $user->createSetupIntent()
]);创建 Setup Intent 并将其传递给视图后,应将其密钥附加到将收集支付方式的元素上。例如,可参考如下「更新支付方式」表单:
<input id="card-holder-name" type="text">
<!-- Stripe Elements Placeholder -->
<div id="card-element"></div>
<button id="card-button" data-secret="{{ $intent->client_secret }}">
Update Payment Method
</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 的 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) {
// Display "error.message" to the user...
} else {
// The card has been verified successfully...
}
});卡经 Stripe 验证后,可将得到的 setupIntent.payment_method 标识符传回 Laravel 应用并绑定到客户。该支付方式既可添加为新的支付方式,也可用于更新默认支付方式。你也可以立即用该支付方式标识符创建新订阅。
INFO
若想了解更多关于 Setup Intents 与收集客户支付详情的信息,请参阅 Stripe 提供的概述。
单次扣款的支付方式
当然,对客户支付方式进行单次扣款时,支付方式标识符只需使用一次。受 Stripe 限制,你不能将客户已存储的默认支付方式用于单次扣款。必须让客户通过 Stripe.js 库输入支付方式详情。例如,可参考如下表单:
<input id="card-holder-name" type="text">
<!-- Stripe Elements Placeholder -->
<div id="card-element"></div>
<button id="card-button">
Process Payment
</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 的 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) {
// Display "error.message" to the user...
} else {
// The card has been verified successfully...
}
});若卡验证成功,可将 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();WARNING
客户的默认付款方式只能用于开具发票和创建新订阅。由于 Stripe 的限制,它可能无法用于单次收费。
添加支付方式
要添加新的付款方式,你可以在计费模型上调用 addPaymentMethod 方法,并传递付款方式标识符:
$user->addPaymentMethod($paymentMethod);INFO
要了解如何检索付款方式标识符,请查看 payment method storage documentation。
删除支付方式
要删除付款方式,你可以在要删除的 Laravel\Cashier\PaymentMethod 实例上调用 delete 方法:
$paymentMethod->delete();deletePaymentMethod 方法将从计费模型中删除特定的付款方式:
$user->deletePaymentMethod('pm_visa');deletePaymentMethods 方法将删除计费模型的所有付款方式信息:
$user->deletePaymentMethods();默认情况下,此方法将删除每种类型的付款方式。要删除特定类型的付款方式,你可以将 type 作为参数传递给该方法:
$user->deletePaymentMethods('sepa_debit');WARNING
如果用户有有效订阅,你的应用程序不应允许他们删除默认付款方式。
订阅
订阅提供了一种为客户设置定期付款的方法。由 Cashier 管理的 Stripe 订阅提供多种订阅价格、订阅数量、试用等支持。
创建订阅
要创建订阅,请首先检索计费模型的实例,该实例通常是 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 方法接受 a Stripe payment method identifier 或 Stripe PaymentMethod 对象,将开始订阅并使用可计费模型的 Stripe 客户 ID 和其他相关计费信息更新你的数据库。
WARNING
将付款方式标识符直接传递给 create 订阅方式也会自动将其添加到用户存储的付款方式中。
通过发票邮件收取周期性付款
你可以指示 Stripe 在每次定期付款到期时通过电子邮件向客户发送发票,而不是自动收取客户的定期付款。然后,客户可以在收到发票后手动支付发票。通过发票收取定期付款时,客户无需预先提供付款方式:
$user->newSubscription('default', 'price_monthly')->createAndSendInvoice();客户在取消订阅之前必须支付发票的时间由 days_until_due 选项决定。默认情况下,这是 30 天;但是,如果你愿意,你可以为此选项提供特定值:
$user->newSubscription('default', 'price_monthly')->createAndSendInvoice([], [
'days_until_due' => 30
]);数量
如果你想在创建订阅时设置特定的 quantity 价格,则应在创建订阅之前调用订阅构建器上的 quantity 方法:
$user->newSubscription('default', 'price_monthly')
->quantity(5)
->create($paymentMethod);附加详情
如果你想指定 Stripe 支持的其他 customer 或 subscription 选项,你可以通过将它们作为第二个和第三个参数传递给 create 方法来实现:
$user->newSubscription('default', 'price_monthly')->create($paymentMethod, [
'email' => $email,
], [
'metadata' => ['note' => 'Some extra information.'],
]);优惠券
如果你想在创建订阅时应用优惠券,可以使用 withCoupon 方法:
$user->newSubscription('default', 'price_monthly')
->withCoupon('code')
->create($paymentMethod);或者,如果你想应用 Stripe promotion code,你可以使用 withPromotionCode 方法:
$user->newSubscription('default', 'price_monthly')
->withPromotionCode('promo_code_id')
->create($paymentMethod);给定的促销代码 ID 应是分配给促销代码的 Stripe API ID,而不是面向客户的促销代码。如果你需要根据给定的面向客户的促销代码查找促销代码 ID,你可以使用 findPromotionCode 方法:
// Find a promotion code ID by its customer facing code...
$promotionCode = $user->findPromotionCode('SUMMERSALE');
// Find an active promotion code ID by its customer facing code...
$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');请记住,你应该使用分配给促销代码的 Stripe API ID,而不是面向客户的促销代码。在给定时间只能将一张优惠券或促销代码应用于客户或订阅。
有关此主题的更多信息,请参阅有关 coupons 和 promotion codes 的 Stripe 文档。
添加订阅
如果你想向已有默认付款方式的客户添加订阅,你可以在订阅构建器上调用 add 方法:
use App\Models\User;
$user = User::find(1);
$user->newSubscription('default', 'price_monthly')->add();从 Stripe 控制台创建订阅
你还可以从 Stripe 仪表板本身创建订阅。执行此操作时,Cashier 将同步新添加的订阅并为其分配 default 类型。要自定义分配给仪表板创建的订阅的订阅类型,define Webhook event handlers。
此外,你只能通过 Stripe 仪表板创建一种类型的订阅。如果你的应用程序提供使用不同类型的多个订阅,则只能通过 Stripe 仪表板添加一种类型的订阅。
最后,你应该始终确保应用程序提供的每种订阅类型仅添加一个活动订阅。如果客户有两个 default 订阅,则 Cashier 只会使用最近添加的订阅,即使这两个订阅都会与你应用程序的数据库同步。
检查订阅状态
一旦客户订阅了你的应用程序,你就可以使用各种便捷的方法轻松检查他们的订阅状态。首先,如果客户有有效订阅,则 subscribed 方法会返回 true,即使该订阅当前处于试用期内。 subscribed 方法接受订阅的类型作为其第一个参数:
if ($user->subscribed('default')) {
// ...
}subscribed 方法也非常适合 route middleware,允许你根据用户的订阅状态过滤对路由和控制器的访问:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureUserIsSubscribed
{
/**
* Handle an incoming request.
*
* @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')) {
// This user is not a paying customer...
return redirect('/billing');
}
return $next($request);
}
}如果你想确定用户是否仍在试用期内,可以使用 onTrial 方法。此方法可用于确定是否应向用户显示警告,表明他们仍处于试用期:
if ($user->subscription('default')->onTrial()) {
// ...
}subscribedToProduct 方法可用于根据给定 Stripe 产品的标识符确定用户是否订阅了给定产品。在 Stripe 中,产品是价格的集合。在此示例中,我们将确定用户的 default 订阅是否主动订阅了应用程序的「高级」产品。给定的 Stripe 产品标识符应与 Stripe 仪表板中的产品标识符之一相对应:
if ($user->subscribedToProduct('prod_premium', 'default')) {
// ...
}通过将数组传递给 subscribedToProduct 方法,你可以确定用户的 default 订阅是否主动订阅应用程序的「基本」或「高级」产品:
if ($user->subscribedToProduct(['prod_basic', 'prod_premium'], 'default')) {
// ...
}subscribedToPrice 方法可用于确定客户的订阅是否对应于给定的价格 ID:
if ($user->subscribedToPrice('price_basic_monthly', 'default')) {
// ...
}recurring 方法可用于确定用户当前是否已订阅且不再处于试用期内:
if ($user->subscription('default')->recurring()) {
// ...
}WARNING
如果用户有两个相同类型的订阅,则 subscription 方法将始终返回最近的订阅。例如,用户可能有两条类型为 default 的订阅记录;然而,其中一个订阅可能是旧的、过期的订阅,而另一个是当前的、活动的订阅。最新的订阅将始终被返回,而较旧的订阅将保留在数据库中以供历史回顾。
已取消订阅状态
要确定用户是否曾经是活跃订阅者但已取消订阅,你可以使用 canceled 方法:
if ($user->subscription('default')->canceled()) {
// ...
}你还可以确定用户是否已取消订阅,但仍处于「宽限期」,直到订阅完全到期。例如,如果用户在 3 月 5 日取消了原定于 3 月 10 日到期的订阅,则该用户将处于「宽限期」直至 3 月 10 日。请注意,在此期间 subscribed 方法仍返回 true:
if ($user->subscription('default')->onGracePeriod()) {
// ...
}要确定用户是否已取消订阅并且不再处于「宽限期」内,你可以使用 ended 方法:
if ($user->subscription('default')->ended()) {
// ...
}未完成与逾期状态
如果订阅在创建后需要二次付款操作,则订阅将被标记为 incomplete。订阅状态存储在 Cashier 的 subscriptions 数据库表的 Stripe_status 列中。
同样,如果交换价格时需要二次付款操作,则订阅将被标记为 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 状态时仍被视为有效,你可以使用 Cashier 提供的 keepPastDueSubscriptionsActive 和 keepIncompleteSubscriptionsActive 方法。通常,应在 App\Providers\AppServiceProvider 的 register 方法中调用这些方法:
use Laravel\Cashier\Cashier;
/**
* Register any application services.
*/
public function register(): void
{
Cashier::keepPastDueSubscriptionsActive();
Cashier::keepIncompleteSubscriptionsActive();
}WARNING
当订阅处于 incomplete 状态时,在确认付款之前无法更改。因此,当订阅处于 incomplete 状态时,swap 和 updateQuantity 方法将引发异常。
订阅查询作用域
大多数订阅状态也可用作查询范围,以便你可以轻松地查询数据库以查找处于给定状态的订阅:
// Get all active subscriptions...
$subscriptions = Subscription::query()->active()->get();
// Get all of the canceled subscriptions for a user...
$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');如果客户正在试用,试用期将保持不变。此外,如果订阅存在「数量」,则该数量也将得到维护。
如果你想交换价格并取消客户当前所处的任何试用期,你可以调用 skipTrial 方法:
$user->subscription('default')
->skipTrial()
->swap('price_yearly');如果你想交换价格并立即向客户开具发票,而不是等待下一个结算周期,你可以使用 swapAndInvoice 方法:
$user = User::find(1);
$user->subscription('default')->swapAndInvoice('price_yearly');按比例计费
默认情况下,Stripe 在价格之间交换时按比例收取费用。 noProrate 方法可用于更新订阅价格,而不按比例分配费用:
$user->subscription('default')->noProrate()->swap('price_yearly');有关订阅按比例分配的更多信息,请参阅Stripe 文档。
WARNING
在 swapAndInvoice 方法之前执行 noProrate 方法不会对按比例分配产生影响。总会开具发票。
订阅数量
有时订阅会受到「数量」的影响。例如,项目管理应用程序可能对每个项目每月收取 10 美元的费用。你可以使用 incrementQuantity 和 decrementQuantity 方法轻松增加或减少你的订阅数量:
use App\Models\User;
$user = User::find(1);
$user->subscription('default')->incrementQuantity();
// Add five to the subscription's current quantity...
$user->subscription('default')->incrementQuantity(5);
$user->subscription('default')->decrementQuantity();
// Subtract five from the subscription's current quantity...
$user->subscription('default')->decrementQuantity(5);或者,你可以使用 updateQuantity 方法设置特定数量:
$user->subscription('default')->updateQuantity(10);noProrate 方法可用于更新订阅的数量,而不按比例分配费用:
$user->subscription('default')->noProrate()->updateQuantity(10);有关订阅数量的更多信息,请参阅Stripe 文档。
多产品订阅的数量
如果你的订阅是 subscription with multiple products,你应该将你希望增加或减少的数量的价格 ID 作为第二个参数传递给增量/减量方法:
$user->subscription('default')->incrementQuantity(1, 'price_chat');多产品订阅
Subscription with multiple products 允许你将多个计费产品分配给单个订阅。例如,假设你正在构建一个客户服务「帮助台」应用程序,其基本订阅价格为每月 10 美元,但提供实时聊天附加产品,每月额外支付 15 美元。多个产品的订阅信息存储在 Cashier 的 subscription_items 数据库表中。
你可以通过将价格数组作为第二个参数传递给 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');WARNING
你不能删除订阅的最后价格。相反,你应该简单地取消订阅。
交换价格
你还可以更改包含多个产品的订阅所附加的价格。例如,假设客户订阅了 price_basic 附加产品,并且你希望将客户从 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');数量
如果你想更新单个订阅价格的数量,你可以使用 existing quantity methods 将价格 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');WARNING
当订阅有多个价格时,Subscription 模型上的 Stripe_price 和 quantity 属性将为 null。要访问各个价格属性,你应该使用 Subscription 模型上提供的 items 关系。
订阅项
当订阅有多个价格时,它将在数据库的 subscription_items 表中存储多个订阅「项目」。你可以通过订阅上的 items 关系访问这些内容:
use App\Models\User;
$user = User::find(1);
$subscriptionItem = $user->subscription('default')->items->first();
// Retrieve the Stripe price and quantity for a specific item...
$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();按用量计费
Usage based billing 允许你根据客户在计费周期内的产品使用情况向他们收费。例如,你可以根据客户每月发送的短信或电子邮件的数量向他们收费。
要开始使用用量计费,你首先需要在 Stripe 仪表板中创建一个带有 usage based billing model 和 meter 的新产品。创建仪表后,存储关联的事件名称和仪表 ID,你将需要报告和检索使用情况。然后,使用 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 报告他们的使用情况,以便可以准确地向他们计费。要报告计量事件的使用情况,你可以在 Billable 模型上使用 reportMeterEvent 方法:
$user = User::find(1);
$user->reportMeterEvent('emails-sent');默认情况下,计费周期中添加的「使用数量」为 1。或者,你可以传递特定数量的「使用量」以添加到客户在计费周期内的使用量:
$user = User::find(1);
$user->reportMeterEvent('emails-sent', quantity: 15);要检索仪表的客户事件摘要,你可以使用 Billable 实例的 meterEventSummaries 方法:
$user = User::find(1);
$meterUsage = $user->meterEventSummaries($meterId);
$meterUsage->first()->aggregated_value // 10有关仪表事件摘要的更多信息,请参阅 Stripe 的 Meter Event Summary object documentation。
对于 list all meters,你可以使用 Billable 实例的 meters 方法:
$user = User::find(1);
$user->meters();订阅税费
WARNING
你可以automatically calculate taxes using Stripe Tax,而不是手动计算税率
要指定用户为订阅支付的税率,你应该在计费模型上实现 taxRates 方法并返回包含 Stripe 税率 ID 的数组。你可以在 your Stripe dashboard 中定义这些税率:
/**
* The tax rates that should apply to the customer's subscriptions.
*
* @return array<int, string>
*/
public function taxRates(): array
{
return ['txr_id'];
}taxRates 方法使你能够按客户应用税率,这对于跨越多个国家/地区和税率的用户群可能会有所帮助。
如果你提供多种产品的订阅,则可以通过在计费模型上实施 priceTaxRates 方法来为每个价格定义不同的税率:
/**
* The tax rates that should apply to the customer's subscriptions.
*
* @return array<string, array<int, string>>
*/
public function priceTaxRates(): array
{
return [
'price_monthly' => ['txr_id'],
];
}WARNING
taxRates 方法仅适用于订阅费用。如果你使用收银台进行「一次性」收费,则需要手动指定当时的税率。
同步税率
更改 taxRates 方法返回的硬编码税率 ID 时,用户任何现有订阅的税收设置将保持不变。如果你希望使用新的 taxRates 值更新现有订阅的税值,则应在用户的订阅实例上调用 syncTaxRates 方法:
$user->subscription('default')->syncTaxRates();这还将同步具有多个产品的订阅的任何商品税率。如果你的应用程序提供多种产品的订阅,则应确保你的计费模型实现 priceTaxRates 方法 discussed above。
免税
Cashier还提供 isNotTaxExempt、isTaxExempt 和 reverseChargeApplies 方法来确定客户是否免税。这些方法将调用 Stripe API 来确定客户的免税状态:
use App\Models\User;
$user = User::find(1);
$user->isTaxExempt();
$user->isNotTaxExempt();
$user->reverseChargeApplies();WARNING
这些方法也可用于任何 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 billing cycle documentation
取消订阅
要取消订阅,请对用户的订阅调用 cancel 方法:
$user->subscription('default')->cancel();取消订阅后,Cashier 将自动在你的 subscriptions 数据库表中设置 ends_at 列。此列用于了解 subscribed 方法何时应开始返回 false。
例如,如果客户在 3 月 1 日取消订阅,但订阅预计要到 3 月 5 日才结束,则 subscribed 方法将继续返回 true,直到 3 月 5 日。这样做是因为通常允许用户继续使用应用程序,直到其计费周期结束。
你可以使用 onGracePeriod 方法确定用户是否已取消订阅但仍处于「宽限期」:
if ($user->subscription('default')->onGracePeriod()) {
// ...
}如果你希望立即取消订阅,请对用户的订阅调用 cancelNow 方法:
$user->subscription('default')->cancelNow();如果你希望立即取消订阅并对任何剩余的未开票计量使用量或新的/待定的按比例分配发票项目开具发票,请在用户的订阅上调用 cancelNowAndInvoice 方法:
$user->subscription('default')->cancelNowAndInvoice();你还可以选择在特定时间取消订阅:
$user->subscription('default')->cancelAt(
now()->plus(days: 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 中的价格配置的任何默认试用期。
WARNING
如果客户的订阅未在试用结束日期之前取消,他们将在试用期满后立即付费,因此你应确保将试用结束日期通知你的用户。
trialUntil 方法允许你提供一个 DateTime 实例来指定试用期何时结束:
use Illuminate\Support\Carbon;
$user->newSubscription('default', 'price_monthly')
->trialUntil(Carbon::now()->plus(days: 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()->plus(days: 10),
]);WARNING
请务必在计费模型的类定义中为 trial_ends_at 属性添加 date cast。
Cashier 将此类试用称为「通用试用」,因为它不附加到任何现有订阅。如果当前日期未超过 trial_ends_at 的值,则计费模型实例上的 onTrial 方法将返回 true:
if ($user->onTrial()) {
// User is within their trial period...
}一旦你准备好为用户创建实际订阅,你就可以像往常一样使用 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()) {
// User is within their "generic" trial period...
}延长试用期
extendTrial 方法允许你在创建订阅后延长订阅的试用期。如果试用期已过期并且客户已支付订阅费用,你仍然可以为他们提供延长试用期。试用期内所花费的时间将从客户的下一张发票中扣除:
use App\Models\User;
$subscription = User::find(1)->subscription('default');
// End the trial 7 days from now...
$subscription->extendTrial(
now()->plus(days: 7)
);
// Add an additional 5 days to the trial...
$subscription->extendTrial(
$subscription->trial_ends_at->plus(days: 5)
);处理 Stripe Webhook
INFO
你可以使用 the Stripe CLI 来帮助在本地开发期间测试 Webhook。
Stripe 可以通过 Webhooks 向你的应用程序通知各种事件。默认情况下,指向 Cashier 的 Webhook 控制器的路由由 Cashier 服务提供者自动注册。该控制器将处理所有传入的 Webhook 请求。
默认情况下,Cashier Webhook 控制器将自动处理取消订阅失败次数过多(由你的 Stripe 设置定义)、客户更新、客户删除、订阅更新和付款方式更改;但是,我们很快就会发现,你可以扩展此控制器来处理你喜欢的任何 Stripe Webhook 事件。
为了确保你的应用程序可以处理 Stripe Webhook,请务必在 Stripe 控制面板中配置 Webhook URL。默认情况下,Cashier 的 Webhook 控制器响应 /Stripe/Webhook URL 路径。你应在 Stripe 控制面板中启用的所有 Webhook 的完整列表如下:
customer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedcustomer.updatedcustomer.deletedpayment_method.automatically_updatedinvoice.payment_action_requiredinvoice.payment_succeeded
为了方便起见,Cashier 包含一个 Cashier:Webhook Artisan 命令。此命令将在 Stripe 中创建一个 Webhook,用于侦听 Cashier 所需的所有事件:
php artisan cashier:webhook默认情况下,创建的 Webhook 将指向由 APP_URL 环境变量和 Cashier 附带的 Cashier.Webhook 路由定义的 URL。如果你想使用不同的 URL,则可以在调用命令时提供 --url 选项:
php artisan cashier:webhook --url "https://example.com/stripe/webhook"创建的 Webhook 将使用与你的 Cashier 版本兼容的 Stripe API 版本。如果你想使用不同的 Stripe 版本,你可以提供 --api-version 选项:
php artisan cashier:webhook --api-version="2019-12-03"创建后,Webhook 将立即激活。如果你希望创建 Webhook 但在准备好之前将其禁用,则可以在调用命令时提供 --disabled 选项:
php artisan cashier:webhook --disabledWARNING
确保使用 Cashier 附带的 Webhook signature verification 中间件保护传入的 Stripe Webhook 请求。
Webhook 与 CSRF 防护
由于 Stripe Webhook 需要绕过 Laravel 的 CSRF protection,因此你应该确保 Laravel 不会尝试验证传入 Stripe Webhook 的 CSRF 令牌。为此,你应该在应用程序的 bootstrap/app.php 文件中将 Stripe/* 从 CSRF 保护中排除:
->withMiddleware(function (Middleware $middleware): void {
$middleware->validateCsrfTokens(except: [
'stripe/*',
]);
})定义 Webhook 事件处理器
Cashier 自动处理因收费失败和其他常见 Stripe Webhook 事件而导致的订阅取消。但是,如果你想要处理其他 Webhook 事件,你可以通过侦听 Cashier 调度的以下事件来实现:
Laravel\Cashier\Events\WebhookReceivedLaravel\Cashier\Events\WebhookHandled
这两个事件都包含 Stripe Webhook 的完整负载。例如,如果你希望处理 invoice.payment_succeeded Webhook,你可以注册一个 listener 来处理该事件:
<?php
namespace App\Listeners;
use Laravel\Cashier\Events\WebhookReceived;
class StripeEventListener
{
/**
* Handle received Stripe webhooks.
*/
public function handle(WebhookReceived $event): void
{
if ($event->payload['type'] === 'invoice.payment_succeeded') {
// Handle the incoming event...
}
}
}验证 Webhook 签名
为了保护你的网络挂钩,你可以使用 Stripe's Webhook signatures。为了方便起见,Cashier 自动包含一个中间件,用于验证传入的 Stripe Webhook 请求是否有效。
要启用 Webhook 验证,请确保在应用程序的 .env 文件中设置 STRIPE_WEBHOOK_SECRET 环境变量。可以从你的 Stripe 账户仪表板检索 Webhook secret。
单次扣款
简单扣款
若要对客户进行一次性扣款,可在可计费模型实例上使用 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) {
// ...
}WARNING
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 仪表板中访问有关每个产品销售情况的更好的分析和数据。
WARNING
invoice、invoicePrice 和 invoiceFor 方法将创建 Stripe 发票,该发票将重试失败的计费尝试。如果你不希望发票重试失败的收费,则需要在第一次失败的收费后使用 Stripe API 关闭它们。
创建 Payment Intent
你可以通过在可计费模型实例上调用 pay 方法来创建新的 Stripe 付款意图。调用此方法将创建一个封装在 Laravel\Cashier\Payment 实例中的付款意图:
use Illuminate\Http\Request;
Route::post('/pay', function (Request $request) {
$payment = $request->user()->pay(
$request->get('amount')
);
return $payment->client_secret;
});After creating the payment intent, you can return the client secret to your application's frontend so that the user can complete the payment in their browser. To read more about building entire payment flows using Stripe payment intents, please consult the 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;
});WARNING
pay 和 payWith 方法接受以应用程序使用的货币的最低分母表示的付款金额。例如,如果客户以美元付款,则应以美分指定金额。
退款
若需要退款 Stripe 扣款,可使用 refund 方法。该方法以 Stripe payment intent ID 作为第一个参数:
$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 }}">Download</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 之前,你应该使用 Composer 安装 Dompdf 库,这是 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 还可以使用自定义发票渲染器。默认情况下,Cashier 使用 DompdfInvoiceRenderer 实现,该实现利用 dompdf PHP 库来生成 Cashier 的发票。但是,你可以通过实现 Laravel\Cashier\Contracts\InvoiceRenderer 接口来使用任何你想要的渲染器。例如,你可能希望使用对第三方 PDF 渲染服务的 API 调用来渲染发票 PDF:
use Illuminate\Support\Facades\Http;
use Laravel\Cashier\Contracts\InvoiceRenderer;
use Laravel\Cashier\Invoice;
class ApiInvoiceRenderer implements InvoiceRenderer
{
/**
* Render the given invoice and return the raw PDF bytes.
*/
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();
}
}实施发票渲染器合约后,你应该更新应用程序的 config/Cashier.php 配置文件中的 Cashier.invoices.renderer 配置值。该配置值应设置为自定义渲染器实现的类名称。
结账
Cashier Stripe also provides support for Stripe Checkout. Stripe Checkout takes the pain out of implementing custom pages to accept payments by providing a pre-built, hosted payment page.
以下文档包含有关如何开始使用 Stripe Checkout with Cashier 的信息。要了解有关 Stripe Checkout 的更多信息,你还应该考虑查看 Stripe's own documentation on Checkout。
产品结账
你可以在计费模型上使用 Checkout 方法对 Stripe 仪表板中创建的现有产品执行结账。 Checkout 方法将启动一个新的 Stripe Checkout 会话。默认情况下,你需要传递 Stripe 价格 ID:
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 的结账页面。默认情况下,当用户成功完成或取消购买时,他们将被重定向到你的 home 路线位置,但你可以使用 success_url 和 cancel_url 选项指定自定义回调 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 在调用你的 URL 时将结帐会话 ID 添加为查询字符串参数。为此,请将文字字符串 {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 不允许 user redeemable promotion codes。幸运的是,有一种简单的方法可以为你的结账页面启用这些功能。为此,你可以调用 allowPromotionCodes 方法:
use Illuminate\Http\Request;
Route::get('/product-checkout', function (Request $request) {
return $request->user()
->allowPromotionCodes()
->checkout('price_tshirt');
});单次扣款结账
你还可以对尚未在 Stripe 仪表板中创建的临时产品执行简单的收费。为此,你可以在计费模型上使用 CheckoutCharge 方法,并向其传递计费金额、产品名称和可选数量。当客户访问此路线时,他们将被重定向到 Stripe 的结账页面:
use Illuminate\Http\Request;
Route::get('/charge-checkout', function (Request $request) {
return $request->user()->checkoutCharge(1200, 'T-Shirt', 5);
});WARNING
使用 CheckoutCharge 方法时,Stripe 将始终在你的 Stripe 仪表板中创建新产品和价格。因此,我们建议你在 Stripe 仪表板中预先创建产品,并使用 Checkout 方法。
订阅结账
WARNING
使用 Stripe Checkout 进行订阅需要你在 Stripe 仪表板中启用 customer.subscription.created Webhook。此 Webhook 将在你的数据库中创建订阅记录并存储所有相关的订阅项目。
你还可以使用 Stripe Checkout 发起订阅。使用 Cashier 的订阅构建器方法定义订阅后,你可以调用 Checkout 方法。当客户访问此路线时,他们将被重定向到 Stripe 的结账页面:
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();
});WARNING
遗憾的是,在开始订阅时,Stripe Checkout 并不支持所有订阅计费选项。在订阅构建器上使用 anchorBillingCycleOn 方法、设置按比例分配行为或设置付款行为在 Stripe Checkout 会话期间不会产生任何影响。请咨询 the Stripe Checkout Session API documentation 以查看哪些参数可用。
Stripe Checkout 与试用期
当然,你可以在构建将使用 Stripe Checkout 完成的订阅时定义试用期:
$checkout = Auth::user()->newSubscription('default', 'price_monthly')
->trialDays(3)
->checkout();但是,试用期必须至少为 48 小时,这是 Stripe Checkout 支持的最短试用时间。
订阅与 Webhook
请记住,Stripe 和 Cashier 通过 Webhook 更新订阅状态,因此当客户输入付款信息后返回应用程序时,订阅可能尚未激活。为了处理这种情况,你可能希望显示一条消息,通知用户他们的付款或订阅正在等待处理。
收集税号
Checkout 还支持收集客户的税号。要在结账会话中启用此功能,请在创建会话时调用 collectTaxIds 方法:
$checkout = $user->collectTaxIds()->checkout('price_tshirt');调用此方法时,客户将可以使用一个新的复选框,使他们能够表明他们是否作为公司进行购买。如果是这样,他们将有机会提供其税号。
WARNING
如果你已在应用程序的服务提供者中配置了 automatic tax collection,则此功能将自动启用,无需调用 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 Webhook 事件,因此请确保 configure your Stripe Webhook 实际将此事件发送到你的应用程序。在 Stripe 仪表板中启用 Webhook 后,你可以handle the Webhook with Cashier。 Webhook 负载中包含的对象将是一个 Checkout object,你可以检查它以履行客户的订单。
处理失败付款
有时,订阅付款或单笔费用可能会失败。发生这种情况时,Cashier 将抛出 Laravel\Cashier\Exceptions\IncompletePayment 异常,通知你发生了这种情况。捕获此异常后,你有两种如何继续的选择。
首先,你可以将客户重定向到收银台附带的专用付款确认页面。该页面已具有通过出纳服务提供者注册的关联命名路线。因此,你可以捕获 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 安全」确认。确认付款后,用户将被重定向到上面指定的 redirect 参数提供的 URL。重定向后,message(字符串)和 success(整数)查询字符串变量将添加到 URL。支付页面目前支持以下支付方式类型:
- 信用卡
- 支付宝
- 联系银行
- BECS 直接借记
- 每股收益
- 吉罗支付
- 理想的
- SEPA 直接借记
或者,你也可以让 Stripe 代为处理付款确认。此时不必重定向到付款确认页,可在 Stripe 控制台中设置 Stripe 自动账单邮件。不过,若捕获到 IncompletePayment 异常,仍应告知用户将收到含后续付款确认说明的邮件。
以下方法可能会引发付款异常:使用 Billable 特征的模型上的 charge、invoiceFor 和 invoice。与订阅交互时,SubscriptionBuilder 上的 create 方法以及 Subscription 和 SubscriptionItem 型号上的 incrementAndInvoice 和 swapAndInvoice 方法可能会抛出未完成付款异常。
可以使用计费模型或订阅实例上的 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) {
// Get the payment intent status...
$exception->payment->status;
// Check specific conditions...
if ($exception->payment->requiresPaymentMethod()) {
// ...
} elseif ($exception->payment->requiresConfirmation()) {
// ...
}
}确认付款
某些付款方式需要额外的数据才能确认付款。例如,SEPA 付款方式在付款过程中需要额外的「授权」数据。你可以使用 withPaymentConfirmationOptions 方法将此数据提供给Cashier:
$subscription->withPaymentConfirmationOptions([
'mandate_data' => '...',
])->swap('price_xxx');你可以查阅Stripe API documentation来查看确认付款时接受的所有选项。
强客户认证 (SCA)
如果你的企业或你的客户之一位于欧洲,你将需要遵守欧盟的强客户认证 (SCA) 法规。这些法规由欧盟于 2019 年 9 月实施,旨在防止支付欺诈。幸运的是,Stripe 和 Cashier 已准备好构建符合 SCA 的应用程序。
WARNING
需要额外确认的付款
SCA 法规通常需要额外验证才能确认和处理付款。发生这种情况时,Cashier 将抛出 Laravel\Cashier\Exceptions\IncompletePayment 异常,通知你需要额外的验证。有关如何处理这些异常的更多信息,请参阅 handling failed payments 的文档。
Stripe 或 Cashier 呈现的支付确认屏幕可以根据特定银行或发卡机构的支付流程进行定制,并且可以包括额外的卡确认、临时小额费用、单独的设备身份验证或其他形式的验证。
未完成与逾期状态
当付款需要额外确认时,订阅将保持在 incomplete 或 past_due 状态,如其 Stripe_status 数据库列所示。付款确认完成后,Cashier 将自动激活客户的订阅,并且 Stripe 通过 Webhook 通知你的申请已完成。
有关 incomplete 和 past_due 状态的更多信息,请参阅 our additional documentation on these states。
离线付款通知
由于 SCA 法规要求客户偶尔验证其付款详细信息(即使订阅处于活动状态),因此Cashier可以在需要非会话付款确认时向客户发送通知。例如,当订阅续订时,可能会发生这种情况。可以通过将 CASHIER_PAYMENT_NOTIFICATION 环境变量设置为通知类来启用Cashier的付款通知。默认情况下,此通知处于禁用状态。当然,Cashier 包含一个你可以用于此目的的通知类,但如果需要,你可以自由提供自己的通知类:
CASHIER_PAYMENT_NOTIFICATION=Laravel\Cashier\Notifications\ConfirmPayment为了确保发送会话外付款确认通知,请验证你的应用程序的 Stripe Webhooks are configured 以及 Stripe 仪表板中的 invoice.payment_action_required Webhook 已启用。此外,你的 Billable 模型还应该使用 Laravel 的 Illuminate\Notifications\Notifiable 特征。
WARNING
即使客户手动进行需要额外确认的付款,也会发送通知。不幸的是,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\StripeClient 客户端,你可以在 Cashier 类上调用 Stripe 方法。例如,你可以使用此方法访问 StripeClient 实例并从你的 Stripe 帐户中检索价格列表:
use Laravel\Cashier\Cashier;
$prices = Cashier::stripe()->prices->all();测试
当测试使用 Cashier 的应用程序时,你可以模拟对 Stripe API 的实际 HTTP 请求;但是,这需要你部分地重新实现 Cashier 自己的行为。因此,我们建议你允许测试使用实际的 Stripe API。虽然速度较慢,但它使你更有信心你的应用程序按预期工作,并且任何缓慢的测试都可以放在自己的 Pest / PHPUnit 测试组中。
测试时,请记住,Cashier 本身已经有一个很棒的测试套件,因此你应该只专注于测试你自己的应用程序的订阅和支付流程,而不是每个底层的 Cashier 行为。
首先,将 Stripe 密钥的 testing 版本添加到你的 phpunit.xml 文件中:
<env name="STRIPE_SECRET" value="sk_test_<your-key>"/>现在,在测试中与 Cashier 交互时,会向 Stripe 测试环境发送真实 API 请求。为方便起见,你应在 Stripe 测试账户中预先填入测试期间可能用到的订阅 / 价格。
INFO
为了测试各种计费场景,例如信用卡拒绝和失败,你可以使用 Stripe 提供的各种 testing card numbers and tokens。