Skip to content

Laravel Cashier

介绍

Laravel Cashier 提供了一个对 Stripe 订阅计费服务的表达性、流畅的接口。它几乎处理了所有你不想编写的订阅计费样板代码。除了基本的订阅管理,Cashier 还可以处理优惠券、交换订阅、订阅“数量”、取消宽限期,甚至生成发票 PDF。

升级 Cashier

在升级到新版本的 Cashier 时,务必仔细查看 升级指南

exclamation

为了防止破坏性更改,Cashier 使用固定的 Stripe API 版本。Cashier 12 使用 Stripe API 版本 2020-03-02。Stripe API 版本将在小版本更新中更新,以便利用新的 Stripe 功能和改进。

安装

首先,使用 Composer 安装 Stripe 的 Cashier 包:

php
composer require laravel/cashier
exclamation

为确保 Cashier 正确处理所有 Stripe 事件,请记得 设置 Cashier 的 webhook 处理

数据库迁移

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

php
php artisan migrate

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

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

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

php
use Laravel\Cashier\Cashier;

Cashier::ignoreMigrations();
exclamation

Stripe 建议用于存储 Stripe 标识符的任何列都应区分大小写。因此,你应该确保 stripe_id 列的排序规则在 MySQL 中设置为例如 utf8_bin。更多信息可以在 Stripe 文档 中找到。

配置

可计费模型

在使用 Cashier 之前,将 Billable trait 添加到你的模型定义中。这个 trait 提供了各种方法,允许你执行常见的计费任务,例如创建订阅、应用优惠券和更新支付方式信息:

php
use Laravel\Cashier\Billable;

class User extends Authenticatable
{
    use Billable;
}

Cashier 假定你的可计费模型将是 Laravel 附带的 App\User 类。如果你希望更改此设置,可以在 .env 文件中指定不同的模型:

php
CASHIER_MODEL=App\User
exclamation

如果你使用的是 Laravel 提供的 App\User 模型以外的模型,你需要发布并更改提供的 迁移 以匹配你的替代模型的表名。

API 密钥

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

php
STRIPE_KEY=your-stripe-key
STRIPE_SECRET=your-stripe-secret

货币配置

Cashier 的默认货币是美元 (USD)。你可以通过设置 CASHIER_CURRENCY 环境变量来更改默认货币:

php
CASHIER_CURRENCY=eur

除了配置 Cashier 的货币外,你还可以指定一个用于在发票上显示货币值时的区域设置。Cashier 内部使用 PHP 的 NumberFormatter 来设置货币区域设置:

php
CASHIER_CURRENCY_LOCALE=nl_BE
exclamation

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

日志记录

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

php
CASHIER_LOGGER=stack

客户

检索客户

你可以使用 Cashier::findBillable 方法通过其 Stripe ID 检索客户。这将返回可计费模型的实例:

php
use Laravel\Cashier\Cashier;

$user = Cashier::findBillable($stripeId);

创建客户

有时,你可能希望在不开始订阅的情况下创建一个 Stripe 客户。你可以使用 createAsStripeCustomer 方法实现这一点:

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

一旦客户在 Stripe 中创建,你可以在稍后开始订阅。你还可以使用可选的 $options 数组传递任何由 Stripe API 支持的其他参数:

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

如果你想在可计费实体已经是 Stripe 中的客户时返回客户对象,可以使用 asStripeCustomer 方法:

php
$stripeCustomer = $user->asStripeCustomer();

如果你不确定可计费实体是否已经是 Stripe 中的客户,但想返回客户对象,可以使用 createOrGetStripeCustomer 方法。如果不存在,它将创建一个新的客户:

php
$stripeCustomer = $user->createOrGetStripeCustomer();

更新客户

有时,你可能希望直接使用其他信息更新 Stripe 客户。你可以使用 updateStripeCustomer 方法实现这一点:

php
$stripeCustomer = $user->updateStripeCustomer($options);

账单门户

Stripe 提供了一种 简单的方法来设置账单门户,以便你的客户可以管理他们的订阅、支付方式和查看他们的账单历史。你可以使用控制器或路由中的 redirectToBillingPortal 方法将用户重定向到账单门户:

php
use Illuminate\Http\Request;

public function billingPortal(Request $request)
{
    return $request->user()->redirectToBillingPortal();
}

默认情况下,当用户完成管理他们的订阅时,他们可以返回到应用程序的 home 路由。你可以通过将 URL 作为参数传递给 redirectToBillingPortal 方法来提供用户应返回的自定义 URL:

php
use Illuminate\Http\Request;

public function billingPortal(Request $request)
{
    return $request->user()->redirectToBillingPortal(
        route('billing')
    );
}

如果你只想生成账单门户的 URL,可以使用 billingPortalUrl 方法:

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

支付方式

存储支付方式

为了使用 Stripe 创建订阅或执行“一次性”收费,你需要存储支付方式并从 Stripe 检索其标识符。实现这一点的方法取决于你计划将支付方式用于订阅还是单次收费,因此我们将在下面分别进行检查。

订阅的支付方式

当将信用卡存储到客户以供将来使用时,必须使用 Stripe Setup Intents API 来安全地收集客户的支付方式详细信息。“Setup Intent”表示 Stripe 将对客户的支付方式进行收费的意图。Cashier 的 Billable trait 包含 createSetupIntent 方法,以便轻松创建新的 Setup Intent。你应该从将呈现收集客户支付方式详细信息的表单的路由或控制器中调用此方法:

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

在创建 Setup Intent 并将其传递给视图后,你应该将其秘密附加到将收集支付方式的元素上。例如,考虑这个“更新支付方式”表单:

php
<input id="card-holder-name" type="text">

<!-- Stripe Elements 占位符 -->
<div id="card-element"></div>

<button id="card-button" data-secret="{{ $intent->client_secret }}">
    更新支付方式
</button>

接下来,可以使用 Stripe.js 库将 Stripe 元素附加到表单并安全地收集客户的支付详细信息:

php
<script src="https://js.stripe.com/v3/"></script>

<script>
    const stripe = Stripe('stripe-public-key');

    const elements = stripe.elements();
    const cardElement = elements.create('card');

    cardElement.mount('#card-element');
</script>

接下来,可以使用 Stripe 的 confirmCardSetup 方法验证卡并从 Stripe 检索安全的“支付方式标识符”:

php
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) {
        // 向用户显示“error.message”...
    } else {
        // 卡已成功验证...
    }
});

在卡被 Stripe 验证后,你可以将生成的 setupIntent.payment_method 标识符传递给你的 Laravel 应用程序,在那里可以将其附加到客户。支付方式可以作为 添加新的支付方式用于更新默认支付方式。你还可以立即使用支付方式标识符 创建新的订阅

lightbulb

如果你想了解有关 Setup Intents 和收集客户支付详细信息的更多信息,请 查看 Stripe 提供的此概述

单次收费的支付方式

当然,当对客户的支付方式进行单次收费时,我们只需要使用支付方式标识符一次。由于 Stripe 的限制,你不能使用客户的存储默认支付方式进行单次收费。你必须允许客户使用 Stripe.js 库输入他们的支付方式详细信息。例如,考虑以下表单:

php
<input id="card-holder-name" type="text">

<!-- Stripe Elements 占位符 -->
<div id="card-element"></div>

<button id="card-button">
    处理支付
</button>

接下来,可以使用 Stripe.js 库将 Stripe 元素附加到表单并安全地收集客户的支付详细信息:

php
<script src="https://js.stripe.com/v3/"></script>

<script>
    const stripe = Stripe('stripe-public-key');

    const elements = stripe.elements();
    const cardElement = elements.create('card');

    cardElement.mount('#card-element');
</script>

接下来,可以使用 Stripe 的 createPaymentMethod 方法验证卡并从 Stripe 检索安全的“支付方式标识符”:

php
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) {
        // 向用户显示“error.message”...
    } else {
        // 卡已成功验证...
    }
});

如果卡验证成功,你可以将 paymentMethod.id 传递给你的 Laravel 应用程序并处理 单次收费

检索支付方式

paymentMethods 方法在可计费模型实例上返回一个 Laravel\Cashier\PaymentMethod 实例的集合:

php
$paymentMethods = $user->paymentMethods();

要检索默认支付方式,可以使用 defaultPaymentMethod 方法:

php
$paymentMethod = $user->defaultPaymentMethod();

你还可以使用 findPaymentMethod 方法检索可计费模型拥有的特定支付方式:

php
$paymentMethod = $user->findPaymentMethod($paymentMethodId);

确定用户是否有支付方式

要确定可计费模型是否有默认支付方式附加到其帐户,请使用 hasDefaultPaymentMethod 方法:

php
if ($user->hasDefaultPaymentMethod()) {
    //
}

要确定可计费模型是否至少有一种支付方式附加到其帐户,请使用 hasPaymentMethod 方法:

php
if ($user->hasPaymentMethod()) {
    //
}

更新默认支付方式

updateDefaultPaymentMethod 方法可用于更新客户的默认支付方式信息。此方法接受 Stripe 支付方式标识符,并将新支付方式指定为默认计费支付方式:

php
$user->updateDefaultPaymentMethod($paymentMethod);

要将默认支付方式信息与 Stripe 中的客户默认支付方式信息同步,可以使用 updateDefaultPaymentMethodFromStripe 方法:

php
$user->updateDefaultPaymentMethodFromStripe();
exclamation

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

添加支付方式

要添加新的支付方式,可以在可计费用户上调用 addPaymentMethod 方法,传递支付方式标识符:

php
$user->addPaymentMethod($paymentMethod);
lightbulb

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

删除支付方式

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

php
$paymentMethod->delete();

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

php
$user->deletePaymentMethods();
exclamation

如果用户有活动订阅,你应该阻止他们删除默认支付方式。

订阅

创建订阅

要创建订阅,首先检索可计费模型的实例,通常是 App\User 的实例。一旦检索到模型实例,可以使用 newSubscription 方法创建模型的订阅:

php
$user = User::find(1);

$user->newSubscription('default', 'price_premium')->create($paymentMethod);

传递给 newSubscription 方法的第一个参数应该是订阅的名称。如果你的应用程序只提供一个订阅,你可以将其称为 defaultprimary。第二个参数是用户订阅的特定计划。此值应与 Stripe 中计划的价格标识符相对应。

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

exclamation

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

数量

如果你希望在创建订阅时为计划设置特定数量,可以使用 quantity 方法:

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

其他详细信息

如果你希望指定其他客户或订阅详细信息,可以通过 create 方法的第二个和第三个参数传递它们:

php
$user->newSubscription('default', 'price_monthly')->create($paymentMethod, [
    'email' => $email,
], [
    'metadata' => ['note' => '一些额外信息。'],
]);

要了解 Stripe 支持的其他字段,请查看 Stripe 关于 客户创建订阅创建 的文档。

优惠券

如果你希望在创建订阅时应用优惠券,可以使用 withCoupon 方法:

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

添加订阅

如果你希望为已经设置了默认支付方式的客户添加订阅,可以在使用 newSubscription 方法时使用 add 方法:

php
$user = User::find(1);

$user->newSubscription('default', 'price_premium')->add();

检查订阅状态

一旦用户订阅了你的应用程序,可以使用各种方便的方法轻松检查他们的订阅状态。首先,subscribed 方法返回 true,如果用户有活动订阅,即使订阅当前在试用期内:

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

subscribed 方法也是 路由中间件 的一个很好的候选者,允许你根据用户的订阅状态过滤对路由和控制器的访问:

php
public function handle($request, Closure $next)
{
    if ($request->user() && ! $request->user()->subscribed('default')) {
        // 该用户不是付费客户...
        return redirect('billing');
    }

    return $next($request);
}

如果你想确定用户是否仍在试用期内,可以使用 onTrial 方法。此方法可用于向用户显示警告,告知他们仍在试用期内:

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

subscribedToPlan 方法可用于根据给定的 Stripe 价格 ID 确定用户是否订阅了给定计划。在此示例中,我们将确定用户的 default 订阅是否积极订阅了 monthly 计划:

php
if ($user->subscribedToPlan('price_monthly', 'default')) {
    //
}

通过将数组传递给 subscribedToPlan 方法,可以确定用户的 default 订阅是否积极订阅了 monthlyyearly 计划:

php
if ($user->subscribedToPlan(['price_monthly', 'price_yearly'], 'default')) {
    //
}

recurring 方法可用于确定用户当前是否订阅并且不再在试用期内:

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

取消的订阅状态

要确定用户是否曾经是活跃订阅者,但已取消其订阅,可以使用 cancelled 方法:

php
if ($user->subscription('default')->cancelled()) {
    //
}

你还可以确定用户是否已取消其订阅,但仍在其“宽限期”内,直到订阅完全过期。例如,如果用户在 3 月 5 日取消订阅,而订阅原定于 3 月 10 日到期,则用户在 3 月 10 日之前处于“宽限期”。请注意,在此期间 subscribed 方法仍返回 true

php
if ($user->subscription('default')->onGracePeriod()) {
    //
}

要确定用户是否已取消其订阅并且不再在其“宽限期”内,可以使用 ended 方法:

php
if ($user->subscription('default')->ended()) {
    //
}

订阅范围

大多数订阅状态也可用作查询范围,以便你可以轻松查询数据库中处于给定状态的订阅:

php
// 获取所有活动订阅...
$subscriptions = Subscription::query()->active()->get();

// 获取用户的所有已取消订阅...
$subscriptions = $user->subscriptions()->cancelled()->get();

以下是可用范围的完整列表:

php
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。当你的订阅处于这些状态之一时,它将不会激活,直到客户确认其支付。可以使用可计费模型或订阅实例上的 hasIncompletePayment 方法检查订阅是否有不完整的支付:

php
if ($user->hasIncompletePayment('default')) {
    //
}

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

当订阅有不完整的支付时,你应该将用户引导到 Cashier 的支付确认页面,传递 latestPayment 标识符。可以使用订阅实例上的 latestPayment 方法检索此标识符:

php
<a href="{{ route('cashier.payment', $subscription->latestPayment()->id) }}">
    请确认你的支付。
</a>

如果你希望订阅在 past_due 状态时仍被视为活动,可以使用 Cashier 提供的 keepPastDueSubscriptionsActive 方法。通常,此方法应在 AppServiceProviderregister 方法中调用:

php
use Laravel\Cashier\Cashier;

/**
 * 注册任何应用程序服务。
 *
 * @return void
 */
public function register()
{
    Cashier::keepPastDueSubscriptionsActive();
}
exclamation

当订阅处于 incomplete 状态时,无法更改,直到支付得到确认。因此,当订阅处于 incomplete 状态时,swapupdateQuantity 方法将抛出异常。

更改计划

在用户订阅你的应用程序后,他们可能偶尔想要更改为新的订阅计划。要将用户交换到新的订阅计划,请将计划的价格标识符传递给 swap 方法:

php
$user = App\User::find(1);

$user->subscription('default')->swap('provider-price-id');

如果用户处于试用期,试用期将保持不变。此外,如果订阅存在“数量”,该数量也将保持不变。

如果你希望交换计划并取消用户当前的试用期,可以使用 skipTrial 方法:

php
$user->subscription('default')
        ->skipTrial()
        ->swap('provider-price-id');

如果你希望交换计划并立即向用户开具发票,而不是等待他们的下一个计费周期,可以使用 swapAndInvoice 方法:

php
$user = App\User::find(1);

$user->subscription('default')->swapAndInvoice('provider-price-id');

分摊

默认情况下,Stripe 在计划之间交换时会分摊费用。可以使用 noProrate 方法更新订阅而不分摊费用:

php
$user->subscription('default')->noProrate()->swap('provider-price-id');

有关订阅分摊的更多信息,请查阅 Stripe 文档

exclamation

swapAndInvoice 方法之前执行 noProrate 方法对分摊没有影响。发票将始终开具。

订阅数量

有时订阅会受到“数量”的影响。例如,你的应用程序可能会按每个帐户每月 每个用户 收取 $10。要轻松增加或减少订阅数量,请使用 incrementQuantitydecrementQuantity 方法:

php
$user = User::find(1);

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

// 在订阅的当前数量上增加五个...
$user->subscription('default')->incrementQuantity(5);

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

// 在订阅的当前数量上减少五个...
$user->subscription('default')->decrementQuantity(5);

或者,可以使用 updateQuantity 方法设置特定数量:

php
$user->subscription('default')->updateQuantity(10);

可以使用 noProrate 方法更新订阅数量而不分摊费用:

php
$user->subscription('default')->noProrate()->updateQuantity(10);

有关订阅数量的更多信息,请查阅 Stripe 文档

exclamation

请注意,当处理多计划订阅时,上述数量方法需要额外的“计划”参数。

多计划订阅

多计划订阅 允许你为单个订阅分配多个计费计划。例如,假设你正在构建一个客户服务“帮助台”应用程序,该应用程序的基本订阅为每月 $10,但提供一个额外的实时聊天计划,每月额外收费 $15:

php
$user = User::find(1);

$user->newSubscription('default', [
    'price_monthly',
    'chat-plan',
])->create($paymentMethod);

现在,客户将在其 default 订阅上拥有两个计划。两个计划都将在各自的计费间隔内收费。你还可以使用 quantity 方法指示每个计划的特定数量:

php
$user = User::find(1);

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

或者,可以使用 plan 方法动态添加额外的计划及其数量:

php
$user = User::find(1);

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

或者,可以在稍后时间向现有订阅添加新计划:

php
$user = User::find(1);

$user->subscription('default')->addPlan('chat-plan');

上面的示例将添加新计划,客户将在下一个计费周期中为其收费。如果你希望立即向客户收费,可以使用 addPlanAndInvoice 方法:

php
$user->subscription('default')->addPlanAndInvoice('chat-plan');

如果你希望以特定数量添加计划,可以将数量作为 addPlanaddPlanAndInvoice 方法的第二个参数传递:

php
$user = User::find(1);

$user->subscription('default')->addPlan('chat-plan', 5);

可以使用 removePlan 方法从订阅中删除计划:

php
$user->subscription('default')->removePlan('chat-plan');
exclamation

你不能删除订阅上的最后一个计划。相反,你应该简单地取消订阅。

交换

你还可以更改附加到多计划订阅的计划。例如,假设你在 basic-plan 订阅上有一个 chat-plan 附加计划,并且你想升级到 pro-plan 计划:

php
$user = User::find(1);

$user->subscription('default')->swap(['pro-plan', 'chat-plan']);

执行上述代码时,具有 basic-plan 的底层订阅项将被删除,并保留具有 chat-plan 的订阅项。此外,将为新的 pro-plan 创建一个新的订阅项。

你还可以指定订阅项选项。例如,你可能需要指定订阅计划数量:

php
$user = User::find(1);

$user->subscription('default')->swap([
    'pro-plan' => ['quantity' => 5],
    'chat-plan'
]);

如果你想在订阅上交换单个计划,可以在订阅项本身上使用 swap 方法。这种方法很有用,例如,如果你想保留订阅项上的所有现有元数据。

php
$user = User::find(1);

$user->subscription('default')
        ->findItemOrFail('basic-plan')
        ->swap('pro-plan');

分摊

默认情况下,Stripe 在订阅中添加或删除计划时会分摊费用。如果你希望在不分摊的情况下进行计划调整,应在计划操作上链接 noProrate 方法:

php
$user->subscription('default')->noProrate()->removePlan('chat-plan');

数量

如果你希望更新单个订阅计划的数量,可以使用 现有数量方法 并将计划名称作为附加参数传递给方法:

php
$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');
exclamation

当你在订阅上设置多个计划时,Subscription 模型上的 stripe_planquantity 属性将为 null。要访问单个计划,应使用 Subscription 模型上可用的 items 关系。

订阅项

当订阅有多个计划时,它将在数据库的 subscription_items 表中有多个订阅“项”。可以通过订阅上的 items 关系访问这些项:

php
$user = User::find(1);

$subscriptionItem = $user->subscription('default')->items->first();

// 检索特定项的 Stripe 计划和数量...
$stripePlan = $subscriptionItem->stripe_plan;
$quantity = $subscriptionItem->quantity;

你还可以使用 findItemOrFail 方法检索特定计划:

php
$user = User::find(1);

$subscriptionItem = $user->subscription('default')->findItemOrFail('chat-plan');

订阅税

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

php
public function taxRates()
{
    return ['tax-rate-id'];
}

taxRates 方法允许你在模型级别应用税率,这对于跨多个国家和税率的用户群可能很有帮助。如果你正在处理多计划订阅,可以在可计费模型上实现 planTaxRates 方法,为每个计划定义不同的税率:

php
public function planTaxRates()
{
    return [
        'plan-id' => ['tax-rate-id'],
    ];
}
exclamation

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

同步税率

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

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

这也将同步任何订阅项税率,因此请确保你也正确更改 planTaxRates 方法。

税收豁免

Cashier 还提供了通过调用 Stripe API 确定客户是否免税的方法。isNotTaxExemptisTaxExemptreverseChargeApplies 方法可用于可计费模型:

php
$user = User::find(1);

$user->isTaxExempt();
$user->isNotTaxExempt();
$user->reverseChargeApplies();

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

订阅锚定日期

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

php
use App\User;
use Carbon\Carbon;

$user = User::find(1);

$anchor = Carbon::parse('first day of next month');

$user->newSubscription('default', 'price_premium')
            ->anchorBillingCycleOn($anchor->startOfDay())
            ->create($paymentMethod);

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

取消订阅

要取消订阅,请在用户的订阅上调用 cancel 方法:

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

当订阅被取消时,Cashier 将自动设置数据库中的 ends_at 列。此列用于知道何时 subscribed 方法应开始返回 false。例如,如果客户在 3 月 1 日取消订阅,但订阅原定于 3 月 5 日结束,则 subscribed 方法将继续返回 true,直到 3 月 5 日。

可以使用 onGracePeriod 方法确定用户是否已取消其订阅但仍在其“宽限期”内:

php
if ($user->subscription('default')->onGracePeriod()) {
    //
}

如果你希望立即取消订阅,请在用户的订阅上调用 cancelNow 方法:

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

恢复订阅

如果用户已取消其订阅并且你希望恢复它,请使用 resume 方法。用户 必须 仍在其宽限期内才能恢复订阅:

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

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

订阅试用

提前支付方式

如果你希望在仍然收集支付方式信息的情况下向客户提供试用期,应在创建订阅时使用 trialDays 方法:

php
$user = User::find(1);

$user->newSubscription('default', 'price_monthly')
            ->trialDays(10)
            ->create($paymentMethod);

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

exclamation

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

trialUntil 方法允许你提供一个 DateTime 实例来指定试用期应何时结束:

php
use Carbon\Carbon;

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

可以使用用户实例的 onTrial 方法或订阅实例的 onTrial 方法确定用户是否在其试用期内。以下两个示例是相同的:

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

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

在 Stripe / Cashier 中定义试用天数

你可以选择在 Stripe 仪表板中定义计划的试用天数,或者始终使用 Cashier 显式传递它们。如果你选择在 Stripe 中定义计划的试用天数,你应该知道新订阅,包括过去曾有订阅的客户的新订阅,将始终获得试用期,除非你显式调用 trialDays(0) 方法。

不提前支付方式

如果你希望在不收集用户支付方式信息的情况下提供试用期,可以在用户记录上设置 trial_ends_at 列为你想要的试用期结束日期。这通常在用户注册期间完成:

php
$user = User::create([
    // 填充其他用户属性...
    'trial_ends_at' => now()->addDays(10),
]);
exclamation

确保在你的模型定义中为 trial_ends_at 添加 日期变换器

Cashier 将此类型的试用称为“通用试用”,因为它不附加到任何现有订阅。用户实例上的 onTrial 方法将返回 true,如果当前日期未超过 trial_ends_at 的值:

php
if ($user->onTrial()) {
    // 用户在其试用期内...
}

你还可以使用 onGenericTrial 方法,如果你希望具体知道用户在其“通用”试用期内,并且尚未创建实际订阅:

php
if ($user->onGenericTrial()) {
    // 用户在其“通用”试用期内...
}

一旦你准备好为用户创建实际订阅,可以像往常一样使用 newSubscription 方法:

php
$user = User::find(1);

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

延长试用期

extendTrial 方法允许你在创建订阅后延长试用期:

php
// 试用期在 7 天后结束...
$subscription->extendTrial(
    now()->addDays(7)
);

// 在试用期上再增加 5 天...
$subscription->extendTrial(
    $subscription->trial_ends_at->addDays(5)
);

如果试用期已过期,客户已经为订阅付费,你仍然可以为他们提供延长的试用期。试用期内的时间将从客户的下一张发票中扣除。

处理 Stripe Webhooks

lightbulb

你可以使用 Stripe CLI 来帮助在本地开发期间测试 webhooks。

Stripe 可以通过 webhooks 通知你的应用程序各种事件。默认情况下,通过 Cashier 服务提供者配置了指向 Cashier 的 webhook 控制器的路由。此控制器将处理所有传入的 webhook 请求。

默认情况下,此控制器将自动处理取消订阅(如果有太多失败的收费,具体由你的 Stripe 设置定义)、客户更新、客户删除、订阅更新和支付方式更改;但是,正如我们将很快发现的那样,你可以扩展此控制器以处理任何你喜欢的 webhook 事件。

为了确保你的应用程序可以处理 Stripe webhooks,请确保在 Stripe 控制面板中配置 webhook URL。默认情况下,Cashier 的 webhook 控制器监听 /stripe/webhook URL 路径。你应该在 Stripe 控制面板中配置的所有 webhooks 的完整列表如下:

  • customer.subscription.updated
  • customer.subscription.deleted
  • customer.updated
  • customer.deleted
  • invoice.payment_action_required
exclamation

确保使用 Cashier 附带的 webhook 签名验证 中间件保护传入请求。

Webhooks 和 CSRF 保护

由于 Stripe webhooks 需要绕过 Laravel 的 CSRF 保护,请确保在 VerifyCsrfToken 中间件中将 URI 列为例外,或者将路由列在 web 中间件组之外:

php
protected $except = [
    'stripe/*',
];

定义 Webhook 事件处理程序

Cashier 自动处理失败收费时的订阅取消,但如果你有其他想要处理的 webhook 事件,请扩展 Webhook 控制器。你的方法名称应符合 Cashier 的预期约定,具体来说,方法应以 handle 和你希望处理的 webhook 的“驼峰式”名称为前缀。例如,如果你希望处理 invoice.payment_succeeded webhook,应在控制器中添加 handleInvoicePaymentSucceeded 方法:

php
<?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 控制器的路由。这将覆盖默认的已提供路由:

php
Route::post(
    'stripe/webhook',
    '\App\Http\Controllers\WebhookController@handleWebhook'
);

Cashier 在接收到 webhook 时会发出 Laravel\Cashier\Events\WebhookReceived 事件,并在 Cashier 处理 webhook 时发出 Laravel\Cashier\Events\WebhookHandled 事件。这两个事件都包含 Stripe webhook 的完整负载。

处理失败的订阅

如果客户的信用卡过期怎么办?不用担心 - Cashier 的 Webhook 控制器将为你取消客户的订阅。失败的支付将自动被控制器捕获和处理。当 Stripe 确定订阅失败时(通常在三次失败的支付尝试后),控制器将取消客户的订阅。

验证 Webhook 签名

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

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

单次收费

简单收费

exclamation

charge 方法接受你希望收费的金额,单位为应用程序使用的货币的 最低分母

如果你希望对订阅客户的支付方式进行“一次性”收费,可以在可计费模型实例上使用 charge 方法。你需要 提供支付方式标识符 作为第二个参数:

php
// Stripe 接受以分为单位的收费...
$stripeCharge = $user->charge(100, $paymentMethod);

charge 方法接受一个数组作为其第三个参数,允许你将任何选项传递给底层 Stripe 收费创建。请查阅 Stripe 文档,了解创建收费时可用的选项:

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

你还可以在没有底层客户或用户的情况下使用 charge 方法:

php
use App\User;

$stripeCharge = (new User)->charge(100, $paymentMethod);

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

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

带发票的收费

有时你可能需要进行一次性收费,但也需要为收费生成发票,以便你可以向客户提供 PDF 收据。invoiceFor 方法可以让你做到这一点。例如,让我们为客户开具 $5.00 的“一次性费用”发票:

php
// Stripe 接受以分为单位的收费...
$user->invoiceFor('One Time Fee', 500);

发票将立即对用户的默认支付方式收费。invoiceFor 方法还接受一个数组作为其第三个参数。此数组包含发票项的计费选项。方法接受的第四个参数也是一个数组。此最终参数接受发票本身的计费选项:

php
$user->invoiceFor('Stickers', 500, [
    'quantity' => 50,
], [
    'default_tax_rates' => ['tax-rate-id'],
]);
exclamation

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

退款

如果你需要退款 Stripe 收费,可以使用 refund 方法。此方法接受 Stripe Payment Intent ID 作为其第一个参数:

php
$payment = $user->charge(100, $paymentMethod);

$user->refund($payment->id);

发票

检索发票

可以使用 invoices 方法轻松检索可计费模型的发票数组:

php
$invoices = $user->invoices();

// 在结果中包含待处理的发票...
$invoices = $user->invoicesIncludingPending();

可以使用 findInvoice 方法检索特定发票:

php
$invoice = $user->findInvoice($invoiceId);

显示发票信息

在为客户列出发票时,可以使用发票的辅助方法显示相关的发票信息。例如,你可能希望在表格中列出每张发票,允许用户轻松下载其中的任何一张:

php
<table>
    @foreach ($invoices as $invoice)
        <tr>
            <td>{{ $invoice->date()->toFormattedDateString() }}</td>
            <td>{{ $invoice->total() }}</td>
            <td><a href="/user/invoice/{{ $invoice->id }}">下载</a></td>
        </tr>
    @endforeach
</table>

生成发票 PDF

在路由或控制器中,可以使用 downloadInvoice 方法生成发票的 PDF 下载。此方法将自动生成适当的 HTTP 响应以将下载发送到浏览器:

php
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 后缀:

php
return $request->user()->downloadInvoice($invoiceId, [
    'vendor' => 'Your Company',
    'product' => 'Your Product',
], 'my-invoice');

处理失败的支付

有时,订阅或单次收费的支付可能会失败。当这种情况发生时,Cashier 将抛出一个 IncompletePayment 异常,通知你发生了这种情况。在捕获此异常后,你有两种选择来继续。

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

php
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 异常,你仍然应该通知用户他们将收到一封电子邮件,其中包含进一步的支付确认说明。

支付异常可能会为以下方法抛出:chargeinvoiceForinvoiceBillable 用户上。处理订阅时,SubscriptionBuilder 上的 create 方法,以及 Subscription 模型上的 incrementAndInvoiceswapAndInvoice 方法可能会抛出异常。

目前有两种类型的支付异常,它们扩展了 IncompletePayment。如果需要,可以分别捕获这些异常,以便自定义用户体验:

  • PaymentActionRequired:这表明 Stripe 需要额外的验证才能确认和处理支付。
  • PaymentFailure:这表明支付由于各种其他原因失败,例如资金不足。

强客户认证

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

exclamation

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

需要额外确认的支付

SCA 规定通常需要额外的验证才能确认和处理支付。当这种情况发生时,Cashier 将抛出一个 PaymentActionRequired 异常,通知你需要这种额外的验证。有关如何处理这些异常的更多信息,请参阅 此处

不完整和逾期状态

当支付需要额外确认时,订阅将保持在 incompletepast_due 状态,如其 stripe_status 数据库列所示。Cashier 将在支付确认完成后通过 webhook 自动激活客户的订阅。

有关 incompletepast_due 状态的更多信息,请参阅 我们的其他文档

离线支付通知

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

php
CASHIER_PAYMENT_NOTIFICATION=Laravel\Cashier\Notifications\ConfirmPayment

为了确保离线支付确认通知被发送,请验证 Stripe webhooks 已为你的应用程序配置,并在你的 Stripe 仪表板中启用 invoice.payment_action_required webhook。此外,你的 Billable 模型还应使用 Laravel 的 Illuminate\Notifications\Notifiable trait。

exclamation

即使客户手动进行需要额外确认的支付时,也会发送通知。不幸的是,Stripe 无法知道支付是手动完成的还是“离线”的。但是,如果客户在确认支付后访问支付页面,他们将只会看到“支付成功”消息。客户将无法意外确认相同的支付两次并产生意外的第二次收费。

Stripe SDK

Cashier 的许多对象都是 Stripe SDK 对象的包装器。如果你希望直接与 Stripe 对象交互,可以使用 asStripe 方法方便地检索它们:

php
$stripeSubscription = $subscription->asStripeSubscription();

$stripeSubscription->application_fee_percent = 5;

$stripeSubscription->save();

你还可以使用 updateStripeSubscription 方法直接更新 Stripe 订阅:

php
$subscription->updateStripeSubscription(['application_fee_percent' => 5]);

测试

在测试使用 Cashier 的应用程序时,你可以模拟对 Stripe API 的实际 HTTP 请求;但是,这需要你部分重新实现 Cashier 自己的行为。因此,我们建议允许你的测试访问实际的 Stripe API。虽然这比较慢,但它提供了更多的信心,确保你的应用程序按预期工作,并且任何慢速测试都可以放在它们自己的 PHPUnit 测试组中。

在测试时,请记住 Cashier 本身已经有一个很好的测试套件,因此你应该只专注于测试你自己应用程序的订阅和支付流程,而不是每个底层的 Cashier 行为。

要开始,请将 Stripe 密钥的 测试 版本添加到你的 phpunit.xml 文件中:

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

现在,每当你在测试时与 Cashier 交互时,它将向你的 Stripe 测试环境发送实际的 API 请求。为了方便起见,你应该预先填充你的 Stripe 测试帐户,使用你可以在测试期间使用的订阅/计划。

lightbulb

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