Skip to content

Laravel Cashier (Stripe)

简介

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

升级 Cashier

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

WARNING

为了防止破坏性变更,Cashier 使用固定的 Stripe API 版本。Cashier 15 使用 Stripe API 版本 2023-10-16。Stripe API 版本将在次要版本中更新,以利用 Stripe 的新功能和改进。

安装

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

shell
composer require laravel/cashier

安装该包后,使用 vendor:publish Artisan 命令发布 Cashier 的迁移文件:

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

然后,迁移你的数据库:

shell
php artisan migrate

Cashier 的迁移将向你的 users 表添加几个列。它们还将创建一个新的 subscriptions 表来保存所有客户的订阅,以及一个 subscription_items 表用于多价格订阅。

如果你愿意,也可以使用 vendor:publish Artisan 命令发布 Cashier 的配置文件:

shell
php artisan vendor:publish --tag="cashier-config"

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

WARNING

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

配置

Billable 模型

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

php
    use Laravel\Cashier\Billable;

    class User extends Authenticatable
    {
        use Billable;
    }

Cashier 假设你的可计费模型将是 Laravel 附带的 App\Models\User 类。如果你想更改这一点,可以通过 useCustomerModel 方法指定不同的模型。这个方法通常应该在你的 AppServiceProvider 类的 boot 方法中调用:

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

    /**
     * 引导任何应用程序服务。
     */
    public function boot(): void
    {
        Cashier::useCustomerModel(User::class);
    }

WARNING

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

API 密钥

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

ini
STRIPE_KEY=your-stripe-key
STRIPE_SECRET=your-stripe-secret
STRIPE_WEBHOOK_SECRET=your-stripe-webhook-secret

WARNING

你应该确保在应用程序的 .env 文件中定义了 STRIPE_WEBHOOK_SECRET 环境变量,因为这个变量用于确保传入的 webhooks 确实来自 Stripe。

货币配置

Cashier 的默认货币是美元(USD)。你可以通过在应用程序的 .env 文件中设置 CASHIER_CURRENCY 环境变量来更改默认货币:

ini
CASHIER_CURRENCY=eur

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

ini
CASHIER_CURRENCY_LOCALE=nl_BE

WARNING

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

税务配置

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

php
    use Laravel\Cashier\Cashier;

    /**
     * 引导任何应用程序服务。
     */
    public function boot(): void
    {
        Cashier::calculateTaxes();
    }

一旦启用了税费计算,任何新的订阅和任何一次性发票都将接受自动税费计算。

为了使这个功能正常工作,你客户的计费详细信息,如客户的姓名、地址和税号,需要同步到 Stripe。你可以使用 Cashier 提供的客户数据同步税号方法来完成这个操作。

日志记录

Cashier 允许你指定在记录致命 Stripe 错误时使用的日志通道。你可以通过在应用程序的 .env 文件中定义 CASHIER_LOGGER 环境变量来指定日志通道:

ini
CASHIER_LOGGER=stack

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

使用自定义模型

你可以自由扩展 Cashier 内部使用的模型,方法是定义你自己的模型并扩展相应的 Cashier 模型:

php
    use Laravel\Cashier\Subscription as CashierSubscription;

    class Subscription extends CashierSubscription
    {
        // ...
    }

定义你的模型后,你可以通过 Laravel\Cashier\Cashier 类指示 Cashier 使用你的自定义模型。通常,你应该在应用程序的 App\Providers\AppServiceProvider 类的 boot 方法中告知 Cashier 你的自定义模型:

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

    /**
     * 引导任何应用程序服务。
     */
    public function boot(): void
    {
        Cashier::useSubscriptionModel(Subscription::class);
        Cashier::useSubscriptionItemModel(SubscriptionItem::class);
    }

快速入门

销售产品

NOTE

在使用 Stripe Checkout 之前,你应该在 Stripe 仪表板中定义具有固定价格的产品。此外,你应该配置 Cashier 的 webhook 处理

通过你的应用程序提供产品和订阅计费可能会让人感到畏惧。然而,多亏了 Cashier 和 Stripe Checkout,你可以轻松构建现代、强大的支付集成。

要向客户收取非经常性的单次收费产品费用,我们将使用 Cashier 将客户引导至 Stripe Checkout,在那里他们将提供支付详细信息并确认购买。一旦通过 Checkout 完成支付,客户将被重定向到你在应用程序中选择的成功 URL:

php
    use Illuminate\Http\Request;

    Route::get('/checkout', function (Request $request) {
        $stripePriceId = 'price_deluxe_album';

        $quantity = 1;

        return $request->user()->checkout([$stripePriceId => $quantity], [
            'success_url' => route('checkout-success'),
            'cancel_url' => route('checkout-cancel'),
        ]);
    })->name('checkout');

    Route::view('/checkout/success', 'checkout.success')->name('checkout-success');
    Route::view('/checkout/cancel', 'checkout.cancel')->name('checkout-cancel');

如上面的例子所示,我们将使用 Cashier 提供的 checkout 方法将客户重定向到 Stripe Checkout,以处理给定的"价格标识符"。在使用 Stripe 时,"价格"指的是特定产品的定义价格

如果需要,checkout 方法将自动在 Stripe 中创建一个客户,并将该 Stripe 客户记录与你应用程序数据库中相应的用户关联。完成结账会话后,客户将被重定向到专门的成功或取消页面,你可以在那里向客户显示信息性消息。

向 Stripe Checkout 提供元数据

在销售产品时,通常会通过你自己的应用程序定义的 CartOrder 模型来跟踪已完成的订单和购买的产品。当将客户重定向到 Stripe Checkout 以完成购买时,你可能需要提供一个现有的订单标识符,以便在客户被重定向回你的应用程序时,可以将完成的购买与相应的订单关联起来。

为了实现这一点,你可以向 checkout 方法提供一个 metadata 数组。让我们假设当用户开始结账过程时,在我们的应用程序中创建了一个待处理的 Order。请记住,这个例子中的 CartOrder 模型是说明性的,并非由 Cashier 提供。你可以根据自己应用程序的需求自由实现这些概念:

php
    use App\Models\Cart;
    use App\Models\Order;
    use Illuminate\Http\Request;

    Route::get('/cart/{cart}/checkout', function (Request $request, Cart $cart) {
        $order = Order::create([
            'cart_id' => $cart->id,
            'price_ids' => $cart->price_ids,
            'status' => 'incomplete',
        ]);

        return $request->user()->checkout($order->price_ids, [
            'success_url' => route('checkout-success').'?session_id={CHECKOUT_SESSION_ID}',
            'cancel_url' => route('checkout-cancel'),
            'metadata' => ['order_id' => $order->id],
        ]);
    })->name('checkout');

如上面的例子所示,当用户开始结账过程时,我们将向 checkout 方法提供所有与购物车/订单相关的 Stripe 价格标识符。当然,你的应用程序负责在客户添加这些项目时将它们与"购物车"或订单关联。我们还通过 metadata 数组向 Stripe Checkout 会话提供了订单的 ID。最后,我们在 Checkout 成功路由中添加了 CHECKOUT_SESSION_ID 模板变量。当 Stripe 将客户重定向回你的应用程序时,这个模板变量将自动填充为 Checkout 会话 ID。

接下来,让我们构建 Checkout 成功路由。这是用户在通过 Stripe Checkout 完成购买后将被重定向到的路由。在这个路由中,我们可以检索 Stripe Checkout 会话 ID 和相关的 Stripe Checkout 实例,以访问我们提供的元数据并相应地更新客户的订单:

php
    use App\Models\Order;
    use Illuminate\Http\Request;
    use Laravel\Cashier\Cashier;

    Route::get('/checkout/success', function (Request $request) {
        $sessionId = $request->get('session_id');

        if ($sessionId === null) {
            return;
        }

        $session = Cashier::stripe()->checkout->sessions->retrieve($sessionId);

        if ($session->payment_status !== 'paid') {
            return;
        }

        $orderId = $session['metadata']['order_id'] ?? null;

        $order = Order::findOrFail($orderId);

        $order->update(['status' => 'completed']);

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

请参阅 Stripe 的文档以获取更多关于 Checkout 会话对象包含的数据的信息。

销售订阅

NOTE

在使用 Stripe Checkout 之前,你应该在 Stripe 仪表板中定义具有固定价格的产品。此外,你应该配置 Cashier 的 webhook 处理

通过你的应用程序提供产品和订阅计费可能会让人感到畏惧。然而,多亏了 Cashier 和 Stripe Checkout,你可以轻松构建现代、强大的支付集成。

为了学习如何使用 Cashier 和 Stripe Checkout 销售订阅,让我们考虑一个简单的订阅服务场景,该服务有一个基本的月度(price_basic_monthly)和年度(price_basic_yearly)计划。这两个价格可以在我们的 Stripe 仪表板中归类为"Basic"产品(pro_basic)。此外,我们的订阅服务可能还提供一个专家计划 pro_expert

首先,让我们了解客户如何订阅我们的服务。当然,你可以想象客户可能会在我们应用程序的定价页面上点击"Basic"计划的"订阅"按钮。这个按钮或链接应该将用户引导到一个 Laravel 路由,该路由为他们选择的计划创建 Stripe Checkout 会话:

php
    use Illuminate\Http\Request;

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

如上面的例子所示,我们将把客户重定向到一个 Stripe Checkout 会话,该会话将允许他们订阅我们的 Basic 计划。成功结账或取消后,客户将被重定向回我们提供给 checkout 方法的 URL。为了知道他们的订阅何时实际开始(因为某些支付方式可能需要几秒钟来处理),我们还需要配置 Cashier 的 webhook 处理

现在客户可以开始订阅了,我们需要限制应用程序的某些部分,以便只有订阅用户才能访问。当然,我们始终可以通过 Cashier 的 Billable trait 提供的 subscribed 方法来确定用户当前的订阅状态:

blade
@if ($user->subscribed())
    <p>你已订阅。</p>
@endif

我们甚至可以轻松确定用户是否订阅了特定产品或价格:

blade
@if ($user->subscribedToProduct('pro_basic'))
    <p>你已订阅我们的 Basic 产品。</p>
@endif

@if ($user->subscribedToPrice('price_basic_monthly'))
    <p>你已订阅我们的月度 Basic 计划。</p>
@endif

构建订阅中间件

为了方便起见,你可能希望创建一个中间件,用于确定传入请求是否来自已订阅的用户。一旦定义了这个中间件,你就可以轻松地将其分配给路由,以防止未订阅的用户访问该路由:

php
    <?php

    namespace App\Http\Middleware;

    use Closure;
    use Illuminate\Http\Request;
    use Symfony\Component\HttpFoundation\Response;

    class Subscribed
    {
        /**
         * 处理传入请求。
         */
        public function handle(Request $request, Closure $next): Response
        {
            if (! $request->user()?->subscribed()) {
                // 将用户重定向到计费页面并要求他们订阅...
                return redirect('/billing');
            }

            return $next($request);
        }
    }

一旦定义了中间件,你就可以将其分配给路由:

php
    use App\Http\Middleware\Subscribed;

    Route::get('/dashboard', function () {
        // ...
    })->middleware([Subscribed::class]);

允许客户管理他们的计费计划

当然,客户可能想要将他们的订阅计划更改为另一个产品或"等级"。最简单的方法是将客户引导到 Stripe 的客户计费门户,它提供了一个托管的用户界面,允许客户下载发票、更新他们的支付方式和更改订阅计划。

首先,在你的应用程序中定义一个链接或按钮,将用户引导到一个 Laravel 路由,我们将使用该路由来启动计费门户会话:

blade
<a href="{{ route('billing') }}">
    计费
</a>

接下来,让我们定义启动 Stripe 客户计费门户会话并将用户重定向到门户的路由。redirectToBillingPortal 方法接受用户在退出门户时应返回的 URL:

php
    use Illuminate\Http\Request;

    Route::get('/billing', function (Request $request) {
        return $request->user()->redirectToBillingPortal(route('dashboard'));
    })->middleware(['auth'])->name('billing');

NOTE

只要你配置了 Cashier 的 webhook 处理,Cashier 就会通过检查来自 Stripe 的传入 webhooks 自动保持你的应用程序的 Cashier 相关数据库表同步。因此,例如,当用户通过 Stripe 的客户计费门户取消他们的订阅时,Cashier 将接收相应的 webhook 并在你的应用程序数据库中将订阅标记为"已取消"。

客户

检索客户

你可以使用 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 客户对象,但不确定该可计费模型是否已经是 Stripe 中的客户,你可以使用 createOrGetStripeCustomer 方法。如果 Stripe 中还不存在客户,这个方法将在 Stripe 中创建一个新客户:

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

更新客户

有时,你可能希望直接用额外信息更新 Stripe 客户。你可以使用 updateStripeCustomer 方法来完成这个操作。这个方法接受一个 Stripe API 支持的客户更新选项数组:

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

余额

Stripe 允许你贷记或借记客户的"余额"。之后,这个余额将在新发票上被贷记或借记。要检查客户的总余额,你可以使用可计费模型上可用的 balance 方法。balance 方法将返回客户货币的格式化字符串表示:

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

要贷记客户的余额,你可以向 creditBalance 方法提供一个值。如果你愿意,你还可以提供一个描述:

php
    $user->creditBalance(500, '高级客户充值。');

debitBalance 方法提供一个值将借记客户的余额:

php
    $user->debitBalance(300, '不当使用惩罚。');

applyBalance 方法将为客户创建新的客户余额交易。你可以使用 balanceTransactions 方法检索这些交易记录,这可能对于向客户提供贷记和借记日志很有用:

php
    // 检索所有交易...
    $transactions = $user->balanceTransactions();
    foreach ($transactions as $transaction) {
        // 交易金额...
        $amount = $transaction->amount(); // $2.31

        // 检索相关发票(如果可用)...
        $invoice = $transaction->invoice();
    }

税号

Cashier 提供了一种简单的方法来管理客户的税号。例如,taxIds 方法可用于检索分配给客户的所有 税号 作为一个集合:

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

你也可以通过其标识符检索客户的特定税号:

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

你可以通过提供有效的 类型 和值来创建新的税号:

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

createTaxId 方法将立即将增值税 ID 添加到客户的账户。增值税 ID 的验证也由 Stripe 完成;然而,这是一个异步过程。你可以通过订阅 customer.tax_id.updated webhook 事件并检查 增值税 ID 的 verification 参数 来获得验证更新的通知。有关处理 webhooks 的更多信息,请查阅定义 webhook 处理程序的文档

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

php
    $user->deleteTaxId('txi_belgium');

将客户数据同步到 Stripe

通常,当你的应用程序的用户更新他们的姓名、电子邮件地址或 Stripe 也存储的其他信息时,你应该通知 Stripe 这些更新。这样做可以确保 Stripe 的信息副本与你的应用程序保持同步。

为了自动化这个过程,你可以在你的可计费模型上定义一个事件监听器,该监听器响应模型的 updated 事件。然后,在你的事件监听器中,你可以在模型上调用 syncStripeCustomerDetails 方法:

php
    use App\Models\User;
    use function Illuminate\Events\queueable;

    /**
     * 模型的 "booted" 方法。
     */
    protected static function booted(): void
    {
        static::updated(queueable(function (User $customer) {
            if ($customer->hasStripeId()) {
                $customer->syncStripeCustomerDetails();
            }
        }));
    }

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

你可以通过重写 Cashier 提供的各种方法来自定义用于将客户信息同步到 Stripe 的列。例如,你可以重写 stripeName 方法来自定义在 Cashier 将客户信息同步到 Stripe 时应被视为客户"名称"的属性:

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

类似地,你可以重写 stripeEmailstripePhonestripeAddressstripePreferredLocales 方法。这些方法将在更新 Stripe 客户对象时将信息同步到相应的客户参数。如果你希望完全控制客户信息同步过程,你可以重写 syncStripeCustomerDetails 方法。

计费门户

Stripe 提供了一种简单的方式来设置计费门户,以便你的客户可以管理他们的订阅、支付方式,并查看他们的计费历史。你可以通过在可计费模型上调用 redirectToBillingPortal 方法,从控制器或路由中将用户重定向到计费门户:

php
    use Illuminate\Http\Request;

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

默认情况下,当用户完成管理他们的订阅后,他们将能够通过 Stripe 计费门户内的链接返回到你应用程序的 home 路由。你可以通过将 URL 作为参数传递给 redirectToBillingPortal 方法来提供一个自定义 URL,用户应该返回到该 URL:

php
    use Illuminate\Http\Request;

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

如果你想生成计费门户的 URL 而不生成 HTTP 重定向响应,你可以调用 billingPortalUrl 方法:

php
    $url = $request->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 并将其传递给视图后,你应该将其密钥附加到将收集支付方式的元素上。例如,考虑这个"更新支付方式"表单:

html
<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 Element 附加到表单,并安全地收集客户的支付详细信息:

html
<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 检索安全的"支付方式标识符":

js
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 应用程序,在那里可以将其附加到客户。支付方式可以作为新的支付方式添加用于更新默认支付方式。你也可以立即使用支付方式标识符创建新的订阅

NOTE

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

单次收费的支付方式

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

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

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

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

定义这样的表单后,可以使用 Stripe.js 库将 Stripe Element 附加到表单,并安全地收集客户的支付详细信息:

html
<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 检索安全的"支付方式标识符":

js
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 应用程序并处理单次收费

WARNING

charge 方法接受以你应用程序使用的货币的最小面额表示的支付金额。例如,如果客户以美元支付,金额应该以美分指定。

检索支付方式

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

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

默认情况下,这个方法将返回所有类型的支付方式。要检索特定类型的支付方式,你可以将 type 作为参数传递给该方法:

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

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

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

你可以使用 findPaymentMethod 方法检索附加到可计费模型的特定支付方式:

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

判断是否存在支付方式

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

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

你可以使用 hasPaymentMethod 方法来确定可计费模型是否至少有一个支付方式附加到其账户:

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

这个方法将确定可计费模型是否有任何支付方式。要确定模型是否存在特定类型的支付方式,你可以将 type 作为参数传递给该方法:

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

更新默认支付方式

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

php
    $user->updateDefaultPaymentMethod($paymentMethod);

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

php
    $user->updateDefaultPaymentMethodFromStripe();

WARNING

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

添加支付方式

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

php
    $user->addPaymentMethod($paymentMethod);

NOTE

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

删除支付方式

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

php
    $paymentMethod->delete();

deletePaymentMethod 方法将从可计费模型中删除特定的支付方式:

php
    $user->deletePaymentMethod('pm_visa');

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

php
    $user->deletePaymentMethods();

默认情况下,这个方法将删除所有类型的支付方式。要删除特定类型的支付方式,你可以将 type 作为参数传递给该方法:

php
    $user->deletePaymentMethods('sepa_debit');

WARNING

如果用户有活跃的订阅,你的应用程序不应允许他们删除他们的默认支付方式。

订阅

订阅提供了一种为你的客户设置定期付款的方式。Cashier 管理的 Stripe 订阅支持多个订阅价格、订阅数量、试用期等。

创建订阅

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

php
    use Illuminate\Http\Request;

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

        // ...
    });

传递给 newSubscription 方法的第一个参数应该是订阅的内部类型。如果你的应用程序只提供单一订阅,你可能会称之为 defaultprimary。这个订阅类型仅用于内部应用程序使用,不应显示给用户。此外,它不应包含空格,并且在创建订阅后不应更改。第二个参数是用户订阅的特定价格。这个值应该对应于 Stripe 中价格的标识符。

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

WARNING

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

通过发票邮件收取定期付款

你可以指示 Stripe 在客户的定期付款到期时向客户发送发票邮件,而不是自动收取客户的定期付款。然后,客户可以在收到发票后手动支付。通过发票收取定期付款时,客户不需要预先提供支付方式:

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

客户在订阅被取消之前支付发票的时间由 days_until_due 选项决定。默认情况下,这是 30 天;但是,如果你愿意,你可以为这个选项提供一个特定的值:

php
    $user->newSubscription('default', 'price_monthly')->createAndSendInvoice([], [
        'days_until_due' => 30
    ]);

订阅数量

如果你想在创建订阅时为价格设置特定的数量,你应该在创建订阅之前在订阅构建器上调用 quantity 方法:

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

附加详情

如果你想指定 Stripe 支持的额外客户订阅选项,你可以将它们作为第二和第三个参数传递给 create 方法:

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

优惠券

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

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

或者,如果你想应用 Stripe 促销代码,你可以使用 withPromotionCode 方法:

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

给定的促销代码 ID 应该是 Stripe 分配给促销代码的 API ID,而不是面向客户的促销代码。如果你需要根据给定的面向客户的促销代码找到促销代码 ID,你可以使用 findPromotionCode 方法:

php
    // 通过面向客户的代码查找促销代码 ID...
    $promotionCode = $user->findPromotionCode('SUMMERSALE');

    // 通过面向客户的代码查找活跃的促销代码 ID...
    $promotionCode = $user->findActivePromotionCode('SUMMERSALE');

在上面的例子中,返回的 $promotionCode 对象是 Laravel\Cashier\PromotionCode 的一个实例。这个类装饰了一个底层的 Stripe\PromotionCode 对象。你可以通过调用 coupon 方法来检索与促销代码相关的优惠券:

php
    $coupon = $user->findPromotionCode('SUMMERSALE')->coupon();

优惠券实例允许你确定折扣金额,以及优惠券是代表固定折扣还是基于百分比的折扣:

php
    if ($coupon->isPercentage()) {
        return $coupon->percentOff().'%'; // 21.5%
    } else {
        return $coupon->amountOff(); // $5.99
    }

你也可以检索当前应用于客户或订阅的折扣:

php
    $discount = $billable->discount();

    $discount = $subscription->discount();

返回的 Laravel\Cashier\Discount 实例装饰了一个底层的 Stripe\Discount 对象实例。你可以通过调用 coupon 方法来检索与这个折扣相关的优惠券:

php
    $coupon = $subscription->discount()->coupon();

如果你想将新的优惠券或促销代码应用于客户或订阅,你可以通过 applyCouponapplyPromotionCode 方法来实现:

php
    $billable->applyCoupon('coupon_id');
    $billable->applyPromotionCode('promotion_code_id');

    $subscription->applyCoupon('coupon_id');
    $subscription->applyPromotionCode('promotion_code_id');

记住,你应该使用分配给促销代码的 Stripe API ID,而不是面向客户的促销代码。在给定时间只能将一个优惠券或促销代码应用于客户或订阅。

有关此主题的更多信息,请查阅 Stripe 关于优惠券促销代码的文档。

添加订阅

如果你想为已经有默认支付方式的客户添加订阅,你可以在订阅构建器上调用 add 方法:

php
    use App\Models\User;

    $user = User::find(1);

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

从 Stripe 仪表板创建订阅

你也可以从 Stripe 仪表板本身创建订阅。当这样做时,Cashier 将同步新添加的订阅,并为它们分配一个 default 类型。要自定义分配给仪表板创建的订阅的订阅类型,定义 webhook 事件处理程序

此外,你只能通过 Stripe 仪表板创建一种类型的订阅。如果你的应用程序提供使用不同类型的多个订阅,只有一种类型的订阅可以通过 Stripe 仪表板添加。

最后,你应该始终确保只添加一个与你的应用程序提供的订阅类型相对应的活跃订阅。如果客户有两个 default 订阅,即使两个都会与你的应用程序的数据库同步,Cashier 也只会使用最近添加的订阅。

检查订阅状态

一旦客户订阅了你的应用程序,你可以使用各种方便的方法轻松检查他们的订阅状态。首先,subscribed 方法如果客户有活跃订阅,即使订阅当前处于试用期内,也会返回 truesubscribed 方法接受订阅类型作为其第一个参数:

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

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

php
    <?php

    namespace App\Http\Middleware;

    use Closure;
    use Illuminate\Http\Request;
    use Symfony\Component\HttpFoundation\Response;

    class EnsureUserIsSubscribed
    {
        /**
         * 处理传入请求。
         *
         * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
         */
        public function handle(Request $request, Closure $next): Response
        {
            if ($request->user() && ! $request->user()->subscribed('default')) {
                // 这个用户不是付费客户...
                return redirect('/billing');
            }

            return $next($request);
        }
    }

如果你想确定用户是否仍在试用期内,你可以使用 onTrial 方法。这个方法可以用来确定是否应该向用户显示他们仍在试用期的警告:

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

subscribedToProduct 方法可用于确定用户是否订阅了基于给定 Stripe 产品标识符的给定产品。在 Stripe 中,产品是价格的集合。在这个例子中,我们将确定用户的 default 订阅是否主动订阅了应用程序的"高级"产品。给定的 Stripe 产品标识符应该对应于你在 Stripe 仪表板中的产品之一的标识符:

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

通过向 subscribedToProduct 方法传递一个数组,你可以确定用户的 default 订阅是否主动订阅了应用程序的"基本"或"高级"产品:

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

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

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

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

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

WARNING

已取消的订阅状态

要确定用户是否曾经是活跃订阅者但已取消了他们的订阅,你可以使用 canceled 方法:

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

你也可以确定用户是否已取消他们的订阅,但仍在他们的"宽限期"内,直到订阅完全过期。例如,如果用户在 3 月 5 日取消了原定于 3 月 10 日到期的订阅,用户在 3 月 10 日之前处于他们的"宽限期"。请注意,在此期间 subscribed 方法仍然返回 true:

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

要确定用户是否已取消他们的订阅且不再处于他们的"宽限期"内,你可以使用 ended 方法:

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

不完整和逾期状态

如果订阅在创建后需要二次支付操作,订阅将被标记为 incomplete。订阅状态存储在 Cashier 的 subscriptions 数据库表的 stripe_status 列中。

同样,如果在交换价格时需要二次支付操作,订阅将被标记为 past_due。当你的订阅处于这些状态之一时,在客户确认他们的付款之前,它将不会处于活跃状态。可以使用可计费模型或订阅实例上的 hasIncompletePayment 方法来确定订阅是否有不完整的付款:

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

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

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

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

如果你希望订阅在 past_dueincomplete 状态下仍被视为活跃,你可以使用 Cashier 提供的 keepPastDueSubscriptionsActivekeepIncompleteSubscriptionsActive 方法。通常,这些方法应该在你的 App\Providers\AppServiceProviderregister 方法中调用:

php
    use Laravel\Cashier\Cashier;

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

WARNING

当订阅处于 incomplete 状态时,在付款确认之前无法更改。因此,当订阅处于 incomplete 状态时,swapupdateQuantity 方法将抛出异常。

订阅范围

大多数订阅状态也可以作为查询范围使用,这样你就可以轻松地查询数据库以查找处于给定状态的订阅:

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

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

可用范围的完整列表如下:

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

更改价格

订阅客户后,他们偶尔可能想要更改为新的订阅价格。要将客户切换到新价格,将 Stripe 价格的标识符传递给 swap 方法。切换价格时,假定用户希望重新激活他们之前取消的订阅。给定的价格标识符应该对应于 Stripe 仪表板中可用的 Stripe 价格标识符:

php
    use App\Models\User;

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

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

如果客户处于试用期,试用期将被保留。此外,如果订阅存在"数量",该数量也将被保留。

如果你想切换价格并取消客户当前处于的任何试用期,你可以调用 skipTrial 方法:

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

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

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

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

按比例计算

默认情况下,Stripe 在切换价格时会按比例计算费用。noProrate 方法可用于更新订阅的价格而不按比例计算费用:

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

有关订阅按比例计算的更多信息,请查阅 Stripe 文档

WARNING

swapAndInvoice 方法之前执行 noProrate 方法对按比例计算没有影响。将始终发出发票。

订阅数量

有时订阅会受到"数量"的影响。例如,项目管理应用可能对每个项目每月收费 10 美元。你可以使用 incrementQuantitydecrementQuantity 方法轻松增加或减少你的订阅数量:

php
    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 方法设置特定数量:

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

noProrate 方法可用于更新订阅的数量而不按比例计算费用:

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

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

多产品订阅的数量

如果你的订阅是多产品订阅,你应该将你希望增加或减少数量的价格的 ID 作为第二个参数传递给增加/减少方法:

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

多产品订阅

多产品订阅允许你将多个计费产品分配给单个订阅。例如,假设你正在构建一个客户服务"帮助台"应用程序,其基本订阅价格为每月 10 美元,但提供实时聊天附加产品,额外收费每月 15 美元。多产品订阅的信息存储在 Cashier 的 subscription_items 数据库表中。

你可以通过将价格数组作为第二个参数传递给 newSubscription 方法来为给定订阅指定多个产品:

php
    use Illuminate\Http\Request;

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

        // ...
    });

在上面的例子中,客户将有两个价格附加到他们的 default 订阅。两个价格都将在各自的计费周期内收费。如有必要,你可以使用 quantity 方法为每个价格指定特定数量:

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

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

如果你想向现有订阅添加另一个价格,你可以调用订阅的 addPrice 方法:

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

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

上面的例子将添加新价格,客户将在下一个计费周期被收取费用。如果你想立即向客户收费,你可以使用 addPriceAndInvoice 方法:

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

如果你想添加具有特定数量的价格,你可以将数量作为 addPriceaddPriceAndInvoice 方法的第二个参数传递:

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

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

你可以使用 removePrice 方法从订阅中移除价格:

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

WARNING

你不能移除订阅的最后一个价格。相反,你应该简单地取消订阅。

交换价格

你也可以更改附加到多产品订阅的价格。例如,假设一个客户有一个带有 price_chat 附加产品的 price_basic 订阅,你想将客户从 price_basic 升级到 price_pro 价格:

php
    use App\Models\User;

    $user = User::find(1);

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

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

你也可以通过将键/值对数组传递给 swap 方法来指定订阅项选项。例如,你可能需要指定订阅价格数量:

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

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

如果你想交换订阅上的单个价格,你可以使用订阅项本身的 swap 方法。如果你想保留订阅其他价格的所有现有元数据,这种方法特别有用:

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

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

按比例计算

默认情况下,Stripe 在向多产品订阅添加或移除价格时会按比例计算费用。如果你想在不按比例计算的情况下进行价格调整,你应该在价格操作上链接 noProrate 方法:

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

交换数量

如果你想更新单个订阅价格的数量,你可以通过将价格的 ID 作为额外参数传递给方法来使用现有的数量方法:

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

WARNING

当订阅有多个价格时,Subscription 模型上的 stripe_pricequantity 属性将为 null。要访问单个价格属性,你应该使用 Subscription 模型上可用的 items 关系。

订阅项

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

php
    use App\Models\User;

    $user = User::find(1);

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

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

你也可以使用 findItemOrFail 方法检索特定价格:

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

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

多个订阅

Stripe 允许你的客户同时拥有多个订阅。例如,你可能经营一个健身房,提供游泳订阅和举重订阅,每个订阅可能有不同的定价。当然,客户应该能够订阅其中一个或两个计划。

当你的应用程序创建订阅时,你可以向 newSubscription 方法提供订阅的类型。类型可以是任何表示用户正在启动的订阅类型的字符串:

php
    use Illuminate\Http\Request;

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

        // ...
    });

在这个例子中,我们为客户启动了一个月度游泳订阅。然而,他们可能想在以后切换到年度订阅。在调整客户的订阅时,我们可以简单地在 swimming 订阅上交换价格:

php
    $user->subscription('swimming')->swap('price_swimming_yearly');

当然,你也可以完全取消订阅:

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

计量计费

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

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

php
    use Illuminate\Http\Request;

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

        // ...
    });

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

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

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

报告使用情况

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

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

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

默认情况下,"使用量"为 1 被添加到计费期。或者,你可以传递一个特定的"使用量"来添加到客户在计费期的使用量:

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

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

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

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

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

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

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

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

检索使用记录

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

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

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

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

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

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

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

    @foreach ($usageRecords as $usageRecord)
        - 期间开始:{{ $usageRecord['period']['start'] }}
        - 期间结束:{{ $usageRecord['period']['end'] }}
        - 总使用量:{{ $usageRecord['total_usage'] }}
    @endforeach

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

订阅税费

WARNING

与其手动计算税率,你可以使用 Stripe Tax 自动计算税费

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

php
    /**
     * 应该应用于客户订阅的税率。
     *
     * @return array<int, string>
     */
    public function taxRates(): array
    {
        return ['txr_id'];
    }

taxRates 方法使你能够在客户基础上应用税率,这对于跨越多个国家和税率的用户群可能很有帮助。

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

php
    /**
     * 应该应用于客户订阅的税率。
     *
     * @return array<string, array<int, string>>
     */
    public function priceTaxRates(): array
    {
        return [
            'price_monthly' => ['txr_id'],
        ];
    }

WARNING

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

同步税率

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

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

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

税收豁免

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

php
    use App\Models\User;

    $user = User::find(1);

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

WARNING

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

订阅锚定日期

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

php
    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 方法:

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

当订阅被取消时,Cashier 将自动在你的数据库的 subscriptions 表中设置 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();

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

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

你也可以选择在特定时刻取消订阅:

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

最后,你应该始终在删除相关用户模型之前取消用户订阅:

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

    $user->delete();

恢复订阅

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

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

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

订阅试用期

预先获取支付方式

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

php
    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 中为价格配置的任何默认试用期。

WARNING

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()) {
        // ...
    }

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

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

要确定现有试用是否已过期,你可以使用 hasExpiredTrial 方法:

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

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

在 Stripe / Cashier 中定义试用天数

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

无需预先获取支付方式

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

php
    use App\Models\User;

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

WARNING

确保为你的可计费模型类定义中的 trial_ends_at 属性添加 日期转换

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

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

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

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

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

要检索用户的试用结束日期,你可以使用 trialEndsAt 方法。如果用户正在试用期内,此方法将返回一个 Carbon 日期实例,如果他们不在试用期内则返回 null。如果你想获取特定订阅而不是默认订阅的试用结束日期,你也可以传递一个可选的订阅类型参数:

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

你也可以使用 onGenericTrial 方法,如果你想特别知道用户是否在他们的"通用"试用期内,并且尚未创建实际订阅:

php
    if ($user->onGenericTrial()) {
        // 用户在他们的"通用"试用期内...
    }

延长试用期

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

php
    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 Webhooks

NOTE

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

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

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

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

  • customer.subscription.created
  • customer.subscription.updated
  • customer.subscription.deleted
  • customer.updated
  • customer.deleted
  • payment_method.automatically_updated
  • invoice.payment_action_required
  • invoice.payment_succeeded

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

shell
php artisan cashier:webhook

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

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

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

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

创建后,webhook 将立即激活。如果你希望创建 webhook 但使其处于禁用状态,直到你准备好,你可以在调用命令时提供 --disabled 选项:

shell
php artisan cashier:webhook --disabled

WARNING

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

Webhooks 和 CSRF 保护

由于 Stripe webhooks 需要绕过 Laravel 的 CSRF 保护,请确保 Laravel 不会尝试验证传入 Stripe webhooks 的 CSRF 令牌。为此,你应该在应用程序的 bootstrap/app.php 文件中将 stripe/* 从 CSRF 保护中排除:

php
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->validateCsrfTokens(except: [
            'stripe/*',
        ]);
    })

定义 Webhook 事件处理器

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

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

这两个事件都包含完整的 Stripe webhook 有效载荷。例如,如果你希望处理 invoice.payment_succeeded webhook,你可以注册一个将处理该事件的 监听器:

php
    <?php

    namespace App\Listeners;

    use Laravel\Cashier\Events\WebhookReceived;

    class StripeEventListener
    {
        /**
         * 处理接收到的 Stripe webhooks。
         */
        public function handle(WebhookReceived $event): void
        {
            if ($event->payload['type'] === 'invoice.payment_succeeded') {
                // 处理传入的事件...
            }
        }
    }

验证 Webhook 签名

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

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

单次收费

简单收费

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

php
    use Illuminate\Http\Request;

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

        // ...
    });

charge 方法接受一个数组作为其第三个参数,允许你将任何你希望的选项传递给底层的 Stripe 收费创建。更多关于创建收费时可用选项的信息可以在 Stripe 文档中找到:

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

你也可以在没有底层客户或用户的情况下使用 charge 方法。要实现这一点,在你的应用程序的可计费模型的新实例上调用 charge 方法:

php
    use App\Models\User;

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

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

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

WARNING

charge 方法接受以你应用程序使用的货币的最小面额表示的支付金额。例如,如果客户以美元支付,金额应该以美分指定。

发票收费

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

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

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

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

invoicePrice 类似,你可以使用 tabPrice 方法为多个项目(每张发票最多 250 个项目)创建一次性收费,方法是将它们添加到客户的"标签"中,然后为客户开具发票。例如,我们可以为客户开具五件衬衫和两个杯子的发票:

php
    $user->tabPrice('price_tshirt', 5);
    $user->tabPrice('price_mug', 2);
    $user->invoice();

或者,你可以使用 invoiceFor 方法对客户的默认支付方式进行"一次性"收费:

php
    $user->invoiceFor('一次性费用', 500);

尽管 invoiceFor 方法可供你使用,但建议你使用预定义价格的 invoicePricetabPrice 方法。通过这样做,你将可以在 Stripe 仪表板中访问更好的分析和数据,了解你的销售情况。

WARNING

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

创建支付意向

你可以通过在可计费模型实例上调用 pay 方法来创建新的 Stripe 支付意向。调用此方法将创建一个包装在 Laravel\Cashier\Payment 实例中的支付意向:

php
    use Illuminate\Http\Request;

    Route::post('/pay', function (Request $request) {
        $payment = $request->user()->pay(
            $request->get('amount')
        );

        return $payment->client_secret;
    });

创建支付意向后,你可以将客户端密钥返回给应用程序的前端,以便用户可以在浏览器中完成支付。要阅读更多关于使用 Stripe 支付意向构建整个支付流程的信息,请查阅 Stripe 文档

使用 pay 方法时,你的 Stripe 仪表板中默认启用的支付方法将可供客户使用。或者,如果你只想允许使用一些特定的支付方法,你可以使用 payWith 方法:

php
    use Illuminate\Http\Request;

    Route::post('/pay', function (Request $request) {
        $payment = $request->user()->payWith(
            $request->get('amount'), ['card', 'bancontact']
        );

        return $payment->client_secret;
    });

WARNING

paypayWith 方法接受以你应用程序使用的货币的最小面额表示的支付金额。例如,如果客户以美元支付,金额应该以美分指定。

退款

如果你需要退还 Stripe 收费,你可以使用 refund 方法。这个方法接受 Stripe 支付意向 ID 作为其第一个参数:

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

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

发票

检索发票

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

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

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

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

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

php
    $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 }}">下载</a></td>
            </tr>
        @endforeach
    </table>

即将到来的发票

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

php
    $invoice = $user->upcomingInvoice();

同样,如果客户有多个订阅,你也可以检索特定订阅的即将到来的发票:

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

预览订阅发票

使用 previewInvoice 方法,你可以在进行价格更改之前预览发票。这将允许你确定在进行给定的价格更改时,你的客户的发票会是什么样子:

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

你可以将价格数组传递给 previewInvoice 方法,以预览具有多个新价格的发票:

php
    $invoice = $user->subscription('default')->previewInvoice(['price_yearly', 'price_metered']);

生成发票 PDF

在生成发票 PDF 之前,你应该使用 Composer 安装 Dompdf 库,这是 Cashier 的默认发票渲染器:

php
composer require dompdf/dompdf

在路由或控制器中,你可以使用 downloadInvoice 方法生成给定发票的 PDF 下载。这个方法将自动生成下载发票所需的正确 HTTP 响应:

php
    use Illuminate\Http\Request;

    Route::get('/user/invoice/{invoice}', function (Request $request, string $invoiceId) {
        return $request->user()->downloadInvoice($invoiceId);
    });

默认情况下,发票上的所有数据都来自存储在 Stripe 中的客户和发票数据。文件名基于你的 app.name 配置值。但是,你可以通过提供一个数组作为 downloadInvoice 方法的第二个参数来自定义其中的一些数据。这个数组允许你自定义信息,如你的公司和产品详情:

php
    return $request->user()->downloadInvoice($invoiceId, [
        'vendor' => '你的公司',
        'product' => '你的产品',
        'street' => '主街 1 号',
        'location' => '2000 安特卫普,比利时',
        'phone' => '+32 499 00 00 00',
        'email' => 'info@example.com',
        'url' => 'https://example.com',
        'vendorVat' => 'BE123456789',
    ]);

downloadInvoice 方法还允许通过其第三个参数使用自定义文件名。这个文件名将自动附加 .pdf:

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

自定义发票渲染器

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

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

    class ApiInvoiceRenderer implements InvoiceRenderer
    {
        /**
         * 渲染给定的发票并返回原始 PDF 字节。
         */
        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 通过提供预构建的托管支付页面,消除了实现自定义页面接受支付的痛苦。

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

产品结账

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

php
    use Illuminate\Http\Request;

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

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

php
    use Illuminate\Http\Request;

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

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

php
    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 作为查询字符串参数添加。为此,在你的 success_url 查询字符串中添加文字字符串 {CHECKOUT_SESSION_ID}。Stripe 将用实际的结账会话 ID 替换这个占位符:

php
    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 不允许 用户可兑换的促销代码。幸运的是,有一种简单的方法可以为你的 Checkout 页面启用这些。为此,你可以调用 allowPromotionCodes 方法:

php
    use Illuminate\Http\Request;

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

单次收费结账

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

php
    use Illuminate\Http\Request;

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

WARNING

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

订阅结账

WARNING

使用 Stripe Checkout 进行订阅需要你在 Stripe 仪表板中启用 customer.subscription.created webhook。这个 webhook 将在你的数据库中创建订阅记录,并存储所有相关的订阅项目。

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

php
    use Illuminate\Http\Request;

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

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

php
    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'),
            ]);
    });

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

php
    use Illuminate\Http\Request;

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

WARNING

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

Stripe Checkout 和试用期

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

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

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

订阅和 Webhook

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

收集税号

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

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

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

WARNING

如果您已在应用程序的服务提供商中配置了自动征税,则此功能将自动启用,无需调用 collectTaxIds 方法。

访客结账

使用 Checkout::guest 方法,您可以为应用程序中没有 “account” 的 guest 启动 checkout 会话:

php
    use Illuminate\Http\Request;
    use Laravel\Cashier\Checkout;

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

与为现有用户创建签帐会话时类似,您可以利用 Laravel\Cashier\CheckoutBuilder 实例上可用的其他方法来自定义来宾签帐会话:

php
    use Illuminate\Http\Request;
    use Laravel\Cashier\Checkout;

    Route::get('/product-checkout', function (Request $request) {
        return Checkout::guest()
            ->withPromotionCode('promo-code')
            ->create('price_tshirt', [
                'success_url' => route('your-success-route'),
                'cancel_url' => route('your-cancel-route'),
            ]);
    });

访客结账完成后,Stripe 可以调度 checkout.session.completed webhook 事件,因此请确保配置您的 Stripe webhook 以实际将此事件发送到您的应用程序。在 Stripe 控制面板中启用 webhook 后,您可以使用 Cashier 处理 webhook。Webhook 有效负载中包含的对象将是一个 checkout 对象,您可以检查该对象以完成客户的订单。

处理付款失败

有时,订阅或单笔费用的付款可能会失败。发生这种情况时,Cashier 将引发异常 Laravel\Cashier\Exceptions\IncompletePayment ,通知您发生了这种情况。捕获此异常后,您有两个选项可以继续操作。

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

php
    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 要求的任何其他操作,例如“3DS 验证”确认。确认付款后,用户将被重定向到上面指定的 redirect 参数提供的 URL。重定向时,消息(字符串)和成功(整数)查询字符串变量将添加到 URL 中。支付页面目前支持以下付款方式类型:

  • Credit Cards 信用卡
  • Alipay 支付宝
  • Bancontact Bancontact 公司
  • BECS Direct Debit BECS 直接借记
  • EPS 每股收益
  • Giropay
  • iDEAL 理想
  • SEPA Direct Debit SEPA 直接借记

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

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

确定现有订阅是否具有未完成的付款,可以在计费模型或订阅实例上使用 hasIncompletePayment 方法完成:

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

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

您可以通过检查 exception 实例上的 payment 属性来派生未完成付款的特定状态:

php
    use Laravel\Cashier\Exceptions\IncompletePayment;

    try {
        $user->charge(1000, 'pm_card_threeDSecure2Required');
    } catch (IncompletePayment $exception) {
        // Get the payment intent status...
        $exception->payment->status;

        // Check specific conditions...
        if ($exception->payment->requiresPaymentMethod()) {
            // ...
        } elseif ($exception->payment->requiresConfirmation()) {
            // ...
        }
    }

确认付款

某些付款方式需要额外的数据来确认付款。例如,SEPA 付款方式在付款过程中需要额外的“授权”数据。您可以使用以下 withPaymentConfirmationOptions 方法向收银员提供这些数据:

php
    $subscription->withPaymentConfirmationOptions([
        'mandate_data' => '...',
    ])->swap('price_xxx');

您可以查阅 Stripe API 文档,查看确认付款时接受的所有选项。

客户认证

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

WARNING

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

需要额外确认的付款

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

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

未完成和逾期状态

当付款需要额外确认时,订阅将保持未完成past_due状态,如其 stripe_status 数据库列所示。付款确认完成后,收银员将自动激活客户的订阅,并且 Stripe 会通过 webhook 通知您的申请完成。

有关未完成past_due状态的更多信息,请参阅我们关于这些状态的其他文档

会话外付款通知

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

ini
CASHIER_PAYMENT_NOTIFICATION=Laravel\Cashier\Notifications\ConfirmPayment

要确保发送会话外付款确认通知,请确认为您的应用程序配置了 Stripe webhook,并且已在 Stripe 管理平台中启用该 invoice.payment_action_required webhook。此外,您的 Billable 模型还应使用 Laravel 的 Illuminate\Notifications\Notifiable 特征。

WARNING

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

开发工具包

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]);

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

php
    use Laravel\Cashier\Cashier;

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

测试

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

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

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

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

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

NOTE

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