Laravel Cashier
简介
Laravel Cashier 提供了直观而流畅的接口来接入 Stripe 付费订阅服务,它可以处理几乎所有的让你头疼的付费订阅代码。除了基本的订阅管理之外,Cashier 还可以帮你处理优惠券、交换订阅、订阅数量、取消宽限期,甚至还可以生成 PDF 电子发票。
升级 Cashier
当你升级到新版本 Cashier 时,,请务必仔细阅读 Cashier 升级指南。
注意:为防止破坏性的更改,Cashier 使用固定的 Stripe API 版本。Cashier 10.1 利用 Stripe API
2019-08-14
版本。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;
Cashier::ignoreMigrations();
注意:Stripe 建议用来存储 stripe 标识符的所有列应该大小写敏感。因此,以 MySQL 数据库为例,你应该确保
stripe_id
列的列排序规则设置为utf8_bin
,更多信息可以在 Stripe 文档 中找到。
配置
Billable 模型
在使用 Cashier 之前, 添加 Billable
Trait 到你的模型定义中。 这个 Trait 提供了多种方法以便你执行常见的支付任务,如创建订阅、申请优惠券以及更新支付方式信息:
use Laravel\Cashier\Billable;
class User extends Authenticatable
{
use Billable;
}
Cashier 暂定你的 Billable 模型是 Laravel 附带的 App\User
类,如果你想修改请在你的 .env
文件中指定一个不同的模型:
CASHIER_MODEL=App\User
注意:如果你使用的不是 Laravel 提供的
App\User
模型,你应该发布并更改提供的 migrations 以匹配你的代替模型数据表名。
API 密钥
接下来,您需要在 .env
中配置您的 Stripe 密钥。您可以在 Stripe 的控制面板中获取 Stripe API 密钥。
STRIPE_KEY=your-stripe-key
STRIPE_SECRET=your-stripe-secret
货币配置
Cashier 的默认货币是美元 (USD)。您可以设置 CASHIER_CURRENCY
来更改默认的货币:
CASHIER_CURRENCY=eur
为了配置 Cashier 使用的货币,您也许需要指定一个额外的 Locale(本地化) 参数,用于格式化在账单上面显示的金额。Cashier 内部使用 PHP 的 NumberFormatter
类 来格式化数字:
CASHIER_CURRENCY_LOCALE=nl_BE
注意:要使用除了
en
之外的 Locale,确保ext-intl
扩展在服务器上安装并配置好。
日志记录
Cashier 允许你指定在记录所有 Stripe 相关异常时使用的日志通道。你可以使用 CASHIER_LOGGER
环境变量指定日志通道:
CASHIER_LOGGER=stack
客户
创建客户
You can retrieve a customer by their Stripe ID using the Cashier::findBillable
method. This will return an instance of the Billable model:
use Laravel\Cashier\Cashier;
$user = Cashier::findBillable($stripeId);
Creating Customers
有时,您可能希望在未订阅的情况下创建 Stripe 客户。您可以使用 createAsStripeCustomer
方法完成此操作:
$stripeCustomer = $user->createAsStripeCustomer();
一旦在 Stripe 中创建了客户,你可以在以后的某个日期开始订阅。你也可以使用一个可选的 $options
数组来传入 Stripe API 支持的任何额外参数:
$stripeCustomer = $user->createAsStripeCustomer($options);
如果可计费实体已经是 Stripe 中的客户,你还可以使用 createOrGetStripeCustomer
方法来返回客户对象。
$stripeCustomer = $user->createOrGetStripeCustomer();
支付方式
有时,你可能希望直接用附加信息更新 Stripe 客户。你可以使用 updateStripeCustomer
方法来完成:
$stripeCustomer = $user->updateStripeCustomer($options);
支付方式
储存支付方式
为了使用 Stripe 创建订阅或是一次性扣费,您将需要从 Stripe 获取支付方式并储存他的识别码。基于您计划使用的支付方式是用来一次性扣费还是订阅,我们有两种方案来达成,接下来我们一起来看一下这两种方法。
用于订阅制的支付方法
为了储存用户的信用卡供以后使用,Stripe 的 “Setup Intent” API 需要安全的获取用户的信用卡信息。“Setup Intent” 指示 Stripe 用户支付方式的意象。Cashier 的Blillable
trait 包含了 createSetupIntent
方法来快速创建一个 “Setup Intent”。您应该在路由或者控制器调用这个方法来获取用户的支付方式意象:
return view('update-payment-method', [
'intent' => $user->createSetupIntent()
]);
当您创建完 “Setup Intent” 并传递给视图后,您应该将它的密钥元素附加到获取用户支付信息的表单中。如下面这个“更新支付方式”表单:
<input id="card-holder-name" type="text">
<!-- Stripe Elements Placeholder -->
<div id="card-element"></div>
<button id="card-button" data-secret="{{ $intent->client_secret }}">
Update Payment Method
</button>
接下来,Stripe.js 库会调用这个密钥并安全地抓取用户的支付信息:
<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
方法] (https://stripe.com/docs/stripe-js/reference#stripe-handle-card-setup)来获取一个安全的“支付方式识别码%E6%9D%A5%E8%8E%B7%E5%8F%96%E4%B8%80%E4%B8%AA%E5%AE%89%E5%85%A8%E7%9A%84%E2%80%9C%E6%94%AF%E4%BB%98%E6%96%B9%E5%BC%8F%E8%AF%86%E5%88%AB%E7%A0%81)”:
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) {
// 显示错误信息
} else {
// 卡片已成功验证
}
});
当 Stripe 完成对客户支付方式的校验后,您可以将setupIntent.payment_method
识别码返回给您的应用并附加给客户。这次支付使用的支付方式可 被添加为新的支付方式 或者 用于更新默认支付方式。您可以立即使用这个识别码来 创建一个订阅 。
Tip:如果您希望了解更多有关 “Setup Intent” 与获取收集用户支付详情的信息,请参阅 Stripe 的文档 。
用于一次性收费的支付方式
当然,用于一次性收费的支付识别码我们只需要使用一次。由于 Stripe 的限制,您无法使用客户的一次性收费的支付方式。您需要用 Stripe.js 库来让客户填写他们的支付详情。让我们来看一下下面这个支付表单:
<input id="card-holder-name" type="text">
<!-- Stripe Elements Placeholder -->
<div id="card-element"></div>
<button id="card-button">
处理支付流程
</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) {
// 错误信息
} else {
// 卡片验证成功
}
});
如果卡片验证成功,您可以将 paymentMethod.id
传递给您的应用并进行 一次性付费 。
获取支付方式
可计费模型实例上的 paymentMethods
方法返回一个 Laravel\Cashier\PaymentMethod
实例集合:
$paymentMethods = $user->paymentMethods();
要检索默认的付款方法,可使用 defaultPaymentMethod
方法:
$paymentMethod = $user->defaultPaymentMethod();
你也可以使用 findPaymentMethod
方法检索可计费模型拥有的特定支付方法:
$paymentMethod = $user->findPaymentMethod($paymentMethodId);
确定用户是否有支付方式
要确定一个可计费的模型是否有一个附加到其帐户的支付方法,请使用 hasPaymentMethod
方法:
if ($user->hasPaymentMethod()) {
//
}
更新默认的支付方式
updateDefaultPaymentMethod
可用来更新用户的默认支付方式。这个方法接受 Stripe 的支付方式识别码并且会将新分配的识别码设为默认的支付方式:
$user->updateDefaultPaymentMethod($paymentMethod);
要在 Stripe 内同步您与客户的默认支付方式,您可以使用updateDefaultPaymentMethodFromStripe
方法:
$user->updateDefaultPaymentMethodFromStripe();
注意:客户的默认支付方式只能用户创建发票或订阅,由于 Stripe 的限制,某些支付方式不适用于一次性收费。
添加支付方式
要添加新的支付方式,你可以在 Billable
用户上调用 addPaymentMethod
方法,并传递支付方式识别码:
$user->addPaymentMethod($paymentMethod);
Tip:要学习如何获取并储存支付方式识别码,请参阅 支付方式储存
删除支付方式
要删除一个支付方式,你可以调用 Laravel\Cashier\PaymentMethod
上的 delete
方法进行删除:
$paymentMethod->delete();
deletePaymentMethods
方法将删除Billable模型的所有相关付款方式的信息:
$user->deletePaymentMethods();
注意:如果用户的订阅比较活跃,那么应该阻止他们删除默认的付款方式。
订阅
创建订阅
创建订阅,首先需要获取到一个 Billable 模型实例,这通常是 App\User
的一个实例。一旦您获取了模型实例,您可以使用 newSubscription
方法创建模型的订阅:
$user = User::find(1);
$user->newSubscription('default', 'premium')->create($paymentMethod);
newSubscription
方法的第一个参数应该是订阅的名称。如果您的应用程序只提供一个订阅,那么您可以将其设置为 main
or primary
。第二个参数是用户订阅的 Stripe 计划。这个值应该与 Stripe 或 Braintree 中的标识符对应。
create
方法接受一个 支付方法标识符或者 Stripe PaymentMethod
对象后将开始订阅, 并使用客户 ID 和其他相关的账单信息更新数据库。
注意:直接传递支付方法标识符到
create()
方法也可以将用户的付款方式储存到数据库。
用户其他的详细信息
如果您想要指定用户其他的详细信息,您可以通过将它们作为第二个参数传递给 create
方法:
$user->newSubscription('default', 'monthly')->create($paymentMethod, [
'email' => $email,
]);
要了解更多关于 Stripe 支持的额外字段,请查看 Stripe 的内容创建客户文档。
优惠券
如果您想在创建订阅时使用优惠券,您可以使用 withCoupon
方法:
$user->newSubscription('default', 'monthly')
->withCoupon('code')
->create($paymentMethod);
检查订阅状态
一旦用户在您的应用程序订阅了,您可以使用各种方便的方法轻松地检查他们的订阅状态。首先,如果用户有一个激活的订阅,那么 subscribed
的方法将返回 true
,即使订阅当前处于试用阶段:
if ($user->subscribed('default')) {
//
}
这个 subscribed
方法还可以在 路由中间件使用,允许您根据用户的订阅状态对路由和控制器进行访问:
public function handle($request, Closure $next)
{
if ($request->user() && ! $request->user()->subscribed('default')) {
// 该用户不是付费客户......
return redirect('billing');
}
return $next($request);
}
如果您想要确定用户是否仍然处于试用阶段,您可以使用 onTrial
方法。这个方法对于向用户显示他们仍然处于试用期的警告是很有用的:
if ($user->subscription('default')->onTrial()) {
//
}
基于给定的 Stripe 计划 ID,可以使用 subscribedToPlan
方法来确定用户是否订阅了该计划。在本例中,我们将确定用户的 main
订阅是否激活了 monthly
计划:
if ($user->subscribedToPlan('monthly', 'default')) {
//
}
通过向 subscribedToPlan
方法传递一个数组,你可以确定用户的 default
订阅是积极订阅 monthly
还是 yearly
计划:
if ($user->subscribedToPlan(['monthly', 'yearly'], 'default')) {
//
}
recurring
方法可用于确定用户目前是否已订阅,是否已不在试用期内:
if ($user->subscription('default')->recurring()) {
//
}
取消订阅状态
为了确定用户是否曾经订阅,但是已经取消了他们的订阅,您可以使用 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
。订阅状态存储在 stripe_status
Cashier subscriptions
数据库表的列中。
同样,如果在交换计划时需要进行二次付款操作,则订阅将被标记为 past_due
。当您的订阅处于这两种状态时,在客户确认付款之前,它将不会处于活动状态。检查订阅是否有不完整的付款可以使用 hasIncompletePayment
Billable模型或订阅实例上的方法完成:
if ($user->hasIncompletePayment('default')) {
//
}
if ($user->subscription('default')->hasIncompletePayment()) {
//
}
如果订阅的付款不完整,您应该将用户定向到收银员的付款确认页面,并传递latestPayment
标识符。您可以使用 latestPayment
订阅实例上的可用方法来检索此标识符:
<a href="{{ route('cashier.payment', $subscription->latestPayment()->id) }}">
Please confirm your payment.
</a>
如果您希望订阅在 past_due
状态下仍然被认为是活动的,您可以使用 Cashier 提供的 keepPastDueSubscriptionsActive
方法。通常,这个方法应该在你的 AppServiceProvider
的 register
方法中调用:
use Laravel\Cashier\Cashier;
/**
* Register any application services.
*
* @return void
*/
public function register()
{
Cashier::keepPastDueSubscriptionsActive();
}
注意:订阅处于某种
incomplete
状态时,在确认付款之前无法更改。因此,当订阅处于某种状态时,swap
和updateQuantity
方法将抛出异常incomplete
。
修改订阅计划
用户在您的应用程序中订阅了之后,他们可能会偶尔想要更改一个新的订阅计划。要将一个用户切换到一个新的订阅,需将订阅计划的标识符传递给 swap
方法:
$user = App\User::find(1);
$user->subscription('default')->swap('provider-plan-id');
如果用户在试用期,试用期的期限会被保留。另外,如果订阅的数量存在「份额」,那么该份额也将保持。
如果你想在更改用户订阅计划的时候取消用户当前订阅的试用期,可以使用 skipTrial
方法:
$user->subscription('default')
->skipTrial()
->swap('provider-plan-id');
如果你想在更改用户订阅计划的时候就为用户开具发票信息,而不是等待下一个结算周期, 你可以使用 swapAndInvoice
方法:
$user = App\User::find(1);
$user->subscription('default')->swapAndInvoice('provider-plan-id');
Prorations
默认情况下,Stripe 在计划之间交换时按比例收取费用。noProrate
方法可用于更新订阅而不按比例收取费用:
$user->subscription('default')->noProrate()->swap('provider-plan-id');
有关订阅比例的更多信息,请参阅 Stripe 文档 。
订阅量
有些时候订阅是会受「数量」影响的。举个例子,你的应用程序的付费方式可能是每个账户 $10 / 月。你可以使用 incrementQuantity
和 decrementQuantity
方法轻松地增加或减少你的订阅量:
$user = User::find(1);
$user->subscription('default')->incrementQuantity();
// 对当前的订阅量加5...
$user->subscription('default')->incrementQuantity(5);
$user->subscription('default')->decrementQuantity();
// 对当前的订阅量减5...
$user->subscription('default')->decrementQuantity(5);
或者,你可以使用 updateQuantity
方法设定一个特定的数量:
$user->subscription('default')->updateQuantity(10);
noProrate
方法可用于更新订阅的数量,而不会对收费进行定价:
$user->subscription('default')->noProrate()->updateQuantity(10);
要获得更多关于订阅量的信息,请参考 Stripe 文档 。
订阅税额
在计费模式上实现 taxPercentage
方法,并且返回一个 0 到 100 不超过 2 位小数的数字,用来指定用户在订阅中支付的税率百分比。
public function taxPercentage()
{
return 20;
}
taxPercentage
方法使你能够在模型的基础上应用税率,这对于一个跨越多个国家和税率的用户群可能有帮助。
注意:
taxPercentage
方法只适用于付费订阅模式。如果你用 charges 来做「一次性」收费,你需要同时手工指定税率。
同步税率百分比
当更改 taxPercentage
方法返回的硬编码值时,用户的任何现有订阅的税率设置将保持不变。如果要用返回的 taxPercentage
值更新现有订阅的税率,应在用户的订阅实例上调用 syncTaxPercentage
方法:
$user->subscription('default')->syncTaxPercentage();
订阅锚定日期
默认情况下,计费周期锚定是创建订阅的日期,如果使用试用期,则是试用结束的日期。如果要修改账单锚定日期,可以使用 anchorBillingCycleOn
方法:
use App\User;
use Carbon\Carbon;
$user = User::find(1);
$anchor = Carbon::parse('first day of next month');
$user->newSubscription('default', 'premium')
->anchorBillingCycleOn($anchor->startOfDay())
->create($paymentMethod);
有关管理订阅计费周期的详细信息,请参阅 Stripe 计划周期文档
取消订阅
在用户订阅上调用 cancel
方法用来取消订阅:
$user->subscription('default')->cancel();
当一个订阅被取消时,Cashier 将会自动在你的数据库中设置 ends_at
列。这个列经常被用来获悉 subscribed
字段何时应该开始返回 false
。例如,如果客户在 3 月 1 日取消订阅,但是订阅计划直到 3 月 5 日才结束,subscribed
方法将会继续返回 true
一直到 3 月 5 日。
你可以使用 onGracePeriod
方法确定用户是否确定订阅,但是仍然存在一个「宽限期」:
if ($user->subscription('default')->onGracePeriod()) {
//
}
如果你想马上取消订阅,请在用户的订阅中调用 cancelNow
方法:
$user->subscription('default')->cancelNow();
恢复订阅
如果一个用已经取消订阅,你可以在你希望恢复它的时候使用 resume 方法。用户 必须 仍然在他们的宽限期内才可以恢复订阅:
$user->subscription('default')->resume();
如果用户已取消订阅,然后在订阅宽限期前恢复该订阅,他们将不会被立即计费。相反,他们的订阅将会被重新激活,需要按照原来的支付流程再次进行支付。
试用订阅
以信用卡订阅
如果你想给你的顾客提供试用期,同时收集支付方法信息,那么你应该在创建订阅使用 trialDays
方法:
$user = User::find(1);
$user->newSubscription('default', 'monthly')
->trialDays(10)
->create($paymentMethod);
该方法会在数据库订阅记录上设置订阅期结束时间,以便告知 Sripe 在此之前不要计算用户的账单信息。当使用 trialDays
方法时,Cashier将覆盖 Stripe 中配置的所有默认试用期。
注意:如果顾客没有在试用期结束前取消订阅,订阅会被自动结算,所以你应该确保告知你的用户他们的试用结束期。
trialUntil
方法允许提供 DateTime
实例指定试用结束期:
use Carbon\Carbon;
$user->newSubscription('default', 'monthly')
->trialUntil(Carbon::now()->addDays(10))
->create($paymentMethod);
你可以使用用户实例的 onTrial
方法或者订阅实例的 onTrial
方法判断用户是否处于试用期。下面两个示例等价:
if ($user->onTrial('default')) {
//
}
if ($user->subscription('default')->onTrial()) {
//
}
非信用卡订阅
如果你不想在提供试用期的时候收集用户支付方式信息,只需设置用户记录的 trial_ends_at
列为期望的试用期结束日期即可,这通常在用户注册期间完成:
$user = User::create([
// 填充其他用户属性……
'trial_ends_at' => now()->addDays(10),
]);
注意:确保已添加
trial_ends_at
日期修改器 到模型定义。
Cashier 把这种类型的引用称为「一般体验」,因为它没有关联任何已存在的订阅。如果当前的日期没有超过 trail_ends_at
值, User
实例的 onTrial
方法将会返回 true
:
if ($user->onTrial()) {
// User is within their trial period...
}
如果你希望明确的知道用户处于「一般」试用期,并且还未创建实际的订阅,那么你可以使用 onGenericTrial
方法:
if ($user->onGenericTrial()) {
// User is within their "generic" trial period...
}
如果你准备给用户创建实际的订阅,通常你可以使用 newSubsription
方法:
$user = User::find(1);
$user->newSubscription('default', 'monthly')->create($paymentMethod);
扩展 Trials
extendTrial
方法允许你在订阅试用期后延长订阅的:
// 从现在起 7 天内结束试用…
$subscription->extendTrial(
now()->addDays(7)
);
// 再增加5天的试用期…
$subscription->extendTrial(
$subscription->trial_ends_at->addDays(5)
);
如果试用已经过期,并且已经向客户收取了订阅费用,您仍然可以为他们提供扩展试用。在试用期内所花费的时间将从客户的下一次发票中扣除。
处理 Stripe Webhooks
提示:你可以使用 Laravel Valet's中的
valet share
命令来帮助你在本地开发期间测试webhook。
Stripe 可以通过webhooks通知应用各种各样的事件。 默认情况下,需要定义一个 Cashier 的 webhook 控制器的路由。这个控制器用来处理所有传入的webhook请求。
默认情况下,这个控制器将会自动对支付失败次数过多(这个次数可以在 Stripe 设置中定义)的订阅进行取消;此外,我们很快会发现,你可以扩展这个控制器去处理任何你想要处理的 webhook 事件。
为了确保您的应用可以处理Stripe webhook,请务必在Stripe控制面板中配置webhook URL。Stripe 控制面板中应该配置的所有webhook完整列表如下:
customer.subscription.updated
customer.subscription.deleted
customer.updated
customer.deleted
invoice.payment_action_required
注意:请确保使用 Cashier 的 webhook 验签 中间件来保护传入请求。
Webhooks & CSRF 保护
因为 Stripe webhooks 需要绕过 Laraval 的 CSRF 保护, 请确保在您的 VerifyCsrfToken
中间件含有 URI ,或者将其置于 web
中间件组之外:
protected $except = [
'stripe/*',
];
定义 Webhook 事件处理程序
Cashier 对于失败支付自动进行取消订阅,但是如果您有其他的 Stripe Webhook 事件希望去处理,可以扩展 Webhook 控制器。您的方法名应该与 Cashier 期望的约定相符,更具体的说,您希望处理 Stripe webhook 的方法应该以 handle
和 「驼峰」 名为前缀。举例来说,如果您希望处理 invoice.payment_succeeded
的 webhook,您应该在控制器添加handleInvoicePaymentSucceeded
方法:
<?php
namespace App\Http\Controllers;
use Laravel\Cashier\Http\Controllers\WebhookController as CashierController;
class WebhookController extends CashierController
{
/**
* Handle invoice payment succeeded.
*
* @param array $payload
* @return \Symfony\Component\HttpFoundation\Response
*/
public function handleInvoicePaymentSucceeded($payload)
{
// 此处处理事件
}
}
接下来,在 routes/web.php
文件中定义 Cashier 控制器的路由, 这将会覆盖掉默认的路由:
Route::post(
'stripe/webhook',
'\App\Http\Controllers\WebhookController@handleWebhook'
);
当一个 webhook 被 Cashier 处理时,Cashier 发出一个 Laravel\Cashier\Events\WebhookReceived
事件,当一个 webhook 被 Cashier 处理时,发出一个 Laravel\Cashier\Events\WebhookHandled
事件。这两个事件都包含了 Stripe webhook 的完整有效负载。
订阅失败
如果用户的信用卡过期怎么办?不用担心 - Cashier 包含了一个 Webhook 控制器可以轻松为您取消用户的订阅。 该控制器会在 Stripe 判断订阅失败后(通常尝试支付失败 3 次及以上)取消用户的订阅。
Webhook 验签
为了保护 Webhook,您需要使用 Stripe 的 Webhook 签名。为了方便,Cashier 包含一个中间件,用于验证传入 Stripe webhook 的请求是否有效。
如果要启用 Webhook 验证,请确保在 .env
配置文件中设置了 STRIPE_WEBHOOK_SECRET
的值。 Webhook的 secret
可以从 Stripe 用户控制面板中找到。
一次性支付
简单支付
注意:
charge
方法接收您想支付于 应用程序使用的货币的最小单位 的金额。
如果您想对订阅客户的信用卡收取「一次性」费用,可以在可计费模型实例上使用 charge
方法。 您需要将 支付方式识别码 作为第二个参数:
// Stripe 接收分为单位的费用...
$stripeCharge = $user->charge(100, $paymentMethod);
charge
方法接受一个数组作为它的第三个参数, 允许您创建支付时将任何您想要的选项传递给底层的 Stripe。有关在创建支付时可用的选项,请参阅 Stripe 文档:
$user->charge(100, $paymentMethod, [
'custom_option' => $value,
]);
如果支付失败, charge
方法将会抛出异常。如果支付成功,一个 Laravel\Cashier\Payment
实例会从该方法返回:
try {
$payment = $user->charge(100, $paymentMethod);
} catch (Exception $e) {
//
}
发票
有时您可能需要支付一次性费用同时也需要生成费用发票,以便可以向客户提供 PDF 文件格式的收据。 invoiceFor
方法可以让您做到这一点。 例如,向客户开具 5.00 美元的「一次性费用」发票:
// Stripe 接收分为单位的费用...
$user->invoiceFor('One Time Fee', 500);
账单会立刻向用户的默认支付方式收取费用。 invoiceFor
方法接收一个数组作为第三个参数,这个数组包含了所购内容的账单选项。接收的第四个参数也是一个数组, 这最后一个参数包含了发票自身的一些选项:
$user->invoiceFor('Stickers', 500, [
'quantity' => 50,
], [
'tax_percent' => 21,
]);
注意:
invoiceFor
方法将会创建 Stripe 发票,该发票将会在支付失败后重试。如果您不想失败后重试,您需要在第一次支付失败后调用 Stripe API 关闭它。
关于退款
如果您需要处理退款,您可以使用 refund
方法。此方法接受 Stripe Payment Intent ID 作为其第一个参数:
$payment = $user->charge(100, $paymentMethod);
$user->refund($payment->id);
发票
您可以使用 invoices
方法轻松获取账单模型的发票数组:
$invoices = $user->invoices();
// 结果包含处理中的发票...
$invoices = $user->invoicesIncludingPending();
当列出客户发票清单时,可以使用发票辅助函数来显示相关的发票信息。例如,您可能希望在表格中列出每张发票,从而方便客户下载它们:
<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',
]);
});
Strong Customer Authentication
如果您的业务立足于欧洲,那么您就需要遵守 Strong Customer Authentication (SCA) 法规。这些法规由欧盟发布于 2019 年 9 月,意在预防支付欺诈。 幸运的是, Stripe 与 Cashier 已经准备好构建符合 SCA 的应用。
注意:在您开始之前, 请先阅读 Stripe's guide on PSD2 and SCA 与 新 SCA API 文档 。
需要额外验证的支付
SCA 通常在处理支付时需要额外的验证步骤。当这种情况出现时, Cashier 会抛出 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安全信息”来确认支付。当他们完成确认后,用户会被重定向到上方 redirect
定义的URL。
或者,您可以让 Stripe 来为您处理此次支付确认。在这种情况下,您可以在 Stripe 仪表板设置 自动账单邮箱 。如果捕获到IncompletePayment
,您还是要告知用户他们会在邮箱内收到处理支付的引导邮件。
IncompletePayment
异常会由以下方法抛出:charge
, invoiceFor
, 和在 Billable
(可收费)用户的 invoice
方法。 当处理订阅时, 在 IncompletePayment
中的 create
方法,与在 IncompletePayment
中的 incrementAndInvoice
和 swapAndInvoice
会抛出异常。
账单未完成与已逾期
每当账单需要额外确认时,订阅在数据库中的 stripe_status
列留下 incomplete
或 past_due
状态。当 Webhook 确认支付已完成时,Cashier 会立即激活用户的订阅。
如果您要了解更多关于 incomplete
与 past_due
状态的信息,请参考额外文档 。
非会话支付通知
尽管用户订阅已经激活,但 SCA 要求用户时不时验证其支付信息。 Cashier 可以发送一封邮件提醒用户需要非会话支付通知。例如,当用户订阅需要续费时, Cashier 的支付通知可以在环境变量中将 CASHIER_PAYMENT_NOTIFICATION
分配给一个类来启用。这默认是禁用的。当然,如果您不想使用 Cashier 提供的这个通知类,您可以使用自己的类来完成:
CASHIER_PAYMENT_NOTIFICATION=Laravel\Cashier\Notifications\ConfirmPayment
为了确保非会话支付通知送达, 您需要确认已经在 Stripe 仪表盘完成 Stripe Webhook 配置和已启用invoice.payment_action_required
Webhook。 您的 Billable
模型应使用 Laravel 的 Illuminate\Notifications\Notifiable
trait。
注意:当用户手动支付了一个需要额外确认的订单时,通知仍然会被发送。不幸的是,Stripe 无法确认用户是手动支付的还是使用非会话(Off-Session)方式支付。但是,用户确认支付以后返回支付页面,他们可以看见一个简单的 “支付成功” 提示。这样用户就无法再意外情况下再次确认支付,导致二次付费的情况发生。
Stripe SDK
Cashier 的许多对象都是 Stripe SDK 对象的包装器。如果你想与 Stripe 对象直接交互,你可以方便地使用 asStripe
方法检索它们:
$stripeSubscription = $subscription->asStripeSubscription();
$stripeSubscription->update(['application_fee_percent' => 5]);
本文章首发在 LearnKu.com 网站上。
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接 我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
Laravel China 社区:https://learnku.com/docs/laravel/7.x/billing/7511