Laravel Cashier (Stripe)
介绍
Laravel Cashier Stripe 提供了一个表达流畅的接口,用于 Stripe 的订阅计费服务。它处理几乎所有您不想编写的样板订阅计费代码。除了基本的订阅管理,Cashier 还可以处理优惠券、交换订阅、订阅“数量”、取消宽限期,甚至生成发票 PDF。
升级 Cashier
在升级到新版本的 Cashier 时,您需要仔细查看 升级指南。
为了防止破坏性更改,Cashier 使用固定的 Stripe API 版本。Cashier 15 使用 Stripe API 版本 2023-10-16
。Stripe API 版本将在小版本更新中进行更新,以利用新的 Stripe 功能和改进。
安装
首先,使用 Composer 包管理器安装 Stripe 的 Cashier 包:
composer require laravel/cashier
安装包后,使用 vendor:publish
Artisan 命令发布 Cashier 的迁移:
php artisan vendor:publish --tag="cashier-migrations"
然后,迁移您的数据库:
php artisan migrate
Cashier 的迁移将向您的 users
表添加几个列。它们还将创建一个新的 subscriptions
表,以保存您客户的所有订阅,以及一个 subscription_items
表,用于具有多个价格的订阅。
如果您愿意,您还可以使用 vendor:publish
Artisan 命令发布 Cashier 的配置文件:
php artisan vendor:publish --tag="cashier-config"
最后,为了确保 Cashier 正确处理所有 Stripe 事件,请记得 配置 Cashier 的 webhook 处理。
Stripe 建议任何用于存储 Stripe 标识符的列应区分大小写。因此,您应确保在使用 MySQL 时,stripe_id
列的排序规则设置为 utf8_bin
。有关更多信息,请参阅 Stripe 文档。
配置
可计费模型
在使用 Cashier 之前,请将 Billable
特性添加到您的可计费模型定义中。通常,这将是 App\Models\User
模型。此特性提供了多种方法,使您能够执行常见的计费任务,例如创建订阅、应用优惠券和更新支付方式信息:
use Laravel\Cashier\Billable;
class User extends Authenticatable
{
use Billable;
}
Cashier 假定您的可计费模型将是随 Laravel 一起提供的 App\Models\User
类。如果您希望更改此设置,可以通过 useCustomerModel
方法指定不同的模型。此方法通常应在 AppServiceProvider
类的 boot
方法中调用:
use App\Models\Cashier\User;
use Laravel\Cashier\Cashier;
/**
* 启动任何应用程序服务。
*/
public function boot(): void
{
Cashier::useCustomerModel(User::class);
}
如果您使用的是除 Laravel 提供的 App\Models\User
模型以外的模型,则需要发布并更改提供的 Cashier 迁移,以匹配您替代模型的表名。
API 密钥
接下来,您应在应用程序的 .env
文件中配置您的 Stripe API 密钥。您可以从 Stripe 控制面板中检索您的 Stripe API 密钥:
STRIPE_KEY=your-stripe-key
STRIPE_SECRET=your-stripe-secret
STRIPE_WEBHOOK_SECRET=your-stripe-webhook-secret
您应确保在应用程序的 .env
文件中定义 STRIPE_WEBHOOK_SECRET
环境变量,因为此变量用于确保传入的 webhook 实际上来自 Stripe。
货币配置
默认的 Cashier 货币是美元 (USD)。您可以通过在应用程序的 .env
文件中设置 CASHIER_CURRENCY
环境变量来更改默认货币:
CASHIER_CURRENCY=eur
除了配置 Cashier 的货币外,您还可以指定用于格式化发票上显示的货币值的区域设置。内部,Cashier 使用 PHP 的 NumberFormatter
类 来设置货币区域:
CASHIER_CURRENCY_LOCALE=nl_BE
为了使用 en
以外的区域设置,请确保在您的服务器上安装并配置 ext-intl
PHP 扩展。
税务配置
得益于 Stripe Tax,可以自动计算 Stripe 生成的所有发票的税费。您可以通过在应用程序的 App\Providers\AppServiceProvider
类的 boot
方法中调用 calculateTaxes
方法来启用自动税费计算:
use Laravel\Cashier\Cashier;
/**
* 启动任何应用程序服务。
*/
public function boot(): void
{
Cashier::calculateTaxes();
}
启用税费计算后,生成的任何新订阅和一次性发票将自动计算税费。
为了使此功能正常工作,您的客户的账单详细信息(例如客户的姓名、地址和税务 ID)需要与 Stripe 同步。您可以使用 Cashier 提供的 客户数据同步 和 税务 ID 方法来实现这一点。
日志记录
Cashier 允许您指定在记录致命 Stripe 错误时使用的日志通道。您可以通过在应用程序的 .env
文件中定义 CASHIER_LOGGER
环境变量来指定日志通道:
CASHIER_LOGGER=stack
由 API 调用 Stripe 生成的异常将通过应用程序的默认日志通道记录。
使用自定义模型
您可以通过定义自己的模型并扩展相应的 Cashier 模型来自由扩展 Cashier 内部使用的模型:
use Laravel\Cashier\Subscription as CashierSubscription;
class Subscription extends CashierSubscription
{
// ...
}
定义模型后,您可以通过 Laravel\Cashier\Cashier
类指示 Cashier 使用您的自定义模型。通常,您应在应用程序的 App\Providers\AppServiceProvider
类的 boot
方法中告知 Cashier 有关您的自定义模型:
use App\Models\Cashier\Subscription;
use App\Models\Cashier\SubscriptionItem;
/**
* 启动任何应用程序服务。
*/
public function boot(): void
{
Cashier::useSubscriptionModel(Subscription::class);
Cashier::useSubscriptionItemModel(SubscriptionItem::class);
}
快速入门
销售产品
在使用 Stripe Checkout 之前,您应在 Stripe 控制面板中定义具有固定价格的产品。此外,您应 配置 Cashier 的 webhook 处理。
通过您的应用程序提供产品和订阅计费可能会让人感到害怕。然而,得益于 Cashier 和 Stripe Checkout,您可以轻松构建现代、强大的支付集成。
要对非重复的单次收费产品向客户收费,我们将利用 Cashier 将客户引导到 Stripe Checkout,在那里他们将提供支付详细信息并确认购买。支付完成后,客户将被重定向到您选择的应用程序中的成功 URL:
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 提供元数据
在销售产品时,通常需要通过您自己定义的 Cart
和 Order
模型跟踪已完成的订单和购买的产品。当将客户重定向到 Stripe Checkout 完成购买时,您可能需要提供现有订单标识符,以便在客户重定向回应用程序时将已完成的购买与相应的订单关联。
为此,您可以向 checkout
方法提供一个 metadata
数组。假设在用户开始结账流程时,在我们的应用程序中创建了一个待处理的 Order
。请记住,示例中的 Cart
和 Order
模型是说明性的,并不是 Cashier 提供的。您可以根据自己应用程序的需求自由实现这些概念:
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_SESSION_ID
模板变量。当 Stripe 将客户重定向回应用程序时,此模板变量将自动填充为结账会话 ID。
接下来,让我们构建结账成功路由。这是用户在通过 Stripe Checkout 完成购买后将被重定向到的路由。在此路由中,我们可以检索 Stripe Checkout 会话 ID 和相关的 Stripe Checkout 实例,以便访问我们提供的元数据并相应地更新客户的订单:
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 的文档。
销售订阅
在使用 Stripe Checkout 之前,您应在 Stripe 控制面板中定义具有固定价格的产品。此外,您应 配置 Cashier 的 webhook 处理。
通过您的应用程序提供产品和订阅计费可能会让人感到害怕。然而,得益于 Cashier 和 Stripe Checkout,您可以轻松构建现代、强大的支付集成。
要了解如何使用 Cashier 和 Stripe Checkout 销售订阅,让我们考虑一个简单的场景,即具有基本月度 (price_basic_monthly
) 和年度 (price_basic_yearly
) 计划的订阅服务。这两个价格可以在我们的 Stripe 控制面板中归类为“基本”产品 (pro_basic
)。此外,我们的订阅服务可能还提供专家计划,标记为 pro_expert
。
首先,让我们发现客户如何订阅我们的服务。当然,您可以想象客户可能会在我们应用程序的定价页面上单击“订阅”按钮。此按钮或链接应将用户引导到一个 Laravel 路由,该路由为他们选择的计划创建 Stripe Checkout 会话:
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 会话,允许他们订阅我们的基本计划。在成功结账或取消后,客户将被重定向回我们提供给 checkout
方法的 URL。要知道他们的订阅何时实际开始(因为某些支付方式需要几秒钟来处理),我们还需要 配置 Cashier 的 webhook 处理。
现在客户可以开始订阅,我们需要限制应用程序的某些部分,以便只有订阅用户可以访问它们。当然,我们可以随时通过 Cashier 的 Billable
特性提供的 subscribed
方法来确定用户的当前订阅状态:
@if ($user->subscribed())
<p>您已订阅。</p>
@endif
我们甚至可以轻松确定用户是否订阅了特定产品或价格:
@if ($user->subscribedToProduct('pro_basic'))
<p>您已订阅我们的基本产品。</p>
@endif
@if ($user->subscribedToPrice('price_basic_monthly'))
<p>您已订阅我们的月度基本计划。</p>
@endif
构建已订阅中间件
为了方便起见,您可能希望创建一个 中间件,以确定传入请求是否来自已订阅的用户。定义此中间件后,您可以轻松将其分配给路由,以防止未订阅的用户访问该路由:
<?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);
}
}
定义中间件后,您可以将其分配给路由:
use App\Http\Middleware\Subscribed;
Route::get('/dashboard', function () {
// ...
})->middleware([Subscribed::class]);
允许客户管理他们的计费计划
当然,客户可能希望将其订阅计划更改为其他产品或“层”。最简单的方法是将客户引导到 Stripe 的 客户账单门户,该门户提供了一个托管用户界面,允许客户下载发票、更新其支付方式和更改订阅计划。
首先,在您的应用程序中定义一个链接或按钮,将用户引导到一个 Laravel 路由,我们将利用该路由启动账单门户会话:
<a href="{{ route('billing') }}">
账单
</a>
接下来,让我们定义启动 Stripe 客户账单门户会话并将用户重定向到门户的路由。redirectToBillingPortal
方法接受用户在退出门户时应返回的 URL:
use Illuminate\Http\Request;
Route::get('/billing', function (Request $request) {
return $request->user()->redirectToBillingPortal(route('dashboard'));
})->middleware(['auth'])->name('billing');
只要您已配置 Cashier 的 webhook 处理,Cashier 将通过检查来自 Stripe 的传入 webhook 自动保持应用程序的 Cashier 相关数据库表的同步。因此,例如,当用户通过 Stripe 的客户账单门户取消其订阅时,Cashier 将接收相应的 webhook,并在应用程序的数据库中将订阅标记为“已取消”。
客户
检索客户
您可以使用 Cashier::findBillable
方法通过其 Stripe ID 检索客户。此方法将返回可计费模型的实例:
use Laravel\Cashier\Cashier;
$user = Cashier::findBillable($stripeId);
创建客户
有时,您可能希望在不开始订阅的情况下创建 Stripe 客户。您可以使用 createAsStripeCustomer
方法来实现:
$stripeCustomer = $user->createAsStripeCustomer();
客户在 Stripe 中创建后,您可以在稍后的日期开始订阅。您可以提供一个可选的 $options
数组,以传递任何其他 Stripe API 支持的客户创建参数:
$stripeCustomer = $user->createAsStripeCustomer($options);
如果您想返回可计费模型的 Stripe 客户对象,可以使用 asStripeCustomer
方法:
$stripeCustomer = $user->asStripeCustomer();
如果您想检索给定可计费模型的 Stripe 客户对象,但不确定该可计费模型是否已经是 Stripe 中的客户,则可以使用 createOrGetStripeCustomer
方法。如果不存在,则此方法将在 Stripe 中创建一个新客户:
$stripeCustomer = $user->createOrGetStripeCustomer();
更新客户
有时,您可能希望直接使用附加信息更新 Stripe 客户。您可以使用 updateStripeCustomer
方法来实现。此方法接受一个数组,其中包含 Stripe API 支持的客户更新选项:
$stripeCustomer = $user->updateStripeCustomer($options);
余额
Stripe 允许您为客户的“余额”进行信用或借记。稍后,此余额将在新发票上进行信用或借记。要检查客户的总余额,您可以使用可计费模型上可用的 balance
方法。balance
方法将返回客户货币的余额的格式化字符串表示:
$balance = $user->balance();
要为客户的余额提供信用,您可以向 creditBalance
方法提供一个值。如果愿意,您还可以提供描述:
$user->creditBalance(500, '高级客户充值。');
向 debitBalance
方法提供一个值将借记客户的余额:
$user->debitBalance(300, '不当使用罚款。');
applyBalance
方法将为客户创建新的客户余额交易。您可以使用 balanceTransactions
方法检索这些交易记录,这可能对提供客户审核信用和借记记录的日志很有用:
// 检索所有交易...
$transactions = $user->balanceTransactions();
foreach ($transactions as $transaction) {
// 交易金额...
$amount = $transaction->amount(); // $2.31
// 检索相关发票(如果可用)...
$invoice = $transaction->invoice();
}
税务 ID
Cashier 提供了一种简单的方法来管理客户的税务 ID。例如,taxIds
方法可用于检索分配给客户的所有 税务 ID,以集合的形式:
$taxIds = $user->taxIds();
您还可以通过其标识符检索客户的特定税务 ID:
$taxId = $user->findTaxId('txi_belgium');
您可以通过向 createTaxId
方法提供有效的 类型 和值来创建新的税务 ID:
$taxId = $user->createTaxId('eu_vat', 'BE0123456789');
createTaxId
方法将立即将 VAT ID 添加到客户的帐户中。 VAT ID 的验证也由 Stripe 进行;但是,这是一个异步过程。您可以通过订阅 customer.tax_id.updated
webhook 事件并检查 VAT ID 的 verification
参数 来接收验证更新的通知。有关处理 webhook 的更多信息,请参阅 定义 webhook 处理程序的文档。
您可以使用 deleteTaxId
方法删除税务 ID:
$user->deleteTaxId('txi_belgium');
与 Stripe 同步客户数据
通常,当您的应用程序用户更新其姓名、电子邮件地址或其他也存储在 Stripe 中的信息时,您应通知 Stripe 更新。通过这样做,Stripe 的信息副本将与您应用程序中的信息保持同步。
要自动化此过程,您可以在可计费模型上定义一个事件监听器,该监听器对模型的 updated
事件做出反应。然后,在事件监听器中,您可以调用模型上的 syncStripeCustomerDetails
方法:
use App\Models\User;
use function Illuminate\Events\queueable;
/**
* 模型的“已启动”方法。
*/
protected static function booted(): void
{
static::updated(queueable(function (User $customer) {
if ($customer->hasStripeId()) {
$customer->syncStripeCustomerDetails();
}
}));
}
现在,每当更新客户模型时,其信息将与 Stripe 同步。为了方便起见,Cashier 会在客户首次创建时自动与 Stripe 同步客户的信息。
您可以通过重写 Cashier 提供的各种方法来自定义用于与 Stripe 同步客户信息的列。例如,您可以重写 stripeName
方法,以自定义在 Cashier 将客户信息同步到 Stripe 时应视为客户“姓名”的属性:
/**
* 获取应同步到 Stripe 的客户姓名。
*/
public function stripeName(): string|null
{
return $this->company_name;
}
同样,您可以重写 stripeEmail
、stripePhone
、stripeAddress
和 stripePreferredLocales
方法。这些方法将在更新 Stripe 客户对象时将信息同步到其对应的客户参数。如果您希望完全控制客户信息同步过程,您可以重写 syncStripeCustomerDetails
方法。
账单门户
Stripe 提供了 设置账单门户的简单方法,以便您的客户可以管理其订阅、支付方式和查看其账单历史。您可以通过从控制器或路由调用可计费模型上的 redirectToBillingPortal
方法将用户重定向到账单门户:
use Illuminate\Http\Request;
Route::get('/billing-portal', function (Request $request) {
return $request->user()->redirectToBillingPortal();
});
默认情况下,当用户完成管理其订阅后,他们将能够通过 Stripe 账单门户中的链接返回到您应用程序的 home
路由。您可以通过将 URL 作为参数传递给 redirectToBillingPortal
方法来提供用户应返回的自定义 URL:
use Illuminate\Http\Request;
Route::get('/billing-portal', function (Request $request) {
return $request->user()->redirectToBillingPortal(route('billing'));
});
如果您希望在不生成 HTTP 重定向响应的情况下生成账单门户的 URL,可以调用 billingPortalUrl
方法:
$url = $request->user()->billingPortalUrl(route('billing'));
支付方式
存储支付方式
为了使用 Stripe 创建订阅或执行“单次”收费,您需要存储支付方式并从 Stripe 检索其标识符。实现此目的的方法取决于您是否计划将支付方式用于订阅或单次收费,因此我们将分别检查这两种情况。
用于订阅的支付方式
在为将来由订阅使用的客户存储信用卡信息时,必须使用 Stripe 的“设置意图”API 安全地收集客户的支付方式详细信息。“设置意图”向 Stripe 表示打算向客户的支付方式收费。Cashier 的 Billable
特性包括 createSetupIntent
方法,以轻松创建新的设置意图。您应从将呈现收集客户支付方式详细信息的表单的路由或控制器中调用此方法:
return view('update-payment-method', [
'intent' => $user->createSetupIntent()
]);
创建设置意图并将其传递给视图后,您应将其密钥附加到将收集支付方式的元素。例如,考虑这个“更新支付方式”表单:
<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 附加到表单并安全地收集客户的支付详细信息:
<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 检索安全的“支付方式标识符”:
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 应用程序,在那里可以将支付方式附加到客户。支付方式可以 作为新支付方式添加 或 用于更新默认支付方式。您还可以立即使用支付方式标识符来 创建新订阅。
如果您想了解有关设置意图和收集客户支付详细信息的更多信息,请 查看 Stripe 提供的概述。
单次收费的支付方式
当然,在对客户的支付方式进行单次收费时,我们只需使用支付方式标识符一次。由于 Stripe 的限制,您不能使用客户的存储默认支付方式进行单次收费。您必须允许客户使用 Stripe.js 库输入其支付方式详细信息。例如,考虑以下表单:
<input id="card-holder-name" type="text">
<!-- Stripe Elements 占位符 -->
<div id="card-element"></div>
<button id="card-button">
处理支付
</button>
定义此类表单后,可以使用 Stripe.js 库将 Stripe Element 附加到表单并安全地收集客户的支付详细信息:
<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 检索安全的“支付方式标识符”:
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
实例:
$paymentMethods = $user->paymentMethods();
默认情况下,此方法将返回所有类型的支付方式。要检索特定类型的支付方式,您可以将 type
作为参数传递给该方法:
$paymentMethods = $user->paymentMethods('sepa_debit');
要检索客户的默认支付方式,可以使用 defaultPaymentMethod
方法:
$paymentMethod = $user->defaultPaymentMethod();
您可以使用 findPaymentMethod
方法检索附加到可计费模型的特定支付方式:
$paymentMethod = $user->findPaymentMethod($paymentMethodId);
支付方式存在性
要确定可计费模型是否附加了默认支付方式,可以调用 hasDefaultPaymentMethod
方法:
if ($user->hasDefaultPaymentMethod()) {
// ...
}
您可以使用 hasPaymentMethod
方法确定可计费模型是否附加了至少一个支付方式:
if ($user->hasPaymentMethod()) {
// ...
}
此方法将确定可计费模型是否有任何支付方式。要确定特定类型的支付方式是否存在于模型中,您可以将 type
作为参数传递给该方法:
if ($user->hasPaymentMethod('sepa_debit')) {
// ...
}
更新默认支付方式
updateDefaultPaymentMethod
方法可用于更新客户的默认支付方式信息。此方法接受一个 Stripe 支付方式标识符,并将新支付方式分配为默认计费支付方式:
$user->updateDefaultPaymentMethod($paymentMethod);
要将您的默认支付方式信息与客户的默认支付方式信息同步到 Stripe,您可以使用 updateDefaultPaymentMethodFromStripe
方法:
$user->updateDefaultPaymentMethodFromStripe();
客户的默认支付方式只能用于开票和创建新订阅。由于 Stripe 施加的限制,它不能用于单次收费。
添加支付方式
要添加新支付方式,您可以在可计费模型上调用 addPaymentMethod
方法,传递支付方式标识符:
$user->addPaymentMethod($paymentMethod);
要了解如何检索支付方式标识符,请查看 支付方式存储文档。
删除支付方式
要删除支付方式,您可以在要删除的 Laravel\Cashier\PaymentMethod
实例上调用 delete
方法:
$paymentMethod->delete();
deletePaymentMethod
方法将删除可计费模型的特定支付方式:
$user->deletePaymentMethod('pm_visa');
deletePaymentMethods
方法将删除可计费模型的所有支付方式信息:
$user->deletePaymentMethods();
默认情况下,此方法将删除所有类型的支付方式。要删除特定类型的支付方式,您可以将 type
作为参数传递给该方法:
$user->deletePaymentMethods('sepa_debit');
如果用户有一个活动的订阅,您的应用程序不应允许他们删除其默认支付方式。
订阅
订阅提供了一种为客户设置定期付款的方法。由 Cashier 管理的 Stripe 订阅支持多个订阅价格、订阅数量、试用等。
创建订阅
要创建订阅,首先检索可计费模型的实例,通常将是 App\Models\User
的实例。检索模型实例后,您可以使用 newSubscription
方法创建模型的订阅:
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$request->user()->newSubscription(
'default', 'price_monthly'
)->create($request->paymentMethodId);
// ...
});
传递给 newSubscription
方法的第一个参数应为订阅的内部类型。如果您的应用程序仅提供单个订阅,您可以将其称为 default
或 primary
。此订阅类型仅供内部应用程序使用,不应向用户显示。此外,它不应包含空格,并且在创建订阅后永远不应更改。第二个参数是用户订阅的特定价格。此值应对应于 Stripe 中价格的标识符。
create
方法接受 Stripe 支付方式标识符 或 Stripe PaymentMethod
对象,将开始订阅并更新数据库中可计费模型的 Stripe 客户 ID 和其他相关计费信息。
直接将支付方式标识符传递给 create
订阅方法还将自动将其添加到用户的存储支付方式中。
通过发票电子邮件收集定期付款
您可以指示 Stripe 每次到期时向客户发送发票,而不是自动收取客户的定期付款。然后,客户可以在收到发票后手动支付。客户在收集定期付款的发票时不需要提前提供支付方式:
$user->newSubscription('default', 'price_monthly')->createAndSendInvoice();
客户在支付发票之前有多长时间可以支付其发票是由 days_until_due
选项决定的。默认情况下,这是 30 天;但是,如果您愿意,可以为此选项提供特定值:
$user->newSubscription('default', 'price_monthly')->createAndSendInvoice([], [
'days_until_due' => 30
]);
数量
如果您希望在创建订阅时为价格设置特定的 数量,则应在创建订阅之前调用订阅构建器上的 quantity
方法:
$user->newSubscription('default', 'price_monthly')
->quantity(5)
->create($paymentMethod);
其他详细信息
如果您希望指定 Stripe 支持的其他 客户 或 订阅 选项,则可以将它们作为第二和第三个参数传递给 create
方法:
$user->newSubscription('default', 'price_monthly')->create($paymentMethod, [
'email' => $email,
], [
'metadata' => ['note' => '一些额外信息。'],
]);
优惠券
如果您希望在创建订阅时应用优惠券,则可以使用 withCoupon
方法:
$user->newSubscription('default', 'price_monthly')
->withCoupon('code')
->create($paymentMethod);
或者,如果您希望应用 Stripe 促销代码,则可以使用 withPromotionCode
方法:
$user->newSubscription('default', 'price_monthly')
->withPromotionCode('promo_code_id')
->create($paymentMethod);
给定的促销代码 ID 应为分配给促销代码的 Stripe API ID,而不是面向客户的促销代码。如果您需要根据给定的面向客户的促销代码查找促销代码 ID,则可以使用 findPromotionCode
方法:
// 通过其面向客户的代码查找促销代码 ID...
$promotionCode = $user->findPromotionCode('SUMMERSALE');
// 通过其面向客户的代码查找活动促销代码 ID...
$promotionCode = $user->findActivePromotionCode('SUMMERSALE');
在上面的示例中,返回的 $promotionCode
对象是 Laravel\Cashier\PromotionCode
的实例。此类装饰了底层的 Stripe\PromotionCode
对象。您可以通过调用 coupon
方法检索与促销代码相关的优惠券:
$coupon = $user->findPromotionCode('SUMMERSALE')->coupon();
优惠券实例允许您确定折扣金额以及优惠券是固定折扣还是百分比折扣:
if ($coupon->isPercentage()) {
return $coupon->percentOff().'%'; // 21.5%
} else {
return $coupon->amountOff(); // $5.99
}
您还可以检索当前应用于客户或订阅的折扣:
$discount = $billable->discount();
$discount = $subscription->discount();
返回的 Laravel\Cashier\Discount
实例装饰了底层的 Stripe\Discount
对象实例。您可以通过调用 coupon
方法检索与此折扣相关的优惠券:
$coupon = $subscription->discount()->coupon();
如果您希望将新优惠券或促销代码应用于客户或订阅,则可以通过 applyCoupon
或 applyPromotionCode
方法实现:
$billable->applyCoupon('coupon_id');
$billable->applyPromotionCode('promotion_code_id');
$subscription->applyCoupon('coupon_id');
$subscription->applyPromotionCode('promotion_code_id');
请记住,您应使用分配给促销代码的 Stripe API ID,而不是面向客户的促销代码。一次只能将一个优惠券或促销代码应用于客户或订阅。
有关此主题的更多信息,请参阅 Stripe 有关 优惠券 和 促销代码 的文档。
添加订阅
如果您希望向已经拥有默认支付方式的客户添加订阅,可以在订阅构建器上调用 add
方法:
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
方法接受订阅的类型作为第一个参数:
if ($user->subscribed('default')) {
// ...
}
subscribed
方法也非常适合用作 路由中间件,允许您根据用户的订阅状态过滤对路由和控制器的访问:
<?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
方法。此方法对于确定您是否应向用户显示警告,告知他们仍在试用期非常有用:
if ($user->subscription('default')->onTrial()) {
// ...
}
subscribedToProduct
方法可用于确定用户是否订阅了给定产品,基于给定 Stripe 产品的标识符。在 Stripe 中,产品是价格的集合。在此示例中,我们将确定用户的 default
订阅是否积极订阅应用程序的“高级”产品。给定的 Stripe 产品标识符应对应于您在 Stripe 控制面板中的产品标识符:
if ($user->subscribedToProduct('prod_premium', 'default')) {
// ...
}
通过将数组传递给 subscribedToProduct
方法,您可以确定用户的 default
订阅是否积极订阅应用程序的“基本”或“高级”产品:
if ($user->subscribedToProduct(['prod_basic', 'prod_premium'], 'default')) {
// ...
}
subscribedToPrice
方法可用于确定客户的订阅是否对应于给定价格 ID:
if ($user->subscribedToPrice('price_basic_monthly', 'default')) {
// ...
}
recurring
方法可用于确定用户是否当前已订阅且不再处于试用期:
if ($user->subscription('default')->recurring()) {
// ...
}
如果用户有两个相同类型的订阅,则 subscription
方法将始终返回最近的订阅。例如,用户可能有两个类型为 default
的订阅;然而,其中一个订阅可能是旧的、过期的订阅,而另一个是当前的、活动的订阅。始终返回最近的订阅,同时将较旧的订阅保留在数据库中以供历史审查。
取消的订阅状态
要确定用户曾经是活跃订阅者但已取消其订阅,您可以使用 canceled
方法:
if ($user->subscription('default')->canceled()) {
// ...
}
您还可以确定用户是否已取消其订阅,但仍在“宽限期”内,直到订阅完全过期。例如,如果用户在 3 月 5 日取消了原定于 3 月 10 日到期的订阅,则用户在 3 月 10 日之前处于“宽限期”。请注意,在此期间,subscribed
方法仍返回 true
:
if ($user->subscription('default')->onGracePeriod()) {
// ...
}
要确定用户是否已取消其订阅且不再处于“宽限期”,您可以使用 ended
方法:
if ($user->subscription('default')->ended()) {
// ...
}
不完整和逾期状态
如果订阅在创建后需要二次支付操作,则该订阅将标记为 incomplete
。订阅状态存储在 Cashier 的 subscriptions
数据库表的 stripe_status
列中。
同样,如果在交换价格时需要二次支付操作,则该订阅将标记为 past_due
。当您的订阅处于这两种状态中的任何一种时,它将不会处于活动状态,直到客户确认其支付。确定订阅是否存在不完整支付可以通过可计费模型或订阅实例上的 hasIncompletePayment
方法来完成:
if ($user->hasIncompletePayment('default')) {
// ...
}
if ($user->subscription('default')->hasIncompletePayment()) {
// ...
}
当订阅存在不完整支付时,您应将用户引导到 Cashier 的支付确认页面,传递 latestPayment
标识符。您可以使用订阅实例上可用的 latestPayment
方法检索此标识符:
<a href="{{ route('cashier.payment', $subscription->latestPayment()->id) }}">
请确认您的支付。
</a>
如果您希望在 past_due
或 incomplete
状态下仍将订阅视为活动,您可以使用 Cashier 提供的 keepPastDueSubscriptionsActive
和 keepIncompleteSubscriptionsActive
方法。通常,这些方法应在 App\Providers\AppServiceProvider
的 register
方法中调用:
use Laravel\Cashier\Cashier;
/**
* 注册任何应用程序服务。
*/
public function register(): void
{
Cashier::keepPastDueSubscriptionsActive();
Cashier::keepIncompleteSubscriptionsActive();
}
当订阅处于 incomplete
状态时,无法更改它,因此 swap
和 updateQuantity
方法将在订阅处于 incomplete
状态时抛出异常。
订阅范围
大多数订阅状态也可用作查询范围,以便您可以轻松查询数据库中处于给定状态的订阅:
// 获取所有活动订阅...
$subscriptions = Subscription::query()->active()->get();
// 获取用户的所有已取消订阅...
$subscriptions = $user->subscriptions()->canceled()->get();
可用范围的完整列表如下:
Subscription::query()->active();
Subscription::query()->canceled();
Subscription::query()->ended();
Subscription::query()->incomplete();
Subscription::query()->notCanceled();
Subscription::query()->notOnGracePeriod();
Subscription::query()->notOnTrial();
Subscription::query()->onGracePeriod();
Subscription::query()->onTrial();
Subscription::query()->pastDue();
Subscription::query()->recurring();
更改价格
在客户订阅您的应用程序后,他们可能会偶尔希望更改为新订阅价格。要将客户切换到新价格,请将 Stripe 价格的标识符传递给 swap
方法。切换价格时,假定用户希望在之前取消其订阅的情况下重新激活其订阅。给定的价格标识符应对应于 Stripe 控制面板中可用的 Stripe 价格标识符:
use App\Models\User;
$user = App\Models\User::find(1);
$user->subscription('default')->swap('price_yearly');
如果客户正在试用,则将保留试用期。此外,如果订阅存在“数量”,该数量也将保留。
如果您希望切换价格并取消客户当前的试用期,可以调用 skipTrial
方法:
$user->subscription('default')
->skipTrial()
->swap('price_yearly');
如果您希望切换价格并立即向客户开具发票,而不是等待他们的下一个计费周期,则可以使用 swapAndInvoice
方法:
$user = User::find(1);
$user->subscription('default')->swapAndInvoice('price_yearly');
计费调整
默认情况下,Stripe 在切换价格时会进行计费调整。noProrate
方法可用于在不进行计费调整的情况下更新订阅的价格:
$user->subscription('default')->noProrate()->swap('price_yearly');
有关订阅计费调整的更多信息,请参阅 Stripe 文档。
在 swapAndInvoice
方法之前执行 noProrate
方法将无效。将始终发出发票。
订阅数量
有时,订阅会受到“数量”的影响。例如,项目管理应用程序可能会按每个项目每月收费 10 美元。您可以使用 incrementQuantity
和 decrementQuantity
方法轻松增加或减少订阅数量:
use App\Models\User;
$user = User::find(1);
$user->subscription('default')->incrementQuantity();
// 将五个添加到订阅的当前数量...
$user->subscription('default')->incrementQuantity(5);
$user->subscription('default')->decrementQuantity();
// 从订阅的当前数量中减去五个...
$user->subscription('default')->decrementQuantity(5);
或者,您可以使用 updateQuantity
方法设置特定数量:
$user->subscription('default')->updateQuantity(10);
noProrate
方法可用于在不进行计费调整的情况下更新订阅的数量:
$user->subscription('default')->noProrate()->updateQuantity(10);
有关订阅数量的更多信息,请参阅 Stripe 文档。
具有多个产品的订阅的数量
如果您的订阅是 具有多个产品的订阅,则应将您希望增加或减少的价格的 ID 作为第二个参数传递给增量/减量方法:
$user->subscription('default')->incrementQuantity(1, 'price_chat');
具有多个产品的订阅
具有多个产品的订阅 允许您将多个计费产品分配给单个订阅。例如,假设您正在构建一个客户服务“帮助台”应用程序,该应用程序的基本订阅价格为每月 10 美元,但提供一个额外的实时聊天附加产品,每月额外收费 15 美元。有关具有多个产品的订阅的信息存储在 Cashier 的 subscription_items
数据库表中。
您可以通过将价格数组作为第二个参数传递给 newSubscription
方法来为给定订阅指定多个产品:
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$request->user()->newSubscription('default', [
'price_monthly',
'price_chat',
])->create($request->paymentMethodId);
// ...
});
在上面的示例中,客户将有两个价格附加到其 default
订阅。两个价格将在各自的计费周期内收费。如果需要,您可以使用 quantity
方法指示每个价格的特定数量:
$user = User::find(1);
$user->newSubscription('default', ['price_monthly', 'price_chat'])
->quantity(5, 'price_chat')
->create($paymentMethod);
如果您希望向现有订阅添加另一个价格,可以调用订阅的 addPrice
方法:
$user = User::find(1);
$user->subscription('default')->addPrice('price_chat');
上面的示例将添加新价格,客户将在下一个计费周期为其收费。如果您希望立即向客户收费,可以使用 addPriceAndInvoice
方法:
$user->subscription('default')->addPriceAndInvoice('price_chat');
如果您希望以特定数量添加价格,则可以将数量作为 addPrice
或 addPriceAndInvoice
方法的第二个参数传递:
$user = User::find(1);
$user->subscription('default')->addPrice('price_chat', 5);
您可以使用 removePrice
方法从订阅中删除价格:
$user->subscription('default')->removePrice('price_chat');
您不能删除订阅上的最后一个价格。相反,您应该简单地取消订阅。
切换价格
您还可以使用多个产品的订阅更改附加到订阅的价格。例如,假设客户有一个 price_basic
订阅和一个 price_chat
附加产品,您希望将客户从 price_basic
升级到 price_pro
价格:
use App\Models\User;
$user = User::find(1);
$user->subscription('default')->swap(['price_pro', 'price_chat']);
执行上面的示例时,将删除具有 price_basic
的底层订阅项,并保留 price_chat
。此外,将为 price_pro
创建一个新的订阅项。
您还可以通过将键/值对数组传递给 swap
方法来指定订阅项选项。例如,您可能需要指定订阅价格数量:
$user = User::find(1);
$user->subscription('default')->swap([
'price_pro' => ['quantity' => 5],
'price_chat'
]);
如果您希望在订阅上切换单个价格,可以使用订阅项本身的 swap
方法。这种方法特别有用,如果您希望保留订阅的其他价格的所有现有元数据:
$user = User::find(1);
$user->subscription('default')
->findItemOrFail('price_basic')
->swap('price_pro');
计费调整
默认情况下,Stripe 在添加或删除具有多个产品的订阅的价格时会进行计费调整。如果您希望在不进行计费调整的情况下进行价格调整,则应将 noProrate
方法链接到您的价格操作:
$user->subscription('default')->noProrate()->removePrice('price_chat');
数量
如果您希望更新单个订阅价格的数量,可以使用 现有数量方法,并将价格的 ID 作为方法的第二个参数传递:
$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');
当订阅具有多个价格时,stripe_price
和 quantity
属性将在 Subscription
模型中为 null
。要访问单个价格属性,您应使用 Subscription
模型上可用的 items
关系。
订阅项
当订阅具有多个价格时,它将在数据库的 subscription_items
表中具有多个订阅“项”。您可以通过订阅上的 items
关系访问这些项:
use App\Models\User;
$user = User::find(1);
$subscriptionItem = $user->subscription('default')->items->first();
// 检索特定项的 Stripe 价格和数量...
$stripePrice = $subscriptionItem->stripe_price;
$quantity = $subscriptionItem->quantity;
您还可以使用 findItemOrFail
方法检索特定价格:
$user = User::find(1);
$subscriptionItem = $user->subscription('default')->findItemOrFail('price_chat');
多个订阅
Stripe 允许您的客户同时拥有多个订阅。例如,您可能经营一家健身房,提供游泳订阅和举重订阅,每个订阅可能有不同的定价。当然,客户应该能够订阅其中一个或两个计划。
当您的应用程序创建订阅时,您可以将订阅的类型提供给 newSubscription
方法。类型可以是表示用户正在发起的订阅的任何字符串:
use Illuminate\Http\Request;
Route::post('/swimming/subscribe', function (Request $request) {
$request->user()->newSubscription('swimming')
->price('price_swimming_monthly')
->create($request->paymentMethodId);
// ...
});
在此示例中,我们为客户发起了每月游泳的订阅。然而,他们可能希望稍后切换到年度订阅。调整客户的订阅时,我们可以简单地切换 swimming
订阅上的价格:
$user->subscription('swimming')->swap('price_swimming_yearly');
当然,您还可以完全取消订阅:
$user->subscription('swimming')->cancel();
基于使用的计费
基于使用的计费 允许您根据客户在计费周期内的产品使用情况向其收费。例如,您可能会根据客户每月发送的短信或电子邮件数量向其收费。
要开始使用基于使用的计费,您首先需要在 Stripe 控制面板中创建一个具有 基于使用的计费模型 和 计量器 的新产品。创建计量器后,存储相关事件名称和计量器 ID,您将需要报告和检索使用情况。然后,使用 meteredPrice
方法将计量价格 ID 添加到客户订阅中:
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$request->user()->newSubscription('default')
->meteredPrice('price_metered')
->create($request->paymentMethodId);
// ...
});
您还可以通过 Stripe Checkout 启动计量订阅:
$checkout = Auth::user()
->newSubscription('default', [])
->meteredPrice('price_metered')
->checkout();
return view('your-checkout-view', [
'checkout' => $checkout,
]);
报告使用情况
随着客户使用您的应用程序,您将向 Stripe 报告其使用情况,以便他们能够准确计费。要报告计量事件的使用情况,您可以使用可计费模型上的 reportMeterEvent
方法:
$user = User::find(1);
$user->reportMeterEvent('emails-sent');
默认情况下,将为计费周期添加“使用数量”为 1。或者,您可以传递特定的“使用”数量,以将其添加到客户的计费周期的使用情况:
$user = User::find(1);
$user->reportMeterEvent('emails-sent', quantity: 15);
要检索客户的计量器的事件摘要,您可以使用可计费实例的 meterEventSummaries
方法:
$user = User::find(1);
$meterUsage = $user->meterEventSummaries($meterId);
$meterUsage->first()->aggregated_value // 10
有关计量事件摘要的更多信息,请参阅 Stripe 的 计量事件摘要对象文档。
要 列出所有计量器,您可以使用可计费实例的 meters
方法:
$user = User::find(1);
$user->meters();
订阅税务
不要手动计算税率,您可以 使用 Stripe Tax 自动计算税费
要指定用户在订阅上支付的税率,您应在可计费模型上实现 taxRates
方法,并返回包含 Stripe 税率 ID 的数组。您可以在 Stripe 控制面板 中定义这些税率:
/**
* 应适用于客户订阅的税率。
*
* @return array<int, string>
*/
public function taxRates(): array
{
return ['txr_id'];
}
taxRates
方法使您能够按客户逐个应用税率,这可能对跨多个国家和税率的用户群体很有帮助。
如果您提供具有多个产品的订阅,则可以通过在可计费模型上实现 priceTaxRates
方法为每个价格定义不同的税率:
/**
* 应适用于客户订阅的税率。
*
* @return array<string, array<int, string>>
*/
public function priceTaxRates(): array
{
return [
'price_monthly' => ['txr_id'],
];
}
taxRates
方法仅适用于订阅收费。如果您使用 Cashier 进行“单次”收费,则需要在该时刻手动指定税率。
同步税率
在更改 taxRates
方法返回的硬编码税率 ID 时,用户的现有订阅的税务设置将保持不变。如果您希望使用新的 taxRates
值更新现有订阅的税务值,则应在用户的订阅实例上调用 syncTaxRates
方法:
$user->subscription('default')->syncTaxRates();
这还将与具有多个产品的订阅同步任何项目税率。如果您的应用程序提供具有多个产品的订阅,则应确保您的可计费模型实现上述讨论的 priceTaxRates
方法。
税务豁免
Cashier 还提供 isNotTaxExempt
、isTaxExempt
和 reverseChargeApplies
方法,以确定客户是否免税。这些方法将调用 Stripe API 以确定客户的税务豁免状态:
use App\Models\User;
$user = User::find(1);
$user->isTaxExempt();
$user->isNotTaxExempt();
$user->reverseChargeApplies();
这些方法也可以在任何 Laravel\Cashier\Invoice
对象上使用。但是,当在 Invoice
对象上调用时,这些方法将确定创建发票时的豁免状态。
订阅锚定日期
默认情况下,计费周期锚定日期为订阅创建的日期,或者如果使用试用期,则为试用期结束的日期。如果您希望修改计费锚定日期,可以使用 anchorBillingCycleOn
方法:
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$anchor = Carbon::parse('下个月的第一天');
$request->user()->newSubscription('default', 'price_monthly')
->anchorBillingCycleOn($anchor->startOfDay())
->create($request->paymentMethodId);
// ...
});
有关管理订阅计费周期的更多信息,请参阅 Stripe 计费周期文档。
取消订阅
要取消订阅,请在用户的订阅上调用 cancel
方法:
$user->subscription('default')->cancel();
当订阅被取消时,Cashier 将自动在您的 subscriptions
数据库表中设置 ends_at
列。此列用于知道何时 subscribed
方法应开始返回 false
。
例如,如果客户在 3 月 1 日取消订阅,但订阅原定于 3 月 5 日到期,则 subscribed
方法将继续返回 true
,直到 3 月 5 日。这是因为用户通常被允许在计费周期结束之前继续使用应用程序。
您可以使用 onGracePeriod
方法确定用户是否已取消其订阅,但仍在宽限期内:
if ($user->subscription('default')->onGracePeriod()) {
// ...
}
如果您希望立即取消订阅,可以在用户的订阅上调用 cancelNow
方法:
$user->subscription('default')->cancelNow();
如果您希望立即取消订阅并对任何未开票的计量使用或新的/待处理的计费调整发票项目进行开票,可以在用户的订阅上调用 cancelNowAndInvoice
方法:
$user->subscription('default')->cancelNowAndInvoice();
您还可以选择在特定时刻取消订阅:
$user->subscription('default')->cancelAt(
now()->addDays(10)
);
最后,您应始终在删除相关用户模型之前取消用户的订阅:
$user->subscription('default')->cancelNow();
$user->delete();
恢复订阅
如果客户取消了他们的订阅并且您希望恢复它,您可以在订阅上调用 resume
方法。客户必须仍在其“宽限期”内才能恢复订阅:
$user->subscription('default')->resume();
如果客户取消了订阅,然后在订阅完全过期之前恢复该订阅,客户将不会立即被收费。相反,他们的订阅将被重新激活,并将在原始计费周期上收费。
订阅试用
提前收集支付方式
如果您希望在仍然收集支付方式信息的情况下为客户提供试用期,您应该在创建订阅时使用 trialDays
方法:
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$request->user()->newSubscription('default', 'price_monthly')
->trialDays(10)
->create($request->paymentMethodId);
// ...
});
此方法将在数据库中的订阅记录上设置试用期结束日期,并指示 Stripe 在此日期之后才开始向客户收费。当使用 trialDays
方法时,Cashier 将覆盖在 Stripe 中为价格配置的任何默认试用期。
如果客户的订阅在试用结束日期之前未被取消,他们将在试用期结束后立即被收费,因此您应该确保通知用户他们的试用结束日期。
trialUntil
方法允许您提供一个 DateTime
实例,指定试用期应何时结束:
use Carbon\Carbon;
$user->newSubscription('default', 'price_monthly')
->trialUntil(Carbon::now()->addDays(10))
->create($paymentMethod);
您可以使用用户实例的 onTrial
方法或订阅实例的 onTrial
方法来确定用户是否在试用期内。以下两个示例是等效的:
if ($user->onTrial('default')) {
// ...
}
if ($user->subscription('default')->onTrial()) {
// ...
}
您可以使用 endTrial
方法立即结束订阅试用:
$user->subscription('default')->endTrial();
要确定现有试用是否已过期,您可以使用 hasExpiredTrial
方法:
if ($user->hasExpiredTrial('default')) {
// ...
}
if ($user->subscription('default')->hasExpiredTrial()) {
// ...
}
在 Stripe / Cashier 中定义试用天数
您可以选择在 Stripe 控制面板中定义您的价格的试用天数,或者始终使用 Cashier 显式传递它们。如果您选择在 Stripe 中定义价格的试用天数,请注意,新的订阅,包括过去有过订阅的客户的新订阅,将始终获得试用期,除非您显式调用 skipTrial()
方法。
不提前收集支付方式
如果您希望在不提前收集用户支付方式信息的情况下提供试用期,您可以将用户记录上的 trial_ends_at
列设置为您希望的试用结束日期。这通常在用户注册期间完成:
use App\Models\User;
$user = User::create([
// ...
'trial_ends_at' => now()->addDays(10),
]);
确保在您的可计费模型类定义中为 trial_ends_at
属性添加 日期转换。
Cashier 将这种类型的试用称为“通用试用”,因为它不附加到任何现有订阅上。可计费模型实例上的 onTrial
方法将在当前日期未超过 trial_ends_at
的值时返回 true
:
if ($user->onTrial()) {
// 用户在试用期内...
}
一旦您准备好为用户创建实际的订阅,您可以像往常一样使用 newSubscription
方法:
$user = User::find(1);
$user->newSubscription('default', 'price_monthly')->create($paymentMethod);
要检索用户的试用结束日期,您可以使用 trialEndsAt
方法。如果用户在试用期内,该方法将返回一个 Carbon 日期实例,如果不在试用期内则返回 null
。如果您希望获取特定订阅的试用结束日期,您还可以传递一个可选的订阅类型参数:
if ($user->onTrial()) {
$trialEndsAt = $user->trialEndsAt('main');
}
如果您希望具体知道用户是否在“通用”试用期内并且尚未创建实际订阅,您还可以使用 onGenericTrial
方法:
if ($user->onGenericTrial()) {
// 用户在“通用”试用期内...
}
延长试用
extendTrial
方法允许您在订阅创建后延长订阅的试用期。如果试用期已经过期并且客户已经开始为订阅付费,您仍然可以为他们提供延长的试用期。试用期内的时间将从客户的下一个发票中扣除:
use App\Models\User;
$subscription = User::find(1)->subscription('default');
// 在 7 天后结束试用...
$subscription->extendTrial(
now()->addDays(7)
);
// 将试用期再延长 5 天...
$subscription->extendTrial(
$subscription->trial_ends_at->addDays(5)
);
处理 Stripe Webhooks
您可以使用 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 所需的所有事件:
php artisan cashier:webhook
默认情况下,创建的 webhook 将指向由 APP_URL
环境变量定义的 URL 和 Cashier 中包含的 cashier.webhook
路由。如果您希望使用不同的 URL,可以在调用命令时提供 --url
选项:
php artisan cashier:webhook --url "https://example.com/stripe/webhook"
创建的 webhook 将使用与您版本的 Cashier 兼容的 Stripe API 版本。如果您希望使用不同的 Stripe 版本,可以提供 --api-version
选项:
php artisan cashier:webhook --api-version="2019-12-03"
创建后,webhook 将立即激活。如果您希望创建 webhook 但在准备好之前将其禁用,可以在调用命令时提供 --disabled
选项:
php artisan cashier:webhook --disabled
确保使用 Cashier 包含的 webhook 签名验证 中间件保护传入的 Stripe webhook 请求。
Webhooks 和 CSRF 保护
由于 Stripe webhooks 需要绕过 Laravel 的 CSRF 保护,您应该确保 Laravel 不会尝试验证传入 Stripe webhooks 的 CSRF 令牌。为此,您应该在应用程序的 bootstrap/app.php
文件中将 stripe/*
排除在 CSRF 保护之外:
->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
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
方法的第二个参数:
use Illuminate\Http\Request;
Route::post('/purchase', function (Request $request) {
$stripeCharge = $request->user()->charge(
100, $request->paymentMethodId
);
// ...
});
charge
方法接受一个数组作为其第三个参数,允许您将任何选项传递给底层 Stripe 收费创建。有关创建收费时可用选项的更多信息,请参阅 Stripe 文档:
$user->charge(100, $paymentMethod, [
'custom_option' => $value,
]);
您还可以在没有底层客户或用户的情况下使用 charge
方法。为此,请在您应用程序的可计费模型的新实例上调用 charge
方法:
use App\Models\User;
$stripeCharge = (new User)->charge(100, $paymentMethod);
如果收费失败,charge
方法将抛出异常。如果收费成功,方法将返回一个 Laravel\Cashier\Payment
的实例:
try {
$payment = $user->charge(100, $paymentMethod);
} catch (Exception $e) {
// ...
}
charge
方法接受的支付金额为您应用程序所用货币的最低单位。例如,如果客户以美元支付,则金额应以美分为单位指定。
带发票收费
有时您可能需要进行一次性收费并向客户提供 PDF 发票。invoicePrice
方法可以做到这一点。例如,让我们为客户开具五件新衬衫的发票:
$user->invoicePrice('price_tshirt', 5);
发票将立即从用户的默认支付方式中收费。invoicePrice
方法还接受一个数组作为其第三个参数。此数组包含发票项目的计费选项。该方法接受的第四个参数也是一个数组,应该包含发票本身的计费选项:
$user->invoicePrice('price_tshirt', 5, [
'discounts' => [
['coupon' => 'SUMMER21SALE']
],
], [
'default_tax_rates' => ['txr_id'],
]);
与 invoicePrice
类似,您可以使用 tabPrice
方法通过将多个项目添加到客户的“账单”中,然后向客户开具发票,来为多个项目(每张发票最多 250 个项目)创建一次性收费。例如,我们可以为客户开具五件衬衫和两只杯子的发票:
$user->tabPrice('price_tshirt', 5);
$user->tabPrice('price_mug', 2);
$user->invoice();
或者,您可以使用 invoiceFor
方法对客户的默认支付方式进行“一次性”收费:
$user->invoiceFor('一次性费用', 500);
尽管可以使用 invoiceFor
方法,但建议您使用 invoicePrice
和 tabPrice
方法与预定义价格。这样,您将能够在 Stripe 仪表板中获得更好的分析和数据,了解每种产品的销售情况。
invoice
、invoicePrice
和 invoiceFor
方法将创建一个 Stripe 发票,该发票将重试失败的计费尝试。如果您不希望发票重试失败的收费,则需要在第一次失败收费后使用 Stripe API 关闭它们。
创建支付意图
您可以通过在可计费模型实例上调用 pay
方法来创建新的 Stripe 支付意图。调用此方法将创建一个包装在 Laravel\Cashier\Payment
实例中的支付意图:
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
方法:
use Illuminate\Http\Request;
Route::post('/pay', function (Request $request) {
$payment = $request->user()->payWith(
$request->get('amount'), ['card', 'bancontact']
);
return $payment->client_secret;
});
pay
和 payWith
方法接受的支付金额为您应用程序所用货币的最低单位。例如,如果客户以美元支付,则金额应以美分为单位。
退款
如果您需要退款 Stripe 收费,可以使用 refund
方法。此方法接受 Stripe 支付意图 ID 作为其第一个参数:
$payment = $user->charge(100, $paymentMethodId);
$user->refund($payment->id);
发票
检索发票
您可以使用 invoices
方法轻松检索可计费模型的发票数组。invoices
方法返回 Laravel\Cashier\Invoice
实例的集合:
$invoices = $user->invoices();
如果您希望在结果中包含待处理的发票,可以使用 invoicesIncludingPending
方法:
$invoices = $user->invoicesIncludingPending();
您可以使用 findInvoice
方法通过其 ID 检索特定发票:
$invoice = $user->findInvoice($invoiceId);
显示发票信息
在列出客户的发票时,您可以使用发票的方法来显示相关的发票信息。例如,您可能希望在表格中列出每张发票,以便用户轻松下载其中任何一张:
<table>
@foreach ($invoices as $invoice)
<tr>
<td>{{ $invoice->date()->toFormattedDateString() }}</td>
<td>{{ $invoice->total() }}</td>
<td><a href="/user/invoice/{{ $invoice->id }}">下载</a></td>
</tr>
@endforeach
</table>
即将到来的发票
要检索客户的即将到来的发票,您可以使用 upcomingInvoice
方法:
$invoice = $user->upcomingInvoice();
同样,如果客户有多个订阅,您还可以检索特定订阅的即将到来的发票:
$invoice = $user->subscription('default')->upcomingInvoice();
预览订阅发票
使用 previewInvoice
方法,您可以在进行价格更改之前预览发票。这将允许您确定在进行给定价格更改时客户的发票将是什么样子:
$invoice = $user->subscription('default')->previewInvoice('price_yearly');
您可以将价格数组传递给 previewInvoice
方法,以便预览具有多个新价格的发票:
$invoice = $user->subscription('default')->previewInvoice(['price_yearly', 'price_metered']);
生成发票 PDF
在生成发票 PDF 之前,您应该使用 Composer 安装 Dompdf 库,这是 Cashier 的默认发票渲染器:
composer require dompdf/dompdf
在路由或控制器中,您可以使用 downloadInvoice
方法生成给定发票的 PDF 下载。此方法将自动生成所需的 HTTP 响应,以下载发票:
use Illuminate\Http\Request;
Route::get('/user/invoice/{invoice}', function (Request $request, string $invoiceId) {
return $request->user()->downloadInvoice($invoiceId);
});
默认情况下,发票上的所有数据均来自存储在 Stripe 中的客户和发票数据。文件名基于您的 app.name
配置值。但是,您可以通过将数组作为第二个参数提供给 downloadInvoice
方法来自定义某些数据。此数组允许您自定义公司和产品详细信息等信息:
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
结尾:
return $request->user()->downloadInvoice($invoiceId, [], 'my-invoice');
自定义发票渲染器
Cashier 还使使用自定义发票渲染器成为可能。默认情况下,Cashier 使用 DompdfInvoiceRenderer
实现,该实现利用 dompdf PHP 库生成 Cashier 的发票。但是,您可以通过实现 Laravel\Cashier\Contracts\InvoiceRenderer
接口来使用任何渲染器。例如,您可能希望通过 API 调用第三方 PDF 渲染服务来渲染发票 PDF:
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:
use Illuminate\Http\Request;
Route::get('/product-checkout', function (Request $request) {
return $request->user()->checkout('price_tshirt');
});
如果需要,您还可以指定产品数量:
use Illuminate\Http\Request;
Route::get('/product-checkout', function (Request $request) {
return $request->user()->checkout(['price_tshirt' => 15]);
});
当客户访问此路由时,他们将被重定向到 Stripe 的 Checkout 页面。默认情况下,当用户成功完成或取消购买时,他们将被重定向到您的 home
路由位置,但您可以使用 success_url
和 cancel_url
选项指定自定义回调 URL:
use Illuminate\Http\Request;
Route::get('/product-checkout', function (Request $request) {
return $request->user()->checkout(['price_tshirt' => 1], [
'success_url' => route('your-success-route'),
'cancel_url' => route('your-cancel-route'),
]);
});
在定义 success_url
结账选项时,您可以指示 Stripe 在调用您的 URL 时将结账会话 ID 作为查询字符串参数添加。为此,请在 success_url
查询字符串中添加字面字符串 {CHECKOUT_SESSION_ID}
。Stripe 将用实际的结账会话 ID 替换此占位符:
use Illuminate\Http\Request;
use Stripe\Checkout\Session;
use Stripe\Customer;
Route::get('/product-checkout', function (Request $request) {
return $request->user()->checkout(['price_tshirt' => 1], [
'success_url' => route('checkout-success').'?session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => route('checkout-cancel'),
]);
});
Route::get('/checkout-success', function (Request $request) {
$checkoutSession = $request->user()->stripe()->checkout->sessions->retrieve($request->get('session_id'));
return view('checkout.success', ['checkoutSession' => $checkoutSession]);
})->name('checkout-success');
促销代码
默认情况下,Stripe Checkout 不允许 用户可兑换的促销代码。幸运的是,有一种简单的方法可以为您的 Checkout 页面启用这些功能。为此,您可以调用 allowPromotionCodes
方法:
use Illuminate\Http\Request;
Route::get('/product-checkout', function (Request $request) {
return $request->user()
->allowPromotionCodes()
->checkout('price_tshirt');
});
单次收费结账
您还可以对尚未在 Stripe 控制面板中创建的临时产品进行简单收费。为此,您可以在可计费模型上使用 checkoutCharge
方法,并传递一个可收费的金额、产品名称和可选数量。当客户访问此路由时,他们将被重定向到 Stripe 的 Checkout 页面:
use Illuminate\Http\Request;
Route::get('/charge-checkout', function (Request $request) {
return $request->user()->checkoutCharge(1200, 'T-Shirt', 5);
});
使用 checkoutCharge
方法时,Stripe 将始终在您的 Stripe 控制面板中创建一个新产品和价格。因此,我们建议您在 Stripe 控制面板中预先创建产品,并使用 checkout
方法。
订阅结账
使用 Stripe Checkout 进行订阅需要您在 Stripe 控制面板中启用 customer.subscription.created
webhook。此 webhook 将在您的数据库中创建订阅记录并存储所有相关的订阅项。
您还可以使用 Stripe Checkout 启动订阅。在使用 Cashier 的订阅构建器方法定义订阅后,您可以调用 checkout
方法。当客户访问此路由时,他们将被重定向到 Stripe 的 Checkout 页面:
use Illuminate\Http\Request;
Route::get('/subscription-checkout', function (Request $request) {
return $request->user()
->newSubscription('default', 'price_monthly')
->checkout();
});
与产品结账一样,您可以自定义成功和取消 URL:
use Illuminate\Http\Request;
Route::get('/subscription-checkout', function (Request $request) {
return $request->user()
->newSubscription('default', 'price_monthly')
->checkout([
'success_url' => route('your-success-route'),
'cancel_url' => route('your-cancel-route'),
]);
});
当然,您还可以为订阅结账启用促销代码:
use Illuminate\Http\Request;
Route::get('/subscription-checkout', function (Request $request) {
return $request->user()
->newSubscription('default', 'price_monthly')
->allowPromotionCodes()
->checkout();
});
不幸的是,Stripe Checkout 在启动订阅时不支持所有订阅计费选项。使用订阅构建器上的 anchorBillingCycleOn
方法、设置比例行为或设置支付行为将对 Stripe Checkout 会话没有任何影响。请查阅 Stripe Checkout 会话 API 文档 以查看可用的参数。
Stripe Checkout 和试用期
当然,您可以在构建将通过 Stripe Checkout 完成的订阅时定义试用期:
$checkout = Auth::user()->newSubscription('default', 'price_monthly')
->trialDays(3)
->checkout();
但是,试用期必须至少为 48 小时,这是 Stripe Checkout 支持的最短试用时间。
订阅和 Webhooks
请记住,Stripe 和 Cashier 通过 webhooks 更新订阅状态,因此在客户在输入支付信息后返回应用程序时,订阅可能尚未处于活动状态。要处理这种情况,您可能希望显示一条消息,告知用户他们的支付或订阅正在等待。
收集税号
结账还支持收集客户的税号。要在结账会话中启用此功能,请在创建会话时调用 collectTaxIds
方法:
$checkout = $user->collectTaxIds()->checkout('price_tshirt');
当调用此方法时,客户将有一个新的复选框,允许他们指明他们是否以公司身份购买。如果是,他们将有机会提供他们的税号。
如果您已经在应用程序的服务提供者中配置了 自动税收收集,则此功能将自动启用,无需调用 collectTaxIds
方法。
客户结账
使用 Checkout::guest
方法,您可以为没有“帐户”的应用程序访客启动结账会话:
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
实例上可用的其他方法来自定义访客结账会话:
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
异常并将用户重定向到支付确认页面:
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 为您处理支付确认。在这种情况下,您可以在捕获 IncompletePayment
异常时,仍然通知用户他们将收到一封电子邮件,提供进一步的支付确认说明。
支付异常可能会在以下方法中抛出:charge
、invoiceFor
和 invoice
,这些方法在使用 Billable
特性时。与订阅交互时,SubscriptionBuilder
上的 create
方法,以及 Subscription
和 SubscriptionItem
模型上的 incrementAndInvoice
和 swapAndInvoice
方法可能会抛出不完整的支付异常。
确定现有订阅是否存在不完整支付可以通过可计费模型或订阅实例上的 hasIncompletePayment
方法完成:
if ($user->hasIncompletePayment('default')) {
// ...
}
if ($user->subscription('default')->hasIncompletePayment()) {
// ...
}
您可以通过检查异常实例上的 payment
属性来推导不完整支付的具体状态:
use Laravel\Cashier\Exceptions\IncompletePayment;
try {
$user->charge(1000, 'pm_card_threeDSecure2Required');
} catch (IncompletePayment $exception) {
// 获取支付意图状态...
$exception->payment->status;
// 检查特定条件...
if ($exception->payment->requiresPaymentMethod()) {
// ...
} elseif ($exception->payment->requiresConfirmation()) {
// ...
}
}
确认支付
某些支付方式需要额外的数据以确认支付。例如,SEPA 支付方式在支付过程中需要额外的“授权”数据。您可以使用 withPaymentConfirmationOptions
方法将此数据提供给 Cashier:
$subscription->withPaymentConfirmationOptions([
'mandate_data' => '...',
])->swap('price_xxx');
您可以查阅 Stripe API 文档 以查看确认支付时接受的所有选项。
强客户身份验证
如果您的业务或客户位于欧洲,您将需要遵守欧盟的强客户身份验证(SCA)法规。这些法规是欧盟于 2019 年 9 月实施的,旨在防止支付欺诈。幸运的是,Stripe 和 Cashier 已准备好构建符合 SCA 的应用程序。
在开始之前,请查看 Stripe 关于 PSD2 和 SCA 的指南 以及他们关于新 SCA API 的 文档。
需要额外确认的支付
SCA 法规通常要求额外的验证以确认和处理支付。当发生这种情况时,Cashier 将抛出一个 Laravel\Cashier\Exceptions\IncompletePayment
异常,通知您需要额外的验证。有关如何处理这些异常的更多信息,请参阅 处理失败支付 文档。
Stripe 或 Cashier 提供的支付确认屏幕可能会根据特定银行或卡发行人的支付流程进行定制,并可能包括额外的卡确认、临时小额收费、单独的设备身份验证或其他形式的验证。
不完整和逾期状态
当支付需要额外确认时,订阅将保持在 incomplete
或 past_due
状态,如其 stripe_status
数据库列所示。Cashier 将在支付确认完成后自动激活客户的订阅,并通过 webhook 通知您的应用程序。
有关 incomplete
和 past_due
状态的更多信息,请参阅 我们关于这些状态的附加文档。
会话外支付通知
由于 SCA 法规要求客户在其订阅处于活动状态时偶尔验证其支付详细信息,Cashier 可以在需要会话外支付确认时向客户发送通知。例如,这可能发生在订阅续订时。Cashier 的支付通知可以通过将 CASHIER_PAYMENT_NOTIFICATION
环境变量设置为通知类来启用。默认情况下,此通知将被禁用。当然,Cashier 包含一个您可以用于此目的的通知类,但如果需要,您可以提供自己的通知类:
CASHIER_PAYMENT_NOTIFICATION=Laravel\Cashier\Notifications\ConfirmPayment
要确保会话外支付确认通知被发送,请验证 Stripe webhooks 是否配置,并确保在您的 Stripe 控制面板中启用 invoice.payment_action_required
webhook。此外,您的可计费模型还应使用 Laravel 的 Illuminate\Notifications\Notifiable
特性。
即使客户正在手动进行需要额外确认的支付,通知也会被发送。不幸的是,Stripe 无法知道支付是手动完成的还是“会话外”的。但是,客户在确认支付后访问支付页面时将看到“支付成功”消息。客户将不被允许意外确认同一支付两次,从而产生意外的第二次收费。
Stripe SDK
Cashier 的许多对象都是对 Stripe SDK 对象的封装。如果您希望直接与 Stripe 对象交互,可以使用 asStripe
方法方便地检索它们:
$stripeSubscription = $subscription->asStripeSubscription();
$stripeSubscription->application_fee_percent = 5;
$stripeSubscription->save();
您还可以使用 updateStripeSubscription
方法直接更新 Stripe 订阅:
$subscription->updateStripeSubscription(['application_fee_percent' => 5]);
如果您希望直接使用 Stripe\StripeClient
客户端,可以在 Cashier
类上调用 stripe
方法。例如,您可以使用此方法访问 StripeClient
实例并检索您 Stripe 帐户中的价格列表:
use Laravel\Cashier\Cashier;
$prices = Cashier::stripe()->prices->all();
测试
在测试使用 Cashier 的应用程序时,您可以模拟对 Stripe API 的实际 HTTP 请求;但是,这需要您部分重新实现 Cashier 的行为。因此,我们建议允许您的测试访问实际的 Stripe API。虽然这会更慢,但它提供了更多的信心,确保您的应用程序按预期工作,任何慢速测试可以放在自己的 Pest / PHPUnit 测试组中。
在测试时,请记住,Cashier 本身已经有一个很好的测试套件,因此您只需专注于测试您自己应用程序的订阅和支付流程,而不是每个底层 Cashier 行为。
要开始,请将您的 Stripe 密钥的 测试 版本添加到您的 phpunit.xml
文件中:
<env name="STRIPE_SECRET" value="sk_test_<your-key>"/>
现在,每当您在测试期间与 Cashier 交互时,它将向您的 Stripe 测试环境发送实际的 API 请求。为了方便起见,您应该预先填充您的 Stripe 测试帐户,以便在测试期间使用的订阅/价格。
为了测试各种计费场景,例如信用卡拒绝和失败,您可以使用 Stripe 提供的各种 测试卡号和令牌。