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 包添加到项目依赖中:

  1. composer require laravel/cashier

注意:为确保 Cashier 正确的处理所有的 Stripe 事件,请记得 配置 Cashier webhook 处理

数据库迁移

Cashier 服务提供者注册了自己的数据库迁移目录,请记住在安装完包后迁移你的数据库。Cashier 迁移会在你的 users 表中增加几列并创建一个新的 subscriptions 表来保存你所有客户的订阅:

  1. php artisan migrate

如果你需要覆盖 Cashier 包附带的迁移,可以使用 vendor:publish Artisan 命令发布它们:

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

如果你想阻止 Cashier 迁移完全运行,可以使用 Cashier 提供的 ignoreMigrations 。通常,你应该在 AppServiceProviderregister 方法中调用这个方法:

  1. use Laravel\Cashier\Cashier;
  2. Cashier::ignoreMigrations();

注意:Stripe 建议用来存储 stripe 标识符的所有列应该大小写敏感。因此,以 MySQL 数据库为例,你应该确保 stripe_id 列的列排序规则设置为 utf8_bin ,更多信息可以在 Stripe 文档 中找到。

配置

Billable 模型

在使用 Cashier 之前, 添加 Billable Trait 到你的模型定义中。 这个 Trait 提供了多种方法以便你执行常见的支付任务,如创建订阅、申请优惠券以及更新支付方式信息:

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

Cashier 暂定你的 Billable 模型是 Laravel 附带的 App\User 类,如果你想修改请在你的 .env 文件中指定一个不同的模型:

  1. CASHIER_MODEL=App\User

注意:如果你使用的不是 Laravel 提供的 App\User 模型,你应该发布并更改提供的 migrations 以匹配你的代替模型数据表名。

API 密钥

接下来,您需要在 .env 中配置您的 Stripe 密钥。您可以在 Stripe 的控制面板中获取 Stripe API 密钥。

  1. STRIPE_KEY=your-stripe-key
  2. STRIPE_SECRET=your-stripe-secret

货币配置

Cashier 的默认货币是美元 (USD)。您可以设置 CASHIER_CURRENCY 来更改默认的货币:

  1. CASHIER_CURRENCY=eur

为了配置 Cashier 使用的货币,您也许需要指定一个额外的 Locale(本地化) 参数,用于格式化在账单上面显示的金额。Cashier 内部使用 PHP 的 NumberFormatter 来格式化数字:

  1. CASHIER_CURRENCY_LOCALE=nl_BE

注意:要使用除了 en 之外的 Locale,确保 ext-intl 扩展在服务器上安装并配置好。

日志记录

Cashier 允许你指定在记录所有 Stripe 相关异常时使用的日志通道。你可以使用 CASHIER_LOGGER 环境变量指定日志通道:

  1. 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:

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

Creating Customers

有时,您可能希望在未订阅的情况下创建 Stripe 客户。您可以使用 createAsStripeCustomer 方法完成此操作:

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

一旦在 Stripe 中创建了客户,你可以在以后的某个日期开始订阅。你也可以使用一个可选的 $options 数组来传入 Stripe API 支持的任何额外参数:

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

如果可计费实体已经是 Stripe 中的客户,你还可以使用 createOrGetStripeCustomer 方法来返回客户对象。

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

支付方式

有时,你可能希望直接用附加信息更新 Stripe 客户。你可以使用 updateStripeCustomer 方法来完成:

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

支付方式

储存支付方式

为了使用 Stripe 创建订阅或是一次性扣费,您将需要从 Stripe 获取支付方式并储存他的识别码。基于您计划使用的支付方式是用来一次性扣费还是订阅,我们有两种方案来达成,接下来我们一起来看一下这两种方法。

用于订阅制的支付方法

为了储存用户的信用卡供以后使用,Stripe 的 “Setup Intent” API 需要安全的获取用户的信用卡信息。“Setup Intent” 指示 Stripe 用户支付方式的意象。Cashier 的Blillable trait 包含了 createSetupIntent 方法来快速创建一个 “Setup Intent”。您应该在路由或者控制器调用这个方法来获取用户的支付方式意象:

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

当您创建完 “Setup Intent” 并传递给视图后,您应该将它的密钥元素附加到获取用户支付信息的表单中。如下面这个“更新支付方式”表单:

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

接下来,Stripe.js 库会调用这个密钥并安全地抓取用户的支付信息:

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

  1. const cardHolderName = document.getElementById('card-holder-name');
  2. const cardButton = document.getElementById('card-button');
  3. const clientSecret = cardButton.dataset.secret;
  4. cardButton.addEventListener('click', async (e) => {
  5. const { setupIntent, error } = await stripe.confirmCardSetup(
  6. clientSecret, {
  7. payment_method: {
  8. card: cardElement,
  9. billing_details: { name: cardHolderName.value }
  10. }
  11. }
  12. );
  13. if (error) {
  14. // 显示错误信息
  15. } else {
  16. // 卡片已成功验证
  17. }
  18. });

当 Stripe 完成对客户支付方式的校验后,您可以将setupIntent.payment_method 识别码返回给您的应用并附加给客户。这次支付使用的支付方式可 被添加为新的支付方式 或者 用于更新默认支付方式。您可以立即使用这个识别码来 创建一个订阅

Tip:如果您希望了解更多有关 “Setup Intent” 与获取收集用户支付详情的信息,请参阅 Stripe 的文档

用于一次性收费的支付方式

当然,用于一次性收费的支付识别码我们只需要使用一次。由于 Stripe 的限制,您无法使用客户的一次性收费的支付方式。您需要用 Stripe.js 库来让客户填写他们的支付详情。让我们来看一下下面这个支付表单:

  1. <input id="card-holder-name" type="text">
  2. <!-- Stripe Elements Placeholder -->
  3. <div id="card-element"></div>
  4. <button id="card-button">
  5. 处理支付流程
  6. </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 方法。通常,这个方法应该在你的 AppServiceProviderregister 方法中调用:

use Laravel\Cashier\Cashier;

/**
 * Register any application services.
 *
 * @return void
 */
public function register()
{
    Cashier::keepPastDueSubscriptionsActive();
}

注意:订阅处于某种 incomplete 状态时,在确认付款之前无法更改。因此,当订阅处于某种状态时, swapupdateQuantity 方法将抛出异常 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 / 月。你可以使用 incrementQuantitydecrementQuantity 方法轻松地增加或减少你的订阅量:

$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 中的 incrementAndInvoiceswapAndInvoice 会抛出异常。

账单未完成与已逾期

每当账单需要额外确认时,订阅在数据库中的 stripe_status 列留下 incompletepast_due 状态。当 Webhook 确认支付已完成时,Cashier 会立即激活用户的订阅。

如果您要了解更多关于 incompletepast_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