跳至内容

Laravel 计费器(Stripe)

介绍

Laravel 计费器 StripeStripe 的订阅计费服务提供了一个表达性、流畅的接口。它处理了您可能不愿意编写的大部分样板订阅计费代码。除了基本的订阅管理,计费器还可以处理优惠券、切换订阅、订阅“数量”、取消宽限期,甚至生成发票 PDF。

升级计费器

升级到新版本的计费器时,务必仔细阅读 升级指南

exclamation

为了防止出现重大变化,计费器使用固定的 Stripe API 版本。计费器 15 使用 Stripe API 版本 2023-10-16。Stripe API 版本将在次要版本中更新,以便利用 Stripe 的新功能和改进。

安装

首先,使用 Composer 包管理器安装 Stripe 的计费器包

composer require laravel/cashier

安装包后,使用 vendor:publish Artisan 命令发布计费器的迁移

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

然后,迁移您的数据库

php artisan migrate

计费器的迁移将向您的 users 表添加几列。它们还将创建一个新的 subscriptions 表来保存您所有客户的订阅,以及一个 subscription_items 表,用于包含多个价格的订阅。

如果需要,您还可以使用 vendor:publish Artisan 命令发布计费器的配置文件

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

最后,为了确保计费器正确处理所有 Stripe 事件,请记住 配置计费器的 Webhook 处理

exclamation

Stripe 建议用于存储 Stripe 标识符的任何列都应区分大小写。因此,在使用 MySQL 时,应确保 stripe_id 列的列排序设置为 utf8_bin。有关这方面的更多信息,请参阅 Stripe 文档

配置

可计费模型

在使用计费器之前,请在您的可计费模型定义中添加 Billable 特性。通常,这将是 App\Models\User 模型。此特性提供了各种方法,使您能够执行常见的计费任务,例如创建订阅、应用优惠券和更新支付方式信息

use Laravel\Cashier\Billable;
 
class User extends Authenticatable
{
use Billable;
}

计费器假设您的可计费模型将是 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);
}
exclamation

如果您使用的是除 Laravel 提供的 App\Models\User 模型之外的模型,则需要发布和更改 提供的计费器迁移,使其与您的替代模型的表名相匹配。

API 密钥

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

STRIPE_KEY=your-stripe-key
STRIPE_SECRET=your-stripe-secret
STRIPE_WEBHOOK_SECRET=your-stripe-webhook-secret
exclamation

您应该确保在应用程序的 .env 文件中定义了 STRIPE_WEBHOOK_SECRET 环境变量,因为此变量用于确保传入的 Webhook 实际上来自 Stripe。

货币配置

计费器的默认货币是美元(USD)。您可以通过在应用程序的 .env 文件中设置 CASHIER_CURRENCY 环境变量来更改默认货币

CASHIER_CURRENCY=eur

除了配置计费器的货币,您还可以指定一个区域设置,用于在发票上格式化货币值以进行显示。在内部,计费器利用 PHP 的 NumberFormatter 来设置货币区域设置

CASHIER_CURRENCY_LOCALE=nl_BE
exclamation

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

税务配置

感谢 Stripe 税务,可以自动计算 Stripe 生成的所有发票的税务。您可以通过在应用程序的 App\Providers\AppServiceProvider 类的 boot 方法中调用 calculateTaxes 方法来启用自动税务计算

use Laravel\Cashier\Cashier;
 
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Cashier::calculateTaxes();
}

启用税务计算后,生成的任何新订阅和任何一次性发票都将获得自动税务计算。

为了使此功能正常工作,您的客户的计费详细信息(例如客户姓名、地址和税号)需要与 Stripe 同步。您可以使用计费器提供的 客户数据同步税号 方法来完成此操作。

日志记录

计费器允许您指定在记录 Stripe 致命错误时使用的日志通道。您可以通过在应用程序的 .env 文件中定义 CASHIER_LOGGER 环境变量来指定日志通道

CASHIER_LOGGER=stack

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

使用自定义模型

您可以通过定义自己的模型并扩展相应的计费器模型来扩展计费器内部使用的模型

use Laravel\Cashier\Subscription as CashierSubscription;
 
class Subscription extends CashierSubscription
{
// ...
}

定义模型后,您可以通过 Laravel\Cashier\Cashier 类指示计费器使用您的自定义模型。通常,您应该在应用程序的 App\Providers\AppServiceProvider 类的 boot 方法中告知计费器您的自定义模型

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);
}

快速入门

销售产品

lightbulb

在使用 Stripe 结账之前,您应该在您的 Stripe 仪表板中定义具有固定价格的产品。此外,您应该 配置计费器的 Webhook 处理

通过您的应用程序提供产品和订阅计费可能令人生畏。但是,借助计费器和 Stripe 结账,您可以轻松构建现代、强大的支付集成。

要向客户收取非经常性的一次性产品费用,我们将使用 Cashier 将客户引导至 Stripe 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 时,“价格”指的是 特定产品的定义价格

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

向 Stripe Checkout 提供元数据

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

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

use App\Models\Cart;
use App\Models\Order;
use Illuminate\Http\Request;
 
Route::get('/cart/{cart}/checkout', function (Request $request, Cart $cart) {
$order = Order::create([
'cart_id' => $cart->id,
'price_ids' => $cart->price_ids,
'status' => 'incomplete',
]);
 
return $request->user()->checkout($order->price_ids, [
'success_url' => route('checkout-success').'?session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => route('checkout-cancel'),
'metadata' => ['order_id' => $order->id],
]);
})->name('checkout');

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

有关 Checkout 会话对象中包含的数据的更多信息,请参阅 Stripe 文档。

销售订阅

lightbulb

在使用 Stripe 结账之前,您应该在您的 Stripe 仪表板中定义具有固定价格的产品。此外,您应该 配置计费器的 Webhook 处理

通过您的应用程序提供产品和订阅计费可能令人生畏。但是,借助计费器和 Stripe 结账,您可以轻松构建现代、强大的支付集成。

要了解如何使用 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。要了解他们的订阅何时真正开始(因为某些支付方式需要几秒钟才能处理),我们还需要 配置 Cashier 的 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

构建订阅中间件

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

<?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 的 客户账单门户,该门户提供托管用户界面,允许客户下载发票、更新其付款方式以及更改订阅计划。

首先,在您的应用程序中定义一个链接或按钮,将用户重定向到 Laravel 路由,我们将使用该路由启动账单门户会话。

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

接下来,让我们定义启动 Stripe 客户账单门户会话并将用户重定向到门户的路由。redirectToBillingPortal 方法接受用户退出门户时应返回的 URL。

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

只要您配置了 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 数组,以传入 Stripe API 支持的任何其他 客户创建参数

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

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

$stripeCustomer = $user->asStripeCustomer();

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

$stripeCustomer = $user->createOrGetStripeCustomer();

更新客户

有时,您可能希望使用其他信息直接更新 Stripe 客户。您可以使用 updateStripeCustomer 方法来实现此目的。此方法接受一个数组,其中包含 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 方法可用于检索分配给客户的所有 税号 作为集合。

$taxIds = $user->taxIds();

您还可以通过其标识符检索客户的特定税号。

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

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

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

createTaxId 方法将立即将增值税 ID 添加到客户的帐户。Stripe 也会验证增值税 ID;但是,这是一个异步过程。您可以通过订阅 customer.tax_id.updated webhook 事件并检查 增值税 ID 的 verification 参数 来接收验证更新通知。有关处理 webhook 的更多信息,请参阅 有关定义 webhook 处理程序的文档

您可以使用 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;
}

同样,您可以覆盖 stripeEmailstripePhonestripeAddressstripePreferredLocales 方法。当 更新 Stripe 客户对象 时,这些方法会将信息同步到相应的客户参数。如果您想完全控制客户信息同步过程,可以覆盖 syncStripeCustomerDetails 方法。

计费门户

Stripe 提供了 一种简单的方法来设置账单门户,以便您的客户可以管理他们的订阅、付款方式并查看他们的账单历史记录。您可以通过从控制器或路由中的可计费模型调用 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'));
});

如果您想在不生成 HTTP 重定向响应的情况下生成账单门户的 URL,可以调用 billingPortalUrl 方法。

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

支付方式

存储支付方式

为了使用 Stripe 创建订阅或执行“一次性”收费,您需要存储付款方式并从 Stripe 检索其标识符。用于完成此操作的方法取决于您是否打算将付款方式用于订阅或单次收费,因此我们将分别在下面进行检查。

订阅的付款方式

当存储客户的信用卡信息以备订阅将来使用时,必须使用 Stripe 的“设置意向”API 安全地收集客户的付款方式详细信息。“设置意向”表示 Stripe 意图向客户的付款方式收费。收银员的Billable 特性包含createSetupIntent 方法,可以轻松创建新的设置意向。您应该从将渲染收集客户付款方式详细信息的表单的路由或控制器中调用此方法。

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

创建设置意向并将其传递给视图后,应将其密钥附加到将收集付款方式的元素。例如,考虑此“更新付款方式”表单

<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 元素 附加到表单并安全地收集客户的付款详细信息

<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 方法 验证卡并从 Stripe 中检索安全的“付款方式标识符”。

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 应用程序,在那里它可以附加到客户。付款方式可以添加为新的付款方式,也可以用于更新默认付款方式。您也可以立即使用付款方式标识符来创建新的订阅

lightbulb

如果您想了解更多关于设置意向和收集客户付款详细信息的信息,请查看 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 元素 附加到表单并安全地收集客户的付款详细信息

<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 方法 验证卡并从 Stripe 中检索安全的“付款方式标识符”。

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();
exclamation

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

添加支付方式

要添加新的付款方式,您可以调用可计费模型上的addPaymentMethod 方法,并传递付款方式标识符

$user->addPaymentMethod($paymentMethod);
lightbulb

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

删除支付方式

要删除付款方式,您可以在要删除的Laravel\Cashier\PaymentMethod 实例上调用delete 方法

$paymentMethod->delete();

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

$user->deletePaymentMethod('pm_visa');

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

$user->deletePaymentMethods();

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

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

如果用户拥有有效的订阅,则您的应用程序不应允许他们删除其默认付款方式。

订阅

订阅提供了一种为您的客户设置定期付款的方式。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 方法的第一个参数应该是订阅的内部类型。如果您的应用程序只提供单一订阅,您可能会将其称为defaultprimary。此订阅类型仅用于内部应用程序使用,不打算向用户展示。此外,它不应包含空格,并且在创建订阅后不应更改。第二个参数是用户订阅的具体价格。此值应与 Stripe 中的价格标识符相对应。

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

exclamation

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

通过发票电子邮件收集定期付款

您可以指示 Stripe 在每次客户的定期付款到期时向客户发送电子邮件发票,而不是自动收取客户的定期付款。然后,客户可以在收到发票后手动支付发票。收集通过发票进行的定期付款时,客户无需预先提供付款方式

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

客户在取消订阅之前支付发票的时间由days_until_due 选项决定。默认情况下,此值为 30 天;但是,如果您希望,您可以为此选项提供特定值

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

数量

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

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

其他详细信息

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

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

优惠券

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

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

或者,如果您想应用Stripe 促销代码,可以使用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();

如果您想将新的优惠券或促销代码应用于客户或订阅,可以使用applyCouponapplyPromotionCode 方法

$billable->applyCoupon('coupon_id');
$billable->applyPromotionCode('promotion_code_id');
 
$subscription->applyCoupon('coupon_id');
$subscription->applyPromotionCode('promotion_code_id');

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

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

添加订阅

如果您想为已经拥有默认付款方式的客户添加订阅,可以在订阅构建器上调用add 方法

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

从 Stripe 仪表板创建订阅

您也可以从 Stripe 仪表板本身创建订阅。这样做时,Cashier 将同步新添加的订阅并为它们分配default 类型。要自定义分配给仪表板创建的订阅的订阅类型,请定义 Webhook 事件处理程序

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

最后,您应该始终确保每种应用程序提供的订阅类型只添加一个活动订阅。如果客户有两个default 订阅,则 Cashier 仅使用最近添加的订阅,即使两个订阅都将与您的应用程序数据库同步。

检查订阅状态

客户订阅您的应用程序后,您可以轻松地使用各种方便的方法检查其订阅状态。首先,subscribed 方法在客户拥有有效订阅时返回true,即使订阅当前在其试用期内。subscribed 方法接受订阅类型作为其第一个参数

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

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

<?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 订阅是否已积极订阅应用程序的“premium”产品。给定的 Stripe 产品标识符应与 Stripe 仪表板中您产品的标识符之一相对应

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

通过将数组传递给subscribedToProduct 方法,您可以确定用户的default 订阅是否已积极订阅应用程序的“basic”或“premium”产品

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

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

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

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

if ($user->subscription('default')->recurring()) {
// ...
}
exclamation

如果用户拥有两种相同类型的订阅,则 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_dueincomplete 状态时仍被视为活动状态,则可以使用 Cashier 提供的 keepPastDueSubscriptionsActivekeepIncompleteSubscriptionsActive 方法。通常,这些方法应在您的 App\Providers\AppServiceProviderregister 方法中调用

use Laravel\Cashier\Cashier;
 
/**
* Register any application services.
*/
public function register(): void
{
Cashier::keepPastDueSubscriptionsActive();
Cashier::keepIncompleteSubscriptionsActive();
}
exclamation

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

订阅范围

大多数订阅状态也可用作查询范围,因此您可以轻松地查询数据库中处于给定状态的订阅

// 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 文档

exclamation

swapAndInvoice 方法之前执行 noProrate 方法不会对比例分配产生任何影响。始终会开具发票。

订阅数量

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

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 文档

包含多个产品的订阅的数量

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

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

包含多个产品的订阅

包含多个产品的订阅 允许您将多个计费产品分配给单个订阅。例如,假设您正在构建一个客户服务“帮助台”应用程序,该应用程序的基本订阅价格为每月 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');

如果您希望以特定数量添加价格,则可以将数量作为 addPriceaddPriceAndInvoice 方法的第二个参数传递

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

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

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

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

交换价格

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

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

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

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

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

如果您想交换订阅上的单个价格,则可以使用订阅项目本身的 swap 方法。这种方法在您希望保留订阅其他价格上的所有现有元数据时特别有用

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

比例分配

默认情况下,Stripe 在向包含多个产品的订阅添加或删除价格时会按比例分配费用。如果您希望在不按比例分配的情况下进行价格调整,则应将 noProrate 方法链接到您的价格操作

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

数量

如果您希望更新单个订阅价格上的数量,则可以使用 现有的数量方法,将价格的 ID 作为附加参数传递给方法

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

当订阅包含多个价格时,Subscription 模型上的 stripe_pricequantity 属性将为 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();

计量计费

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

要开始使用按使用量计费,您首先需要在 Stripe 仪表板中创建一个带有按使用量计费价格的新产品。然后,使用 meteredPrice 将按使用量计费价格 ID 添加到客户订阅

use Illuminate\Http\Request;
 
Route::post('/user/subscribe', function (Request $request) {
$request->user()->newSubscription('default')
->meteredPrice('price_metered')
->create($request->paymentMethodId);
 
// ...
});

您也可以通过 Stripe Checkout 开始按使用量计费订阅

$checkout = Auth::user()
->newSubscription('default', [])
->meteredPrice('price_metered')
->checkout();
 
return view('your-checkout-view', [
'checkout' => $checkout,
]);

报告使用情况

当您的客户使用您的应用程序时,您将向 Stripe 报告他们的使用情况,以便他们能够准确地开具发票。要增加按使用量计费订阅的使用情况,可以使用 reportUsage 方法

$user = User::find(1);
 
$user->subscription('default')->reportUsage();

默认情况下,将向结算周期添加 1 个“使用量”。或者,您可以传递特定数量的“使用情况”以添加到客户在本结算周期的使用情况

$user = User::find(1);
 
$user->subscription('default')->reportUsage(15);

如果您的应用程序在单个订阅上提供多个价格,则需要使用 reportUsageFor 方法来指定要为其报告使用情况的按使用量计费价格

$user = User::find(1);
 
$user->subscription('default')->reportUsageFor('price_metered', 15);

有时,您可能需要更新之前报告的使用情况。要实现此目的,您可以将时间戳或 DateTimeInterface 实例作为第二个参数传递给 reportUsage。这样做时,Stripe 将更新在该时间报告的使用情况。只要给定的日期和时间仍在当前结算周期内,您就可以继续更新之前的使用情况记录

$user = User::find(1);
 
$user->subscription('default')->reportUsage(5, $timestamp);

检索使用情况记录

要检索客户的过去使用情况,可以使用订阅实例的 usageRecords 方法

$user = User::find(1);
 
$usageRecords = $user->subscription('default')->usageRecords();

如果您的应用程序在单个订阅上提供多个价格,则可以使用 usageRecordsFor 方法指定要为其检索使用情况记录的按使用量计费价格

$user = User::find(1);
 
$usageRecords = $user->subscription('default')->usageRecordsFor('price_metered');

usageRecordsusageRecordsFor 方法返回一个包含使用情况记录的关联数组的 Collection 实例。您可以迭代此数组以显示客户的总使用情况

@foreach ($usageRecords as $usageRecord)
- Period Starting: {{ $usageRecord['period']['start'] }}
- Period Ending: {{ $usageRecord['period']['end'] }}
- Total Usage: {{ $usageRecord['total_usage'] }}
@endforeach

有关返回的所有使用情况数据的完整参考以及如何使用 Stripe 的基于光标的分页,请咨询 Stripe 官方 API 文档

订阅税

exclamation

无需手动计算税率,您可以 使用 Stripe Tax 自动计算税款

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

/**
* 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'],
];
}
exclamation

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

同步税率

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

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

这也将同步包含多个产品的订阅的任何商品税率。如果您的应用程序提供包含多个产品的订阅,您应该确保您的可计费模型实现了上面 讨论的 priceTaxRates 方法。

免税

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

use App\Models\User;
 
$user = User::find(1);
 
$user->isTaxExempt();
$user->isNotTaxExempt();
$user->reverseChargeApplies();
exclamation

这些方法也适用于任何 Laravel\Cashier\Invoice 对象。但是,当在 Invoice 对象上调用时,这些方法将确定创建发票时的免税状态。

订阅锚定日期

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

use Illuminate\Http\Request;
 
Route::post('/user/subscribe', function (Request $request) {
$anchor = Carbon::parse('first day of next month');
 
$request->user()->newSubscription('default', 'price_monthly')
->anchorBillingCycleOn($anchor->startOfDay())
->create($request->paymentMethodId);
 
// ...
});

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

取消订阅

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

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

当订阅被取消时,Cashier 会自动在您的 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()->addDays(10)
);

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

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

恢复订阅

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

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

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

订阅试用

使用预付支付方式

如果您希望在事先收集付款方式信息的同时为您的客户提供试用期,您应该在创建订阅时使用 trialDays 方法。

use Illuminate\Http\Request;
 
Route::post('/user/subscribe', function (Request $request) {
$request->user()->newSubscription('default', 'price_monthly')
->trialDays(10)
->create($request->paymentMethodId);
 
// ...
});

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

exclamation

如果客户的订阅在试用期结束日期之前没有被取消,他们将在试用期结束后立即被收费,因此您应该确保通知您的用户他们的试用期结束日期。

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

use Carbon\Carbon;
 
$user->newSubscription('default', 'price_monthly')
->trialUntil(Carbon::now()->addDays(10))
->create($paymentMethod);

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

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

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

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

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

if ($user->hasExpiredTrial('default')) {
// ...
}
 
if ($user->subscription('default')->hasExpiredTrial()) {
// ...
}

在 Stripe/Cashier 中定义试用天数

您可以选择在 Stripe 仪表板中定义您的价格应获得多少天的试用期,或者始终使用 Cashier 明确传递它们。如果您选择在 Stripe 中定义您的价格的试用天数,您应该注意,新的订阅,包括过去拥有订阅的客户的新的订阅,将始终获得试用期,除非您明确调用 skipTrial() 方法。

不使用预付支付方式

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

use App\Models\User;
 
$user = User::create([
// ...
'trial_ends_at' => now()->addDays(10),
]);
exclamation

请确保在您的可计费模型类的类定义中为 trial_ends_at 属性添加一个 日期转换

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()->addDays(7)
);
 
// Add an additional 5 days to the trial...
$subscription->extendTrial(
$subscription->trial_ends_at->addDays(5)
);

处理 Stripe Webhook

lightbulb

您可以使用 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 中创建一个 Webhook,该 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 --disabled
exclamation

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

Webhook 和 CSRF 保护

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

->withMiddleware(function (Middleware $middleware) {
$middleware->validateCsrfTokens(except: [
'stripe/*',
]);
})

定义 Webhook 事件处理程序

Cashier 自动处理因失败收费而取消的订阅和其他常见的 Stripe Webhook 事件。但是,如果您有其他要处理的 Webhook 事件,您可以通过监听 Cashier 派发的以下事件来做到这一点。

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

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

<?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 签名

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

要启用 Webhook 验证,请确保在应用程序的 .env 文件中设置 STRIPE_WEBHOOK_SECRET 环境变量。Webhook secret 可以从您的 Stripe 帐户仪表板中检索。

单次收费

简单收费

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

use Illuminate\Http\Request;
 
Route::post('/purchase', function (Request $request) {
$stripeCharge = $request->user()->charge(
100, $request->paymentMethodId
);
 
// ...
});

charge 方法接受一个数组作为其第三个参数,允许您将您希望传递的任何选项传递给底层的 Stripe 收费创建。有关在创建收费时可用的选项的更多信息,请参阅 Stripe 文档

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

您也可以在没有底层客户或用户的情况下使用 charge 方法。要实现这一点,请在应用程序的可计费模型的新实例上调用 charge 方法。

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

如果收费失败,charge 方法将抛出异常。如果收费成功,将从方法返回 Laravel\Cashier\Payment 的实例。

try {
$payment = $user->charge(100, $paymentMethod);
} catch (Exception $e) {
// ...
}
exclamation

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 方法可供您使用,但建议您使用 invoicePricetabPrice 方法以及预定义价格。通过这样做,您将能够在 Stripe 仪表板中更好地分析和查看有关每种产品的销售数据。

exclamation

invoiceinvoicePriceinvoiceFor 方法将创建 Stripe 发票,该发票将重试失败的计费尝试。如果您不希望发票重试失败的收费,则需要在第一次失败的收费后使用 Stripe API 关闭它们。

创建支付意图

您可以通过在可计费模型实例上调用 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;
});

创建付款意图后,您可以将客户端密钥返回给应用程序的前端,以便用户可以在浏览器中完成付款。要了解有关使用 Stripe 付款意图构建完整付款流程的更多信息,请参阅 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;
});
exclamation

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

退款收费

如果您需要退款 Stripe 收费,可以使用 refund 方法。此方法接受 Stripe 付款意图 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' => '[email protected]',
'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 还提供对 Stripe Checkout 的支持。Stripe Checkout 通过提供一个预先构建的、托管的付款页面,消除了实现自定义页面以接受付款的麻烦。

以下文档包含有关如何开始使用 Cashier 的 Stripe Checkout 的信息。要了解有关 Stripe Checkout 的更多信息,您还应考虑查看 Stripe 关于 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_urlcancel_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 不允许 用户可赎回的促销代码。幸运的是,有一个简单的方法可以为您的结账页面启用这些代码。为此,您可以调用 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);
});
exclamation

使用 checkoutCharge 方法时,Stripe 将始终在您的 Stripe 仪表板中创建新的产品和价格。因此,我们建议您提前在 Stripe 仪表板中创建产品,并改用 checkout 方法。

订阅结账

exclamation

使用 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();
});
exclamation

不幸的是,Stripe Checkout 不支持启动订阅时的所有订阅计费选项。在订阅构建器上使用 anchorBillingCycleOn 方法、设置按比例分配行为或设置付款行为将对 Stripe Checkout 会话没有任何影响。请参阅 Stripe Checkout 会话 API 文档,以查看哪些参数可用。

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');

调用此方法时,客户将可以使用一个新的复选框,允许他们指示他们是否以公司的名义购买。如果是,他们将有机会提供他们的税号。

exclamation

如果您已在应用程序的服务提供商中配置了 自动税务收取,则此功能将自动启用,无需调用 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 事件,因此请确保 配置您的 Stripe Webhook 以实际将此事件发送到您的应用程序。在 Stripe 仪表板中启用 Webhook 后,您可以 使用 Cashier 处理 Webhook。Webhook 有效负载中包含的对象将是 checkout 对象,您可以检查该对象以完成客户的订单。

处理付款失败

有时,订阅或一次性收费的付款可能会失败。发生这种情况时,Cashier 将抛出 Laravel\Cashier\Exceptions\IncompletePayment 异常,告知您发生了这种情况。在捕获此异常后,您有两个选项可供选择如何继续。

首先,您可以将您的客户重定向到 Cashier 附带的专用付款确认页面。此页面已经拥有一个关联的命名路由,该路由通过 Cashier 的服务提供商注册。因此,您可以捕获 `IncompletePayment` 异常并将用户重定向到付款确认页面。

use Laravel\Cashier\Exceptions\IncompletePayment;
 
try {
$subscription = $user->newSubscription('default', 'price_monthly')
->create($paymentMethod);
} catch (IncompletePayment $exception) {
return redirect()->route(
'cashier.payment',
[$exception->payment->id, 'redirect' => route('home')]
);
}

在付款确认页面上,系统会提示客户再次输入他们的信用卡信息,并执行 Stripe 要求的任何额外操作,例如“3D Secure”确认。确认付款后,用户将被重定向到上面指定的 `redirect` 参数提供的 URL。重定向后,`message`(字符串)和 `success`(整数)查询字符串变量将被添加到 URL 中。付款页面目前支持以下付款方式类型

  • 信用卡
  • 支付宝
  • Bancontact
  • BECS 直接借记
  • EPS
  • Giropay
  • iDEAL
  • 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 文档 以查看确认付款时接受的所有选项。

强客户认证

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

exclamation

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

需要额外确认的付款

SCA 规定通常需要额外的验证才能确认和处理付款。发生这种情况时,Cashier 会抛出 `Laravel\Cashier\Exceptions\IncompletePayment` 异常,通知您需要额外的验证。有关如何处理这些异常的更多信息,请参阅有关 处理失败的付款 的文档。

Stripe 或 Cashier 展示的付款确认屏幕可能根据特定的银行或卡发行机构的付款流程进行调整,并可能包含额外的卡确认、临时小额收费、独立设备认证或其他形式的验证。

不完整和逾期状态

当付款需要额外确认时,订阅将保持在 `incomplete` 或 `past_due` 状态,如其 `stripe_status` 数据库列所示。Cashier 将在付款确认完成后自动激活客户的订阅,并且您的应用程序通过 Stripe 的网络钩子收到其完成的通知。

有关 `incomplete` 和 `past_due` 状态的更多信息,请参考 我们有关这些状态的其他文档

脱机付款通知

由于 SCA 规定要求客户偶尔验证他们的付款详细信息,即使他们的订阅处于活动状态,Cashier 可以在脱机付款确认需要时向客户发送通知。例如,这可能发生在订阅续订时。Cashier 的付款通知可以通过将 `CASHIER_PAYMENT_NOTIFICATION` 环境变量设置为通知类来启用。默认情况下,此通知已禁用。当然,Cashier 包含一个您可以用于此目的的通知类,但您可以自由地提供自己的通知类(如果需要)。

CASHIER_PAYMENT_NOTIFICATION=Laravel\Cashier\Notifications\ConfirmPayment

要确保脱机付款确认通知已发送,请验证您的应用程序是否 配置了 Stripe 网络钩子 以及您的 Stripe 仪表板中是否启用了 `invoice.payment_action_required` 网络钩子。此外,您的 `Billable` 模型还应使用 Laravel 的 `Illuminate\Notifications\Notifiable` 特性。

exclamation

即使客户手动进行需要额外确认的付款,也会发送通知。不幸的是,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 密钥的**测试**版本添加到您的 `phpunit.xml` 文件中。

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

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

lightbulb

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