Cashier
简介
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 扩展包自带的数据库迁移,可以使用 Artisan 命令 vendor:publish 发布这些迁移文件:
$ php artisan vendor:publish --tag="cashier-migrations"
如果想要阻止 Cashier 迁移的完整运行,可以使用 Cashier 提供的 ignoreMigrations
方法。通常,该方法应该在 AppServiceProvider
的 register
方法中调用:
use Laravel\Cashier\Cashier;
Cashier::ignoreMigrations();
注:Stripe 建议任何用于存储 Stripe 标识符的字段都应该是大小写敏感的,因此,你需要确保将 stripe_id 字段被设置为 utf8_bin(MySQL),更多信息可以参考 Stripe 官方文档。
配置
Billable 模型
接下来,添加 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 模型,则需要发布并修改默认的Cashier 迁移文件以匹配我们使用模型对应的表名。
API 密钥
接下来,你需要在 .env 文件中配置 Stripe 密钥,你可以从 Stripe 后台控制面板中获取这些 Stripe API 密钥:
STRIPE_KEY=your-stripe-key
STRIPE_SECRET=your-stripe-secret
货币配置
Cashier 默认货币是美元(USD),可以通过设置 CASHIER_CURRENCY
环境变量来修改默认的货币:
CASHIER_CURRENCY=eur
除了配置 Cashier 的货币之外,你还可以在格式化用于显示在发票上的金额时指定本地化配置。在底层,Cashier 使用了 PHP 的 NumberFormatter 类 来设置本地货币:
CASHIER_CURRENCY_LOCALE=nl_BE
注:为了使用本地化配置而不是 en,需要确保安装了 PHP ext-intl 扩展并在服务器上配置启用。
日志
Cashier 允许我们指定日志通道来记录所有与 Stripe 相关的异常,可以通过环境变量 CASHIER_LOGGER
来指定:
CASHIER_LOGGER=stack
顾客
获取顾客
我们可以使用 Cashier::findBillable
方法通过 Stripe ID 获取顾客信息。该方法返回的是一个 Billable 模型实例:
use Laravel\Cashier\Cashier;
$user = Cashier::findBillable($stripeId);
创建顾客
有时候,我们可能希望在不开始订阅的情况下创建一个 Stripe 顾客。这可以通过 createAsStripeCustomer 方法来实现:
$user->createAsStripeCustomer();
顾客在 Stripe 中创建后,可以过一段时间再开始订阅。我们还可以使用可选的 $options
数组传入所有 Stripe API 支持的额外参数:
$stripeCustomer = $user->createAsStripeCustomer($options);
此外,如果已经是 Stripe 用户,则直接返回对应的顾客对象,否则重新创建,这种场景可以使用 createOrGetStripeCustomer
方法:
$stripeCustomer = $user->createOrGetStripeCustomer();
如果想返回客户对象但不确定可计费实体是否已经是 Stripe 中的客户,则可以使用createOrGetStripeCustomer
方法。如果不存在,此方法将在 Stripe 中创建一个新客户:
$stripeCustomer = $user->createOrGetStripeCustomer();
更新顾客
有时候,我们可能想要使用额外的信息直接更新 Stripe 顾客信息,这可以通过 updateStripeCustomer 方法来完成:
$stripeCustomer = $user->updateStripeCustomer($options);
计费门户
Stripe 提供了一种设置计费门户的简单方法,以便我们的客户可以管理他们的订阅、付款方式并查看他们的计费历史记录。可以使用控制器或路由中的redirectToBillingPortal
方法将用户重定向到计费门户:
use Illuminate\Http\Request;
public function billingPortal(Request $request)
{
return $request->user()->redirectToBillingPortal();
}
默认情况下,当用户完成订阅管理后,他们可以返回到home。我们可以通过将 URL 作为参数传递给redirectToBillingPortal
方法来提供用户应该返回的自定义 URL :
use Illuminate\Http\Request;
public function billingPortal(Request $request)
{
return $request->user()->redirectToBillingPortal(
route('billing')
);
}
如果只想生成计费门户的 URL,可以使用billingPortalUrl
方法:
$url = $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.handleCardSetup(
clientSecret, cardElement, {
payment_method_data: {
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 模型是否有对应的支付方法,可以使用 hasPaymentMethod 方法:
if ($user->hasPaymentMethod()) {
//
}
要确定可计费模型是否至少有一种付款方式附加到其帐户,请使下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();
deletePaymentMethods
方法将会删除指定 Billable 模型上的所有支付方法信息:
$user->deletePaymentMethods();
注:如果某个用户拥有一个有效的订阅,你需要避免删除默认的支付方法。
订阅
创建订阅
要创建一个订阅,首先要获取一个账单模型的实例,通常是 App\User 的实例。获取到该模型实例之后,可以使用 newSubscription
方法来创建该模型的订阅:
$user = User::find(1);
$user->newSubscription('default', 'premium')->create($token);
第一个传递给 newSubscription 方法的参数是该订阅的名字,如果应用只有一个订阅,可以将其称作 main 或 primary,第二个参数用于指定用户订阅的计划,该值对应 Stripe 中相应计划的标识符。
接收 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', 'monthly')->create($paymentMethod, [
'email' => $email,
]);
优惠券
如果想要在创建订阅的时候使用优惠券,可以使用 withCoupon
方法:
$user->newSubscription('default', 'monthly')
->withCoupon('code')
->create($paymentMethod);
添加订阅
如果想向已经设置了默认付款方式的客户添加订阅,可以在使用newSubscription方法时使用add方法:
$user = User::find(1);
$user->newSubscription('default', 'price_premium')->add();
检查订阅状态
用户订阅应用后,我们可以使用各种便利的方法来简单检查订阅状态。首先,如果用户有一个有效的订阅,则 subscribed
方法返回 true,即使订阅现在处于试用期:
if ($user->subscribed('default')) {
//
}
subscribed
方法还可以用于路由中间件,基于用户订阅状态允许你对路由和控制器的访问进行过滤:
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('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()) {
//
}
订阅范围
大多数订阅状态也可用作查询范围,以便可以轻松查询数据库中处于给定状态的订阅:
// 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();
未完成和过期状态
如果某个订阅要求创建完订阅后进行二次付款操作,将被标记为 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 方法会抛出异常。
修改计划
用户订阅应用后,偶尔想要改变到新的订阅计划,要将用户切换到新的订阅,传递计划标识符到 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');
按比例分配
默认情况下,Stripe 会在订阅计划间切换时按比例进行分配,noProrate 可用于在修改订阅计划时不使用按比例分配机制:
$user->subscription('default')->noProrate()->swap('provider-plan-id');
订阅数量
有时候订阅也会被数量影响,例如,应用中每个账户每月需要付费$10,要简单增加或减少订阅数量,使用 incrementQuantity 和 decrementQuantity 方法:
$user = User::find(1);
$user->subscription('main')->incrementQuantity();
// Add five to the subscription's current quantity...
$user->subscription('main')->incrementQuantity(5);
$user->subscription('main')->decrementQuantity();
// Subtract five to the subscription's current quantity...
$user->subscription('main')->decrementQuantity(5);
我们也可以使用 updateQuantity
方法指定数量:
$user->subscription('main')->updateQuantity(10);
noProrate
方法可用于更新订阅数量而无需对收费进行评级:
$user->subscription('main')->noProrate()->updateQuantity(10);
想要了解更多订阅数量信息,查阅相关Stripe 官方文档。
多计划订阅
多计划订阅允许我们为单个订阅分配多个计费计划。例如,假设我们正在构建一个客户服务“帮助台”应用程序,该应用程序的基本订阅费用为每月 10 美元,但提供每月额外 15 美元的实时聊天附加计划:
$user = User::find(1);
$user->newSubscription('default', [
'price_monthly',
'chat-plan',
])->create($paymentMethod);
现在,客户将有两个default订阅计划。两个计划都将按各自的计费间隔收费。我们还可以使用quantity
方法来指示每个计划的具体数量:
$user = User::find(1);
$user->newSubscription('default', ['price_monthly', 'chat-plan'])
->quantity(5, 'chat-plan')
->create($paymentMethod);
或者,我们可以使用plan
方法动态添加额外计划及其数量:
$user = User::find(1);
$user->newSubscription('default', 'price_monthly')
->plan('chat-plan', 5)
->create($paymentMethod);
或者,我们可以稍后向现有订阅添加新计划:
$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计划:
$user = User::find(1);
$user->subscription('default')->swap(['pro-plan', 'chat-plan']);
执行上述代码时,basic-plan
删除带有 的底层订阅项,chat-plan
保留带有 的订阅项。此外,还会为新的订阅项目pro-plan创建一个新的订阅项目。
我们还可以指定订阅项目选项。例如,可能需要指定订阅计划数量:
$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
关系访问这些:
$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');
订阅税金
要指定用户对订阅支付的税率,在可计费模型上实现taxRates
方法,然后返回包含税率ID的数组。
public function taxRates()
{
return ['tax-rate-id'];
}
taxRates
将使我们可以在模型基础上使用税率,对跨越不同国家不同税率的用户很有用。如果正在使用多计划订阅,我们可以通过在计费模型上实施planTaxRates
方法来为每个计划定义不同的税率:
public function planTaxRates()
{
return [
'plan-id' => ['tax-rate-id'],
];
}
同步税率
更改taxRates
方法返回的硬编码税率 ID 时,用户的任何现有订阅的税收设置将保持不变。如果希望使用taxTaxRates
返回值更新现有订阅的税值,应该调用用户订阅实例上的syncTaxRates
方法:
$user->subscription('default')->syncTaxRates();
这也将同步任何订阅项目税率,因此请确保我们也能正确更改planTaxRates
方法。
免税
Cashier 还提供了通过调用 Stripe API 来确定客户是否免税的方法。isNotTaxExempt
,isTaxExempt
和reverseChargeApplies
方法可在计费模式上获得:
$user = User::find(1);
$user->isTaxExempt();
$user->isNotTaxExempt();
$user->reverseChargeApplies();
这些方法也可用于任何Laravel\Cashier\Invoice
对象。但是,在Invoice对象上调用这些方法时,这些方法将在创建发票时确定豁免状态。
订阅锚定日期
默认情况下,支付周期锚点就是订阅创建的日期,或者如果使用了试用期的话,该日期就是订阅期结束的日子。如果你想要编辑支付锚定日期,可以使用 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()) {
//
}
可以使用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);
该方法会在数据库订阅记录上设置试用期结束日期,以便告知 Stripe 在此之前不要计算用户的账单信息。当使用 trialDays 方法时,Cashier 将会覆盖任何默认 Stripe 计划的试用期配置。
注:如果用户的订阅没有在试用期结束之前取消,则会在试用期结束时立即支付,所以要确保通知用户试用期结束时间。
trialUntil
方法允许你提供一个 DateTime 实例来指定试用期什么时候结束:
use Carbon\Carbon;
$user->newSubscription('default', 'monthly')
->trialUntil(Carbon::now()->addDays(10))
->create($paymentMethod);
可以使用用户实例或订阅实例上的 onTrial
方法判断用户是否处于试用期,下面两个例子作用是等价的:
if ($user->onTrial('default')) {
//
}
if ($user->subscription('default')->onTrial()) {
//
}
在 Stripe / Cashier 中定义试用天数
我们可以选择在 Stripe 仪表板中定义计划接收的试用天数,或者始终使用 Cashier
明确传递它们。如果选择在 Stripe 中定义计划的试用天数,我们应该知道新订阅,包括过去订阅过的客户的新订阅,将始终获得试用期,除非我们明确调用trialDays(0)
方法。
不带预先付款信息
如果不想在提供试用期的时候收集用户预付款信息,只需设置用户记录的 trial_ends_at 字段为期望的试用期结束日期即可,这通常在用户注册期间完成:
$user = User::create([
// Populate other user properties...
'trial_ends_at' => Carbon::now()->addDays(10),
]);
注:确保已添加 trial_ends_at 日期修改器到模型定义。
Cashier 将这种类型的试用期看作「通用体验」,因为这种使用并没有附加到任何已经在的订阅,如果当前日期没有超过 trial_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...
}
一旦准备好为用户创建实际的订阅,可以使用 newSubscription
方法:
$user = User::find(1);
$user->newSubscription('default', 'monthly')->create($paymentMethod);
延长试用期
extendTrial
方法可用于在订阅计划创建之后延长试用期:
// 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 Webhooks
注:我们可以使用 Stripe CLI 在本地开发期间测试 Webhooks。
Stripe 可以通过 webhooks 通知应用各种事件,默认情况下,一个指向 Cashier webhook 控制器的路由已经通过 Cashier 服务提供者配置好了,这个控制器将会处理所有进入的 webhook 请求。
默认情况下,这个控制器将会自动对支付失败次数(这个次数可以在 Stripe 设置中定义)过多的订阅进行取消,自动处理顾客更新、删除,以及订阅更新和信用卡更新等操作;此外,我们很快会发现,你可以扩展这个控制器来处理任何你想要处理的 webhook 事件。
为了确保应用可以处理 Stripe Webhooks,确保在 Stripe 控制面板中配置过这个 webhook URL。下面是所有你需要在 Stripe 控制面板中配置的 webhooks 完整列表:
customer.subscription.updated
customer.subscription.deleted
customer.updated
customer.deleted
nvoice.payment_action_required
注:确保通过 Cashier 引入的 webhook 签名验证中间件对输入请求进行保护。
Webhooks & CSRF 防护
由于 Stripe webhook 需要绕开 Laravel 的 CSRF 保护,所以需要将其罗列到 VerifyCsrfToken
中间件的排除列表或者将其置于 web 中间件组之外:
protected $except = [
'stripe/*',
];
定义 Webhook 事件处理器
Cashier 会基于支付失败次数自动取消订阅,但是如果你想要处理额外的 Stripe webhook 事件,扩展 Webhook 控制器即可。定义的方法名需要与 Cashier 约定的格式保持一致,特别是方法名需要以 handle 开头并且是想要处理的 Stripe webhook 的驼峰格式。例如,如果想要处理 invoice.payment_succeeded
webhook,则需要在控制器中添加一个 handleInvoicePaymentSucceeded
方法:
<?php
namespace App\Http\Controllers;
use Laravel\Cashier\Http\Controllers\WebhookController as CashierController;
class WebhookController extends CashierController
{
/**
* Handle a Stripe webhook.
*
* @param array $payload
* @return Response
*/
public function handleInvoicePaymentSucceeded($payload)
{
// Handle The Event
}
}
接下来,在 routes/web.php
中定义一个指向 Cashier 控制器的路由:
Route::post(
'stripe/webhook',
'\App\Http\Controllers\WebhookController@handleWebhook'
);
Cashier 会在接收到 webhook 时触发 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 配置被设置,改配置值可以从 Stripe 账户后台面板中获取。
一次性支付
注:使用 Stripe 时, charge 方法可以接收应用所使用货币对应的最小单位金额。
如果想要使用订阅客户的信用卡一次性结清账单,可以使用 Billable 模型实例上的 charge
方法,该方法需要传入支付方法标识符作为第二个参数:
// Stripe Accepts Charges In Cents...
$stripeCharge = $user->charge(100, $paymentMethod);
charge
方法接收一个数组作为第三个参数,允许你传递任何你想要传递的底层 Stripe 账单创建参数,创建账单时我们可以参考 Stripe 文档提供的可用选项:
$user->charge(100, $paymentMethod, [
'custom_option' => $value,
]);
还可以使用不带底层顾客或用户参数的 charge
方法:
use App\User;
$stripeCharge = (new User)->charge(100, $paymentMethod);
如果支付失败 charge 方法将抛出异常,如果支付成功,该方法会返回 Laravel\Cashier\Payment 实例:
try {
$payment = $user->charge(100, $paymentMethod);
} catch (Exception $e) {
//
}
带发票的支付
有时候我们需要创建一个一次性支付并且同时生成对应发票以便为用户提供一个PDF单据, invoiceFor 方法可以帮助我们实现这个需求。例如,让我们为用户的「一次性费用」生成一张 $5.00 的发票:
// Stripe Accepts Charges In Cents...
$user->invoiceFor('One Time Fee', 500);
该单据会通过用户默认支付方法立即支付, invoiceFor 方法还可以接收一个数组作为第三个参数,该数组包含发票项目的计费选项。该方法的第四个参数也是一个数组,包含的是发票本身的计费选项:
$user->invoiceFor('Stickers', 500, [
'quantity' => 50,
], [
'tax_percent' => 21,
]);
注: invoiceFor 方法会创建一个对失败支付进行重试的 Stripe 单据,如果你不想要单据重试失败的支付,需要在首次支付失败后使用 Stripe API 关闭它们。
退款
如果我们需要对 Stripe 支付进行退款,可以使用 refund 方法,该方法接收 Stripe 支付 ID 作为唯一参数:
$payment = $user->charge(100, $paymentMethod);
$user->refund($payment->id);
发票
检索发票
我们可以使用 invoices
方法轻松获取账单模型的发票数组:
$invoices = $user->invoices();
// Include pending invoices in the results...
$invoices = $user->invoicesIncludingPending();
我们还可以使用 findInvoice
方法检索指定的发票:
$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 会抛出 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,重定向后,该 URL 中会新增 message 和 success 查询字符串参数。
此外,我们也可以允许 Stripe 为你处理这个支付确认流程,在这种情况下,你需要在 Stripe 后台设置 Stripe 的自动支付邮箱,而不是将用户重定向到支付确认页。不过,如果捕获了 IncompletePayment 异常,你仍然需要通知用户他们会收到一封邮件来处理后续的支付确认流程。
支付异常可能会在如下方法中抛出:charge
、invoiceFor
以及 Billable 用户的 invoice
方法。处理订阅时,SubscriptionBuilder 上的 create 方法、Subscription 上的 incrementAndInvoice 和 swapAndInvoice 方法也可能抛出异常。
目前还有两种继承 IncompletePayment
的支付异常类型,我们可以在需要的时候分别捕获以便自定义用户行为:
PaymentActionRequired
:表示 Stripe 需要额外的验证以便确认并处理支付;PaymentFailure
:表示其他原因导致的支付失败,比如资金不足。
存储顾客认证
如果我们的业务主要在欧洲,可能需要遵守 Strong Customer Authentication
(SCA)法规。该发规于 2019 年由欧盟强制实施,以避免付款欺诈。幸运的是,Stripe 和 Cashier 已经为构建兼容 SCA 的应用做好了准备。
注:开始之前,请查看 Stripe SCA 指南以及它们新的 SCA API 文档。
要求额外确认的支付
SCA 发规经常要求额外的验证以便确认和处理支付。当这种情况发生时,Cashier 将会抛出 PaymentActionRequired 异常来通知你需要进行额外验证,更多关于这些异常的处理方式请参考前面的异常处理文档。
未完成和过期状态
当支付需要额外确认时,订阅的 stripe_status 字段会保持 incomplete 或 past_due 状态,Cashier 会在支付确认完成时立即通过 webhook 自动激活顾客的订阅。
想了解更多关于 incomplete 和 past_due 状态的信息,请参考对应文档。
离线支付通知
由于 SCA 法规要求顾客偶尔验证他们的支付细节,即使订阅还是有效的,Cashier 会在离线支付确认需要时发送支付通知给顾客。例如,这将会在订阅更新时发生。Cashier 的支付通知可以通过设置环境变量 CASHIER_PAYMENT_NOTIFICATION 为某个通知类来启用,默认情况下,通知被禁用。当然,Cashier 内置了你可以开箱使用的通知类,不过也可以使用自定义的通知类:
CASHIER_PAYMENT_NOTIFICATION=Laravel\Cashier\Notifications\ConfirmPayment
要确保离线支付确认通知被发送,请验证 Stripe webhooks 已经做好配置,并且 invoice.payment_action_required
webhook 在 Stripe 后台被启用,此外,Billable 模型还应该使用了 Laravel 的 Illuminate\Notifications\Notifiable trait。
注:通知即使在顾客手动发起需要额外确认的支付时也会被发送,不幸的是,Stripe 没有办法知道支付是否是动手完成还是「离线」完成。但是,顾客会在确认支付后访问支付页面时看到「支付成功」消息,该顾客将不被允许对同一个支付进行两次确认以免导致第二次付费。
Stripe SDK
大多数 Cashier 对象都是对 Stripe SDK 对象的封装,如果想要直接和 Stripe 对象打交道,可以通过 asStripe
方法来获取它们:
$stripeSubscription = $subscription->asStripeSubscription();
$stripeSubscription->update(['application_fee_percent' => 5]);
测试
在测试使用 Cashier 的应用程序时,我们可以模拟对 Stripe API 的实际 HTTP 请求;但是,这需要我们部分重新实现 Cashier 自己的行为。因此,我们建议允许测试访问实际的 Stripe API。虽然这较慢,但它提供给了我们应用程序按预期工作的更多的信心,并且任何缓慢的测试都可以放在他们自己的 PHPUnit 测试组中。
测试时,请记住 Cashier 本身已经有一个很棒的测试套件,因此我们应该只专注于测试自己的应用程序的订阅和支付流程,而不是每个底层的 Cashier 行为。
首先,将Stripe 密钥的测试版本添加到我们的phpunit.xml
文件中:
<env name="STRIPE_SECRET" value="sk_test_<your-key>"/>
现在,每当我们在测试与 Cashier 交互时,它都会向 Stripe 测试环境发送实际的 API 请求。为方便起见,我们应该使用订阅/计划预先填写 Stripe 测试帐户,然后可以在测试期间使用这些订阅/计划。