交易工具包 (Stripe)

未匹配的标注

Laravel Cashier (Stripe)

简介

Laravel Cashier StripeStripe 的订阅计费服务提供了一个富有表现力、流畅的接口。它处理了几乎所有你害怕编写的订阅计费样板代码。除了基本的订阅管理,Cashier 还可以处理优惠券、交换订阅、订阅 「数量」、取消宽限期,甚至生成发票 PDF。

升级 Cashier

升级到新版本的 Cashier 时,请务必仔细阅读 升级指南

注意:为了防止破坏性变更,Cashier 使用固定的 Stripe API 版本。 Cashier 13 使用 Stripe API 版本 2020-08-27 。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;

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

注意:Stripe 建议用于存储 Stripe 标识符的任何列都应区分大小写。因此,在使用 MySQL 时,应该确保将 stripe_id 列排序规则设置为 utf8_bin 。更多关于这方面的信息可以在 Stripe 文档 中找到。

配置

订单模型

在使用 Cashier 之前,需要将 Billable trait 添加到可订单模型定义中。通常会放在 App\Models\User 模型中。这个特性提供了多个方法以便执行常用支付任务,如创建订阅、应用优惠券和更新支付方法信息:

use Laravel\Cashier\Billable;

class User extends Authenticatable
{
    use Billable;
}

Cashier 默认假设你的 Billable 模型是 Laravel 自带的 App\Models\User 类。如果需要修改可以在 useCustomerModel 方法定义一个不同的模型。通常此方法在 AppServiceProvider 类的boot方法中被调用:

use App\Models\Cashier\User;
use Laravel\Cashier\Cashier;

/**
 * Bootstrap any application services.
 *
 * @return void
 */
public function boot()
{
    Cashier::useCustomerModel(User::class);
}

注意:如果你使用的不是 Laravel 自带的 App\Models\User 模型,需要发布并修改默认的 Cashier 迁移 文件以匹配你使用模型对应的表名。

API 秘钥

接下来需要在 .env 文件中配置 Stripe 秘钥,可以在 Stripe 后台控制面板中获取Stripe API 秘钥:

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

货币配置

Cashier 默认货币是美元 (USD),可以在 .env 中设置 CASHIER_CURRENCY 环境变量来修改默认的货币配置:

CASHIER_CURRENCY=eur

除了配置 Cashier 的货币之外,还可以在格式化用于显示在发票上的金额时指定本地化配置。在底层,Cashier 使用了 PHP 的 NumberFormatter 来设置本地货币:

CASHIER_CURRENCY_LOCALE=nl_BE

注意:为了使用本地化配置而不是 en,需要确保安装了 PHP ext-intl PHP 扩展并在服务器上启用配置。

税务配置

感谢Stripe 税务,可以自动计算 Stripe 生成的所有发票的税费。 可以通过应用程序的 App\Providers\AppServiceProvider类的 boot 方法中调用 calculateTaxes 来启用自动税务计算:

use Laravel\Cashier\Cashier;

/**
 * Bootstrap any application services.
 *
 * @return void
 */
public function boot()
{
    Cashier::calculateTaxes();
}

启动税务计算后,任何新订阅和生成的一次性发票都会进行自动税务计算。

为了使这个功能正常使用,客户的账单明细中例如客户姓名、住址、发票 ID 需要同步到 Stripe。你可以使用 Cashier 提供的 客户数据同步Tax ID 方法来完成此操作。

注意:遗憾的是,目前不支持计算 单笔交易单笔交易支付。此外 Stripe Tax 目在测试期间仅限“受邀”使用。你可以通过 Stripe Tax 网站请求访问 Stripe 税务。

日志

Cashier 允许你指定日志通道来记录所有与 Stripe 相关的异常。可以通过在 .env 中配置 CASHIER_LOGGER 来指定:

CASHIER_LOGGER=stack

对 Stripe 的 API 调用生成的异常将通过应用程序的默认日志通道记录。

使用自定义模型

你可以通过定义自己的模型并扩展相应的 Cashier 模型来自由扩展 Cashier 内部的模型,增加一些方法:

use Laravel\Cashier\Subscription as CashierSubscription;

class Subscription extends CashierSubscription
{
    // ...
}

定义模型后,可以通过 Laravel\Cashier\Cashier 类配置 Cashier 使用自定义的模型。通常还需要在 App\Providers\AppServiceProvider 类的 boot 中注册一下:

use App\Models\Cashier\Subscription;
use App\Models\Cashier\SubscriptionItem;

/**
 * Bootstrap any application services.
 *
 * @return void
 */
public function boot()
{
    Cashier::useSubscriptionModel(Subscription::class);
    Cashier::useSubscriptionItemModel(SubscriptionItem::class);
}

消费者

查询消费者

你可以使用 Cashier::findBillable 方法通过 Stripe ID 查询消费者信息。该方法返回的是一个 billable 模型实例:

use Laravel\Cashier\Cashier;

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

创建消费者

有时候,你可能希望在不开始订阅的情况下创建一个 Stripe 消费者。这可以通过 createAsStripeCustomer 方法来实现:

$stripeCustomer = $user->createAsStripeCustomer();

消费者在 Stripe 中创建后,可以过一段时间再开始订阅。还可以使用可选的 $options 数组传入所有 Stripe API 支持的创建消费者参数 额外支持的参数:

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

如果你要返回消费者对象,你可以使用 asStripeCustomer 方法:

$stripeCustomer = $user->asStripeCustomer();

此外,可以使用 createOrGetStripeCustomer 方法来获取不确定查询的 Stripe 消费者在 Stripe 中是否已经存在。如果不存在,这个方法会创建一个消费者:

$stripeCustomer = $user->createOrGetStripeCustomer();

更新消费者

有时候,你可能想要使用额外的信息直接更新 Stripe 顾客信息,可以使用 updateStripeCustomer 方法来完成。这个方法接受 Stripe API 数组:

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

余额

Stripe 允许你借记贷记消费者的“余额”。余额将体现在发票上面为借记或贷记。可以使用 balance 方法检测用户的可用余额。balance 方法会返回消费者货币余额格式化后的字符串:

$balance = $user->balance();

要计入用户的余额,可以为 applyBalance 方法提供一个负值。如果愿意的话可以提供描述信息:

$user->applyBalance(-500, 'Premium customer top-up.');

提供一个正数值给 applyBalance 方法将增加客户的余额:

$user->applyBalance(300, 'Bad usage penalty.');

applyBalance 方法会创建一条客户余额流水记录。可以通过调用 balanceTransactions 方法获取余额交易记录,这有助于提供借记或贷记记录给客户查看:

// Retrieve all transactions...
$transactions = $user->balanceTransactions();

foreach ($transactions as $transaction) {
    // Transaction amount...
    $amount = $transaction->amount(); // $2.31

    // Retrieve the related invoice when available...
    $invoice = $transaction->invoice();
}

税号

Cashier 提供了一种管理客户税号的简便方法。taxIds 例如,taxIds 方法可用于检索作为集合分配给客户的所有 税号
$taxIds = $user->taxIds();

您还可以通过标识符检索客户的特定税号:

$taxId = $user->findTaxId('txi_belgium');

您可以通过向 createTaxId 方法提供有效的 type 和值来创建新的税号:

$taxId = $user->createTaxId('eu_vat', 'BE0123456789');

createTaxId 方法将立即将增值税 ID 添加到客户的帐户中。 增值税 ID 的验证也由 Stripe 完成; 然而,这是一个异步的过程。 您可以通过订阅 customer.tax_id.updated webhook 事件并检查 [增值税 ID verification 参数](stripe.com/docs/api/customer_tax_i...- 确认)。 有关处理 webhook 的更多信息,请参阅 有关定义 webhook 处理程序的文档

您可以使用 deleteTaxId 方法删除税号:

$user->deleteTaxId('txi_belgium');

使用 Stripe 同步客户数据

通常,当您的应用程序的用户更新他们的姓名、电子邮件地址或其他也由 Stripe 存储的信息时,您应该通知 Stripe 更新。 这样一来,Stripe 的信息副本将与您的应用程序同步。

要自动执行此操作,您可以在计费模型上定义一个事件侦听器,以响应模型的「更新」事件。然后,在您的事件监听器中,您可以在模型上调用 syncStripeCustomerDetails 方法:

use function Illuminate\Events\queueable;

/**
 * 模型的「引导」方法。
 *
 * @return void
 */
protected static function booted()
{
    static::updated(queueable(function ($customer) {
        if ($customer->hasStripeId()) {
            $customer->syncStripeCustomerDetails();
        }
    }));
}

现在,每次更新您的客户模型时,其信息都会与 Stripe 同步。 为方便起见,Cashier 会在初始创建客户时自动将您客户的信息与 Stripe 同步。

您可以通过覆盖 Cashier 提供的各种方法来自定义用于将客户信息同步到 Stripe 的列。 例如,当 Cashier 将客户信息同步到 Stripe 时,您可以重写 stripeName 方法来自定义应该被视为客户「姓名」的属性:

/**
 * 获取应同步到 Stripe 的客户名称。
 *
 * @return string|null
 */
public function stripeName()
{
    return $this->company_name;
}

同样,您可以复写 stripeEmailstripePhonestripeAddress 方法。 当更新 Stripe 客户对象 时,这些方法会将信息同步到其相应的客户参数。 如果您希望完全控制客户信息同步过程,您可以复写 syncStripeCustomerDetails 方法。

订单入口

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

use Illuminate\Http\Request;

Route::get('/billing-portal', function (Request $request) {
    return $request->user()->redirectToBillingPortal();
});

默认情况下,当用户完成对订阅的管理后,会将能够通过 Stripe 计费门户中的链接返回到应用的 home 路由,你可以通过传递 URL 作为 redirectToBillingPortal 方法的参数来自定义用户返回的 URL:

use Illuminate\Http\Request;

Route::get('/billing-portal', function (Request $request) {
    return $request->user()->redirectToBillingPortal(route('billing'));
});

如果你只想要生成订单入口的 URL,可以使用 billingPortalUrl 方法:

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

支付方式

存储支付方式

为了使用 Stripe 创建订阅或者进行「一次性」支付,你需要存储支付方法并从 Stripe 中获取对应的标识符。这种方式可用于实现你是否计划使用这个支付方法进行订阅还是单次收费,下面我们分别来介绍这两种方法。

用于订阅的支付方法

当我们为消费者存储信用卡支付方式以便将来使用时,必须使用 Stripe Setup Intents API 来安全地收集顾客的支付方式细节,比如回调错误信息 。「Setup Intents」用于告知 Stripe 使用顾客的支付方法进行收费的意图。Cashier 的 Billable Trait 包含了 createSetupIntent 方法来创建新的「Setup Intent」,你需要在渲染收集顾客支付方法细节表单的路由或控制器方法中调用这个方法:

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

创建完 Setup Intent 并将其传递给视图之后,你需要在收集支付方法的元素中添加它的 secret。例如,参考下面这个「更新支付方法」表单:

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

<!-- Stripe Elements Placeholder -->
<div id="card-element"></div>

<button id="card-button" data-secret="{{ $intent->client_secret }}">
    Update Payment Method
</button>

接下来,会通过 Stripe.js 库添加一个 Stripe 元素到表单,并安全地收集顾客的支付细节:

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

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

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

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

然后,使用 Stripe 的 handleCardSetup 方法验证信用卡并从 Stripe 获取一个安全的「支付方法标识符」:

const cardHolderName = document.getElementById('card-holder-name');
const cardButton = document.getElementById('card-button');
const clientSecret = cardButton.dataset.secret;

cardButton.addEventListener('click', async (e) => {
    const { setupIntent, error } = await stripe.confirmCardSetup(
        clientSecret, {
            payment_method: {
                card: cardElement,
                billing_details: { name: cardHolderName.value }
            }
        }
    );

    if (error) {
        // 显示错误信息给用户...
    } else {
        // 信用卡验证成功...
    }
});

在 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();

默认情况下,此方法将返回 card 类型的付款方式。 要检索不同类型的付款方式,您可以将 type 作为参数传递给该方法:

$paymentMethods = $user->paymentMethods('sepa_debit');

要获取消费者默认的支付方法,可以使用 defaultPaymentMethod 方法:

$paymentMethod = $user->defaultPaymentMethod();

还可以使用 findPaymentMethod 方法通过 Billable 模型获取指定支付方法:

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

判断消费者是否拥有支付方法

要判断某个 Billable 模型对应账户是否有默认的支付方法,可以使用 hasDefaultPaymentMethod 方法:

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

要判断某个 Billable 模型对应账户是否有支付方法,可以使用 hasPaymentMethod 方法:

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

此方法将确定计费模型是否具有 card 类型的付款方式。 要确定模型是否存在另一种类型的付款方式,您可以将 type 作为参数传递给该方法:

if ($user->hasPaymentMethod('sepa_debit')) {
    //
}

更新默认支付方式

更新顾客的默认支付方式信息 可以用 updateDefaultPaymentMethod 方法 ,该方法接收一个 Stripe 支付方法标识符并将新的支付方法分配为默认的支付方法:

$user->updateDefaultPaymentMethod($paymentMethod);

要同步应用的默认支付方法信息到 Stripe 顾客的默认支付方式信息,可以使用 updateDefaultPaymentMethodFromStripe 方法:

$user->updateDefaultPaymentMethodFromStripe();

注意:消费者的默认支付方式只能用于发票和创建新的订阅,由于 Stripe 的限制,不能将其用于单次付费。

添加支付方式

要添加新的支付方式,可以调用 Billable 用户的 addPaymentMethod 方法,并传递支付方法标识符:

$user->addPaymentMethod($paymentMethod);

技巧:要了解如何获取支付方法标识符,请参考支付方式存储文档.

删除支付方式

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

$paymentMethod->delete();

删除指定 Billable 模型上的所有支付方法信息可以使用 deletePaymentMethods 方法:

$user->deletePaymentMethod('pm_visa');

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

$user->deletePaymentMethods();

默认情况下,此方法将删除 card 类型的付款方式。 要删除不同类型的付款方式,您可以将 type 作为参数传递给该方法:

$user->deletePaymentMethods('sepa_debit');

注意:如果用户有活动订阅,您的应用程序不应允许他们删除其默认付款方式。

订阅内容

订阅提供了一种为消费者设置定期付款的方式。由收银员管理的 Stripe 订阅 提供对多个订阅计划、订阅数量、试用等的支持。

创建订阅

创建一个订阅,首先要获取一个账单模型的实例,通常是 App\Models\User 的实例。获取到该模型实例之后,可以使用 newSubscription 方法来创建该模型的订阅:

use Illuminate\Http\Request;

Route::post('/user/subscribe', function (Request $request) {
    $request->user()->newSubscription(
        'default', 'price_monthly'
    )->create($request->paymentMethodId);

    // ...
});

newSubscription 方法的第一个参数是该订阅的名字,如果应用只有一个订阅,可以将其称作 default 或 primary,第二个参数用于指定用户订阅的计划,这个值对应 Stripe 中相应计划的标识符。

create 方法接收 Stripe 支付方法标识符或者 Stripe PaymentMethod 对象的 create 方法会自动创建这个 Stripe 订阅,同时更新数据库中 Stripe 的消费者 ID(即 users 表中的 stripe_id)和其它相关的账单信息。

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

通过发票电子邮件收集定期付款

您可以指示 Stripe 在每次定期付款到期时通过电子邮件将发票发送给客户,而不是自动收取客户的定期付款。 然后,客户可以在收到发票后手动支付发票。 通过发票收取定期付款时,客户无需预先提供付款方式:

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

客户在取消订阅之前必须支付发票的时间取决于您在 Stripe 仪表板 中的订阅和发票设置。

数量

如果你想要在创建订阅时设置计划的具体数量,可以使用 quantity 方法:

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

其它细节

如果你想要指定其它消费者或者订阅细节,你可以将其作为第二个参数传递给 create 方法:

$user->newSubscription('default', 'price_monthly')->create($paymentMethod, [
    'email' => $email,
], [
    'metadata' => ['note' => 'Some extra information.'],
]);

优惠券

如果你想在创建订阅的时候使用优惠券,你可以使用 withCoupon 方法:

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

或者,如果你想使用 Stripe 推广代码,可以使用 withPromotionCode 方法:

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

添加订阅

如果你想要为已经有默认支付方式的消费者添加订阅,可以在使用 newSubscription 方法时使用 add 方法:

use App\Models\User;

$user = User::find(1);

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

从 Stripe 面板中创建订阅

你也可以从 Stripe 面板中创建订阅。在面板中创建订阅时候,收银员将同步新添加的订阅,并为它们分配一个名称为 default 的订阅。要自定义分配给仪表板创建订阅的订阅名,扩展’ WebhookController ‘并覆盖 newSubscriptionName 方法。

此外,只能通过 Stripe 面板中创建一种订阅类型。如果应用程序提供使用不同名称的多个订阅,则只能通过 Stripe 面板添加一种订阅类型。

最后,应该始终确保对应用程序提供的每种订阅类型只添加一个活动订阅。如果消费者有两个 default 订阅,那么收银员只会使用最近添加的订阅,即使这两个订阅都将与应用程序的数据库同步。

检查订阅状态

客户订阅您的应用程序后,您可以使用各种便捷的方法检查他们的订阅状态。首先,如果客户有活动订阅,则 subscribed 方法会返回 true,即使订阅当前处于试用期也是如此。subscribed 方法接受订阅的名称作为它的第一个参数:

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

subscribed 方法也可以用于路由中间件 , 基于消费者订阅状态允许你对路由和控制器的访问进行过滤:

<?php

namespace App\Http\Middleware;

use Closure;

class EnsureUserIsSubscribed
{
    /**
     * 处理传入的请求。
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    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()) {
    //
}

subscribedToProduct 方法可用于根据给定 Stripe 产品的标识符确定用户是否订阅了给定产品。 在 Stripe 中,产品是价格的集合。 在此示例中,我们将确定用户的 默认 订阅是否主动订阅了应用程序的 高级 产品。 给定的 Stripe 产品标识符应与您在 Stripe 仪表板中的产品标识符之一相对应:

if ($user->subscribedToProduct('prod_premium', 'default')) {
    //
}

if ($user->subscribedToProduct(['prod_basic', 'prod_premium'], 'default')) {
    //
}

subscribedToPrice 方法可用于确定客户的订阅是否对应于给定的价格 ID:

if ($user->subscribedToPrice('price_basic_monthly', 'default')) {
    //
}

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

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

注意:如果消费者有两个具有相同名称的订阅,则 subscription 方法将始终返回最近的订阅。例如,消费者可能有两条名为 default 的订阅记录;但是,其中一个订阅可能是旧的、过期的订阅,而另一个是当前的、活动的订阅。最新的订阅将始终返回,而旧的订阅将保留在数据库中以进行历史回顾。

已取消的订阅状态

要判断消费者是否曾经是有效的订阅者,但现在取消了订阅,可以使用 cancelled 方法:

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

还可以判断消费者是否曾经取消过订阅,但现在仍然在「宽限期」直到完全失效。例如,如果一个消费者在 3 月 5 号取消了一个实际有效期到 3 月 10 号的订阅,该消费者处于「宽限期」直到 3 月 10 号。注意 subscribed 方法在此期间仍然返回 true

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

要去定消费者已经取消订阅并且不在「宽限期」内,可以使用 ended 方法:

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

未完成和过期状态

如果某个订阅要求创建完订阅后进行二次付款操作,将被标记为 incomplete。订阅状态被存储在 Cashier subscriptions 数据表的 stripe_status 字段。

类似的,如果在切换订阅计划时也需要进行二次付款操作,对应的订阅会被标记为 past_due。当你的订阅处于这种状态时,只有等到消费者确认支付后它们才会被激活。我们可以使用 Billable 模型或者订阅实例的 hasIncompletePayment 方法来检查某个订阅是否存在未完成支付:

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

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

如果某个订阅存在未完成支付,你需要引导消费者到 Cashier 的支付确认页面,并传递 latestPayment 标识符。你可以使用订阅实例上的 latestPayment 方法来获取这个标识符:

<a href="{{ route('cashier.payment', $subscription->latestPayment()->id) }}">
    Please confirm your payment.
</a>

如果你想要订阅在 past_due 状态下依然有效,可以使用 Cashier 提供的 keepPastDueSubscriptionsActive 方法,通常,该方法需要在 AppServiceProvider 的 boot 方法中调用:

use Laravel\Cashier\Cashier;

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

注意:当某个订阅处于 incomplete 状态,只有等到支付被确认后才能进行修改。因此,当订阅处于 incomplete 状态时,执行 swap 和 updateQuantity 方法会抛出异常。

订阅范围

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

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

// Get all of the canceled subscriptions for a user...
$subscriptions = $user->subscriptions()->canceled()->get();

所有内置的订阅查询范围列表如下:

Subscription::query()->active();
Subscription::query()->canceled();
Subscription::query()->ended();
Subscription::query()->incomplete();
Subscription::query()->notCanceled();
Subscription::query()->notOnGracePeriod();
Subscription::query()->notOnTrial();
Subscription::query()->onGracePeriod();
Subscription::query()->onTrial();
Subscription::query()->pastDue();
Subscription::query()->recurring();

修改计划

消费者订阅应用后,有时候想要改变到新的订阅计划,需要将消费者切换到新的订阅,传递计划标识符到 swap 方法:

在交换价格时,假设用户想要重新激活他们的订阅(如果之前取消订阅)。 给定的价格标识符应对应于 Stripe 仪表板中可用的 Stripe 价格标识符:

use App\Models\User;

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

$user->subscription('default')->swap('price_yearly');

如果消费者在试用,试用期将会被维护。还有,如果订阅存在多个,数量也可以被维护。

如果你想要切换计划并取消消费者所在的所有试用期,可以使用 skipTrial 方法:

$user->subscription('default')
        ->skipTrial()
        ->swap('price_yearly');

如果你想要切换计划并立即为消费者开具发票,而不是等到下一个结算周期,可以使用 swapAndInvoice 方法:

$user = User::find(1);

$user->subscription('default')->swapAndInvoice('price_yearly');

按比例分配

默认情况下,Stripe 会在订阅计划间切换时按比例进行分配,noProrate 可用于在修改订阅计划时不使用按比例分配机制:

$user->subscription('default')->noProrate()->swap('price_yearly');

更多关于订阅计划按比例分配的细节,请参考 Stripe 官方文档

注意:在 wapAndInvoice 方法之前执行 noProrate 方法不会影响按比例分配。发票始终都会开具。

订阅数量

有时候订阅也会被数量影响,例如,应用中每个账户每月需要付费 $10,要简单增加或减少订阅数量,使用 incrementQuantity 和 decrementQuantity 方法:

use App\Models\User;

$user = User::find(1);

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

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

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

// 从订阅的当前数量中减去五...
$user->subscription('default')->decrementQuantity(5);

你也可以使用 updateQuantity 方法指定具体的数量:

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

noProrate 方法可用于更新订阅数量而无需对收费进行评级:

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

想要了解更多订阅数量信息,可以参考 Stripe 官方文档.

多方案订阅数量

如果你的订阅是多方案订阅计划 , 你应该将其数量要递增或递减的计划的名称作为第二个参数传递给递增 / 递减方法

$user->subscription('default')->incrementQuantity(1, 'price_chat');

多方案订阅计划

多方案订阅计划 可以分配多个付费方案给单个订阅计划。例如,假设你正在构建一个顾客咨询服务应用,其中包含了一个基础的¥10 / 月的订阅方案,但是如果提供附加的实时聊天服务,需要支付¥15 / 月,订阅信息存储在收银员的 Subscription_items 数据库表中:

可以通过将计划数组作为第二个参数传递给 newSubscription 方法,为给定的订阅指定多个计划:

use Illuminate\Http\Request;

Route::post('/user/subscribe', function (Request $request) {
    $request->user()->newSubscription('default', [
        'price_monthly',
        'price_chat',
    ])->create($request->paymentMethodId);

    // ...
});

在上面的示例代码中,消费者的 default 订阅添加了两个订阅计划。这两个计划都将按各自的计费间隔收费。如果需要,可以使用 Quantity 方法来表示每个计划的具体数量:

$user = User::find(1);

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

如果您想为现有订阅添加其他价格,可以调用订阅的 addPrice 方法:

$user = User::find(1);

$user->subscription('default')->addPrice('price_chat');

上面的示例代码会添加新方案并且顾客会在下个支付周期为其支付。如果你想要让顾客立即支付,可以使用 addPlanAndInvoice 方法:

$user->subscription('default')->addPriceAndInvoice('price_chat');

如果你想要添加指定数量的方案,可以将数量值作为第二个参数传递给 addPlan 或者 addPlanAndInvoice 方法:

$user = User::find(1);

$user->subscription('default')->addPrice('price_chat', 5);

你也可以使用 removePlan 方法从订阅计划中移除这些方案:

$user->subscription('default')->removePrice('price_chat');

注意:不能移除订阅计划中最后一个方案(至少需要保留一个), 相反,需要通过取消订阅的方式移除该方案。

多方案订阅切换

您还可以更改多价订阅的附加价格。 例如,假设客户订阅了带有 price_chat 附加价格的 price_basic,并且您想将客户从 price_basic 升级到 price_pro 价格:

use App\Models\User;

$user = User::find(1);

$user->subscription('default')->swap(['price_pro', 'price_chat']);

执行上面的示例时,带有 price_basic 的底层订阅项目被删除,而带有price_chat 的订阅项目被保留。 此外,还会为 price_pro 创建一个新的订阅项目。

您还可以通过将一组键/值对传递给“swap”方法来指定订阅项目选项。 例如,您可能需要指定订阅价格的数量:

$user = User::find(1);

$user->subscription('default')->swap([
    'price_pro' => ['quantity' => 5],
    'price_chat'
]);

如果您想交换订阅的单一价格,您可以使用订阅项目本身的“swap”方法来实现。 如果您想保留订阅的其他价格上的所有现有元数据,此方法特别有用:

$user = User::find(1);

$user->subscription('default')
        ->findItemOrFail('price_basic')
        ->swap('price_pro');

按比例分配

默认情况下,在多价订阅中添加或删除价格时,Stripe 将按比例收取费用。 如果您想在没有按比例分配的情况下进行价格调整,您应该将 noProrate 方法链接到您的价格操作:

$user->subscription('default')->noProrate()->removePrice('price_chat');

数量

如果您想更新单个订阅价格的数量,您可以使用 现有数量方法 将价格名称作为附加参数传递给该方法:

$user = User::find(1);

$user->subscription('default')->incrementQuantity(5, 'price_chat');

$user->subscription('default')->decrementQuantity(3, 'price_chat');

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

注意:当订阅有多个价格时,“Subscription”模型上的“stripe_price”和“quantity”属性将为“null”。 要访问单个价格属性,您应该使用 Subscription 模型上可用的 items 关系。

订阅项目

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

use App\Models\User;

$user = User::find(1);

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

// 检索特定商品的 Stripe 价格和数量...
$stripePrice = $subscriptionItem->stripe_price;
$quantity = $subscriptionItem->quantity;

您还可以使用 findItemOrFail 方法检索特定价格:

$user = User::find(1);

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

计量计费

计量计费 允许您根据客户在计费周期内的产品使用情况向他们收费。 例如,您可以根据客户每月发送的短信或电子邮件的数量向他们收费。

要开始使用计量计费,您首先需要在 Stripe 仪表板中创建一个具有计量价格的新产品。 然后,使用 meteredPrice 将计量价格 ID 添加到客户订阅中:

use Illuminate\Http\Request;

Route::post('/user/subscribe', function (Request $request) {
    $request->user()->newSubscription('default')
        ->meteredPrice('price_metered')
        ->create($request->paymentMethodId);

    // ...
});

您也可以通过 Stripe Checkout 开始计量订阅:

$checkout = Auth::user()
        ->newSubscription('default', [])
        ->meteredPrice('price_metered')
        ->checkout();

return view('your-checkout-view', [
    'checkout' => $checkout,
]);

报告使用情况

当您的客户使用您的应用程序时,您将向 Stripe 报告他们的使用情况,以便准确计费。 要增加计量订阅的使用量,您可以使用 reportUsage 方法:

$user = User::find(1);

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

默认情况下,计费周期会添加 1 的“使用量”。 或者,您可以传递特定数量的“使用量”以添加到客户在计费期间的使用量:

$user = User::find(1);

$user->subscription('default')->reportUsage(15);

如果您的应用程序在单个订阅中提供多个价格,您将需要使用 reportUsageFor 方法来指定要报告使用情况的计量价格:

$user = User::find(1);

$user->subscription('default')->reportUsageFor('price_metered', 15);

有时,您可能需要更新之前报告的使用情况。 为此,您可以将时间戳或“DateTimeInterface”实例作为第二个参数传递给“reportUsage”。 这样做时,Stripe 将更新在给定时间报告的使用情况。 您可以继续更新以前的使用记录,因为给定的日期和时间仍在当前计费周期内:

$user = User::find(1);

$user->subscription('default')->reportUsage(5, $timestamp);

检索使用记录

要检索客户过去的使用情况,您可以使用订阅实例的 usageRecords 方法:

$user = User::find(1);

$usageRecords = $user->subscription('default')->usageRecords();

如果您的应用程序在单个订阅中提供多个价格,您可以使用 usageRecordsFor 方法指定您希望检索使用记录的计量价格:

$user = User::find(1);

$usageRecords = $user->subscription('default')->usageRecordsFor('price_metered');

usageRecordsusageRecordsFor 方法返回一个包含使用记录关联数组的 Collection 实例。 您可以遍历此数组以显示客户的总使用量:

@foreach ($usageRecords as $usageRecord)
    - Period Starting: {{ $usageRecord['period']['start'] }}
    - Period Ending: {{ $usageRecord['period']['end'] }}
    - Total Usage: {{ $usageRecord['total_usage'] }}
@endforeach

有关返回的所有使用数据以及如何使用 Stripe 的基于光标的分页的完整参考,请参阅 官方 Stripe API 文档

订阅 Taxes

注意:您可以使用 StripeTax 自动计算税金,而不是手动计算税率

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

/**
 * 应适用于客户订阅的税率。
 *
 * @return array
 */
public function taxRates()
{
    return ['txr_id'];
}

taxRates 方法使您可以逐个客户应用税率,这可能有助于跨越多个国家和税率的用户群。

如果您提供多价订阅,您可以通过在计费模型上实现 priceTaxRates 方法为每个价格定义不同的税率:

/**
 * The tax rates that should apply to the customer's subscriptions.
 *
 * @return array
 */
public function priceTaxRates()
{
    return [
        'price_monthly' => ['txr_id'],
    ];
}

注意:taxRates 方法仅适用于订阅费用。 如果您使用 Cashier 进行“一次性”收费,您将需要手动指定当时的税率。

同步税率

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

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

这还将同步任何多价订阅项目税率。 如果您的应用程序提供多价订阅,您应该确保您的计费模型实现了 priceTaxRates 方法 如上所述

免税

Cashier 还提供了 isNotTaxExemptisTaxExemptreverseChargeApplies 方法来确定客户是否免税。 这些方法将调用 Stripe API 来确定客户的免税状态:

use App\Models\User;

$user = User::find(1);

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

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

订阅锚定日期

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

use Illuminate\Http\Request;

Route::post('/user/subscribe', function (Request $request) {
    $anchor = Carbon::parse('first day of next month');

    $request->user()->newSubscription('default', 'price_monthly')
                ->anchorBillingCycleOn($anchor->startOfDay())
                ->create($request->paymentMethodId);

    // ...
});

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

取消订阅

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

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

取消订阅后,Cashier 会自动在您的 subscriptions 数据库表中设置 ends_at 列。 此列用于了解 subscribed 方法应该何时开始返回 false

例如,如果客户在 3 月 1 日取消订阅,但订阅计划到 3 月 5 日才结束,则 subscribed 方法将继续返回 true 直到 3 月 5 日。 这样做是因为通常允许用户继续使用应用程序,直到他们的计费周期结束。

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

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

如果您想立即取消订阅,请在用户订阅上调用 cancelNow 方法:

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

如果您希望立即取消订阅并为任何剩余的未开票计量使用或新的/待定的按比例发票项目开具发票,请在用户的订阅上调用 cancelNowAndInvoice 方法:

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

您也可以选择在特定时间取消订阅:

$user->subscription('default')->cancelAt(
    now()->addDays(10)
);

恢复订阅

如果客户取消了他们的订阅并且您希望恢复它,您可以在订阅上调用 resume 方法。 客户必须仍在其“宽限期”内才能恢复订阅:

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

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

订阅 Trials

预先使用付款方式

如果您想为您的客户提供试用期,同时仍然预先收集付款方式信息,您应该在创建订阅时使用 trialDays 方法:

use Illuminate\Http\Request;

Route::post('/user/subscribe', function (Request $request) {
    $request->user()->newSubscription('default', 'price_monthly')
                ->trialDays(10)
                ->create($request->paymentMethodId);

    // ...
});

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

注意:如果客户的订阅没有在试用期结束日期之前取消,他们将在试用期结束后立即收费,因此您应该确保通知您的用户他们的试用期结束日期。

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

use Carbon\Carbon;

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

您可以使用用户实例的“onTrial”方法或订阅实例的“onTrial”方法来确定用户是否在试用期内。 下面的两个例子是等价的:

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

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

您可以使用 endTrial 方法立即结束订阅试用:

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

在 Stripe 中定义试用天数 / Cashier

您可以选择在 Stripe 仪表板中定义您的价格收到多少试用天,或者始终使用 Cashier 明确传递它们。 如果您选择在 Stripe 中定义价格的试用天数,您应该知道新订阅(包括过去订阅过的客户的新订阅)将始终收到试用期,除非您明确调用 skipTrial() 方法.

没有预付款方式

如果您想提供试用期而不预先收集用户的付款方式信息,您可以将用户记录上的“trial_ends_at”列设置为您想要的试用结束日期。 这通常在用户注册期间完成:

use App\Models\User;

$user = User::create([
    // ...
    'trial_ends_at' => now()->addDays(10),
]);

注意:请务必在计费模型的类定义中为 trial_ends_at 属性添加 日期转换

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

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

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

$user = User::find(1);

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

要检索用户的试用结束日期,您可以使用 trialEndsAt 方法。 如果用户正在试用,此方法将返回一个 Carbon 日期实例,如果不是,则返回“null”。 如果您想获取除默认订阅之外的特定订阅的试用结束日期,您还可以传递可选订阅名称参数:

if ($user->onTrial()) {
    $trialEndsAt = $user->trialEndsAt('main');
}

如果您想明确知道用户处于 “generic” 试用期并且尚未创建实际订阅,您也可以使用 onGenericTrial 方法:

if ($user->onGenericTrial()) {
    // 用户在他们的 “generic” 试用期内...
}

延长试用

extendTrial 方法允许您在创建订阅后延长订阅的试用期。 如果试用期已经过期并且客户已经被收取订阅费用,您仍然可以为他们提供延长试用期。 试用期内花费的时间将从客户的下一张发票中扣除:

use App\Models\User;

$subscription = User::find(1)->subscription('default');

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

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

处理 Stripe webhook

技巧:您可以使用 the Stripe CLI 在本地开发期间帮助测试 webhook。

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

默认情况下,Cashier webhook 控制器将自动处理取消订阅失败的次数过多(由您的 Stripe 设置定义)、客户更新、客户删除、订阅更新和付款方式更改; 然而,我们很快就会发现,你可以扩展这个控制器来处理你喜欢的任何 Stripe webhook 事件。

为确保您的应用程序可以处理 Stripe webhook,请务必在 Stripe 控制面板中配置 webhook URL。 默认情况下,Cashier 的 webhook 控制器响应 /stripe/webhook URL 路径。您应该在 Stripe 控制面板中启用的所有 webhook 的完整列表是:

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

为方便起见,Cashier 包含一个 cashier:webhook Artisan 命令。 此命令将在 Stripe 中创建一个 webhook,用于监听 Cashier 所需的所有事件:

php artisan cashier:webhook

默认情况下,创建的 webhook 将指向由 APP_URL 环境变量和 Cashier 包含的 cashier.webhook 路由定义的 URL。 如果您想使用不同的 URL,可以在调用命令时提供 --url 选项:

php artisan cashier:webhook --url "https://example.com/stripe/webhook"

创建的 webhook 将使用与您的 Cashier 版本兼容的 Stripe API 版本。 如果你想使用不同的 Stripe 版本,你可以提供 --api-version 选项:

php artisan cashier:webhook --api-version="2019-12-03"

创建后,webhook 将立即激活。 如果您希望创建 webhook 但在准备好之前将其禁用,您可以在调用命令时提供 --disabled 选项:

php artisan cashier:webhook --disabled

注意:确保使用 Cashier 包含的 webhook 签名验证 中间件保护传入的 Stripe webhook 请求。

Webhook 和 CSRF 保护

由于 Stripe webhook 需要绕过 Laravel 的 CSRF 保护,因此请务必在应用程序的 App\Http\Middleware\VerifyCsrfToken 中间件中将 URI 列为异常或列出路由在 web 中间件组之外:

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

定义 webhook 事件处理程序

Cashier 自动处理因收费失败和其他常见的 Stripe webhook 事件而取消的订阅。 但是,如果您有其他想要处理的 webhook 事件,您可以通过收听 Cashier 调度的以下事件来实现:

  • Laravel\Cashier\Events\WebhookReceived
  • Laravel\Cashier\Events\WebhookHandled

这两个事件都包含 Stripe webhook 的完整负载。 例如,如果你想处理 invoice.payment_succeeded webhook,你可以注册一个 listener 来处理事件:

<?php

namespace App\Listeners;

use Laravel\Cashier\Events\WebhookReceived;

class StripeEventListener
{
    /**
     * 处理收到的 Stripe webhook。
     *
     * @param  \Laravel\Cashier\Events\WebhookReceived  $event
     * @return void
     */
    public function handle(WebhookReceived $event)
    {
        if ($event->payload['type'] === 'invoice.payment_succeeded') {
            // 处理传入事件...
        }
    }
}

一旦你的监听器被定义,你可以在你的应用程序的EventServiceProvider中注册它:

<?php

namespace App\Providers;

use App\Listeners\StripeEventListener;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Laravel\Cashier\Events\WebhookReceived;

class EventServiceProvider extends ServiceProvider
{
    protected $listen = [
        WebhookReceived::class => [
            StripeEventListener::class,
        ],
    ];
}

验证 Webhook 签名

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

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

单次 Charges

简单 Charge

注意:charge 方法接受您希望以应用程序使用的货币的最低分母收取的金额。例如,使用美元时,金额应以便士表示。

如果您想向客户一次性收费,您可以在可计费模型实例上使用 charge 方法。您需要[提供付款方式标识符](#单笔费用的付款方式)作为 charge 方法的第二个参数:

use Illuminate\Http\Request;

Route::post('/purchase', function (Request $request) {
    $stripeCharge = $request->user()->charge(
        100, $request->paymentMethodId
    );

    // ...
});

charge 方法接受一个数组作为它的第三个参数,允许您将任何您希望的选项传递给底层的 Stripe 电荷创建。 有关创建费用时可用选项的更多信息,请参阅 Stripe 文档

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

您也可以在没有潜在客户或用户的情况下使用“收费”方法。 为此,请在应用程序的计费模型的新实例上调用 charge 方法:

use App\Models\User;

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

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

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

用发票收费

有时您可能需要一次性收费并向客户提供 PDF 收据。 invoicePrice 方法可以让你做到这一点。 例如,让我们为客户开具五件新衬衫的发票:

$user->invoicePrice('price_tshirt', 5);

发票将立即根据用户的默认付款方式收取。 invoicePrice 方法也接受一个数组作为它的第三个参数。 此数组包含发票项目的计费选项。 该方法接受的第四个参数也是一个数组,其中应包含发票本身的计费选项:

$user->invoicePrice('price_tshirt', 5, [
    'discounts' => [
        ['coupon' => 'SUMMER21SALE']
    ],
], [
    'default_tax_rates' => ['txr_id'],
]);

或者,您可以使用 invoiceFor 方法对客户的默认付款方式进行“一次性”收费:

$user->invoiceFor('One Time Fee', 500);

虽然 invoiceFor 方法可供您使用,但建议您使用具有预定义价格的 invoicePrice 方法。 通过这样做,您将可以在您的 Stripe 仪表板中访问更好的分析和数据,以了解您在每个产品的基础上的销售情况。

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

退款费用

如果您需要退还 Stripe 费用,您可以使用 refund 方法。 此方法接受 Stripe payment intent ID 作为其第一个参数:

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

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

发票

检索发票

您可以使用 invoices 方法轻松检索可计费模型的发票数组。 invoices 方法返回 Laravel\Cashier\Invoice 实例的集合:

$invoices = $user->invoices();

如果您想在结果中包含待处理的发票,您可以使用 invoicesIncludingPending 方法:

$invoices = $user->invoicesIncludingPending();

您可以使用 findInvoice 方法通过其 ID 检索特定发票:

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

显示发票信息

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

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

进货发票

要检索客户即将收到的发票,您可以使用 upcomingInvoice 方法:

$invoice = $user->upcomingInvoice();

类似地,如果客户有多个订阅,您还可以检索特定订阅的即将到来的发票:

$invoice = $user->subscription('default')->upcomingInvoice();

预览订阅发票

使用 previewInvoice 方法,您可以在更改价格之前预览发票。 这将允许您确定在进行给定价格更改时客户发票的外观:

$invoice = $user->subscription('default')->previewInvoice('price_yearly');

您可以将一组价格传递给 previewInvoice 方法,以便预览具有多个新价格的发票:
$invoice = $user->subscription('default')->previewInvoice(['price_yearly', 'price_metered']);

生成发票 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',
    ]);
});

默认情况下,发票上的所有数据都来自存储在 Stripe 中的客户和发票数据。 但是,您可以通过提供一个数组作为 downloadInvoice 方法的第二个参数来自定义其中的一些数据。 此数组允许您自定义信息,例如您的公司和产品详细信息:

return $request->user()->downloadInvoice($invoiceId, [
    'vendor' => 'Your Company',
    'product' => 'Your Product',
    'street' => 'Main Str. 1',
    'location' => '2000 Antwerp, Belgium',
    'phone' => '+32 499 00 00 00',
    'email' => 'info@example.com',
    'url' => 'https://example.com',
    'vendorVat' => 'BE123456789',
], 'my-invoice');

downloadInvoice 方法还允许通过其第三个参数自定义文件名。 此文件名将自动以 .pdf 为后缀:

return $request->user()->downloadInvoice($invoiceId, [], 'my-invoice');

自定义发票渲染器

Cashier 还可以使用自定义发票渲染器。 默认情况下,Cashier 使用 DompdfInvoiceRenderer 实现,它利用 dompdf PHP 库来生成 Cashier 的发票。 但是,你可以通过实现 Laravel\Cashier\Contracts\InvoiceRenderer 接口来使用任何你想要的渲染器。 例如,您可能希望使用对第三方 PDF 呈现服务的 API 调用来呈现发票 PDF:

use Illuminate\Support\Facades\Http;
use Laravel\Cashier\Contracts\InvoiceRenderer;
use Laravel\Cashier\Invoice;

class ApiInvoiceRenderer implements InvoiceRenderer
{
    /**
     * 呈现给定的发票并返回原始 PDF 字节。
     *
     * @param  \Laravel\Cashier\Invoice. $invoice
     * @param  array  $data
     * @param  array  $options
     * @return string
     */
    public function render(Invoice $invoice, array $data = [], array $options = []): string
    {
        $html = $invoice->view($data)->render();

        return Http::get('https://example.com/html-to-pdf', ['html' => $html])->get()->body();
    }
}

一旦你实现了发票渲染器合约,你应该在你的应用程序的 config/cashier.php 配置文件中更新 cashier.invoices.renderer 配置值。 此配置值应设置为自定义渲染器实现的类名。

结账

Cashier Stripe 还提供对 Stripe Checkout 的支持。 Stripe Checkout 通过提供预构建的托管支付页面,消除了实施自定义页面以接受付款的痛苦。

以下文档包含有关如何开始使用 Stripe Checkout with Cashier 的信息。 要了解有关 Stripe Checkout 的更多信息,您还应该考虑查看 Stripe 自己的 Checkout 文档

产品结账

您可以在计费模型上使用“checkout”方法对已在 Stripe 仪表板中创建的现有产品执行结帐。 checkout 方法将启动一个新的 Stripe Checkout 会话。 默认情况下,您需要传递 Stripe Price ID:

use Illuminate\Http\Request;

Route::get('/product-checkout', function (Request $request) {
    return $request->user()->checkout('price_tshirt');
});

如果需要,您还可以指定产品数量:

use Illuminate\Http\Request;

Route::get('/product-checkout', function (Request $request) {
    return $request->user()->checkout(['price_tshirt' => 15]);
});

当客户访问此路线时,他们将被重定向到 Stripe 的结帐页面。 默认情况下,当用户成功完成或取消购买时,他们将被重定向到您的 home 路由位置,但您可以使用 success_urlcancel_url 选项指定自定义回调 URL:

use Illuminate\Http\Request;

Route::get('/product-checkout', function (Request $request) {
    return $request->user()->checkout(['price_tshirt' => 1], [
        'success_url' => route('your-success-route'),
        'cancel_url' => route('your-cancel-route'),
    ]);
});

在定义 success_url 结帐选项时,您可以指示 Stripe 在调用 URL 时将结帐会话 ID 添加为查询字符串参数。 为此,请将文字字符串 {CHECKOUT_SESSION_ID} 添加到您的 success_url 查询字符串。Stripe 将用实际的结帐会话 ID 替换此占位符:

use Illuminate\Http\Request;
use Stripe\Checkout\Session;
use Stripe\Customer;

Route::get('/product-checkout', function (Request $request) {
    return $request->user()->checkout(['price_tshirt' => 1], [
        'success_url' => route('checkout-success') . '?session_id={CHECKOUT_SESSION_ID}',
        'cancel_url' => route('checkout-cancel'),
    ]);
});

Route::get('/checkout-success', function (Request $request) {
    $checkoutSession = $request->user()->stripe()->checkout->sessions->retrieve($request->get('session_id'));

    return view('checkout.success', ['checkoutSession' => $checkoutSession]);
})->name('checkout-success');

兑换码

默认情况下,Stripe Checkout 不允许 用户可兑换促销代码。 幸运的是,有一种简单的方法可以为您的结帐页面启用这些功能。 为此,您可以调用 allowPromotionCodes 方法:

use Illuminate\Http\Request;

Route::get('/product-checkout', function (Request $request) {
    return $request->user()
        ->allowPromotionCodes()
        ->checkout('price_tshirt');
});

单次收费结账

您还可以对尚未在 Stripe 仪表板中创建的临时产品进行简单收费。 为此,您可以在计费模型上使用 checkoutCharge 方法,并向其传递可计费金额、产品名称和可选数量。 当客户访问此路线时,他们将被重定向到 Stripe 的结帐页面:

use Illuminate\Http\Request;

Route::get('/charge-checkout', function (Request $request) {
    return $request->user()->checkoutCharge(1200, 'T-Shirt', 5);
});

注意:当使用 checkoutCharge 方法时,Stripe 将始终在您的 Stripe 仪表板中创建新产品和价格。 因此,我们建议您在 Stripe 仪表板中预先创建产品,并改用 checkout 方法。

订阅结帐

注意:使用 Stripe Checkout 进行订阅需要您在 Stripe 仪表板中启用 customer.subscription.created webhook。 此 webhook 将在您的数据库中创建订阅记录并存储所有相关的订阅项。

您也可以使用 Stripe Checkout 来启动订阅。 在使用 Cashier 的订阅构建器方法定义您的订阅后,您可以调用 checkout 方法。 当客户访问此路线时,他们将被重定向到 Stripe 的结帐页面:

use Illuminate\Http\Request;

Route::get('/subscription-checkout', function (Request $request) {
    return $request->user()
        ->newSubscription('default', 'price_monthly')
        ->checkout();
});

与产品结帐一样,您可以自定义成功和取消 URL:

use Illuminate\Http\Request;

Route::get('/subscription-checkout', function (Request $request) {
    return $request->user()
        ->newSubscription('default', 'price_monthly')
        ->checkout([
            'success_url' => route('your-success-route'),
            'cancel_url' => route('your-cancel-route'),
        ]);
});

当然,您也可以为订阅结帐启用促销代码:

use Illuminate\Http\Request;

Route::get('/subscription-checkout', function (Request $request) {
    return $request->user()
        ->newSubscription('default', 'price_monthly')
        ->allowPromotionCodes()
        ->checkout();
});

注意:不幸的是,在开始订阅时,Stripe Checkout 不支持所有订阅计费选项。 在订阅生成器上使用 anchorBillingCycleOn 方法、设置按比例分配行为或设置支付行为在 Stripe Checkout 会话期间不会有任何影响。 请查阅 Stripe Checkout Session API 文档 以查看可用的参数。

条纹结帐和试用期

当然,您可以在构建将使用 Stripe Checkout 完成的订阅时定义试用期:

$checkout = Auth::user()->newSubscription('default', 'price_monthly')
    ->trialDays(3)
    ->checkout();

但是,试用期必须至少为 48 小时,这是 Stripe Checkout 支持的最短试用时间。

Subscriptions & Webhooks

请记住,Stripe 和 Cashier 通过 webhook 更新订阅状态,因此当客户在输入付款信息后返回应用程序时,订阅可能尚未激活。 要处理这种情况,您可能希望显示一条消息,通知用户他们的付款或订阅处于待处理状态。

收税ID

Checkout 还支持收集客户的税号。 要在结帐会话上启用此功能,请在创建会话时调用 collectTaxIds 方法:

$checkout = $user->collectTaxIds()->checkout('price_tshirt');

调用此方法时,客户可以使用一个新的复选框,允许他们指示他们是否作为公司进行采购。 如果是这样,他们将有机会提供他们的税号。

注意:如果您已经在应用程序的服务提供者中配置了自动征税,那么该功能将自动启用,无需调用 collectTaxIds 方法。

处理失败的付款

有时,订阅或单笔费用的付款可能会失败。 当这种情况发生时,Cashier 会抛出一个 Laravel\Cashier\Exceptions\IncompletePayment 异常,通知你发生了这种情况。 捕获此异常后,您有两个选择如何继续。

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

use Laravel\Cashier\Exceptions\IncompletePayment;

try {
    $subscription = $user->newSubscription('default', 'price_monthly')
                            ->create($paymentMethod);
} catch (IncompletePayment $exception) {
    return redirect()->route(
        'cashier.payment',
        [$exception->payment->id, 'redirect' => route('home')]
    );
}

在付款确认页面上,将提示客户再次输入他们的信用卡信息并执行 Stripe 要求的任何其他操作,例如“3D Secure”确认。 确认付款后,用户将被重定向到上面指定的 redirect 参数提供的 URL。 重定向后,“message”(字符串)和“success”(整数)查询字符串变量将被添加到 URL。 支付页面目前支持以下支付方式类型:

  • Credit Cards
  • Alipay
  • Bancontact
  • BECS Direct Debit
  • EPS
  • Giropay
  • iDEAL
  • SEPA Direct Debit

或者,您可以让 Stripe 为您处理付款确认。在这种情况下,您可以在 Stripe 控制面板中设置 Stripe 的自动结算电子邮件,而不是重定向到付款确认页面。但是,如果捕获到 IncompletePayment 异常,您仍应通知用户他们将收到一封包含进一步付款确认说明的电子邮件。

对于使用 Billable 特征的模型,可能会为以下方法引发付款异常:chargeinvoiceForinvoice。与订阅交互时,SubscriptionBuilder 上的 create 方法,以及 SubscriptionSubscriptionItem 模型上的 incrementAndInvoiceswapAndInvoice 方法可能会抛出不完整的支付异常。

可以使用计费模型或订阅实例上的“hasIncompletePayment”方法来确定现有订阅是否有不完整的付款:

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

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

您可以通过检查异常实例上的 payment 属性来得出未完成付款的具体状态:

use Laravel\Cashier\Exceptions\IncompletePayment;

try {
    $user->charge(1000, 'pm_card_threeDSecure2Required');
} catch (IncompletePayment $exception) {
    // 获取付款意向状态...
    $exception->payment->status;

    // 检查具体情况...
    if ($exception->payment->requiresPaymentMethod()) {
        // ...
    } elseif ($exception->payment->requiresConfirmation()) {
        // ...
    }
}

强大的客户认证

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

注意:在开始之前,请查看 关于 PSD2 和 SCA 的 Stripe 指南 以及他们的 [关于新 SCA API 的文档](stripe.com /docs/strong-customer-authentication)。

需要额外确认的付款

SCA 法规通常需要额外的验证才能确认和处理付款。 当这种情况发生时,Cashier 会抛出一个 Laravel\Cashier\Exceptions\IncompletePayment 异常,通知你需要额外的验证。 有关如何处理这些异常的更多信息,请参阅有关 处理失败的付款 的文档。

Stripe 或 Cashier 提供的支付确认屏幕可能会针对特定银行或发卡机构的支付流程进行定制,并且可能包括额外的卡确认、临时小额费用、单独的设备身份验证或其他形式的验证。

不完整和逾期状态

当付款需要额外确认时,订阅将保持在“未完成”或“过去到期”状态,如“stripe_status”数据库列所示。 付款确认完成后,Cashier 将自动激活客户的订阅,并且 Stripe 通过 webhook 通知您的应用程序完成。

有关 incompletepast_due 状态的更多信息,请参阅 我们关于这些状态的附加文档

场外付款通知

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

CASHIER_PAYMENT_NOTIFICATION=Laravel\Cashier\Notifications\ConfirmPayment

为确保发送会话外付款确认通知,请确认您的应用程序的 Stripe webhook 已配置 并且在您的 Stripe 仪表板中启用了 invoice.payment_action_required webhook。 此外,你的 Billable 模型也应该使用 Laravel 的 Illuminate\Notifications\Notifiable trait。

注意:即使客户手动进行需要额外确认的付款,也会发送通知。 不幸的是,Stripe 无法知道付款是手动完成的还是“非会话”完成的。 但是,如果客户在确认付款后访问付款页面,他们只会看到“付款成功”消息。 不允许客户意外确认两次相同的付款并导致意外的第二次收费。

Stripe SDK

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

$stripeSubscription = $subscription->asStripeSubscription();

$stripeSubscription->application_fee_percent = 5;

$stripeSubscription->save();

您也可以使用 updateStripeSubscription 方法直接更新 Stripe 订阅:

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

如果您想直接使用 Stripe\StripeClient 客户端,可以调用 Cashier 类的 stripe 方法。 例如,您可以使用此方法访问“StripeClient”实例并从您的 Stripe 帐户中检索价格列表:

use Laravel\Cashier\Cashier;

$prices = Cashier::stripe()->prices->all();

测试

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

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

首先,将 Stripe 密钥的 testing 版本添加到您的 phpunit.xml 文件中:

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

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

技巧:为了测试各种计费场景,例如信用卡拒付和失败,您可以使用 Stripe 提供的大量的 测试卡号和令牌

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://learnku.com/docs/laravel/9.x/bil...

译文地址:https://learnku.com/docs/laravel/9.x/bil...

上一篇 下一篇
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
贡献者:6
讨论数量: 1
发起讨论 只看当前版本


nilsir
翻译文档缺失部分文章
0 个点赞 | 10 个回复 | 分享 | 课程版本 9.x