交易工具包 (Stripe)
Laravel Cashier (Stripe)
- 简介
- 更新 Cashier
- 安装
- 配置信息
- 消费者
- 支付方式
- 订阅内容
- 订阅试用
- 处理 Stripe Webhook
- 单次收费
- 结帐
- 发票
- 处理支付失败
- 强客户身份验证(SCA)
- Stripe SDK
- 测试
Laravel Cashier (Stripe)
简介
Laravel Cashier 为 Stripe 的订阅计费服务提供了一个富有表现力、流畅的接口。它处理了几乎所有你害怕编写的订阅计费样板代码。除了基本的订阅管理,Cashier 还可以处理优惠券、交换订阅、订阅 「数量」、取消宽限期,甚至生成发票 PDF。
升级 Cashier
升级到新版本的 Cashier 时,请务必仔细阅读 升级指南。
注意:为了防止破坏性变更,Cashier 使用固定的 Stripe API 版本。Cashier 12 使用 Stripe API 版本
2020-03-02
。Stripe API 版本将在次要版本上更新,以利用新的 Stripe 功能和改进。
安装
首先,使用 Composer 为 Stripe 安装 Cashier 扩展包:
composer require laravel/cashier
注意:为确保 Cashier 正确处理所有 Stripe 事件,请记得 设置 Cashier 的 webhook。
数据库迁移
Cashier 的服务提供器注册了自己的数据库迁移目录,因此请记住在安装此包后迁移数据库。Cashier 迁移将向 users
表中添加多个列,并创建一个新的 subscriptions
表来保存客户的所有订阅:
php artisan migrate
如果需要覆盖 Cashier 附带的迁移,可以使用 vendor:publish
Artisan 命令发布它们:
php artisan vendor:publish --tag="cashier-migrations"
如果要阻止 Cashier 的迁移完全运行,可以使用 Cashier 提供的 ignoreMigrations
方法。通常,应在 AppServiceProvider
的 register
方法中调用此方法:
use Laravel\Cashier\Cashier;
/**
* 注册应用服务。
*
* @return void
*/
public function register()
{
Cashier::ignoreMigrations();
}
注意:Stripe 建议用于存储 Stripe 标识符的任何列都应区分大小写。因此,在使用 MySQL 时,应该确保将
stripe_id
列的列排序规则设置为utf8_bin
。有关这方面的更多信息,请参见 Stripe 文档.
简介
Laravel Cashier 为 Stripe's 的订阅计帐服务提供了一个富有表现力、流畅的接口。它处理几乎所有的你所害怕编写的订阅计帐样板代码。除了基本的订阅管理,Cashier 还可以处理优惠券、交换订阅、订阅“数量”、取消宽限期,甚至生成发票 PDF。
升级 Cashier
升级到新版本的 Cashier 时,请务必仔细阅读升级指南。
注意:为了防止重大更改,Cashier 使用固定的 Stripe API 版本。Cashier 12 使用 Stripe API 版本
2020-03-02
。Stripe API 版本将在次要版本上更新,以利用新的 Stripe 功能和改进。
安装
首先,使用 Composer 包管理器为 Stripe 安装 Cashier 包:
composer require laravel/cashier
注意:为确保 Cashier 正确处理所有 Stripe 事件,请记得设置 Cashier 的 webhook。
数据库迁移
Cashier 的服务提供者注册了自己的数据库迁移目录,所以安装此包后记得执行数据库迁移操作。Cashier 迁移将向你的 users
表添加几列,并创建一个新的 subscriptions
表来保存你客户的所有订阅:
php artisan migrate
如果你需要覆盖 Cashier 附带的迁移,您可以使用 vendor:publish
Artisan 命令发布它们:
php artisan vendor:publish --tag="cashier-migrations"
如果你想阻止 Cashier 的迁移完全运行,可以使用 Cashier 提供的 ignoreMigrations
方法。 通常,此方法应在 AppServiceProvider
的 register
方法中调用:
use Laravel\Cashier\Cashier;
/**
* 注册任何应用程序服务。
*
* @return void
*/
public function register()
{
Cashier::ignoreMigrations();
}
注意:Stripe 建议用于存储 Stripe 标识符的任何列都应该区分大小写。因此,你应该确保在使用 MySQL 时将
stripe_id
列的列排序规则设置为utf8_bin
。 更多关于这方面的信息可以在 Stripe 文档中找到 .
配置
订单模型
在使用Cashier
之前,需要将 Billable trait
添加到可订单模型定义中。这个特性提供了多个方法以便执行常用支付任务,如创建订阅、应用优惠券和更新支付方法信息:
use Laravel\Cashier\Billable;
class User extends Authenticatable
{
use Billable;
}
Cashier 默认假设你的 Billable 模型是 Laravel 自带的 App\Models\User
类。如果你想要修改该约定,可以在 .env
文件中指定其它模型:
CASHIER_MODEL=App\Models\User
注意:注:如果你使用的不是 Laravel 自带的
App\Models\User
模型,则需要发布并修改默认的Cashier 迁移文件以匹配你使用模型对应的表名。
API 密钥
接下来,需要在.env
文件中配置 Stripe
密钥,可以从 Stripe
后台控制面板中获取这些 Stripe API 密钥:
STRIPE_KEY=your-stripe-key
STRIPE_SECRET=your-stripe-secret
货币配置
Cashier 默认货币是美元(USD),你可以通过在 .env
设置 CASHIER_CURRENCY
环境变量来修改默认的货币:
美元: CASHIER_CURRENCY=eur
人民币: CASHIER_CURRENCY=rmb
除了配置 Cashier 的货币之外,还可以在格式化用于显示在发票上的金额时指定本地化配置。在底层,Cashier 使用了 PHP 的 NumberFormatter 类 来设置本地货币:
CASHIER_CURRENCY_LOCALE=nl_BE
注意:为了使用本地化配置而不是 en,需要确保安装了 PHP ext-intl 扩展并在服务器上配置启用。
日志
Cashier 允许你指定日志通道来记录所有与 Stripe
相关的异常,你可以通过在 .env
中 配置 CASHIER_LOGGER
来指定:
CASHIER_LOGGER=stack
使用自定义模型
你可以通过定义自己的模型并扩展相应的 Cashier
模型来自由扩展 Cashier
内部的模型,增加一些方法:
use Laravel\Cashier\Subscription as CashierSubscription;
class Subscription extends CashierSubscription
{
// ...
}
定义模型后,可以通过Laravel\Cashier\Cashier
类 配置 Cashier
使用自定义的模型。通常还需要在 App\Providers\AppServiceProvider
的 boot
中注册一下:
use App\Models\Cashier\Subscription;
use App\Models\Cashier\SubscriptionItem;
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
Cashier::useSubscriptionModel(Subscription::class);
Cashier::useSubscriptionItemModel(SubscriptionItem::class);
}
消费者
消费者
你可以使用 Cashier::findBillable
方法通过 Stripe ID 获取顾客信息。该方法返回的是一个 Billable
模型实例:
use Laravel\Cashier\Cashier;
$user = Cashier::findBillable($stripeId);
创建消费者
有时候,你可能希望在不开始订阅的情况下创建一个 Stripe
消费者。这可以通过 createAsStripeCustomer
方法来实现:
$stripeCustomer = $user->createAsStripeCustomer();
消费者在 Stripe 中创建后,可以过一段时间再开始订阅。还可以使用可选的 $options
数组传入所有 Stripe API 支持的额外参数:
$stripeCustomer = $user->createAsStripeCustomer($options);
如果你想要返回顾客对象,可以使用 asStripeCustomer
方法
$stripeCustomer = $user->asStripeCustomer();
此外,可以使用 createOrGetStripeCustomer
方法来获取客户对象, 如果已经是 Stripe 用户,则直接返回对应的顾客对象,否则重新创建:
$stripeCustomer = $user->createOrGetStripeCustomer();
更新消费者
有时候,你可能想要使用额外的信息直接更新 Stripe 顾客信息,这可以通过 updateStripeCustomer
方法来完成,参数 $option
为Stripe API数组:
$stripeCustomer = $user->updateStripeCustomer($options);
订单入口
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'));
});
如果你只想要生成订单入口的 URL,可以使用 billingPortalUrl
方法:
$url = $request->user()->billingPortalUrl(route('billing'));
支付方式
存储支付方式
为了使用 Stripe 创建订阅或者进行「一次性」支付,你需要存储支付方法并从 Stripe
中获取对应的标识符。这种方式可用于实现你是否计划使用这个支付方法进行订阅还是单次收费,下面我们分别来介绍这两种方法。
用于订阅的支付方法
当我们为消费者存储信用卡支付方式以便将来使用时,必须使用 Stripe Setup Intents API 来安全地收集顾客的支付方式细节,比如回调错误信息 。「Setup Intents」用于告知 Stripe 使用顾客的支付方法进行收费的意图。Cashier 的 Billable
Trait 包含了 createSetupIntent
方法来创建新的「Setup Intent」,你需要在渲染收集顾客支付方法细节表单的路由或控制器方法中调用这个方法:
return view('update-payment-method', [
'intent' => $user->createSetupIntent()
]);
创建完 Setup Intent 并将其传递给视图之后,你需要在收集支付方法的元素中添加它的 secret。例如,参考下面这个「更新支付方法」表单:
<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 的 handleCardSetup 方法验证信用卡并从 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 应用,将它和特定用户关联起来。该支付方法既可以以新的支付方法添加,也可以用于更新默认的支付方法。你还可以立即使用这个支付方法标识符来创建一个新的订阅。
技巧:如果你想要了解更多关于 Setup Intents 以及获取顾客支付细节的信息,可以参考 Stripe官方文档.
用于单次付费的支付方法
当然,如果消费者支付方法使用的是单次付费,我们只需要使用支付方法标识符一次即可。由于 Stripe 本身的限制,你不可以使用存储的默认顾客支付方法进行单次付费,必须允许顾客通过 Stripe.js 库进入他们的支付方法细节。例如,参考下面这个表单:
<input id="card-holder-name" type="text">
<!-- Stripe Elements Placeholder -->
<div id="card-element"></div>
<button id="card-button">
Process Payment
</button>
接下来跟上面文档相似,通过 Stripe.js 库添加 Stripe 元素 到这个表单,并安全地收集顾客的支付细节:
<script src="https://js.stripe.com/v3/"></script>
<script>
const stripe = Stripe('stripe-public-key');
const elements = stripe.elements();
const cardElement = elements.create('card');
cardElement.mount('#card-element');
</script>
然后, 使用 Stripe 的 createPaymentMethod 方法验证信用卡并获取一个安全的「支付方法标识符」:
const cardHolderName = document.getElementById('card-holder-name');
const cardButton = document.getElementById('card-button');
cardButton.addEventListener('click', async (e) => {
const { paymentMethod, error } = await stripe.createPaymentMethod(
'card', cardElement, {
billing_details: { name: cardHolderName.value }
}
);
if (error) {
// Display "error.message" to the user...
} else {
// The card has been verified successfully...
}
});
如果信用卡验证成功,就可以传递 paymentMethod.id
到你的 Laravel 应用并处理一次性付费。
获取支付方法
Billable 模型实例上的 paymentMethods
方法会返回 Laravel\Cashier\PaymentMethod
实例集合:
$paymentMethods = $user->paymentMethods();
要获取消费者默认的支付方法,可以使用 defaultPaymentMethod
方法:
$paymentMethod = $user->defaultPaymentMethod();
还可以使用 findPaymentMethod
方法通过 Billable 模型获取指定支付方法:
$paymentMethod = $user->findPaymentMethod($paymentMethodId);
判断消费者是否拥有支付方法
要判断某个 Billable
模型对应账户是否有默认的支付方法,可以使用 hasDefaultPaymentMethod
方法:
if ($user->hasDefaultPaymentMethod()) {
//
}
要判断某个 Billable
模型对应账户是否有支付方法,可以使用 hasPaymentMethod
方法:
if ($user->hasPaymentMethod()) {
//
}
更新默认支付方式
更新顾客的默认支付方式信息 可以用 updateDefaultPaymentMethod
方法 ,该方法接收一个 Stripe 支付方法标识符并将新的支付方法分配为默认的支付方法:
$user->updateDefaultPaymentMethod($paymentMethod);
为了同步应用的默认支付方法信息到 Stripe 顾客的默认支付方式信息,可以使用 updateDefaultPaymentMethodFromStripe
方法:
$user->updateDefaultPaymentMethodFromStripe();
注意:消费者的默认支付方式只能用于发票和创建新的订阅,由于 Stripe 的限制,不能将其用于单次付费。
添加支付方式
要添加新的支付方式,可以调用 Billable 用户的 addPaymentMethod
方法,并传递支付方法标识符:
$user->addPaymentMethod($paymentMethod);
技巧:要了解如何获取支付方法标识符,请参考支付方式存储文档.
删除支付方式
删除一个支付方法,你可以调用要删除的 Laravel\Cashier\PaymentMethod
实例上的 delete
方法:
$paymentMethod->delete();
删除指定 Billable 模型上的所有支付方法信息可以使用 deletePaymentMethods
方法:
$user->deletePaymentMethods();
注意:如果某个用户拥有一个有效的订阅,不能删除默认的支付方法。
订阅内容
订阅提供了一种为消费者设置定期付款的方式。由收银员管理的 Stripe订阅 提供对多个订阅计划、订阅数量、试用等的支持。
创建订阅
创建一个订阅,首先要获取一个账单模型的实例,通常是 App\Models\User
的实例。获取到该模型实例之后,可以使用 newSubscription
方法来创建该模型的订阅:
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$request->user()->newSubscription(
'default', 'price_premium'
)->create($request->paymentMethodId);
// ...
});
newSubscription
方法的第一个参数是该订阅的名字,如果应用只有一个订阅,可以将其称作 default
或 primary
,第二个参数用于指定用户订阅的计划,这个值对应 Stripe
中相应计划的标识符。
create
方法接收 Stripe 支付方法标识符或者 Stripe PaymentMethod
对象的 create
方法会自动创建这个 Stripe 订阅,同时更新数据库中 Stripe 的消费者 ID(即 users
表中的 stripe_id
)和其它相关的账单信息。
注意:直接传递支付方法标识符到 create() 订阅方法还会自动将其添加到用户存储的支付方法中。
数量
如果你想要在创建订阅时设置计划的具体数量,可以使用 quantity
方法:
$user->newSubscription('default', 'price_monthly')
->quantity(5)
->create($paymentMethod);
其它细节
如果你想要指定其它消费者或者订阅细节,你可以将其作为第二个参数传递给 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')
->create($paymentMethod);
添加订阅
如果你想要为已经有默认支付方式的消费者添加订阅,可以在使用 newSubscription
方法时使用 add
方法:
use App\Models\User;
$user = User::find(1);
$user->newSubscription('default', 'price_premium')->add();
从 Stripe 面板中创建订阅
你也可以从Stripe面板中创建订阅。在面板中创建订阅时候, 收银员将同步新添加的订阅,并为它们分配一个名称为default
的订阅。要自定义分配给仪表板创建订阅的订阅名,扩展' WebhookController '并覆盖newSubscriptionName
方法。
此外,只能通过Stripe面板中创建一种订阅类型。如果应用程序提供使用不同名称的多个订阅,则只能通过Stripe面板添加一种订阅类型。
最后,应该始终确保对应用程序提供的每种订阅类型只添加一个活动订阅。如果消费者有两个default
订阅,那么收银员只会使用最近添加的订阅,即使这两个订阅都将与应用程序的数据库同步。
检查订阅状态
消费者订阅你的应用后,你可以使用各种便利的方法来简单检查订阅状态。首先,如果用户有一个有效的订阅,则 subscribed
方法返回 true,即使订阅现在处于试用期:
if ($user->subscribed('default')) {
//
}
subscribed
方法也可以用于路由中间件, 基于消费者订阅状态允许你对路由和控制器的访问进行过滤:
<?php
namespace App\Http\Middleware;
use Closure;
class EnsureUserIsSubscribed
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
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()) {
//
}
subscribedToPlan
方法可用于查询消费者是否基于 Stripe ID 订阅了计划,在本例中,我们会判断用户的 default
订阅是否订阅了 monthly
计划:
if ($user->subscribedToPlan('price_monthly', 'default')) {
//
}
传递数组到 subscribedToPlan
方法可以判断消费者的 default
订阅是否在 monthly
或者 yearly
计划中有效:
if ($user->subscribedToPlan(['price_monthly', 'price_yearly'], 'default')) {
//
}
recurring
方法可用于判定消费者是否已经订阅并且不在试用期:
if ($user->subscription('default')->recurring()) {
//
}
注意:如果消费者有两个具有相同名称的订阅,则
subscription
方法将始终返回最近的订阅。例如,消费者可能有两条名为default
的订阅记录;但是,其中一个订阅可能是旧的、过期的订阅,而另一个是当前的、活动的订阅。最新的订阅将始终返回,而旧的订阅将保留在数据库中以进行历史回顾。
已取消的订阅状态
要判断消费者是否曾经是有效的订阅者,但现在取消了订阅,可以使用 cancelled
方法:
if ($user->subscription('default')->cancelled()) {
//
}
还可以判断消费者是否曾经取消过订阅,但现在仍然在「宽限期」直到完全失效。例如,如果一个消费者在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
。当你的订阅处于这种状态时,只有等到消费者确认支付后它们才会被激活。我们可以使用 Billable
模型或者订阅实例的 hasIncompletePayment
方法来检查某个订阅是否存在未完成支付:
if ($user->hasIncompletePayment('default')) {
//
}
if ($user->subscription('default')->hasIncompletePayment()) {
//
}
如果某个订阅存在未完成支付,你需要引导消费者到 Cashier 的支付确认页面,并传递 latestPayment
标识符。你可以使用订阅实例上的 latestPayment
方法来获取这个标识符:
<a href="{{ route('cashier.payment', $subscription->latestPayment()->id) }}">
Please confirm your payment.
</a>
如果你想要订阅在 past_due
状态下依然有效,可以使用 Cashier 提供的 keepPastDueSubscriptionsActive
方法,通常,该方法需要在 AppServiceProvider
的 boot
方法中调用:
use Laravel\Cashier\Cashier;
/**
* Register any application services.
*
* @return void
*/
public function register()
{
Cashier::keepPastDueSubscriptionsActive();
}
注意:当某个订阅处于
incomplete
状态,只有等到支付被确认后才能进行修改。因此,当订阅处于incomplete
状态时,执行swap
和updateQuantity
方法会抛出异常。
订阅范围
大多数订阅状态也可用作查询范围,以便您可以轻松地查询数据库中处于给定状态的订阅:
// Get all active subscriptions...
$subscriptions = Subscription::query()->active()->get();
// Get all of the cancelled subscriptions for a user...
$subscriptions = $user->subscriptions()->cancelled()->get();
所有内置的订阅查询范围列表如下:
Subscription::query()->active();
Subscription::query()->cancelled();
Subscription::query()->ended();
Subscription::query()->incomplete();
Subscription::query()->notCancelled();
Subscription::query()->notOnGracePeriod();
Subscription::query()->notOnTrial();
Subscription::query()->onGracePeriod();
Subscription::query()->onTrial();
Subscription::query()->pastDue();
Subscription::query()->recurring();
修改计划
消费者订阅应用后,有时候想要改变到新的订阅计划,需要将消费者切换到新的订阅,传递计划标识符到 swap
方法:
use App\Models\User;
$user = App\Models\User::find(1);
$user->subscription('default')->swap('price_id');
如果消费者在试用,试用期将会被维护。还有,如果订阅存在多个,数量也可以被维护。
如果你想要切换计划并取消消费者所在的所有试用期,可以使用 skipTrial
方法:
$user->subscription('default')
->skipTrial()
->swap('price_id');
如果你想要切换计划并立即为消费者开具发票,而不是等到下一个结算周期,可以使用 swapAndInvoice
方法:
$user = User::find(1);
$user->subscription('default')->swapAndInvoice('price_id');
按比例分配
默认情况下,Stripe 会在订阅计划间切换时按比例进行分配,noProrate
可用于在修改订阅计划时不使用按比例分配机制:
$user->subscription('default')->noProrate()->swap('price_id');
更多关于订阅计划按比例分配的细节,请参考 Stripe 官方文档.
注意:在
wapAndInvoice
方法之前执行noProrate
方法不会影响按比例分配。发票始终都会开具。
订阅数量
有时候订阅也会被数量影响,例如,应用中每个账户每月需要付费$10,要简单增加或减少订阅数量,使用 incrementQuantity
和 decrementQuantity
方法:
use App\Models\User;
$user = User::find(1);
$user->subscription('default')->incrementQuantity();
// Add five to the subscription's current quantity...
$user->subscription('default')->incrementQuantity(5);
$user->subscription('default')->decrementQuantity();
// Subtract five from the subscription's current quantity...
$user->subscription('default')->decrementQuantity(5);
你也可以使用 updateQuantity
方法指定具体的数量:
$user->subscription('default')->updateQuantity(10);
noProrate
方法可用于更新订阅数量而无需对收费进行评级:
$user->subscription('default')->noProrate()->updateQuantity(10);
想要了解更多订阅数量信息,可以参考Stripe 官方文档.
多方案订阅数量
如果你的订阅是多方案订阅计划,你应该将其数量要递增或递减的计划的名称作为第二个参数传递给递增/递减方法
$user->subscription('default')->incrementQuantity(1, 'chat-plan');
多方案订阅计划
多方案订阅计划 可以分配多个付费方案给单个订阅计划。例如,假设你正在构建一个顾客咨询服务应用,其中包含了一个基础的¥10/月的订阅方案,但是如果提供附加的实时聊天服务,需要支付¥15/月,阅信息存储在收银员的Subscription_items
数据库表中:
可以通过将计划数组作为第二个参数传递给newSubscription
方法,为给定的订阅指定多个计划:
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$request->user()->newSubscription('default', [
'price_monthly',
'chat-plan',
])->create($request->paymentMethodId);
// ...
});
在上面的示例代码中,消费者的default
订阅添加了两个订阅计划。这两个计划都将按各自的计费间隔收费。如果需要,可以使用Quantity
方法来表示每个计划的具体数量:
$user = User::find(1);
$user->newSubscription('default', ['price_monthly', 'chat-plan'])
->quantity(5, 'chat-plan')
->create($paymentMethod);
或者,给现有订阅添加另一个计划,可以调用订阅的addPlan
方法:
$user = User::find(1);
$user->subscription('default')->addPlan('chat-plan');
上面的示例代码会添加新方案并且顾客会在下个支付周期为其支付。如果你想要让顾客立即支付,可以使用 addPlanAndInvoice
方法:
$user->subscription('default')->addPlanAndInvoice('chat-plan');
如果你想要添加指定数量的方案,可以将数量值作为第二个参数传递给 addPlan
或者 addPlanAndInvoice
方法:
$user = User::find(1);
$user->subscription('default')->addPlan('chat-plan', 5);
你也可以使用 removePlan
方法从订阅计划中移除这些方案:
$user->subscription('default')->removePlan('chat-plan');
注意:不能移除订阅计划中最后一个方案(至少需要保留一个), 相反, 需要通过取消订阅的方式移除该方案。
多方案订阅切换
你还可以在多方案订阅计划中切换不同的方案。例如,假设你正在订阅 basic-plan
服务中的 chat-plan
方案,然后想要升级到 pro-plan
方案,可以这么做:
use App\Models\User;
$user = User::find(1);
$user->subscription('default')->swap(['pro-plan', 'chat-plan']);
在执行上面的示例代码时,底层的 basic-plan
订阅项会被删除,而 chat-plan
订阅项会保留下来,新的订阅项 pro-plan
则会被创建。
你还可以通过将键/值对数组传递给swap
方法来指定订阅项选项。例如,你可能需要指定订阅计划数量:
$user = User::find(1);
$user->subscription('default')->swap([
'pro-plan' => ['quantity' => 5],
'chat-plan'
]);
如果你想要切换订阅计划中的单个方案,可以使用订阅项本身的 swap
方法来实现。这种方式在你想要保留所有已存在订阅项元数据时很有用:
$user = User::find(1);
$user->subscription('default')
->findItemOrFail('basic-plan')
->swap('pro-plan');
按比例分配
默认情况下,添加或移除订阅计划中的付费方案时,Stripe
会按比例分配支付金额。如果你不想要按比例调整,可以在方案操作上使用 noProrate
方法:
$user->subscription('default')->noProrate()->removePlan('chat-plan');
数量
如果你想要更新单个订阅方案的数量,可以使用已存在的数量方法并传入计划名作为额外参数到该方法:
$user = User::find(1);
$user->subscription('default')->incrementQuantity(5, 'chat-plan');
$user->subscription('default')->decrementQuantity(3, 'chat-plan');
$user->subscription('default')->updateQuantity(10, 'chat-plan');
注意:当你在订阅计划中设置了多个方案,
Subscription
模型的stripe_plan
和quantity
属性值会变成 null。要访问独立的订阅方案,可以使用Subscription
模型类中的items
方法。
订阅项
当一个订阅计划有多个方案时,就会有对应的多个订阅项存储在数据表 subscription_items
中,你可以通过订阅计划上的 items
方法访问它们:
use App\Models\User;
$user = User::find(1);
$subscriptionItem = $user->subscription('default')->items->first();
// Retrieve the Stripe plan and quantity for a specific item...
$stripePlan = $subscriptionItem->stripe_plan;
$quantity = $subscriptionItem->quantity;
还可以使用 findItemOrFail
方法获取具体的方案:
$user = User::find(1);
$subscriptionItem = $user->subscription('default')->findItemOrFail('chat-plan');
计量计费
计量计费 允许你根据客户在计费周期内的产品使用情况向客户收费。例如,你可以根据客户每月发送的短信或电子邮件的数量向他们收费。
要开始使用计量计费,首先需要在Stripe
面板板中创建一个带有计量价格的新产品。然后,使用meteredPlan
将计量价格ID添加到客户订阅中:
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$request->user()->newSubscription('default', [])
->meteredPlan('price_metered')
->create($request->paymentMethodId);
// ...
});
可以通过Stripe支付,来开始一个计费订阅:
$checkout = Auth::user()
->newSubscription('default', [])
->meteredPlan('price_metered')
->checkout();
return view('your-checkout-view', [
'checkout' => $checkout,
]);
报告使用
当你的客户使用你的应用程序时,你将向Stripe
报告他们的使用情况,以便准确计费。要增加计量订阅的使用量,可以使用reportUsage
方法:
$user = User::find(1);
$user->subscription('default')->reportUsage();
默认情况下,计费周期的使用量为1。或者,可以通过一个特定的usage
添加到客户的使用量在计费期间:
$user = User::find(1);
$user->subscription('default')->reportUsage(15);
如果你的应用程序在一个订阅中提供多个计划,你将需要使用 reportUsageFor
方法来指定要报告使用情况的计量计划/价格:
$user = User::find(1);
$user->subscription('default')->reportUsageFor('price_metered', 15);
有时,你可能需要更新你先前报告的用法。可以将时间戳或DateTimeInterface
实例作为第二个参数传递给reportUsage
。当这样做时,Stripe
将更新在给定时间报告的使用情况。你可以继续更新以前的使用记录,因为给定的日期和时间仍在当前计费期间:
$user = User::find(1);
$user->subscription('default')->reportUsage(5, $timestamp);
检索使用记录
要检索客户过去的使用情况,可以使用订阅实例的usageRecords
方法:
$user = User::find(1);
$usageRecords = $user->subscription('default')->usageRecords();
如果您的应用在单个订阅上提供多个计划,您可以使用 usageRecordsFor
方法来指定您希望检索使用的计量计划 / 价格:
$user = User::find(1);
$usageRecords = $user->subscription('default')->usageRecordsFor('price_metered');
usageRecords
和 usageRecordsFor
方法返回一个包含使用记录关联数组的集合实例。 您可以遍历此数组以显示客户的总使用量:
@foreach ($usageRecords as $usageRecord) {
- Period Starting: {{ $usageRecord['period']['start'] }}
- Period Ending: {{ $usageRecord['period']['end'] }}
- Total Usage: {{ $usageRecord['total_usage'] }}
@endforeach
有关返回的所有使用数据以及如何使用 Stripe 基于游标的分页的完整参考,请参阅 官方 Stripe API 文档。
订阅税
要指定用户为订阅支付的税率,您应该在计费模型上实现 taxRates
方法并返回一个包含 Stripe 税率 ID 的数组。 您可以在 Stripe 仪表板 中定义这些税率:
/**
* 应适用于客户订阅的税率。
*
* @return array
*/
public function taxRates()
{
return ['tax-rate-id'];
}
taxRates
方法使您能够基于单个客户应用税率,这可能对跨越多个国家和税率的用户群有所帮助。
如果您提供多计划订阅,您可以通过在计费模型上实施 planTaxRates
方法来为每个计划定义不同的税率:
/**
* 应适用于客户订阅的税率。
*
* @return array
*/
public function planTaxRates()
{
return [
'plan-id' => ['tax-rate-id'],
];
}
注意:
taxRates
方法仅适用于订阅费用。 如果您使用 Cashier 进行 「一次性」收费,需要手动指定当时的税率。
同步税率
更改由 taxRates
方法返回的硬编码税率 ID 时,用户的任何现有订阅的税收设置将保持不变。 如果您希望使用新的 taxRates
值更新现有订阅的税值,您应该在用户的订阅实例上调用 syncTaxRates
方法:
$user->subscription('default')->syncTaxRates();
这也将同步任何多计划订阅项目税率。 如果您的应用程序提供多计划订阅,您应该确保您的计费模型实现了 上文提到的 planTaxRates
方法。
免税
Cashier 还提供了 isNotTaxExempt
、isTaxExempt
和 reverseChargeApplies
方法来确定客户是否免税。 这些方法将调用 Stripe API 来确定客户的免税状态:
use App\Models\User;
$user = User::find(1);
$user->isTaxExempt();
$user->isNotTaxExempt();
$user->reverseChargeApplies();
注意:这些方法也适用于任何
Laravel\Cashier\Invoice
对象。 但是,当在Invoice
对象上调用时,这些方法将在创建发票时确定免税状态。
订阅锚定日期
默认情况下,计费周期锚点是订阅的创建日期,如果使用试用期,则是试用结束的日期。 如果您想修改计费锚定日期,您可以使用 anchorBillingCycleOn
方法:
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$anchor = Carbon::parse('first day of next month');
$request->user()->newSubscription('default', 'price_premium')
->anchorBillingCycleOn($anchor->startOfDay())
->create($request->paymentMethodId);
// ...
});
有关管理订阅计费周期的更多信息,请参阅 Stripe 计费周期文档
取消订阅
要取消订阅,请调用用户订阅的 cancel
方法:
$user->subscription('default')->cancel();
当订阅被取消时,Cashier 将自动在您的 subscriptions
数据库表中设置 ends_at
列。 此列用于了解 subscribed
方法应该何时开始返回 false
。
例如,如果客户在 3 月 1 日取消订阅,但订阅计划在 3 月 5 日之前结束,则 subscribed
方法将在 3 月 5 日之前继续返回 true
。 这样做是因为通常允许用户继续使用应用程序,直到他们的计费周期结束。
您可以使用 onGracePeriod
方法确定用户是否已取消订阅但仍处于「宽限期」:
if ($user->subscription('default')->onGracePeriod()) {
//
}
如果您想立即取消订阅,请调用用户订阅的 cancelNow
方法:
$user->subscription('default')->cancelNow();
如果您希望立即取消订阅并对任何剩余的未开票计费使用新的 / 待定比例发票项目开具发票,请在用户订阅上调用 cancelNowAndInvoice
方法:
$user->subscription('default')->cancelNowAndInvoice();
恢复订阅
如果客户取消了他们的订阅并且您希望恢复订阅,您可以调用订阅的 resume
方法。 客户必须仍处于「宽限期」才能恢复订阅:
$user->subscription('default')->resume();
如果客户取消订阅,然后在订阅完全到期之前恢复订阅,则不会立即向客户收费。 相反,他们的订阅将被重新激活,他们将按照原始计费周期计费。
订阅试用
预先收集付款方式
如果您希望为您的客户提供试用期,同时仍然提前收集付款方式信息,则在创建订阅时应使用 trialDays
方法:
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$request->user()->newSubscription('default', 'price_monthly')
->trialDays(10)
->create($request->paymentMethodId);
// ...
});
此方法将在数据库中的订阅记录上设置试用期结束日期,并指示 Stripe 在此日期之后才开始向客户收费。 使用 trialDays
方法时,Cashier 将覆盖为 Stripe 中的计划配置的任何默认试用期。
注意:如果客户的订阅未在试用结束日期之前取消,他们将在试用到期后立即收费,因此您务必将试用结束日期通知您的用户。
trialUntil
方法允许您提供一个 DateTime
实例,指定试用期应该何时结束:
use Carbon\Carbon;
$user->newSubscription('default', 'price_monthly')
->trialUntil(Carbon::now()->addDays(10))
->create($paymentMethod);
您可以使用用户实例的 onTrial
方法或订阅实例的 onTrial
方法来确定用户是否在试用期内。 下面的两个例子是等价的:
if ($user->onTrial('default')) {
//
}
if ($user->subscription('default')->onTrial()) {
//
}
您可以使用 endTrial
方法立即结束订阅试用:
$user->subscription('default')->endTrial();
在 Stripe / Cashier 中定义试用天数
您可以选择在 Stripe 仪表板中定义您的计划的试用天数,或者始终使用 Cashier 明确传递它们。如果您选择在 Stripe 中定义计划的试用天数,您应该知道新订阅,包括过去订阅过的客户的新订阅,将始终获得试用期,除非您明确调用 trialDays(0)
方法。
没有预先收集付款方式
如果您想提供试用期而不预先收集用户的付款方式信息,您可以将用户记录中的trial_ends_at
列设置为您想要的试用结束日期。这通常在用户注册期间完成:
use App\Models\User;
$user = User::create([
// ...
'trial_ends_at' => now()->addDays(10),
]);
注意:请务必在计费模型的类定义中为
trial_ends_at
属性添加 date cast。
Cashier 将这种类型的试用称为 「通用试用」,因为它不附属于任何现有订阅。如果当前日期未超过 trial_ends_at
的值,则可计费模型实例上的 onTrial
方法将返回 true
:
if ($user->onTrial()) {
// 用户在试用期内...
}
一旦你准备好为用户创建一个实际的订阅,你可以像往常一样使用 newSubscription
方法:
$user = User::find(1);
$user->newSubscription('default', 'price_monthly')->create($paymentMethod);
要检索用户的试用结束日期,您可以使用 trialEndsAt
方法。如果用户正在试用,则此方法将返回一个 Carbon 日期实例,否则将返回 null
。如果您想获取特定订阅而不是默认订阅的试用结束日期,您还可以传递一个可选的订阅名称参数:
if ($user->onTrial()) {
$trialEndsAt = $user->trialEndsAt('main');
}
如果您想明确知道用户在他们的「通用」试用期内并且尚未创建实际订阅,您也可以使用 onGenericTrial
方法:
if ($user->onGenericTrial()) {
// 用户在他们的 「通用」试用期内...
}
延长试用期
extendTrial
方法允许您在订阅创建后延长订阅的试用期。如果试用期已经到期并且客户已被收取订阅费用,您仍然可以为他们提供延长试用期。试用期内花费的时间将从客户的下一张发票中扣除:
use App\Models\User;
$subscription = User::find(1)->subscription('default');
// 从现在起 7 天后结束试用...
$subscription->extendTrial(
now()->addDays(7)
);
// 再增加 5 天的试用期...
$subscription->extendTrial(
$subscription->trial_ends_at->addDays(5)
);
处理 Stripe Webhooks
技巧:你可以使用 Stripe CLI 来帮助在本地开发过程中测试 webhooks。
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
invoice.payment_action_required
注意:确保使用 Cashier 包含的 webhook 签名验证 中间件保护传入的 Stripe webhook 请求。
Webhook 和 CSRF 保护
由于 Stripe webhooks 需要绕过 Laravel 的 CSRF 保护,请务必在应用程序的 App\Http\Middleware\VerifyCsrfToken
中间件中将 URI 作为例外列出或在 web
中间件组中排除:
protected $except = [
'stripe/*',
];
定义 Webhook 事件处理程序
Cashier 会自动处理因收费失败和其他常见的 Stripe webhook 事件而取消订阅。但是,如果您有其他想要处理的 webhook 事件,您可以通过扩展 Cashier webhook 控制器来实现。
您的控制器的方法名称应符合 Cashier 的控制器约定。具体来说,方法应该以 handle
和你希望处理的 webhook 的 「驼峰」名称为前缀。例如,如果你想处理 invoice.payment_succeeded
webhook,你应该向控制器添加一个 handleInvoicePaymentSucceeded
方法:
<?php
namespace App\Http\Controllers;
use Laravel\Cashier\Http\Controllers\WebhookController as CashierController;
class WebhookController extends CashierController
{
/**
* 处理发票付款成功。
*
* @param array $payload
* @return \Symfony\Component\HttpFoundation\Response
*/
public function handleInvoicePaymentSucceeded($payload)
{
// 处理传入事件...
}
}
接下来,在应用程序的 routes/web.php
文件中定义一个到 Cashier webhook 控制器的路由。这将覆盖 Cashier 服务提供商注册的默认路由:
use App\Http\Controllers\WebhookController;
Route::post(
'/stripe/webhook',
[WebhookController::class, 'handleWebhook']
);
技巧:Cashier 在收到 webhook 时会发出一个
Laravel\Cashier\Events\WebhookReceived
事件,当一个 webhook 被 Cashier 处理时会发出一个Laravel\Cashier\Events\WebhookHandled
事件。这两个事件都包含 Stripe webhook 的完整负载。
验证 Webhook 签名
为了保护您的 webhook,您可以使用 Stripe 的 webhook 签名。为方便起见,Cashier 自动包含一个中间件,用于验证传入的 Stripe webhook 请求是否有效。
要启用 webhook 验证,请确保在应用程序的 .env
文件中设置了 STRIPE_WEBHOOK_SECRET
环境变量。 webhook secret
可以从您的 Stripe 帐户仪表板中检索。
单次收费
基本使用
注意:
charge
方法接受您希望以您的应用使用的货币的最小单位收取的金额。例如,使用美元时,金额应以便士为单位指定。
如果您想对客户进行一次性收费,您可以在计费模型实例上使用 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) {
//
}
带发票的收费
有时,您可能需要一次性收费并向客户提供 PDF 收据。 invoiceFor
方法可以实现这点。例如,让我们向客户开具 5.00 美元的 「维护费」发票:
$user->invoiceFor('One Time Fee', 500);
发票将立即按照用户的默认付款方式收费。 invoiceFor
方法也接受一个数组作为它的第三个参数。此数组包含发票项目的计费选项。该方法接受的第四个参数也是一个数组,它应该包含发票本身的计费选项:
$user->invoiceFor('Stickers', 500, [
'quantity' => 50,
], [
'default_tax_rates' => ['tax-rate-id'],
]);
注意:
invoiceFor
方法将创建一个 Stripe 发票,它将重试失败的计费。如果您不希望发票重试失败的收费,则需要在第一次收费失败后使用 Stripe API 关闭它们。
退款
如果您需要退还 Stripe 费用,您可以使用 refund
方法。此方法接受 Stripe 支付意图 ID 作为其第一个参数:
$payment = $user->charge(100, $paymentMethodId);
$user->refund($payment->id);
发票
获取发票
您可以使用 invoices
方法轻松获取可计费模型的发票数组。 invoices
方法返回一个 Laravel\Cashier\Invoice
实例的集合:
$invoices = $user->invoices();
如果您想在结果中包含待处理的发票,您可以使用 invoicesInducingPending
方法:
$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>
生成 PDF 发票
在路由或控制器中,您可以使用 downloadInvoice
方法生成给定发票的 PDF 下载。此方法将自动生成下载发票所需的正确 HTTP 响应:
use Illuminate\Http\Request;
Route::get('/user/invoice/{invoice}', function (Request $request, $invoiceId) {
return $request->user()->downloadInvoice($invoiceId, [
'vendor' => 'Your Company',
'product' => 'Your Product',
]);
});
downloadInvoice
方法还允许通过其第三个参数自定义文件名。此文件名将自动为您添加 .pdf
后缀:
return $request->user()->downloadInvoice($invoiceId, [
'vendor' => 'Your Company',
'product' => 'Your Product',
], 'my-invoice');
结账
Cashier Stripe 还提供对 Stripe Checkout 的支持。 Stripe Checkout 通过提供预先构建的托管支付页面,消除了自定义页面的痛苦。
以下文档包含有关如何开始将 Stripe Checkout 与 Cashier 结合使用的信息。要了解有关 Stripe Checkout 的更多信息,您还应该考虑查看 Stripe 自己的 Checkout 文档。
产品结帐
您可以对在可计费模型上使用 checkout
方法在 Stripe 仪表板中创建的现有产品执行结账。 checkout
方法将启动一个新的 Stripe Checkout 会话。默认情况下,您需要传递 Stripe 价格 ID:
$checkout = $user->checkout('price_12345');
return view('your-checkout-view', [
'checkout' => $checkout,
]);
如果需要,您还可以指定产品数量:
$checkout = $user->checkout('price_12345', 15);
将 Checkout 会话实例传递给视图后,可以使用 button
方法呈现将用户引导至 Stripe Checkout 的按钮:
{{ $checkout->button('Buy') }}
当客户单击此按钮时,他们将被重定向到 Stripe 的结帐页面。默认情况下,当用户成功完成购买或取消购买时,他们将被重定向到您的 home
路由位置,但您可以使用 success_url
和 cancel_url
选项指定自定义回调 URL:
$checkout = $user->checkout('price_12345', 1, [
'success_url' => route('your-success-route'),
'cancel_url' => route('your-cancel-route'),
]);
促销代码
默认情况下,Stripe Checkout 不允许 用户可兑换的促销代码。幸运的是,有一种简单的方法可以为您的结帐页面启用这些功能。为此,您可以调用 allowPromotionCodes
方法:
$checkout = $user->allowPromotionCodes()->checkout('price_12345');
单次付费结帐
您还可以对尚未在 Stripe 仪表板中创建的临时产品执行简单收费。为此,您可以在可计费模型上使用 checkoutCharge
方法,并向其传递可计费金额、产品名称和可选数量:
$checkout = $user->checkoutCharge(1200, 'T-Shirt', 5);
return view('your-checkout-view', [
'checkout' => $checkout,
]);
将 Checkout 会话实例传递给视图后,可以使用 button
方法呈现将用户引导至 Stripe Checkout 的按钮:
{{ $checkout->button('Buy') }}
当客户单击此按钮时,他们将被重定向到 Stripe 的结帐页面。
注意:使用
checkoutCharge
方法时,Stripe 将始终在您的 Stripe 仪表板中创建新产品和价格。因此,我们建议您在 Stripe 仪表板中预先创建产品并改用checkout
方法。
订阅结帐
注意:使用 Stripe Checkout 订阅需要您在 Stripe 仪表板中启用
customer.subscription.created
webhook。此 webhook 将在您的数据库中创建订阅记录并存储所有相关订阅项目。
您还可以使用 Stripe Checkout 启动订阅。在使用 Cashier 的订阅构建器方法定义您的订阅后,您可以调用 checkout
方法:
$checkout = Auth::user()
->newSubscription('default', 'price_xxx')
->checkout();
return view('your-checkout-view', [
'checkout' => $checkout,
]);
与产品结账一样,您可以自定义成功和取消 URL:
$checkout = Auth::user()->newSubscription('default', 'price_xxx')->checkout([
'success_url' => route('your-success-route'),
'cancel_url' => route('your-cancel-route'),
]);
当然,您也可以为订阅结帐启用促销代码:
$checkout = Auth::user()->newSubscription('default', 'price_xxx')
->allowPromotionCodes()
->checkout();
将 Checkout 会话实例传递给视图后,可以使用 button
方法呈现将用户引导至 Stripe Checkout 的按钮:
{{ $checkout->button('Subscribe') }}
当客户单击此按钮时,他们将被重定向到 Stripe 的结帐页面。
注意:很遗憾,Stripe Checkout 在开始订阅时不支持所有订阅计费选项。在订阅构建器上使用
anchorBillingCycleOn
方法、设置按比例分配行为或设置付款行为在 Stripe Checkout 会话期间不会产生任何影响。请参阅 Stripe Checkout Session API 文档 以查看哪些参数可用。
Stripe 结帐和试用期
当然,您可以在构建将使用 Stripe Checkout 完成的订阅时定义试用期:
$checkout = Auth::user()->newSubscription('default', 'price_xxx')
->trialDays(3)
->checkout();
但是,试用期必须至少为 48 小时,这是 Stripe Checkout 支持的最短试用时间。
订阅和 Webhook
请记住,Stripe 和 Cashier 通过 webhooks 更新订阅状态,因此当客户在输入付款信息后返回应用程序时,订阅可能尚未激活。为了处理这种情况,您可能希望显示一条消息,通知用户他们的付款或订阅待处理。
结帐按钮的样式
渲染结帐按钮时,您可以使用 class
和 style
选项自定义按钮的样式。这些选项应该在关联数组中作为 button
方法的第二个参数传递:
{{ $checkout->button('Buy', ['class' => 'p-4 bg-blue-500 text-white']) }}
处理失败的付款
有时,订阅或单一费用的付款可能会失败。当这种情况发生时,Cashier 会抛出一个 Laravel\Cashier\Exceptions\IncompletePayment
异常,通知你发生了这种情况。捕获此异常后,您有两个选择如何继续。
首先,您可以将您的客户重定向到 Cashier 附带的专用付款确认页面。该页面已经有一个关联的命名路由,该路由通过 Cashier 的服务提供商注册。因此,您可能会捕获 IncompletePayment
异常并将用户重定向到付款确认页面:
use Laravel\Cashier\Exceptions\IncompletePayment;
try {
$subscription = $user->newSubscription('default', $planId)
->create($paymentMethod);
} catch (IncompletePayment $exception) {
return redirect()->route(
'cashier.payment',
[$exception->payment->id, 'redirect' => route('home')]
);
}
在付款确认页面上,将提示客户再次输入其信用卡信息并执行 Stripe 要求的其他操作,例如「3D Secure」确认。确认付款后,用户将被重定向到上面指定的 redirect
参数提供的 URL。重定向后,message
(字符串)和 success
(整数)查询字符串变量将被添加到 URL 中。
或者,您可以让 Stripe 为您处理付款确认。在这种情况下,您可以在您的 Stripe 仪表板中设置 Stripe 的自动计费电子邮件,而不是重定向到付款确认页面。但是,如果捕获到 IncompletePayment
异常,您仍应通知用户他们将收到一封包含进一步付款确认说明的电子邮件。
以下方法可能会引发付款异常:使用 Billable
trait 的模型上的 charge
、invoiceFor
和 invoice
。在与订阅交互时,SubscriptionBuilder
上的 create
方法以及 Subscription
模型上的 incrementAndInvoice
和 swapAndInvoice
方法可能会抛出未完成支付异常。
可以使用计费模型或订阅实例上的 hasIncompletePayment
方法来确定现有订阅是否有未完成的付款:
if ($user->hasIncompletePayment('default')) {
//
}
if ($user->subscription('default')->hasIncompletePayment()) {
//
}
目前继承了 IncompletePayment
的有两种类型的付款例外情况。如果需要,您可以单独捕获这些,以便您可以自定义用户体验:
Laravel\Cashier\Exceptions\PaymentActionRequired
:这个异常表明 Stripe 需要额外的验证才能确认和处理付款。Laravel\Cashier\Exceptions\PaymentFailure
:此异常表示付款由于各种其他原因而失败,例如可用资金不足。
强大的客户身份验证
如果您的企业位于欧洲,则您需要遵守欧盟的强客户身份验证 (SCA) 法规。欧盟于 2019 年 9 月实施了这些规定,以防止支付欺诈。幸运的是,Stripe 和 Cashier 已准备好构建符合 SCA 的应用程序。
注意:开始之前,请查看 Stripe's guide on PSD2 and SCA 以及他们的 新 SCA API 文档 。
需要额外确认的付款
SCA 法规通常需要额外的验证才能确认和处理付款。当这种情况发生时,Cashier 会抛出一个 Laravel\Cashier\Exceptions\PaymentActionRequired
异常,通知你需要额外的验证。有关如何处理这些异常的更多信息,请参见有关 处理失败付款 的文档。
不完整和过期状态
当付款需要额外确认时,订阅将保持在 incomplete
或 past_due
状态,如其stripe_status
数据库列所示。付款确认完成后,Cashier 将自动激活客户的订阅,并且 Stripe 通过 webhook 通知您的应用程序已完成。
有关 incomplete
和 past_due
状态的更多信息,请参阅 我们关于这些状态的附加文档。
非会话付款通知
由于 SCA 法规要求客户偶尔验证他们的付款详细信息,即使他们的订阅处于活动状态,Cashier 可以在需要非会话付款确认时向客户发送通知。例如,这可能在订阅续订时发生。可以通过将 CASHIER_PAYMENT_NOTIFICATION
环境变量设置为通知类来启用收银员的付款通知。默认情况下,此通知处于禁用状态。当然,Cashier 包含一个通知类,您可以用于此目的,但如果需要,您可以自由提供自己的通知类:
CASHIER_PAYMENT_NOTIFICATION=Laravel\Cashier\Notifications\ConfirmPayment
为确保发送会话外付款确认通知,请验证您的应用程序的 Stripe webhooks 已配置 以及在您的 Stripe 仪表板中启用了 invoice.payment_action_required
webhook。此外,你的 Billable
模型也应该使用 Laravel 的 Illuminate\Notifications\Notifiable
trait。
注意:即使客户手动付款需要额外确认,也会发送通知。不幸的是,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]);
测试
在测试使用 Cashier 的应用程序时,您可以模拟对 Stripe API 的实际 HTTP 请求;但是,这需要您重新实现部分 Cashier 的行为。因此,我们建议允许您的测试访问实际的 Stripe API。虽然这较慢,但它提供了更多的信心,您的应用程序按预期工作,并且任何缓慢的测试都可以放在他们自己的 PHPUnit 测试组中。
测试时,请记住 Cashier 本身已经有一个很棒的测试套件,因此您应该只专注于测试您自己的应用程序的订阅和支付流程,而不是每个底层的 Cashier 行为。
首先,将 Stripe 密钥的 testing 版本添加到您的 phpunit.xml
文件中:
<env name="STRIPE_SECRET" value="sk_test_<your-key>"/>
现在,每当您在测试时与 Cashier 交互时,它都会向您的 Stripe 测试环境发送实际的 API 请求。为方便起见,您应该使用您可能在测试期间使用的订阅 / 计划预先填写您的 Stripe 测试帐户。
技巧:为了测试各种计费场景,例如信用卡拒付和失败,您可以使用 Stripe 提供的大量的 测试卡号和令牌。