跳转至内容

Laravel Cashier (Stripe)

简介

Laravel Cashier StripeStripe 的订阅计费服务提供了一个富有表现力的流式接口。它处理了绝大多数你不想编写的样板式订阅计费代码。除了基础的订阅管理外,Cashier 还可以处理优惠券、切换订阅、订阅“数量”、取消宽限期,甚至生成 PDF 账单。

升级 Cashier

升级到 Cashier 的新版本时,请务必仔细阅读 升级指南

为了防止破坏性变更,Cashier 使用固定的 Stripe API 版本。Cashier 16 使用 Stripe API 版本 2025-06-30.basil。Stripe API 版本会在次要版本更新时同步更新,以便利用新的 Stripe 功能和改进。

安装

首先,使用 Composer 包管理器安装 Stripe 版的 Cashier 包

1composer require laravel/cashier

安装包后,使用 vendor:publish Artisan 命令发布 Cashier 的迁移文件

1php artisan vendor:publish --tag="cashier-migrations"

然后,运行数据库迁移

1php artisan migrate

Cashier 的迁移将在你的 users 表中添加几列。它们还将创建一个新的 subscriptions 表来存放所有客户的订阅,以及一个 subscription_items 表,用于存储包含多种价格的订阅。

如果你愿意,也可以使用 vendor:publish Artisan 命令发布 Cashier 的配置文件

1php artisan vendor:publish --tag="cashier-config"

最后,为了确保 Cashier 能正确处理所有 Stripe 事件,请记得 配置 Cashier 的 Webhook 处理

Stripe 建议任何用于存储 Stripe 标识符的列都应区分大小写。因此,如果你使用的是 MySQL,应确保 stripe_id 列的排序规则设置为 utf8_bin。更多相关信息可在 Stripe 文档 中找到。

配置

可计费模型 (Billable Model)

在使用 Cashier 之前,请在你的可计费模型定义中添加 Billable Trait。通常,这就是 App\Models\User 模型。此 Trait 提供了各种方法,允许你执行常见的计费任务,例如创建订阅、应用优惠券以及更新支付方式信息。

1use Laravel\Cashier\Billable;
2 
3class User extends Authenticatable
4{
5 use Billable;
6}

Cashier 默认你的可计费模型是 Laravel 自带的 App\Models\User 类。如果你想更改此设置,可以通过 useCustomerModel 方法指定一个不同的模型。此方法通常应在 AppServiceProvider 类的 boot 方法中调用。

1use App\Models\Cashier\User;
2use Laravel\Cashier\Cashier;
3 
4/**
5 * Bootstrap any application services.
6 */
7public function boot(): void
8{
9 Cashier::useCustomerModel(User::class);
10}

如果你使用的模型不是 Laravel 自带的 App\Models\User,则需要发布并修改 Cashier 提供的迁移文件,以匹配你自定义模型对应的表名。

API 密钥

接下来,你应该在应用程序的 .env 文件中配置 Stripe API 密钥。你可以从 Stripe 控制面板检索你的 Stripe API 密钥。

1STRIPE_KEY=your-stripe-key
2STRIPE_SECRET=your-stripe-secret
3STRIPE_WEBHOOK_SECRET=your-stripe-webhook-secret

你应该确保在应用程序的 .env 文件中定义了 STRIPE_WEBHOOK_SECRET 环境变量,该变量用于确保收到的 Webhook 请求确实来自 Stripe。

货币配置

Cashier 的默认货币是美元 (USD)。你可以通过在 .env 文件中设置 CASHIER_CURRENCY 环境变量来更改默认货币。

1CASHIER_CURRENCY=eur

除了配置货币外,你还可以指定一个区域设置,用于在账单显示金额时格式化数值。在内部,Cashier 使用 PHP 的 NumberFormatter 来设置货币区域。

1CASHIER_CURRENCY_LOCALE=nl_BE

为了使用除 en 之外的区域设置,请确保服务器上安装并配置了 ext-intl PHP 扩展。

税务配置

得益于 Stripe Tax,可以自动计算由 Stripe 生成的所有账单的税费。你可以在 App\Providers\AppServiceProvider 类的 boot 方法中调用 calculateTaxes 方法来启用自动税费计算。

1use Laravel\Cashier\Cashier;
2 
3/**
4 * Bootstrap any application services.
5 */
6public function boot(): void
7{
8 Cashier::calculateTaxes();
9}

一旦启用了税费计算,所有新的订阅和一次性生成的账单都将自动进行税务计算。

为了使此功能正常工作,需要将客户的账单详细信息(如姓名、地址和税务 ID)同步到 Stripe。你可以使用 Cashier 提供的 客户数据同步税务 ID 方法来实现这一点。

日志

Cashier 允许你在记录 Stripe 致命错误时指定日志通道。你可以在 .env 文件中定义 CASHIER_LOGGER 环境变量来指定日志通道。

1CASHIER_LOGGER=stack

Stripe API 调用生成的异常将通过你的应用程序默认日志通道进行记录。

使用自定义模型

你可以自由扩展 Cashier 内部使用的模型,只需定义你自己的模型并继承相应的 Cashier 模型即可。

1use Laravel\Cashier\Subscription as CashierSubscription;
2 
3class Subscription extends CashierSubscription
4{
5 // ...
6}

定义模型后,你可以通过 Laravel\Cashier\Cashier 类指示 Cashier 使用你的自定义模型。通常,你应该在 App\Providers\AppServiceProvider 类的 boot 方法中向 Cashier 注册你的自定义模型。

1use App\Models\Cashier\Subscription;
2use App\Models\Cashier\SubscriptionItem;
3 
4/**
5 * Bootstrap any application services.
6 */
7public function boot(): void
8{
9 Cashier::useSubscriptionModel(Subscription::class);
10 Cashier::useSubscriptionItemModel(SubscriptionItem::class);
11}

快速入门

销售产品

在使用 Stripe Checkout 之前,你应该在 Stripe 面板中定义具有固定价格的产品。此外,你还应该 配置 Cashier 的 Webhook 处理

通过你的应用程序提供产品和订阅计费可能会让人望而生畏。然而,得益于 Cashier 和 Stripe Checkout,你可以轻松构建现代且健壮的支付集成。

要对非周期性的单次购买产品进行收费,我们将使用 Cashier 将客户引导至 Stripe Checkout,在那里他们将提供支付详细信息并确认购买。一旦通过 Checkout 完成支付,客户将被重定向到你在应用程序中选择的成功 URL。

1use Illuminate\Http\Request;
2 
3Route::get('/checkout', function (Request $request) {
4 $stripePriceId = 'price_deluxe_album';
5 
6 $quantity = 1;
7 
8 return $request->user()->checkout([$stripePriceId => $quantity], [
9 'success_url' => route('checkout-success'),
10 'cancel_url' => route('checkout-cancel'),
11 ]);
12})->name('checkout');
13 
14Route::view('/checkout/success', 'checkout.success')->name('checkout-success');
15Route::view('/checkout/cancel', 'checkout.cancel')->name('checkout-cancel');

如上例所示,我们将利用 Cashier 提供的 checkout 方法将客户重定向到 Stripe Checkout 以处理指定的“价格标识符”。使用 Stripe 时,“价格”是指 针对特定产品定义的价格

如果有必要,checkout 方法会自动在 Stripe 中创建客户,并将该 Stripe 客户记录与你应用程序数据库中相应的用户关联起来。结账会话完成后,客户将被重定向到专门的成功或取消页面,你可以在该页面向客户显示信息提示。

向 Stripe Checkout 提供元数据 (Meta Data)

销售产品时,通常需要通过应用程序定义的 CartOrder 模型来追踪已完成的订单和已购买的产品。当将客户重定向到 Stripe Checkout 以完成购买时,你可能需要提供现有的订单标识符,以便在客户被重定向回应用程序时,能将完成的购买与相应的订单关联起来。

要实现这一点,你可以向 checkout 方法提供一个 metadata 数组。假设当用户开始结账流程时,在我们的应用程序中会创建一个处于待处理状态的 Order。请记住,此示例中的 CartOrder 模型仅供说明,并非由 Cashier 提供。你可以根据应用程序的需求自由实现这些概念。

1use App\Models\Cart;
2use App\Models\Order;
3use Illuminate\Http\Request;
4 
5Route::get('/cart/{cart}/checkout', function (Request $request, Cart $cart) {
6 $order = Order::create([
7 'cart_id' => $cart->id,
8 'price_ids' => $cart->price_ids,
9 'status' => 'incomplete',
10 ]);
11 
12 return $request->user()->checkout($order->price_ids, [
13 'success_url' => route('checkout-success').'?session_id={CHECKOUT_SESSION_ID}',
14 'cancel_url' => route('checkout-cancel'),
15 'metadata' => ['order_id' => $order->id],
16 ]);
17})->name('checkout');

如上例所示,当用户开始结账流程时,我们将把购物车/订单关联的所有 Stripe 价格标识符提供给 checkout 方法。当然,你的应用程序负责在客户添加商品时将这些商品与“购物车”或订单关联起来。我们还通过 metadata 数组向 Stripe Checkout 会话提供了订单 ID。最后,我们在结账成功路由中添加了 CHECKOUT_SESSION_ID 模板变量。当 Stripe 将客户重定向回你的应用程序时,此模板变量将自动填充为结账会话 ID。

接下来,让我们构建结账成功路由。这是用户通过 Stripe Checkout 完成购买后将被重定向到的路由。在此路由中,我们可以检索 Stripe 结账会话 ID 和关联的 Stripe Checkout 实例,以访问我们提供的元数据并相应地更新客户的订单。

1use App\Models\Order;
2use Illuminate\Http\Request;
3use Laravel\Cashier\Cashier;
4 
5Route::get('/checkout/success', function (Request $request) {
6 $sessionId = $request->get('session_id');
7 
8 if ($sessionId === null) {
9 return;
10 }
11 
12 $session = Cashier::stripe()->checkout->sessions->retrieve($sessionId);
13 
14 if ($session->payment_status !== 'paid') {
15 return;
16 }
17 
18 $orderId = $session['metadata']['order_id'] ?? null;
19 
20 $order = Order::findOrFail($orderId);
21 
22 $order->update(['status' => 'completed']);
23 
24 return view('checkout-success', ['order' => $order]);
25})->name('checkout-success');

有关 结账会话对象所包含数据 的更多信息,请参考 Stripe 的文档。

销售订阅

在使用 Stripe Checkout 之前,你应该在 Stripe 面板中定义具有固定价格的产品。此外,你还应该 配置 Cashier 的 Webhook 处理

通过你的应用程序提供产品和订阅计费可能会让人望而生畏。然而,得益于 Cashier 和 Stripe Checkout,你可以轻松构建现代且健壮的支付集成。

要了解如何使用 Cashier 和 Stripe Checkout 销售订阅,让我们考虑一个简单的场景:一个提供基础月付 (price_basic_monthly) 和年付 (price_basic_yearly) 计划的订阅服务。这两个价格可以在 Stripe 面板中归类于一个“Basic”产品 (pro_basic)。此外,我们的订阅服务可能还会提供一个 Expert 计划,标识为 pro_expert

首先,让我们了解客户如何订阅我们的服务。当然,你可以想象客户会在我们应用程序的定价页面上点击“订阅”按钮。此按钮或链接应将用户重定向到一个 Laravel 路由,该路由负责为他们选择的计划创建 Stripe Checkout 会话。

1use Illuminate\Http\Request;
2 
3Route::get('/subscription-checkout', function (Request $request) {
4 return $request->user()
5 ->newSubscription('default', 'price_basic_monthly')
6 ->trialDays(5)
7 ->allowPromotionCodes()
8 ->checkout([
9 'success_url' => route('your-success-route'),
10 'cancel_url' => route('your-cancel-route'),
11 ]);
12});

如上例所示,我们将把客户重定向到一个 Stripe Checkout 会话,这将允许他们订阅我们的 Basic 计划。在结账成功或取消后,客户将被重定向回我们提供给 checkout 方法的 URL。为了知道他们的订阅何时真正开始(因为某些支付方式需要几秒钟来处理),我们还需要 配置 Cashier 的 Webhook 处理

现在客户可以开始订阅了,我们需要限制应用程序的某些部分,以便只有已订阅的用户才能访问。当然,我们可以随时通过 Cashier 的 Billable Trait 提供的 subscribed 方法来确定用户的当前订阅状态。

1@if ($user->subscribed())
2 <p>You are subscribed.</p>
3@endif

我们甚至可以轻松确定用户是否订阅了特定的产品或价格。

1@if ($user->subscribedToProduct('pro_basic'))
2 <p>You are subscribed to our Basic product.</p>
3@endif
4 
5@if ($user->subscribedToPrice('price_basic_monthly'))
6 <p>You are subscribed to our monthly Basic plan.</p>
7@endif

构建已订阅中间件

为了方便起见,你可能希望创建一个 中间件,用于确定传入的请求是否来自已订阅的用户。一旦定义了此中间件,你就可以轻松地将其分配给路由,以防止未订阅的用户访问该路由。

1<?php
2 
3namespace App\Http\Middleware;
4 
5use Closure;
6use Illuminate\Http\Request;
7use Symfony\Component\HttpFoundation\Response;
8 
9class Subscribed
10{
11 /**
12 * Handle an incoming request.
13 */
14 public function handle(Request $request, Closure $next): Response
15 {
16 if (! $request->user()?->subscribed()) {
17 // Redirect user to billing page and ask them to subscribe...
18 return redirect('/billing');
19 }
20 
21 return $next($request);
22 }
23}

定义中间件后,你可以将其分配给路由。

1use App\Http\Middleware\Subscribed;
2 
3Route::get('/dashboard', function () {
4 // ...
5})->middleware([Subscribed::class]);

允许客户管理他们的计费计划

当然,客户可能希望将他们的订阅计划更改为其他产品或“级别”。实现这一点的最简单方法是将客户引导至 Stripe 的 客户结算门户 (Customer Billing Portal),它提供了一个托管的用户界面,允许客户下载账单、更新支付方式并更改订阅计划。

首先,在应用程序内定义一个链接或按钮,将用户引导至我们将用于启动结算门户会话的 Laravel 路由。

1<a href="{{ route('billing') }}">
2 Billing
3</a>

接下来,让我们定义初始化 Stripe 客户结算门户会话并重定向用户的路由。redirectToBillingPortal 方法接受一个 URL,作为用户退出门户时应返回的地址。

1use Illuminate\Http\Request;
2 
3Route::get('/billing', function (Request $request) {
4 return $request->user()->redirectToBillingPortal(route('dashboard'));
5})->middleware(['auth'])->name('billing');

只要你配置了 Cashier 的 Webhook 处理,Cashier 就会通过检查来自 Stripe 的传入 Webhook,自动保持应用程序中与 Cashier 相关的数据库表同步。例如,当用户通过 Stripe 的客户结算门户取消订阅时,Cashier 将收到相应的 Webhook 并在你应用程序的数据库中将该订阅标记为“已取消”。

客户

获取客户

你可以使用 Cashier::findBillable 方法通过 Stripe ID 检索客户。此方法将返回可计费模型的一个实例。

1use Laravel\Cashier\Cashier;
2 
3$user = Cashier::findBillable($stripeId);

创建客户

有时,你可能希望在不开启订阅的情况下创建一个 Stripe 客户。你可以使用 createAsStripeCustomer 方法来实现这一点。

1$stripeCustomer = $user->createAsStripeCustomer();

一旦客户在 Stripe 中创建完成,你可以在以后的日期开启订阅。你可以提供一个可选的 $options 数组,以传递任何 Stripe API 支持的额外客户创建参数

1$stripeCustomer = $user->createAsStripeCustomer($options);

如果你想返回可计费模型的 Stripe 客户对象,可以使用 asStripeCustomer 方法。

1$stripeCustomer = $user->asStripeCustomer();

如果你想检索给定可计费模型的 Stripe 客户对象,但不确定该模型是否已经是 Stripe 中的客户,可以使用 createOrGetStripeCustomer 方法。如果客户尚不存在,此方法将在 Stripe 中创建一个新客户。

1$stripeCustomer = $user->createOrGetStripeCustomer();

更新客户

有时,你可能希望直接用额外信息更新 Stripe 客户。你可以使用 updateStripeCustomer 方法来实现。此方法接受一个包含 Stripe API 支持的客户更新选项 的数组。

1$stripeCustomer = $user->updateStripeCustomer($options);

余额

Stripe 允许你贷记或借记客户的“余额”。稍后,此余额将在新账单中进行抵扣或结算。要查看客户的总余额,你可以使用可计费模型上可用的 balance 方法。balance 方法将返回以客户货币单位格式化后的余额字符串。

1$balance = $user->balance();

要增加客户余额(贷记),你可以为 creditBalance 方法提供一个值。如果需要,还可以提供描述。

1$user->creditBalance(500, 'Premium customer top-up.');

debitBalance 方法提供一个值将扣除客户余额(借记)。

1$user->debitBalance(300, 'Bad usage penalty.');

applyBalance 方法将为客户创建新的余额交易。你可以使用 balanceTransactions 方法检索这些交易记录,这对于提供供客户查看的借贷日志非常有用。

1// Retrieve all transactions...
2$transactions = $user->balanceTransactions();
3 
4foreach ($transactions as $transaction) {
5 // Transaction amount...
6 $amount = $transaction->amount(); // $2.31
7 
8 // Retrieve the related invoice when available...
9 $invoice = $transaction->invoice();
10}

税务 ID

Cashier 提供了一种简单的方法来管理客户的税务 ID。例如,taxIds 方法可用于检索分配给客户的所有 税务 ID,并以集合形式返回。

1$taxIds = $user->taxIds();

你还可以通过标识符检索客户的特定税务 ID。

1$taxId = $user->findTaxId('txi_belgium');

你可以通过向 createTaxId 方法提供有效的 类型 和值来创建新的税务 ID。

1$taxId = $user->createTaxId('eu_vat', 'BE0123456789');

createTaxId 方法将立即把该 VAT ID 添加到客户账户中。VAT ID 的验证也是由 Stripe 完成的;然而,这是一个异步过程。你可以通过订阅 customer.tax_id.updated Webhook 事件并检查 VAT ID 的 verification 参数 来获取验证更新的通知。有关处理 Webhook 的更多信息,请查阅 定义 Webhook 处理器的文档

你可以使用 deleteTaxId 方法删除税务 ID。

1$user->deleteTaxId('txi_belgium');

与 Stripe 同步客户数据

通常,当应用程序用户更新其姓名、电子邮件地址或其他 Stripe 也存储的信息时,你应该通知 Stripe 这些更新。通过这样做,Stripe 上的信息副本将与你的应用程序保持同步。

为了自动化此过程,你可以在可计费模型上定义一个事件监听器,用于响应模型的 updated 事件。然后,在事件监听器内部,你可以调用模型上的 syncStripeCustomerDetails 方法。

1use App\Models\User;
2use function Illuminate\Events\queueable;
3 
4/**
5 * The "booted" method of the model.
6 */
7protected static function booted(): void
8{
9 static::updated(queueable(function (User $customer) {
10 if ($customer->hasStripeId()) {
11 $customer->syncStripeCustomerDetails();
12 }
13 }));
14}

现在,每次客户模型更新时,其信息都会与 Stripe 同步。为方便起见,Cashier 会在最初创建客户时自动将客户信息与 Stripe 同步。

你可以通过覆盖 Cashier 提供的多种方法来自定义用于同步客户信息的列。例如,你可以覆盖 stripeName 方法来自定义当 Cashier 向 Stripe 同步信息时,应被视为客户“姓名”的属性。

1/**
2 * Get the customer name that should be synced to Stripe.
3 */
4public function stripeName(): string|null
5{
6 return $this->company_name;
7}

同样,你可以覆盖 stripeEmailstripePhone(最多 20 个字符)、stripeAddressstripePreferredLocales 方法。当 更新 Stripe 客户对象 时,这些方法会将信息同步到对应的客户参数。如果你想完全控制客户信息同步过程,可以覆盖 syncStripeCustomerDetails 方法。

结算门户 (Billing Portal)

Stripe 提供了一种 设置结算门户的简便方法,以便客户可以管理其订阅、支付方式并查看账单历史记录。你可以通过控制器或路由调用可计费模型上的 redirectToBillingPortal 方法,将用户重定向到结算门户。

1use Illuminate\Http\Request;
2 
3Route::get('/billing-portal', function (Request $request) {
4 return $request->user()->redirectToBillingPortal();
5});

默认情况下,当用户完成订阅管理后,他们可以通过 Stripe 结算门户内的链接返回到你应用程序的 home 路由。你可以通过将 URL 作为参数传递给 redirectToBillingPortal 方法来提供用户应返回的自定义 URL。

1use Illuminate\Http\Request;
2 
3Route::get('/billing-portal', function (Request $request) {
4 return $request->user()->redirectToBillingPortal(route('billing'));
5});

如果你想生成指向结算门户的 URL 而不生成 HTTP 重定向响应,可以调用 billingPortalUrl 方法。

1$url = $request->user()->billingPortalUrl(route('billing'));

支付方式

存储支付方式

为了在 Stripe 中创建订阅或执行“一次性”扣款,你需要存储支付方式并从 Stripe 检索其标识符。实现此目的的方法取决于你打算将支付方式用于订阅还是单次扣款,因此我们将在下面探讨这两种情况。

用于订阅的支付方式

当存储客户的信用卡信息以供订阅将来使用时,必须使用 Stripe 的“Setup Intents” API 来安全地收集客户的支付方式详细信息。“Setup Intent”向 Stripe 表明了收费客户支付方式的意图。Cashier 的 Billable Trait 包含了 createSetupIntent 方法,可以轻松创建新的 Setup Intent。你应该从渲染用于收集客户支付方式详情表单的路由或控制器中调用此方法。

1return view('update-payment-method', [
2 'intent' => $user->createSetupIntent()
3]);

创建 Setup Intent 并将其传递给视图后,你应该将其 secret 附加到收集支付方式的元素上。例如,考虑这个“更新支付方式”表单:

1<input id="card-holder-name" type="text">
2 
3<!-- Stripe Elements Placeholder -->
4<div id="card-element"></div>
5 
6<button id="card-button" data-secret="{{ $intent->client_secret }}">
7 Update Payment Method
8</button>

接下来,可以使用 Stripe.js 库将 Stripe Element 附加到表单,并安全地收集客户的支付详情。

1<script src="https://js.stripe.com/v3/"></script>
2 
3<script>
4 const stripe = Stripe('stripe-public-key');
5 
6 const elements = stripe.elements();
7 const cardElement = elements.create('card');
8 
9 cardElement.mount('#card-element');
10</script>

接下来,可以使用 Stripe 的 confirmCardSetup 方法验证卡片并从 Stripe 检索安全的“支付方式标识符”。

1const cardHolderName = document.getElementById('card-holder-name');
2const cardButton = document.getElementById('card-button');
3const clientSecret = cardButton.dataset.secret;
4 
5cardButton.addEventListener('click', async (e) => {
6 const { setupIntent, error } = await stripe.confirmCardSetup(
7 clientSecret, {
8 payment_method: {
9 card: cardElement,
10 billing_details: { name: cardHolderName.value }
11 }
12 }
13 );
14 
15 if (error) {
16 // Display "error.message" to the user...
17 } else {
18 // The card has been verified successfully...
19 }
20});

在卡片通过 Stripe 验证后,你可以将生成的 setupIntent.payment_method 标识符传递给你的 Laravel 应用程序,在那里它会被附加到客户身上。支付方式可以 添加为新的支付方式用于更新默认支付方式。你还可以立即使用该支付方式标识符来 创建新订阅

如果你想了解更多关于 Setup Intents 和收集客户支付详情的信息,请 查看 Stripe 提供的此概述

用于单次扣款的支付方式

当然,在对客户的支付方式进行单次扣款时,我们只需要使用一次支付方式标识符。由于 Stripe 的限制,你不能将客户存储的默认支付方式用于单次扣款。你必须允许客户使用 Stripe.js 库输入他们的支付方式详情。例如,考虑以下表单:

1<input id="card-holder-name" type="text">
2 
3<!-- Stripe Elements Placeholder -->
4<div id="card-element"></div>
5 
6<button id="card-button">
7 Process Payment
8</button>

定义此类表单后,可以使用 Stripe.js 库将 Stripe Element 附加到表单,并安全地收集客户的支付详情。

1<script src="https://js.stripe.com/v3/"></script>
2 
3<script>
4 const stripe = Stripe('stripe-public-key');
5 
6 const elements = stripe.elements();
7 const cardElement = elements.create('card');
8 
9 cardElement.mount('#card-element');
10</script>

接下来,可以使用 Stripe 的 createPaymentMethod 方法验证卡片并从 Stripe 检索安全的“支付方式标识符”。

1const cardHolderName = document.getElementById('card-holder-name');
2const cardButton = document.getElementById('card-button');
3 
4cardButton.addEventListener('click', async (e) => {
5 const { paymentMethod, error } = await stripe.createPaymentMethod(
6 'card', cardElement, {
7 billing_details: { name: cardHolderName.value }
8 }
9 );
10 
11 if (error) {
12 // Display "error.message" to the user...
13 } else {
14 // The card has been verified successfully...
15 }
16});

如果卡片验证成功,你可以将 paymentMethod.id 传递给你的 Laravel 应用程序并处理 单次扣款

检索支付方式

可计费模型实例上的 paymentMethods 方法返回一个 Laravel\Cashier\PaymentMethod 实例集合。

1$paymentMethods = $user->paymentMethods();

默认情况下,此方法将返回所有类型的支付方式。要检索特定类型的支付方式,你可以将 type 作为参数传递给该方法。

1$paymentMethods = $user->paymentMethods('sepa_debit');

要检索客户的默认支付方式,可以使用 defaultPaymentMethod 方法。

1$paymentMethod = $user->defaultPaymentMethod();

你可以使用 findPaymentMethod 方法检索附加到可计费模型的特定支付方式。

1$paymentMethod = $user->findPaymentMethod($paymentMethodId);

支付方式存在性检查

要确定可计费模型是否已将默认支付方式附加到其账户,请调用 hasDefaultPaymentMethod 方法。

1if ($user->hasDefaultPaymentMethod()) {
2 // ...
3}

你可以使用 hasPaymentMethod 方法来确定可计费模型是否至少有一个支付方式附加到其账户。

1if ($user->hasPaymentMethod()) {
2 // ...
3}

此方法将确定可计费模型是否拥有任何支付方式。要确定模型是否存在特定类型的支付方式,你可以将 type 作为参数传递给该方法。

1if ($user->hasPaymentMethod('sepa_debit')) {
2 // ...
3}

更新默认支付方式

updateDefaultPaymentMethod 方法可用于更新客户的默认支付方式信息。此方法接受 Stripe 支付方式标识符,并将新支付方式指定为默认计费支付方式。

1$user->updateDefaultPaymentMethod($paymentMethod);

要将你的默认支付方式信息与 Stripe 中客户的默认支付方式信息同步,可以使用 updateDefaultPaymentMethodFromStripe 方法。

1$user->updateDefaultPaymentMethodFromStripe();

客户的默认支付方式只能用于开发票和创建新订阅。由于 Stripe 的限制,它不能用于单次扣款。

添加支付方式

要添加新的支付方式,你可以调用可计费模型上的 addPaymentMethod 方法,并传入支付方式标识符。

1$user->addPaymentMethod($paymentMethod);

要了解如何检索支付方式标识符,请查阅 支付方式存储文档

删除支付方式

要删除支付方式,你可以对希望删除的 Laravel\Cashier\PaymentMethod 实例调用 delete 方法。

1$paymentMethod->delete();

deletePaymentMethod 方法将从可计费模型中删除特定的支付方式。

1$user->deletePaymentMethod('pm_visa');

deletePaymentMethods 方法将删除可计费模型的所有支付方式信息。

1$user->deletePaymentMethods();

默认情况下,此方法将删除所有类型的支付方式。要删除特定类型的支付方式,你可以将 type 作为参数传递给该方法。

1$user->deletePaymentMethods('sepa_debit');

如果用户有活动订阅,你的应用程序不应允许他们删除其默认支付方式。

订阅

订阅为你的客户提供了设置周期性支付的方式。由 Cashier 管理的 Stripe 订阅支持多订阅价格、订阅数量、试用等功能。

创建订阅

要创建订阅,首先检索你的可计费模型实例,这通常是 App\Models\User 的实例。检索模型实例后,你可以使用 newSubscription 方法来创建模型的订阅。

1use Illuminate\Http\Request;
2 
3Route::post('/user/subscribe', function (Request $request) {
4 $request->user()->newSubscription(
5 'default', 'price_monthly'
6 )->create($request->paymentMethodId);
7 
8 // ...
9});

传递给 newSubscription 方法的第一个参数应是订阅的内部类型。如果你的应用程序只提供单个订阅,你可以称之为 defaultprimary。此订阅类型仅用于内部应用程序使用,不应向用户显示。此外,它不应包含空格,并且在创建订阅后绝对不应更改。第二个参数是用户正在订阅的具体价格。该值应对应于 Stripe 中价格的标识符。

create 方法接受 一个 Stripe 支付方式标识符 或 Stripe PaymentMethod 对象,它将开始订阅并使用可计费模型的 Stripe 客户 ID 和其他相关计费信息更新你的数据库。

直接将支付方式标识符传递给 create 订阅方法,也会自动将其添加到用户的已存储支付方式中。

通过邮件账单收集周期性支付

你可以不自动收集客户的周期性支付,而是指示 Stripe 在每次周期性支付到期时向客户发送账单邮件。然后,客户在收到账单后可以手动支付。通过账单收集周期性支付时,客户无需预先提供支付方式。

1$user->newSubscription('default', 'price_monthly')->createAndSendInvoice();

客户在订阅被取消前支付账单的时间由 days_until_due 选项确定。默认情况下为 30 天;但是,如果你愿意,可以为此选项提供特定值。

1$user->newSubscription('default', 'price_monthly')->createAndSendInvoice([], [
2 'days_until_due' => 30
3]);

数量

如果你希望在创建订阅时为价格设置特定 数量,你应该在创建订阅之前在订阅构建器上调用 quantity 方法。

1$user->newSubscription('default', 'price_monthly')
2 ->quantity(5)
3 ->create($paymentMethod);

详细信息

如果你想指定 Stripe 支持的额外 客户订阅 选项,可以通过将它们作为第二个和第三个参数传递给 create 方法来实现。

1$user->newSubscription('default', 'price_monthly')->create($paymentMethod, [
2 'email' => $email,
3], [
4 'metadata' => ['note' => 'Some extra information.'],
5]);

优惠券

如果你想在创建订阅时应用优惠券,可以使用 withCoupon 方法。

1$user->newSubscription('default', 'price_monthly')
2 ->withCoupon('code')
3 ->create($paymentMethod);

或者,如果你想应用 Stripe 促销代码,可以使用 withPromotionCode 方法。

1$user->newSubscription('default', 'price_monthly')
2 ->withPromotionCode('promo_code_id')
3 ->create($paymentMethod);

给定的促销代码 ID 应是分配给促销代码的 Stripe API ID,而不是面向客户的促销代码。如果你需要根据面向客户的促销代码查找促销代码 ID,可以使用 findPromotionCode 方法。

1// Find a promotion code ID by its customer facing code...
2$promotionCode = $user->findPromotionCode('SUMMERSALE');
3 
4// Find an active promotion code ID by its customer facing code...
5$promotionCode = $user->findActivePromotionCode('SUMMERSALE');

在上面的示例中,返回的 $promotionCode 对象是 Laravel\Cashier\PromotionCode 的实例。该类修饰了底层的 Stripe\PromotionCode 对象。你可以通过调用 coupon 方法检索与促销代码相关的优惠券。

1$coupon = $user->findPromotionCode('SUMMERSALE')->coupon();

优惠券实例允许你确定折扣金额以及优惠券代表的是固定折扣还是百分比折扣。

1if ($coupon->isPercentage()) {
2 return $coupon->percentOff().'%'; // 21.5%
3} else {
4 return $coupon->amountOff(); // $5.99
5}

你还可以检索当前应用于客户或订阅的折扣。

1$discount = $billable->discount();
2 
3$discount = $subscription->discount();

返回的 Laravel\Cashier\Discount 实例修饰了底层的 Stripe\Discount 对象实例。你可以通过调用 coupon 方法检索与此折扣相关的优惠券。

1$coupon = $subscription->discount()->coupon();

如果你想向客户或订阅应用新的优惠券或促销代码,可以通过 applyCouponapplyPromotionCode 方法进行。

1$billable->applyCoupon('coupon_id');
2$billable->applyPromotionCode('promotion_code_id');
3 
4$subscription->applyCoupon('coupon_id');
5$subscription->applyPromotionCode('promotion_code_id');

请记住,你应该使用分配给促销代码的 Stripe API ID,而不是面向客户的促销代码。一次只能向客户或订阅应用一张优惠券或一个促销代码。

有关此主题的更多信息,请参阅 Stripe 关于 优惠券促销代码 的文档。

添加订阅

如果你想向已经有默认支付方式的客户添加订阅,你可以调用订阅构建器上的 add 方法。

1use App\Models\User;
2 
3$user = User::find(1);
4 
5$user->newSubscription('default', 'price_monthly')->add();

从 Stripe 面板创建订阅

你也可以从 Stripe 面板本身创建订阅。执行此操作时,Cashier 将同步新添加的订阅并为其分配 default 类型。要自定义分配给仪表板创建的订阅的订阅类型,请 定义 Webhook 事件处理器

此外,你只能通过 Stripe 面板创建一种类型的订阅。如果你的应用程序提供使用不同类型的多种订阅,则只能通过 Stripe 面板添加一种类型的订阅。

最后,你应该始终确保每个订阅类型仅添加一个有效订阅。如果客户有两个 default 订阅,即使两者都会与你的应用程序数据库同步,Cashier 也只会使用最近添加的订阅。

检查订阅状态

一旦客户订阅了你的应用程序,你可以使用多种方便的方法轻松检查其订阅状态。首先,如果客户有活动订阅,即使订阅目前处于试用期,subscribed 方法也会返回 truesubscribed 方法接受订阅类型作为其第一个参数。

1if ($user->subscribed('default')) {
2 // ...
3}

subscribed 方法也是 路由中间件 的绝佳选择,允许你根据用户的订阅状态过滤对路由和控制器的访问。

1<?php
2 
3namespace App\Http\Middleware;
4 
5use Closure;
6use Illuminate\Http\Request;
7use Symfony\Component\HttpFoundation\Response;
8 
9class EnsureUserIsSubscribed
10{
11 /**
12 * Handle an incoming request.
13 *
14 * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
15 */
16 public function handle(Request $request, Closure $next): Response
17 {
18 if ($request->user() && ! $request->user()->subscribed('default')) {
19 // This user is not a paying customer...
20 return redirect('/billing');
21 }
22 
23 return $next($request);
24 }
25}

如果你想确定用户是否仍处于试用期,可以使用 onTrial 方法。此方法对于确定是否应向用户显示他们仍处于试用期的警告很有用。

1if ($user->subscription('default')->onTrial()) {
2 // ...
3}

subscribedToProduct 方法可用于根据给定的 Stripe 产品标识符确定用户是否订阅了特定产品。在 Stripe 中,产品是价格的集合。在此示例中,我们将确定用户的 default 订阅是否处于应用程序的“premium”产品的活跃订阅状态。给定的 Stripe 产品标识符应对应于 Stripe 面板中你的产品标识符之一。

1if ($user->subscribedToProduct('prod_premium', 'default')) {
2 // ...
3}

通过将数组传递给 subscribedToProduct 方法,你可以确定用户的 default 订阅是否处于应用程序的“basic”或“premium”产品的活跃订阅状态。

1if ($user->subscribedToProduct(['prod_basic', 'prod_premium'], 'default')) {
2 // ...
3}

subscribedToPrice 方法可用于确定客户的订阅是否对应于给定的价格 ID。

1if ($user->subscribedToPrice('price_basic_monthly', 'default')) {
2 // ...
3}

recurring 方法可用于确定用户是否已订阅且不再处于试用期。

1if ($user->subscription('default')->recurring()) {
2 // ...
3}

如果用户有两个相同类型的订阅,subscription 方法将始终返回最新的订阅。例如,用户可能有两条类型为 default 的订阅记录;但是,其中一个订阅可能是已过期的旧订阅,而另一个是当前的活动订阅。最新的订阅将始终被返回,而旧的订阅则保存在数据库中以供历史审查。

已取消的订阅状态

要确定用户是否曾经是活跃订阅者但已取消订阅,可以使用 canceled 方法。

1if ($user->subscription('default')->canceled()) {
2 // ...
3}

你还可以确定用户是否已取消订阅但仍处于“宽限期”内,直到订阅完全到期。例如,如果用户在 3 月 5 日取消了一个原定于 3 月 10 日到期的订阅,则该用户处于 3 月 10 日之前的“宽限期”内。请注意,在此期间 subscribed 方法仍返回 true

1if ($user->subscription('default')->onGracePeriod()) {
2 // ...
3}

要确定用户是否已取消订阅且不再处于“宽限期”内,可以使用 ended 方法。

1if ($user->subscription('default')->ended()) {
2 // ...
3}

未完成和逾期状态

如果订阅在创建后需要二次支付操作,则订阅将被标记为 incomplete。订阅状态存储在 Cashier 的 subscriptions 数据库表的 stripe_status 列中。

同样,如果在切换价格时需要二次支付操作,订阅将被标记为 past_due。当订阅处于这些状态中的任何一个时,除非客户确认了付款,否则它将不会处于活动状态。确定订阅是否存在未完成付款可以使用可计费模型或订阅实例上的 hasIncompletePayment 方法。

1if ($user->hasIncompletePayment('default')) {
2 // ...
3}
4 
5if ($user->subscription('default')->hasIncompletePayment()) {
6 // ...
7}

当订阅有未完成付款时,你应该将用户引导至 Cashier 的支付确认页面,并传入 latestPayment 标识符。你可以使用订阅实例上提供的 latestPayment 方法来检索此标识符。

1<a href="{{ route('cashier.payment', $subscription->latestPayment()->id) }}">
2 Please confirm your payment.
3</a>

如果你希望订阅在 past_dueincomplete 状态下仍被视为活动状态,可以使用 Cashier 提供的 keepPastDueSubscriptionsActivekeepIncompleteSubscriptionsActive 方法。通常,这些方法应在 App\Providers\AppServiceProviderregister 方法中调用。

1use Laravel\Cashier\Cashier;
2 
3/**
4 * Register any application services.
5 */
6public function register(): void
7{
8 Cashier::keepPastDueSubscriptionsActive();
9 Cashier::keepIncompleteSubscriptionsActive();
10}

当订阅处于 incomplete 状态时,在确认付款之前无法进行更改。因此,当订阅处于 incomplete 状态时,swapupdateQuantity 方法将抛出异常。

订阅作用域 (Subscription Scopes)

大多数订阅状态也可用作查询作用域,以便你可以轻松地查询数据库中处于给定状态的订阅。

1// Get all active subscriptions...
2$subscriptions = Subscription::query()->active()->get();
3 
4// Get all of the canceled subscriptions for a user...
5$subscriptions = $user->subscriptions()->canceled()->get();

可用作用域的完整列表如下:

1Subscription::query()->active();
2Subscription::query()->canceled();
3Subscription::query()->ended();
4Subscription::query()->incomplete();
5Subscription::query()->notCanceled();
6Subscription::query()->notOnGracePeriod();
7Subscription::query()->notOnTrial();
8Subscription::query()->onGracePeriod();
9Subscription::query()->onTrial();
10Subscription::query()->pastDue();
11Subscription::query()->recurring();

更改价格

一旦客户订阅了你的应用程序,他们有时可能想要更改为新的订阅价格。要将客户切换到新价格,请将 Stripe 价格的标识符传递给 swap 方法。切换价格时,假设用户希望在之前取消的情况下重新激活其订阅。给定的价格标识符应对应于 Stripe 面板中可用的 Stripe 价格标识符。

1use App\Models\User;
2 
3$user = App\Models\User::find(1);
4 
5$user->subscription('default')->swap('price_yearly');

如果客户处于试用期,试用期将被保留。此外,如果订阅存在“数量”,该数量也将被保留。

如果你想切换价格并取消客户目前正在进行的任何试用期,可以调用 skipTrial 方法。

1$user->subscription('default')
2 ->skipTrial()
3 ->swap('price_yearly');

如果你想切换价格并立即向客户开具账单,而不是等待下一个计费周期,可以使用 swapAndInvoice 方法。

1$user = User::find(1);
2 
3$user->subscription('default')->swapAndInvoice('price_yearly');

按比例计算 (Prorations)

默认情况下,在价格之间切换时,Stripe 会按比例计算费用。noProrate 方法可用于更新订阅价格而不进行按比例计算。

1$user->subscription('default')->noProrate()->swap('price_yearly');

有关订阅按比例计算的更多信息,请查阅 Stripe 文档

swapAndInvoice 方法之前执行 noProrate 方法对按比例计算无效。账单将始终被开具。

订阅数量

有时订阅会受到“数量”的影响。例如,项目管理应用程序可能每月每个项目收取 10 美元。你可以使用 incrementQuantitydecrementQuantity 方法轻松增加或减少订阅数量。

1use App\Models\User;
2 
3$user = User::find(1);
4 
5$user->subscription('default')->incrementQuantity();
6 
7// Add five to the subscription's current quantity...
8$user->subscription('default')->incrementQuantity(5);
9 
10$user->subscription('default')->decrementQuantity();
11 
12// Subtract five from the subscription's current quantity...
13$user->subscription('default')->decrementQuantity(5);

或者,你可以使用 updateQuantity 方法设置特定数量。

1$user->subscription('default')->updateQuantity(10);

noProrate 方法可用于更新订阅数量而不进行按比例计算。

1$user->subscription('default')->noProrate()->updateQuantity(10);

有关订阅数量的更多信息,请查阅 Stripe 文档

包含多个产品的订阅数量

如果你的订阅是 包含多个产品的订阅,你应该将你想要增加或减少数量的价格 ID 作为第二个参数传递给增加/减少方法。

1$user->subscription('default')->incrementQuantity(1, 'price_chat');

包含多个产品的订阅

包含多个产品的订阅 允许你将多个计费产品分配给单个订阅。例如,假设你正在构建一个客户服务“帮助台”应用程序,该应用程序的基本订阅价格为每月 10 美元,但提供每月额外 15 美元的实时聊天附加产品。包含多个产品的订阅信息存储在 Cashier 的 subscription_items 数据库表中。

你可以通过将价格数组作为第二个参数传递给 newSubscription 方法来为给定订阅指定多个产品。

1use Illuminate\Http\Request;
2 
3Route::post('/user/subscribe', function (Request $request) {
4 $request->user()->newSubscription('default', [
5 'price_monthly',
6 'price_chat',
7 ])->create($request->paymentMethodId);
8 
9 // ...
10});

在上面的示例中,客户的 default 订阅中将附加两个价格。这两个价格将在各自的计费周期内收费。如果有必要,你可以使用 quantity 方法为每个价格指定特定数量。

1$user = User::find(1);
2 
3$user->newSubscription('default', ['price_monthly', 'price_chat'])
4 ->quantity(5, 'price_chat')
5 ->create($paymentMethod);

如果你想向现有订阅添加另一个价格,可以调用订阅的 addPrice 方法。

1$user = User::find(1);
2 
3$user->subscription('default')->addPrice('price_chat');

上面的示例将添加新价格,客户将在下一个计费周期内为其付费。如果你想立即向客户收费,可以使用 addPriceAndInvoice 方法。

1$user->subscription('default')->addPriceAndInvoice('price_chat');

如果你想添加具有特定数量的价格,可以将数量作为 addPriceaddPriceAndInvoice 方法的第二个参数传递。

1$user = User::find(1);
2 
3$user->subscription('default')->addPrice('price_chat', 5);

你可以使用 removePrice 方法从订阅中删除价格。

1$user->subscription('default')->removePrice('price_chat');

你不能删除订阅上的最后一个价格。相反,你应该直接取消该订阅。

切换价格

你还可以更改附加到包含多个产品订阅的价格。例如,假设客户有 price_basic 订阅并附加了 price_chat 产品,你想将客户从 price_basic 升级到 price_pro 价格:

1use App\Models\User;
2 
3$user = User::find(1);
4 
5$user->subscription('default')->swap(['price_pro', 'price_chat']);

当执行上面的示例时,带有 price_basic 的底层订阅项会被删除,而带有 price_chat 的项会被保留。此外,会创建一个用于 price_pro 的新订阅项。

你还可以通过将键值对数组传递给 swap 方法来指定订阅项选项。例如,你可能需要指定订阅价格数量。

1$user = User::find(1);
2 
3$user->subscription('default')->swap([
4 'price_pro' => ['quantity' => 5],
5 'price_chat'
6]);

如果你想切换订阅上的单个价格,可以通过订阅项本身使用 swap 方法。如果你想保留订阅其他价格上的所有现有元数据,这种方法特别有用。

1$user = User::find(1);
2 
3$user->subscription('default')
4 ->findItemOrFail('price_basic')
5 ->swap('price_pro');

按比例计算

默认情况下,当向包含多个产品的订阅添加或删除价格时,Stripe 会按比例计算费用。如果你想进行不带按比例计算的价格调整,你应该在你的价格操作链中调用 noProrate 方法。

1$user->subscription('default')->noProrate()->removePrice('price_chat');

数量

如果你想更新单个订阅价格的数量,可以使用 现有的数量方法,并将价格 ID 作为额外参数传递给该方法。

1$user = User::find(1);
2 
3$user->subscription('default')->incrementQuantity(5, 'price_chat');
4 
5$user->subscription('default')->decrementQuantity(3, 'price_chat');
6 
7$user->subscription('default')->updateQuantity(10, 'price_chat');

当订阅有多个价格时,Subscription 模型上的 stripe_pricequantity 属性将为 null。要访问单个价格属性,你应该使用 Subscription 模型上可用的 items 关系。

订阅项 (Subscription Items)

当订阅有多个价格时,它将在数据库的 subscription_items 表中存储多个订阅“项”。你可以通过订阅上的 items 关系访问它们。

1use App\Models\User;
2 
3$user = User::find(1);
4 
5$subscriptionItem = $user->subscription('default')->items->first();
6 
7// Retrieve the Stripe price and quantity for a specific item...
8$stripePrice = $subscriptionItem->stripe_price;
9$quantity = $subscriptionItem->quantity;

你还可以使用 findItemOrFail 方法检索特定价格。

1$user = User::find(1);
2 
3$subscriptionItem = $user->subscription('default')->findItemOrFail('price_chat');

多个订阅

Stripe 允许你的客户同时拥有多个订阅。例如,你可能经营一家健身房,提供游泳订阅和举重订阅,并且每个订阅可能有不同的定价。当然,客户应该能够订阅其中一个或两个计划。

当你的应用程序创建订阅时,你可以将订阅类型提供给 newSubscription 方法。该类型可以是代表用户正在启动的订阅类型的任何字符串。

1use Illuminate\Http\Request;
2 
3Route::post('/swimming/subscribe', function (Request $request) {
4 $request->user()->newSubscription('swimming')
5 ->price('price_swimming_monthly')
6 ->create($request->paymentMethodId);
7 
8 // ...
9});

在此示例中,我们为客户启动了每月游泳订阅。但是,他们可能以后想切换到年度订阅。在调整客户订阅时,我们可以简单地交换 swimming 订阅上的价格。

1$user->subscription('swimming')->swap('price_swimming_yearly');

当然,你也可以完全取消订阅。

1$user->subscription('swimming')->cancel();

基于用量的计费

基于用量的计费 允许你根据产品在计费周期内的使用情况向客户收费。例如,你可以根据客户每月发送的短信或电子邮件数量收费。

要开始使用基于用量的计费,你需要首先在 Stripe 面板中创建一个具有 基于用量的计费模型计量器 (meter) 的新产品。创建计量器后,存储关联的事件名称和计量器 ID,你需要它们来上报和检索使用情况。然后,使用 meteredPrice 方法将计量的价格 ID 添加到客户订阅中。

1use Illuminate\Http\Request;
2 
3Route::post('/user/subscribe', function (Request $request) {
4 $request->user()->newSubscription('default')
5 ->meteredPrice('price_metered')
6 ->create($request->paymentMethodId);
7 
8 // ...
9});

你还可以通过 Stripe Checkout 启动计量订阅。

1$checkout = Auth::user()
2 ->newSubscription('default', [])
3 ->meteredPrice('price_metered')
4 ->checkout();
5 
6return view('your-checkout-view', [
7 'checkout' => $checkout,
8]);

上报用量

当客户使用你的应用程序时,你将向 Stripe 上报他们的用量,以便可以准确计费。要上报计量事件的用量,你可以在 Billable 模型上使用 reportMeterEvent 方法。

1$user = User::find(1);
2 
3$user->reportMeterEvent('emails-sent');

默认情况下,计费周期会增加 1 的“用量数量”。或者,你可以传递特定的“用量”数值来增加客户在计费周期的用量。

1$user = User::find(1);
2 
3$user->reportMeterEvent('emails-sent', quantity: 15);

要检索客户的计量器事件汇总,可以使用 Billable 实例的 meterEventSummaries 方法。

1$user = User::find(1);
2 
3$meterUsage = $user->meterEventSummaries($meterId);
4 
5$meterUsage->first()->aggregated_value // 10

有关计量器事件汇总的更多信息,请参考 Stripe 的 计量器事件汇总对象文档

列出所有计量器,可以使用 Billable 实例的 meters 方法。

1$user = User::find(1);
2 
3$user->meters();

订阅税费

与其手动计算税率,你可以 通过 Stripe Tax 自动计算税费

要指定用户在订阅中支付的税率,你应该在可计费模型上实现 taxRates 方法,并返回包含 Stripe 税率 ID 的数组。你可以在 你的 Stripe 面板 中定义这些税率。

1/**
2 * The tax rates that should apply to the customer's subscriptions.
3 *
4 * @return array<int, string>
5 */
6public function taxRates(): array
7{
8 return ['txr_id'];
9}

taxRates 方法使你能够按客户为基础应用税率,这对于跨多个国家和税率的用户群体很有帮助。

如果你提供包含多个产品的订阅,可以通过在可计费模型上实现 priceTaxRates 方法来为每个价格定义不同的税率。

1/**
2 * The tax rates that should apply to the customer's subscriptions.
3 *
4 * @return array<string, array<int, string>>
5 */
6public function priceTaxRates(): array
7{
8 return [
9 'price_monthly' => ['txr_id'],
10 ];
11}

taxRates 方法仅适用于订阅扣款。如果你使用 Cashier 进行“一次性”扣款,则需要在当时手动指定税率。

同步税率

当更改 taxRates 方法返回的硬编码税率 ID 时,用户的任何现有订阅的税务设置将保持不变。如果你想使用新的 taxRates 值更新现有订阅的税务值,你应该在用户的订阅实例上调用 syncTaxRates 方法。

1$user->subscription('default')->syncTaxRates();

这也将同步包含多个产品的订阅的所有项目税率。如果你的应用程序提供包含多个产品的订阅,则应确保你的可计费模型实现了 上面讨论的 priceTaxRates 方法

税务豁免

Cashier 还提供 isNotTaxExemptisTaxExemptreverseChargeApplies 方法来确定客户是否免税。这些方法将调用 Stripe API 来确定客户的税务豁免状态。

1use App\Models\User;
2 
3$user = User::find(1);
4 
5$user->isTaxExempt();
6$user->isNotTaxExempt();
7$user->reverseChargeApplies();

这些方法在任何 Laravel\Cashier\Invoice 对象上也可用。但是,当在 Invoice 对象上调用时,这些方法将确定账单创建时的豁免状态。

订阅锚点日期

默认情况下,计费周期锚点是订阅创建的日期,或者如果使用了试用期,则是试用结束的日期。如果你想修改计费锚点日期,可以使用 anchorBillingCycleOn 方法。

1use Illuminate\Http\Request;
2 
3Route::post('/user/subscribe', function (Request $request) {
4 $anchor = Carbon::parse('first day of next month');
5 
6 $request->user()->newSubscription('default', 'price_monthly')
7 ->anchorBillingCycleOn($anchor->startOfDay())
8 ->create($request->paymentMethodId);
9 
10 // ...
11});

有关管理订阅计费周期的更多信息,请查阅 Stripe 计费周期文档

取消订阅

要取消订阅,请调用用户订阅上的 cancel 方法。

1$user->subscription('default')->cancel();

当订阅被取消时,Cashier 将自动设置你的 subscriptions 数据库表中的 ends_at 列。此列用于判断 subscribed 方法何时应该开始返回 false

例如,如果客户在 3 月 1 日取消了订阅,但订阅原定于 3 月 5 日到期,则 subscribed 方法将持续返回 true 直到 3 月 5 日。这是因为用户通常被允许继续使用应用程序直到其计费周期结束。

你可以使用 onGracePeriod 方法确定用户是否已取消订阅但仍处于“宽限期”内。

1if ($user->subscription('default')->onGracePeriod()) {
2 // ...
3}

如果你想立即取消订阅,请调用用户订阅上的 cancelNow 方法。

1$user->subscription('default')->cancelNow();

如果你想立即取消订阅并对任何剩余未入账的计量使用量或新的/待处理的比例账单项开具账单,请调用用户订阅上的 cancelNowAndInvoice 方法。

1$user->subscription('default')->cancelNowAndInvoice();

你还可以选择在特定的时刻取消订阅。

1$user->subscription('default')->cancelAt(
2 now()->plus(days: 10)
3);

最后,你应该始终在删除关联的用户模型之前取消用户的订阅。

1$user->subscription('default')->cancelNow();
2 
3$user->delete();

恢复订阅

如果客户已取消其订阅并且你希望恢复它,你可以调用订阅上的 resume 方法。客户必须仍处于其“宽限期”内才能恢复订阅。

1$user->subscription('default')->resume();

如果客户取消了订阅,然后在订阅完全到期前恢复了该订阅,客户将不会立即被收费。相反,他们的订阅将被重新激活,并将在原始计费周期内被收费。

订阅试用

预先提供支付方式

如果你想在预先收集支付方式信息的同时为客户提供试用期,你应该在创建订阅时使用 trialDays 方法。

1use Illuminate\Http\Request;
2 
3Route::post('/user/subscribe', function (Request $request) {
4 $request->user()->newSubscription('default', 'price_monthly')
5 ->trialDays(10)
6 ->create($request->paymentMethodId);
7 
8 // ...
9});

此方法将设置数据库中订阅记录的试用期结束日期,并指示 Stripe 在此日期之前不开始向客户收费。使用 trialDays 方法时,Cashier 将覆盖 Stripe 中为该价格配置的任何默认试用期。

如果客户的订阅在试用结束日期之前没有被取消,他们将在试用期结束后立即被收费,因此请务必通知你的用户他们的试用结束日期。

trialUntil 方法允许你提供一个指定试用期应何时结束的 DateTime 实例。

1use Illuminate\Support\Carbon;
2 
3$user->newSubscription('default', 'price_monthly')
4 ->trialUntil(Carbon::now()->plus(days: 10))
5 ->create($paymentMethod);

你可以使用用户实例上的 onTrial 方法或订阅实例上的 onTrial 方法来确定用户是否处于试用期。以下两个示例是等效的。

1if ($user->onTrial('default')) {
2 // ...
3}
4 
5if ($user->subscription('default')->onTrial()) {
6 // ...
7}

你可以使用 endTrial 方法立即结束订阅试用。

1$user->subscription('default')->endTrial();

要确定现有的试用期是否已过期,你可以使用 hasExpiredTrial 方法。

1if ($user->hasExpiredTrial('default')) {
2 // ...
3}
4 
5if ($user->subscription('default')->hasExpiredTrial()) {
6 // ...
7}

在 Stripe / Cashier 中定义试用天数

你可以选择在 Stripe 面板中定义价格接收多少试用天数,或者始终使用 Cashier 显式传递它们。如果你选择在 Stripe 中定义价格的试用天数,你应该意识到新的订阅(包括过去有过订阅的客户的新订阅)将始终收到试用期,除非你显式调用 skipTrial() 方法。

无需预先提供支付方式

如果你想在不预先收集用户支付方式信息的情况下提供试用期,你可以将用户记录上的 trial_ends_at 列设置为你想要的试用结束日期。这通常在用户注册期间完成。

1use App\Models\User;
2 
3$user = User::create([
4 // ...
5 'trial_ends_at' => now()->plus(days: 10),
6]);

请务必在可计费模型的类定义中为 trial_ends_at 属性添加 日期转换 (date cast)

Cashier 将这种类型的试用称为“通用试用 (generic trial)”,因为它未附加到任何现有订阅。如果当前日期未超过 trial_ends_at 的值,可计费模型实例上的 onTrial 方法将返回 true

1if ($user->onTrial()) {
2 // User is within their trial period...
3}

一旦你准备好为用户创建实际订阅,你可以像往常一样使用 newSubscription 方法。

1$user = User::find(1);
2 
3$user->newSubscription('default', 'price_monthly')->create($paymentMethod);

要检索用户的试用结束日期,可以使用 trialEndsAt 方法。如果用户处于试用期,此方法将返回 Carbon 日期实例,如果不是,则返回 null。如果你想获取除默认订阅之外的特定订阅的试用结束日期,也可以传递可选的订阅类型参数。

1if ($user->onTrial()) {
2 $trialEndsAt = $user->trialEndsAt('main');
3}

如果你想明确知道用户处于“通用”试用期内且尚未创建实际订阅,也可以使用 onGenericTrial 方法。

1if ($user->onGenericTrial()) {
2 // User is within their "generic" trial period...
3}

延长试用

extendTrial 方法允许你在订阅创建后延长订阅的试用期。如果试用期已经过期且客户已经被收取订阅费用,你仍然可以为他们提供延长的试用期。试用期内花费的时间将从客户的下一个账单中扣除。

1use App\Models\User;
2 
3$subscription = User::find(1)->subscription('default');
4 
5// End the trial 7 days from now...
6$subscription->extendTrial(
7 now()->plus(days: 7)
8);
9 
10// Add an additional 5 days to the trial...
11$subscription->extendTrial(
12 $subscription->trial_ends_at->plus(days: 5)
13);

处理 Stripe Webhooks

你可以使用 Stripe CLI 来帮助在本地开发期间测试 Webhook。

Stripe 可以通过 Webhook 通知你的应用程序各种事件。默认情况下,指向 Cashier Webhook 控制器的路由由 Cashier 服务提供者自动注册。此控制器将处理所有传入的 Webhook 请求。

默认情况下,Cashier Webhook 控制器将自动处理取消具有过多失败扣款的订阅(由你的 Stripe 设置定义)、客户更新、客户删除、订阅更新和支付方式变更;然而,正如我们将很快发现的,你可以扩展此控制器以处理任何你喜欢的 Stripe Webhook 事件。

为确保你的应用程序能够处理 Stripe Webhook,请务必在 Stripe 控制面板中配置 Webhook URL。默认情况下,Cashier 的 Webhook 控制器响应 /stripe/webhook URL 路径。你应该在 Stripe 控制面板中启用的所有 Webhook 的完整列表是:

  • customer.subscription.created
  • customer.subscription.updated
  • customer.subscription.deleted
  • customer.updated
  • customer.deleted
  • payment_method.automatically_updated
  • invoice.payment_action_required
  • invoice.payment_succeeded

为方便起见,Cashier 包含了一个 cashier:webhook Artisan 命令。此命令将在 Stripe 中创建一个监听 Cashier 所需所有事件的 Webhook。

1php artisan cashier:webhook

默认情况下,创建的 Webhook 将指向由 APP_URL 环境变量定义的 URL 和 Cashier 包含的 cashier.webhook 路由。如果你想使用不同的 URL,可以在调用该命令时提供 --url 选项。

1php artisan cashier:webhook --url "https://example.com/stripe/webhook"

创建的 Webhook 将使用与你的 Cashier 版本兼容的 Stripe API 版本。如果你想使用不同的 Stripe 版本,可以提供 --api-version 选项。

1php artisan cashier:webhook --api-version="2019-12-03"

创建后,Webhook 将立即生效。如果你希望在准备好之前创建 Webhook 但使其保持禁用状态,可以在调用该命令时提供 --disabled 选项。

1php artisan cashier:webhook --disabled

确保使用 Cashier 包含的 Webhook 签名验证 中间件来保护传入的 Stripe Webhook 请求。

Webhook 与 CSRF 保护

由于 Stripe Webhook 需要绕过 Laravel 的 CSRF 保护,因此你应该确保 Laravel 不会尝试验证传入 Stripe Webhook 的 CSRF 令牌。为此,你应该在应用程序的 bootstrap/app.php 文件中将 stripe/* 从 CSRF 保护中排除。

1->withMiddleware(function (Middleware $middleware): void {
2 $middleware->preventRequestForgery(except: [
3 'stripe/*',
4 ]);
5})

定义 Webhook 事件处理器

Cashier 会自动处理订阅扣款失败的取消以及其他常见的 Stripe Webhook 事件。但是,如果你有其他想要处理的 Webhook 事件,可以通过监听 Cashier 分发的以下事件来实现:

  • Laravel\Cashier\Events\WebhookReceived
  • Laravel\Cashier\Events\WebhookHandled

这两个事件都包含 Stripe Webhook 的完整有效负载。例如,如果你想处理 invoice.payment_succeeded Webhook,可以注册一个 监听器 来处理该事件。

1<?php
2 
3namespace App\Listeners;
4 
5use Laravel\Cashier\Events\WebhookReceived;
6 
7class StripeEventListener
8{
9 /**
10 * Handle received Stripe webhooks.
11 */
12 public function handle(WebhookReceived $event): void
13 {
14 if ($event->payload['type'] === 'invoice.payment_succeeded') {
15 // Handle the incoming event...
16 }
17 }
18}

验证 Webhook 签名

为了保护你的 Webhook,你可以使用 Stripe 的 Webhook 签名。为方便起见,Cashier 自动包含了一个验证传入 Stripe Webhook 请求是否有效的中间件。

要启用 Webhook 验证,请确保在应用程序的 .env 文件中设置了 STRIPE_WEBHOOK_SECRET 环境变量。Webhook secret 可以从你的 Stripe 账户控制面板中检索。

单次扣款

简单扣款

如果你想对客户进行一次性扣款,可以使用可计费模型实例上的 charge 方法。你需要 提供一个支付方式标识符 作为 charge 方法的第二个参数。

1use Illuminate\Http\Request;
2 
3Route::post('/purchase', function (Request $request) {
4 $stripeCharge = $request->user()->charge(
5 100, $request->paymentMethodId
6 );
7 
8 // ...
9});

charge 方法接受一个数组作为其第三个参数,允许你将任何你想要的选项传递给底层的 Stripe 扣款创建过程。有关创建扣款时可用的选项的更多信息,请查阅 Stripe 文档

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

你也可以在没有底层客户或用户的情况下使用 charge 方法。为此,请在你应用程序的可计费模型的新实例上调用 charge 方法。

1use App\Models\User;
2 
3$stripeCharge = (new User)->charge(100, $paymentMethod);

如果扣款失败,charge 方法将抛出异常。如果扣款成功,该方法将返回一个 Laravel\Cashier\Payment 实例。

1try {
2 $payment = $user->charge(100, $paymentMethod);
3} catch (Exception $e) {
4 // ...
5}

charge 方法接受以应用程序所用货币的最小面值单位表示的支付金额。例如,如果客户以美元支付,金额应以美分表示。

通过账单扣款

有时你可能需要进行一次性扣款并为你的客户提供 PDF 账单。invoicePrice 方法正好可以实现这一点。例如,让我们为客户购买的五件新衬衫开具账单:

1$user->invoicePrice('price_tshirt', 5);

该账单将立即从用户的默认支付方式中扣除。invoicePrice 方法也接受一个数组作为其第三个参数。此数组包含账单项目的计费选项。该方法接受的第四个参数也是一个数组,应包含账单本身的计费选项。

1$user->invoicePrice('price_tshirt', 5, [
2 'discounts' => [
3 ['coupon' => 'SUMMER21SALE']
4 ],
5], [
6 'default_tax_rates' => ['txr_id'],
7]);

invoicePrice 类似,你可以使用 tabPrice 方法通过将多个项目(每个账单最多 250 个项目)添加到客户的“标签页”并随后向客户开具账单,来为多个项目创建一次性扣款。例如,我们可以为客户的五件衬衫和两个马克杯开具账单:

1$user->tabPrice('price_tshirt', 5);
2$user->tabPrice('price_mug', 2);
3$user->invoice();

或者,你可以使用 invoiceFor 方法对客户的默认支付方式进行“一次性”扣款。

1$user->invoiceFor('One Time Fee', 500);

虽然可以使用 invoiceFor 方法,但建议使用带有预定义价格的 invoicePricetabPrice 方法。通过这样做,你将在 Stripe 面板中获得有关按产品销售额的更好分析和数据。

invoiceinvoicePriceinvoiceFor 方法将创建一条 Stripe 账单,该账单将在计费失败时重试。如果你不希望账单在扣款失败后重试,则需要在第一次扣款失败后使用 Stripe API 关闭它们。

创建支付意图 (Payment Intents)

你可以通过在可计费模型实例上调用 pay 方法来创建新的 Stripe 支付意图。调用此方法将创建一个封装在 Laravel\Cashier\Payment 实例中的支付意图。

1use Illuminate\Http\Request;
2 
3Route::post('/pay', function (Request $request) {
4 $payment = $request->user()->pay(
5 $request->get('amount')
6 );
7 
8 return $payment->client_secret;
9});

创建支付意图后,你可以将 client secret 返回到应用程序的前端,以便用户可以在浏览器中完成支付。要阅读有关使用 Stripe 支付意图构建完整支付流程的更多信息,请查阅 Stripe 文档

使用 pay 方法时,你的 Stripe 面板中启用的默认支付方式将可供客户使用。或者,如果你只想允许使用某些特定的支付方式,可以使用 payWith 方法。

1use Illuminate\Http\Request;
2 
3Route::post('/pay', function (Request $request) {
4 $payment = $request->user()->payWith(
5 $request->get('amount'), ['card', 'bancontact']
6 );
7 
8 return $payment->client_secret;
9});

paypayWith 方法接受以应用程序所用货币的最小面值单位表示的支付金额。例如,如果客户以美元支付,金额应以美分表示。

退款

如果你需要退还 Stripe 扣款,可以使用 refund 方法。此方法接受 Stripe 支付意图 ID 作为其第一个参数。

1$payment = $user->charge(100, $paymentMethodId);
2 
3$user->refund($payment->id);

账单 (Invoices)

检索账单

你可以使用 invoices 方法轻松检索可计费模型账单的数组。invoices 方法返回 Laravel\Cashier\Invoice 实例的集合。

1$invoices = $user->invoices();

如果你想在结果中包含待处理的账单,可以使用 invoicesIncludingPending 方法。

1$invoices = $user->invoicesIncludingPending();

你可以使用 findInvoice 方法通过其 ID 检索特定账单。

1$invoice = $user->findInvoice($invoiceId);

显示账单信息

列出客户账单时,可以使用账单的方法来显示相关的账单信息。例如,你可能希望在表格中列出每个账单,允许用户轻松下载其中任何一个。

1<table>
2 @foreach ($invoices as $invoice)
3 <tr>
4 <td>{{ $invoice->date()->toFormattedDateString() }}</td>
5 <td>{{ $invoice->total() }}</td>
6 <td><a href="/user/invoice/{{ $invoice->id }}">Download</a></td>
7 </tr>
8 @endforeach
9</table>

即将到来的账单

要检索客户即将到来的账单,可以使用 upcomingInvoice 方法。

1$invoice = $user->upcomingInvoice();

同样,如果客户有多个订阅,你也可以检索特定订阅即将到来的账单。

1$invoice = $user->subscription('default')->upcomingInvoice();

预览订阅账单

使用 previewInvoice 方法,你可以在进行价格更改之前预览账单。这将允许你确定在进行给定价格更改后客户的账单是什么样子。

1$invoice = $user->subscription('default')->previewInvoice('price_yearly');

你可以将价格数组传递给 previewInvoice 方法,以便预览包含多个新价格的账单。

1$invoice = $user->subscription('default')->previewInvoice(['price_yearly', 'price_metered']);

生成 PDF 账单

在生成 PDF 账单之前,你应该使用 Composer 安装 Dompdf 库,它是 Cashier 的默认账单渲染器。

1composer require dompdf/dompdf

在路由或控制器中,你可以使用 downloadInvoice 方法来生成给定账单的 PDF 下载。此方法将自动生成下载账单所需的正确 HTTP 响应。

1use Illuminate\Http\Request;
2 
3Route::get('/user/invoice/{invoice}', function (Request $request, string $invoiceId) {
4 return $request->user()->downloadInvoice($invoiceId);
5});

默认情况下,账单上的所有数据均来自存储在 Stripe 中的客户和账单数据。文件名基于你的 app.name 配置值。但是,你可以通过将数组作为第二个参数传递给 downloadInvoice 方法来自定义其中一些数据。此数组允许你自定义诸如公司和产品详细信息等信息。

1return $request->user()->downloadInvoice($invoiceId, [
2 'vendor' => 'Your Company',
3 'product' => 'Your Product',
4 'street' => 'Main Str. 1',
5 'location' => '2000 Antwerp, Belgium',
6 'phone' => '+32 499 00 00 00',
7 'email' => '[email protected]',
8 'url' => 'https://example.com',
9 'vendorVat' => 'BE123456789',
10]);

downloadInvoice 方法还允许通过其第三个参数使用自定义文件名。此文件名将自动添加 .pdf 后缀。

1return $request->user()->downloadInvoice($invoiceId, [], 'my-invoice');

自定义账单渲染器

Cashier 也可以使用自定义账单渲染器。默认情况下,Cashier 使用 DompdfInvoiceRenderer 实现,它利用 dompdf PHP 库来生成账单。但是,你可以通过实现 Laravel\Cashier\Contracts\InvoiceRenderer 接口来使用任何你想要的渲染器。例如,你可能希望通过对第三方 PDF 渲染服务的 API 调用来渲染 PDF 账单。

1use Illuminate\Support\Facades\Http;
2use Laravel\Cashier\Contracts\InvoiceRenderer;
3use Laravel\Cashier\Invoice;
4 
5class ApiInvoiceRenderer implements InvoiceRenderer
6{
7 /**
8 * Render the given invoice and return the raw PDF bytes.
9 */
10 public function render(Invoice $invoice, array $data = [], array $options = []): string
11 {
12 $html = $invoice->view($data)->render();
13 
14 return Http::get('https://example.com/html-to-pdf', ['html' => $html])->get()->body();
15 }
16}

实现账单渲染器契约后,你应该在应用程序的 config/cashier.php 配置文件中更新 cashier.invoices.renderer 配置值。此配置值应设置为你的自定义渲染器实现的类名。

Checkout (结账)

Cashier Stripe 还提供对 Stripe Checkout 的支持。Stripe Checkout 通过提供预构建的托管支付页面,省去了实施自定义支付页面所带来的麻烦。

以下文档包含有关如何开始使用 Cashier 和 Stripe Checkout 的信息。要了解有关 Stripe Checkout 的更多信息,你还应该考虑查阅 Stripe 关于 Checkout 的文档

产品结账

你可以使用可计费模型上的 checkout 方法,为已在 Stripe 面板中创建的现有产品执行结账。checkout 方法将启动一个新的 Stripe Checkout 会话。默认情况下,你需要传递 Stripe 价格 ID。

1use Illuminate\Http\Request;
2 
3Route::get('/product-checkout', function (Request $request) {
4 return $request->user()->checkout('price_tshirt');
5});

如果需要,还可以指定产品数量。

1use Illuminate\Http\Request;
2 
3Route::get('/product-checkout', function (Request $request) {
4 return $request->user()->checkout(['price_tshirt' => 15]);
5});

当客户访问此路由时,他们将被重定向到 Stripe 的结账页面。默认情况下,当用户成功完成或取消购买时,他们将被重定向到你的 home 路由位置,但你可以使用 success_urlcancel_url 选项指定自定义回调 URL。

1use Illuminate\Http\Request;
2 
3Route::get('/product-checkout', function (Request $request) {
4 return $request->user()->checkout(['price_tshirt' => 1], [
5 'success_url' => route('your-success-route'),
6 'cancel_url' => route('your-cancel-route'),
7 ]);
8});

定义 success_url 结账选项时,你可以指示 Stripe 在调用你的 URL 时将结账会话 ID 作为查询字符串参数添加。为此,请将字面字符串 {CHECKOUT_SESSION_ID} 添加到你的 success_url 查询字符串中。Stripe 会将此占位符替换为实际的结账会话 ID。

1use Illuminate\Http\Request;
2use Stripe\Checkout\Session;
3use Stripe\Customer;
4 
5Route::get('/product-checkout', function (Request $request) {
6 return $request->user()->checkout(['price_tshirt' => 1], [
7 'success_url' => route('checkout-success').'?session_id={CHECKOUT_SESSION_ID}',
8 'cancel_url' => route('checkout-cancel'),
9 ]);
10});
11 
12Route::get('/checkout-success', function (Request $request) {
13 $checkoutSession = $request->user()->stripe()->checkout->sessions->retrieve($request->get('session_id'));
14 
15 return view('checkout.success', ['checkoutSession' => $checkoutSession]);
16})->name('checkout-success');

促销代码

默认情况下,Stripe Checkout 不允许 用户兑换促销代码。幸运的是,有一种简单的方法可以在你的结账页面启用它们。为此,可以调用 allowPromotionCodes 方法。

1use Illuminate\Http\Request;
2 
3Route::get('/product-checkout', function (Request $request) {
4 return $request->user()
5 ->allowPromotionCodes()
6 ->checkout('price_tshirt');
7});

单次扣款结账

你还可以为尚未在 Stripe 面板中创建的临时产品进行简单扣款。为此,你可以在可计费模型上使用 checkoutCharge 方法,并向其传递应付金额、产品名称和可选的数量。当客户访问此路由时,他们将被重定向到 Stripe 的结账页面。

1use Illuminate\Http\Request;
2 
3Route::get('/charge-checkout', function (Request $request) {
4 return $request->user()->checkoutCharge(1200, 'T-Shirt', 5);
5});

使用 checkoutCharge 方法时,Stripe 总是会在你的 Stripe 面板中创建一个新产品和价格。因此,我们建议你预先在 Stripe 面板中创建产品,并改用 checkout 方法。

订阅结账

使用 Stripe Checkout 进行订阅需要你在 Stripe 面板中启用 customer.subscription.created Webhook。此 Webhook 将在你的数据库中创建订阅记录并存储所有相关的订阅项。

你也可以使用 Stripe Checkout 来启动订阅。在使用 Cashier 的订阅构建器方法定义你的订阅后,可以调用 checkout 方法。当客户访问此路由时,他们将被重定向到 Stripe 的结账页面。

1use Illuminate\Http\Request;
2 
3Route::get('/subscription-checkout', function (Request $request) {
4 return $request->user()
5 ->newSubscription('default', 'price_monthly')
6 ->checkout();
7});

就像产品结账一样,你可以自定义成功和取消 URL。

1use Illuminate\Http\Request;
2 
3Route::get('/subscription-checkout', function (Request $request) {
4 return $request->user()
5 ->newSubscription('default', 'price_monthly')
6 ->checkout([
7 'success_url' => route('your-success-route'),
8 'cancel_url' => route('your-cancel-route'),
9 ]);
10});

当然,你也可以为订阅结账启用促销代码。

1use Illuminate\Http\Request;
2 
3Route::get('/subscription-checkout', function (Request $request) {
4 return $request->user()
5 ->newSubscription('default', 'price_monthly')
6 ->allowPromotionCodes()
7 ->checkout();
8});

遗憾的是,Stripe Checkout 在启动订阅时不支持所有订阅计费选项。在订阅构建器上使用 anchorBillingCycleOn 方法、设置按比例计算行为或设置支付行为在 Stripe Checkout 会话期间不会有任何效果。请查阅 Stripe Checkout Session API 文档 以了解哪些参数可用。

Stripe Checkout 和试用期

当然,你可以在构建将使用 Stripe Checkout 完成的订阅时定义试用期。

1$checkout = Auth::user()->newSubscription('default', 'price_monthly')
2 ->trialDays(3)
3 ->checkout();

但是,试用期必须至少为 48 小时,这是 Stripe Checkout 支持的最短试用时间。

订阅和 Webhook

请记住,Stripe 和 Cashier 通过 Webhook 更新订阅状态,因此客户输入支付信息返回应用程序后,订阅可能尚未处于活动状态。要处理此场景,你可能希望显示一条消息,告知用户他们的付款或订阅正在处理中。

收集税务 ID

Checkout 还支持收集客户的税务 ID。要在结账会话中启用此功能,请在创建会话时调用 collectTaxIds 方法。

1$checkout = $user->collectTaxIds()->checkout('price_tshirt');

调用此方法后,客户将看到一个新的复选框,允许他们指示是否以公司身份购买。如果是,他们将有机会提供他们的税务 ID 号码。

如果你已经在应用程序的服务提供者中配置了 自动税务收集,那么此功能将自动启用,无需调用 collectTaxIds 方法。

访客结账

使用 Checkout::guest 方法,你可以为应用程序中没有“账户”的访客启动结账会话。

1use Illuminate\Http\Request;
2use Laravel\Cashier\Checkout;
3 
4Route::get('/product-checkout', function (Request $request) {
5 return Checkout::guest()->create('price_tshirt', [
6 'success_url' => route('your-success-route'),
7 'cancel_url' => route('your-cancel-route'),
8 ]);
9});

与为现有用户创建结账会话类似,你可以利用 Laravel\Cashier\CheckoutBuilder 实例上可用的额外方法来自定义访客结账会话。

1use Illuminate\Http\Request;
2use Laravel\Cashier\Checkout;
3 
4Route::get('/product-checkout', function (Request $request) {
5 return Checkout::guest()
6 ->withPromotionCode('promo-code')
7 ->create('price_tshirt', [
8 'success_url' => route('your-success-route'),
9 'cancel_url' => route('your-cancel-route'),
10 ]);
11});

访客结账完成后,Stripe 可以分发一个 checkout.session.completed Webhook 事件,因此请务必 配置你的 Stripe Webhook 以实际将此事件发送到你的应用程序。在 Stripe 面板中启用 Webhook 后,你就可以 使用 Cashier 处理 Webhook。Webhook 有效负载中包含的对象将是一个 结账对象,你可以对其进行检查以履行客户的订单。

处理支付失败

有时,订阅或单次扣款的支付可能会失败。当发生这种情况时,Cashier 将会抛出一个 Laravel\Cashier\Exceptions\IncompletePayment 异常来通知你。捕获此异常后,你有两种处理方式。

首先,你可以将客户重定向到 Cashier 自带的专用支付确认页面。此页面已经拥有通过 Cashier 服务提供者注册的相关路由。因此,你可以捕获 IncompletePayment 异常并将用户重定向到该支付确认页面。

1use Laravel\Cashier\Exceptions\IncompletePayment;
2 
3try {
4 $subscription = $user->newSubscription('default', 'price_monthly')
5 ->create($paymentMethod);
6} catch (IncompletePayment $exception) {
7 return redirect()->route(
8 'cashier.payment',
9 [$exception->payment->id, 'redirect' => route('home')]
10 );
11}

在支付确认页面上,客户将被提示重新输入信用卡信息,并执行 Stripe 所需的任何额外操作,例如“3D Secure”验证。确认支付后,用户将被重定向到上述 redirect 参数指定的 URL。重定向时,URL 将会添加 message(字符串)和 success(整数)查询字符串变量。目前支付页面支持以下支付方式类型:

  • 信用卡 (Credit Cards)
  • 支付宝 (Alipay)
  • Bancontact
  • BECS 直接借记 (BECS Direct Debit)
  • EPS
  • Giropay
  • iDEAL
  • SEPA 直接借记 (SEPA Direct Debit)

另外,你也可以让 Stripe 为你处理支付确认。在这种情况下,无需重定向到支付确认页面,你可以在 Stripe 仪表板中 设置 Stripe 的自动账单邮件。不过,如果捕获到 IncompletePayment 异常,你仍然应该告知用户,他们将收到一封包含进一步支付确认说明的电子邮件。

在使用 Billable trait 的模型上,以下方法可能会抛出支付异常:chargeinvoiceForinvoice。在处理订阅时,SubscriptionBuilder 上的 create 方法,以及 SubscriptionSubscriptionItem 模型上的 incrementAndInvoiceswapAndInvoice 方法也可能会抛出支付不完整异常。

你可以使用可计费模型或订阅实例上的 hasIncompletePayment 方法来确定现有订阅是否存在支付不完整的情况。

1if ($user->hasIncompletePayment('default')) {
2 // ...
3}
4 
5if ($user->subscription('default')->hasIncompletePayment()) {
6 // ...
7}

你可以通过检查异常实例上的 payment 属性来获取支付不完整的具体状态。

1use Laravel\Cashier\Exceptions\IncompletePayment;
2 
3try {
4 $user->charge(1000, 'pm_card_threeDSecure2Required');
5} catch (IncompletePayment $exception) {
6 // Get the payment intent status...
7 $exception->payment->status;
8 
9 // Check specific conditions...
10 if ($exception->payment->requiresPaymentMethod()) {
11 // ...
12 } elseif ($exception->payment->requiresConfirmation()) {
13 // ...
14 }
15}

确认支付

某些支付方式需要额外数据才能确认支付。例如,SEPA 支付方式在支付过程中需要额外的“授权 (mandate)”数据。你可以使用 withPaymentConfirmationOptions 方法将这些数据提供给 Cashier。

1$subscription->withPaymentConfirmationOptions([
2 'mandate_data' => '...',
3])->swap('price_xxx');

你可以查阅 Stripe API 文档 以查看确认支付时接受的所有选项。

强客户身份验证 (Strong Customer Authentication)

如果你的企业或你的客户位于欧洲,则需要遵守欧盟的强客户身份验证 (SCA) 法规。这些法规由欧盟于 2019 年 9 月实施,旨在防止支付欺诈。幸运的是,Stripe 和 Cashier 已为构建符合 SCA 标准的应用程序做好了准备。

在开始之前,请查阅 Stripe 关于 PSD2 和 SCA 的指南 以及他们 关于新 SCA API 的文档

需要额外确认的支付

SCA 法规通常需要额外的验证来确认和处理支付。当发生这种情况时,Cashier 将抛出一个 Laravel\Cashier\Exceptions\IncompletePayment 异常,通知你需要额外的验证。关于如何处理这些异常的更多信息,可以在 处理支付失败 的文档中找到。

由 Stripe 或 Cashier 提供的支付确认界面可能会根据特定银行或发卡机构的支付流程进行调整,并可能包含额外的卡片验证、临时小额扣款、独立的设备认证或其他形式的验证。

不完整与逾期状态

当支付需要额外确认时,订阅将保持在 incompletepast_due 状态,具体显示在其 stripe_status 数据库列中。一旦支付确认完成,且你的应用程序通过 Webhook 从 Stripe 收到完成通知,Cashier 将自动激活客户的订阅。

有关 incompletepast_due 状态的更多信息,请参阅 我们关于这些状态的补充文档

非会话支付通知 (Off-Session Payment Notifications)

由于 SCA 法规要求客户即使在订阅处于活跃状态时,也偶尔需要验证其支付详情,因此当需要进行非会话支付确认时,Cashier 可以向客户发送通知。例如,当订阅续订时可能会发生这种情况。你可以通过将 CASHIER_PAYMENT_NOTIFICATION 环境变量设置为通知类来启用 Cashier 的支付通知。默认情况下,此通知是禁用的。当然,Cashier 包含了一个可用于此目的的通知类,但如果需要,你也可以自由提供自己的通知类。

1CASHIER_PAYMENT_NOTIFICATION=Laravel\Cashier\Notifications\ConfirmPayment

为确保非会话支付确认通知能够送达,请验证你的应用程序是否已 配置了 Stripe Webhook,并且在你的 Stripe 仪表板中启用了 invoice.payment_action_required Webhook。此外,你的 Billable 模型还应使用 Laravel 的 Illuminate\Notifications\Notifiable trait。

即使客户正在手动进行需要额外确认的支付时,通知也会被发送。遗憾的是,Stripe 无法知道支付是手动完成的还是“非会话”完成的。但是,如果客户在确认支付后访问支付页面,他们只会看到“支付成功”的消息。客户不会被允许意外地确认同一笔支付两次,从而避免产生意外的二次扣款。

Stripe SDK

Cashier 的许多对象都是 Stripe SDK 对象的包装器。如果你想直接与 Stripe 对象交互,可以使用 asStripe 方法方便地获取它们。

1$stripeSubscription = $subscription->asStripeSubscription();
2 
3$stripeSubscription->application_fee_percent = 5;
4 
5$stripeSubscription->save();

你也可以使用 updateStripeSubscription 方法直接更新 Stripe 订阅。

1$subscription->updateStripeSubscription(['application_fee_percent' => 5]);

如果你想直接使用 Stripe\StripeClient 客户端,可以调用 Cashier 类上的 stripe 方法。例如,你可以使用此方法访问 StripeClient 实例,并从你的 Stripe 账户中获取价格列表。

1use Laravel\Cashier\Cashier;
2 
3$prices = Cashier::stripe()->prices->all();

测试

在测试使用 Cashier 的应用程序时,你可以模拟对 Stripe API 的实际 HTTP 请求;但这要求你部分重现 Cashier 本身的行为。因此,我们建议让你的测试访问真实的 Stripe API。虽然这样速度较慢,但它能让你更有把握确保应用程序按预期运行,且任何缓慢的测试都可以放置在它们自己的 Pest / PHPUnit 测试组中。

测试时请记住,Cashier 本身已经拥有完善的测试套件,因此你应该只专注于测试你自己应用程序的订阅和支付流程,而不是测试每一个底层的 Cashier 行为。

要开始测试,请将你的 Stripe 密钥的 testing 版本添加到 phpunit.xml 文件中:

1<env name="STRIPE_SECRET" value="sk_test_<your-key>"/>

现在,每当你在测试中与 Cashier 交互时,它都会向你的 Stripe 测试环境发送真实的 API 请求。为方便起见,你应该预先在你的 Stripe 测试账户中填充可在测试期间使用的订阅/价格。

为了测试各种计费场景,例如信用卡拒绝和失败,你可以使用 Stripe 提供的各种 测试卡号和令牌