Skip to content

Laravel Cashier (Stripe)

介绍

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

升级 Cashier

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

exclamation

为了防止破坏性更改,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 处理

exclamation

Stripe 建议用于存储 Stripe 标识符的任何列都应区分大小写。因此,当使用 MySQL 时,你应确保 stripe_id 列的排序规则设置为 utf8_bin。有关更多信息,请参阅 Stripe 文档

配置

可计费模型

在使用 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);
}
exclamation

如果你使用的是 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
exclamation

你应确保在应用程序的 .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
exclamation

为了使用 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 提供的 客户数据同步税号 方法来完成此操作。

exclamation

对于 单次收费单次收费结账,不计算税款。

日志记录

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

快速入门

销售产品

lightbulb

在使用 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')->name('checkout-success');
Route::view('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');

如上例所示,当用户开始结账过程时,我们将所有购物车/订单的相关 Stripe 价格标识符提供给 checkout 方法。当然,你的应用程序负责在客户添加它们时将这些项目与“购物车”或订单关联。我们还通过 metadata 数组将订单的 ID 提供给 Stripe Checkout 会话。最后,我们在 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 会话对象包含的数据 的更多信息。

销售订阅

lightbulb

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

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

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

首先,让我们了解客户如何订阅我们的服务。当然,你可以想象客户可能会在我们应用程序的定价页面上点击“订阅”按钮。此按钮或链接应将用户引导到一个 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');
lightbulb

只要你已配置 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 中创建一个新客户(如果尚不存在):

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, 'Premium customer top-up.');

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

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

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

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

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

createTaxId 方法将立即将 VAT ID 添加到客户的帐户中。VAT ID 的验证也由 Stripe 完成;然而,这是一个异步过程。你可以通过订阅 customer.tax_id.updated webhook 事件并检查 VAT 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:

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 的“设置意图”API 来安全地收集客户的支付方式详细信息。“设置意图”表示 Stripe 收取客户支付方式的意图。Cashier 的 Billable trait 包括 createSetupIntent 方法,以便轻松创建新的设置意图。你应从将呈现收集客户支付方式详细信息的表单的路由或控制器调用此方法:

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

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

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

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

lightbulb

如果你想了解有关设置意图和收集客户支付详细信息的更多信息,请查看 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 元素 附加到表单并安全地收集客户的支付详细信息:

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

检索支付方式

可计费模型实例上的 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();
exclamation

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

添加支付方式

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

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

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

删除支付方式

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

php
$paymentMethod->delete();

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

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

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

php
$user->deletePaymentMethods();

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

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

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

订阅

订阅提供了一种为客户设置定期付款的方法。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 和其他相关计费信息。

exclamation

直接将支付方式标识符传递给 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' => 'Some extra information.'],
]);

优惠券

如果你希望在创建订阅时应用优惠券,可以使用 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 方法返回 true,如果客户有一个活动的订阅,即使订阅当前处于试用期。subscribed 方法接受订阅的类型作为其第一个参数:

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 订阅是否积极订阅了应用程序的“premium”产品。给定的 Stripe 产品标识符应对应于 Stripe 仪表板中产品的标识符之一:

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

通过将数组传递给 subscribedToProduct 方法,你可以确定用户的 default 订阅是否积极订阅了应用程序的“basic”或“premium”产品:

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

如果用户有两个具有相同类型的订阅,subscription 方法将始终返回最新的订阅。例如,用户可能有两个类型为 default 的订阅记录;然而,其中一个订阅可能是旧的、过期的订阅,而另一个是当前的、活动的订阅。最新的订阅将始终返回,而旧的订阅将保留在数据库中以供历史审查。

已取消订阅状态

要确定用户是否曾经是活跃订阅者但已取消其订阅,可以使用 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();
}
exclamation

当订阅处于 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 文档

exclamation

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

你可能无法删除订阅的最后一个价格。相反,你应该简单地取消订阅。

交换价格

你还可以更改附加到具有多个产品的订阅的价格。例如,假设客户有一个 price_basic 订阅,并附加一个 price_chat 附加产品,你希望将客户从 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');
exclamation

当订阅有多个价格时,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 方法返回一个包含使用记录关联数组的集合实例。你可以遍历此数组以显示客户的总使用量:

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

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

订阅税

exclamation

你可以使用 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'],
    ];
}
exclamation

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

这些方法也可用于任何 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 中为价格配置的任何默认试用期。

exclamation

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

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

php
use Carbon\Carbon;

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

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

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

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

你可以使用 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),
]);
exclamation

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

Cashier 将此类型的试用称为“通用试用”,因为它不附加到任何现有订阅。可计费模型实例上的 onTrial 方法将在当前日期未超过 trial_ends_at 的值时返回 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

lightbulb

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

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

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

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

  • 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 环境变量定义的 URL 和 Cashier 附带的 cashier.webhook 路由。如果你希望使用其他 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
exclamation

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

Webhooks 和 CSRF 保护

由于 Stripe webhooks 需要绕过 Laravel 的 CSRF 保护,请确保在应用程序的 App\Http\Middleware\VerifyCsrfToken 中间件中将 URI 列为例外,或将路由列在 web 中间件组之外:

php
protected $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') {
            // 处理传入的事件...
        }
    }
}

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

php
<?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 签名

为了保护你的 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) {
    // ...
}
exclamation

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('One Time Fee', 500);

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

exclamation

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;
});
exclamation

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

显示发票信息

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

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

即将到来的发票

要检索客户的即将到来的发票,可以使用 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' => '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',
]);

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 添加为查询字符串参数。为此,将字面字符串 {CHECKOUT_SESSION_ID} 添加到 success_url 查询字符串中。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);
});
exclamation

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

订阅结账

exclamation

使用 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();
});
exclamation

不幸的是,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 支持的最短试用时间。

订阅和 Webhooks

请记住,Stripe 和 Cashier 通过 webhooks 更新订阅状态,因此当客户在输入支付信息后返回应用程序时,订阅可能尚未激活。为处理这种情况,你可能希望显示一条消息,告知用户其支付或订阅正在处理中。

收集税号

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

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

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

exclamation

如果你已经在应用程序的服务提供者中配置了自动税款收集,则此功能将自动启用,无需调用 collectTaxIds 方法。

访客结账

使用 Checkout::guest 方法,你可以为没有“帐户”的应用程序访客启动结账会话:

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

  • 信用卡
  • Alipay
  • Bancontact
  • BECS 直接借记
  • EPS
  • Giropay
  • iDEAL
  • SEPA 直接借记

或者,你可以允许 Stripe 为你处理支付确认。在这种情况下,除了重定向到支付确认页面之外,你还可以在 Stripe 仪表板中设置 Stripe 的自动计费电子邮件。然而,如果捕获到 IncompletePayment 异常,你仍然应通知用户他们将收到一封电子邮件,其中包含进一步的支付确认说明。

支付异常可能会在以下方法中抛出:chargeinvoiceForinvoice 在使用 Billable trait 的模型上。当与订阅交互时,SubscriptionBuilder 上的 create 方法,以及 SubscriptionSubscriptionItem 模型上的 incrementAndInvoiceswapAndInvoice 方法可能会抛出不完整支付异常。

确定现有订阅是否有不完整支付可以通过在可计费模型或订阅实例上使用 hasIncompletePayment 方法来实现:

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

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

你可以通过检查异常实例上的 payment 属性来推导不完整支付的具体状态:

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

确认支付

某些支付方式需要额外的数据以确认支付。例如,SEPA 支付方式在支付过程中需要额外的“授权”数据。你可以使用 withPaymentConfirmationOptions 方法将此数据提供给 Cashier:

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

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

强客户认证

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

exclamation

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

需要额外确认的支付

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

Stripe 或 Cashier 提供的支付确认屏幕可以根据特定银行或卡发行商的支付流程进行定制,并可能包括额外的卡确认、临时小额收费、单独的设备认证或其他形式的验证。

不完整和逾期状态

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

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

离线支付通知

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

ini
CASHIER_PAYMENT_NOTIFICATION=Laravel\Cashier\Notifications\ConfirmPayment

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

exclamation

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

Stripe SDK

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

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

$stripeSubscription->application_fee_percent = 5;

$stripeSubscription->save();

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

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

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

php
use Laravel\Cashier\Cashier;

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

测试

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

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

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

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

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

lightbulb

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