Laravel Cashier (Stripe)
- 简介
- 升级 Cashier
- 安装
- 配置
- 快速入门
- 客户
- 付款方式
- 订阅
- 订阅试用
- 处理 Stripe Webhook
- 单次收费
- 结账
- 发票
- 处理失败的付款
- 强客户身份验证 (SCA)
- Stripe SDK
- 测试
简介
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
trait 添加到你的可计费模型定义中。通常,这将是 App\Models\User
模型。此 trait 提供了各种方法,使你可以执行常见的计费任务,例如创建订阅、应用优惠券和更新付款方式信息。
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; /** * Bootstrap any application services. */public function boot(): void{ Cashier::useCustomerModel(User::class);}
如果你使用的是 Laravel 提供的 App\Models\User
模型以外的模型,则需要发布和修改提供的 Cashier 迁移 以匹配你的替代模型的表名。
API 密钥
接下来,你应该在应用程序的 .env
文件中配置你的 Stripe API 密钥。你可以从 Stripe 控制面板中检索你的 Stripe API 密钥。
STRIPE_KEY=your-stripe-keySTRIPE_SECRET=your-stripe-secretSTRIPE_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; /** * Bootstrap any application services. */public function boot(): void{ Cashier::calculateTaxes();}
启用税款计算后,生成的任何新订阅和任何一次性发票都将进行自动税款计算。
为了使此功能正常工作,需要将客户的账单详细信息(例如客户的姓名、地址和税号)与 Stripe 同步。你可以使用 Cashier 提供的 客户数据同步 和 税号 方法来实现此目的。
日志记录
Cashier 允许你指定在记录 Stripe 致命错误时要使用的日志通道。你可以通过在应用程序的 .env
文件中定义 CASHIER_LOGGER
环境变量来指定日志通道。
CASHIER_LOGGER=stack
由对 Stripe 的 API 调用生成的异常将通过应用程序的默认日志通道记录。
使用自定义模型
你可以通过定义自己的模型并扩展相应的 Cashier 模型来自由扩展 Cashier 内部使用的模型。
use Laravel\Cashier\Subscription as CashierSubscription; class Subscription extends CashierSubscription{ // ...}
定义模型后,你可以通过 Laravel\Cashier\Cashier
类指示 Cashier 使用你的自定义模型。通常,你应该在应用程序的 App\Providers\AppServiceProvider
类的 boot
方法中通知 Cashier 你的自定义模型。
use App\Models\Cashier\Subscription;use App\Models\Cashier\SubscriptionItem; /** * Bootstrap any application services. */public function boot(): void{ Cashier::useSubscriptionModel(Subscription::class); Cashier::useSubscriptionItemModel(SubscriptionItem::class);}
快速入门
销售产品
在使用 Stripe Checkout 之前,你应该在 Stripe 仪表板中定义具有固定价格的产品。此外,你应该 配置 Cashier 的 Webhook 处理。
通过你的应用程序提供产品和订阅计费可能会令人望而却步。但是,借助 Cashier 和 Stripe Checkout,你可以轻松构建现代、强大的支付集成。
要向客户收取非经常性的一次性收费产品费用,我们将利用 Cashier 将客户引导至 Stripe Checkout,在那里他们将提供其支付详细信息并确认其购买。通过 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
数组向 Stripe Checkout 会话提供订单 ID。最后,我们将 CHECKOUT_SESSION_ID
模板变量添加到 Checkout 成功路由。当 Stripe 将客户重定向回您的应用程序时,此模板变量将自动填充 Checkout 会话 ID。
接下来,让我们构建 Checkout 成功路由。这是用户通过 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');
有关Checkout 会话对象包含的数据的更多信息,请参阅 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>You are subscribed.</p>@endif
我们甚至可以轻松确定用户是否订阅了特定产品或价格。
@if ($user->subscribedToProduct('pro_basic')) <p>You are subscribed to our Basic product.</p>@endif @if ($user->subscribedToPrice('price_basic_monthly')) <p>You are subscribed to our monthly Basic plan.</p>@endif
构建订阅中间件
为方便起见,您可能希望创建一个中间件,用于确定传入请求是否来自已订阅的用户。定义此中间件后,您可以轻松地将其分配给路由,以防止未订阅的用户访问该路由。
<?php namespace App\Http\Middleware; use Closure;use Illuminate\Http\Request;use Symfony\Component\HttpFoundation\Response; class Subscribed{ /** * Handle an incoming request. */ public function handle(Request $request, Closure $next): Response { if (! $request->user()?->subscribed()) { // Redirect user to billing page and ask them to subscribe... return redirect('/billing'); } return $next($request); }}
定义中间件后,您可以将其分配给路由。
use App\Http\Middleware\Subscribed; Route::get('/dashboard', function () { // ...})->middleware([Subscribed::class]);
允许客户管理其账单计划
当然,客户可能希望将其订阅计划更改为其他产品或“层级”。最简单的方法是将客户定向到 Stripe 的客户账单门户,该门户提供托管用户界面,允许客户下载发票、更新其付款方式和更改订阅计划。
首先,在您的应用程序中定义一个链接或按钮,用于将用户定向到我们将用于启动账单门户会话的 Laravel 路由。
<a href="{{ route('billing') }}"> 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, 'Premium customer top-up.');
向 debitBalance
方法提供一个值将借记客户的余额。
$user->debitBalance(300, 'Bad usage penalty.');
applyBalance
方法将为客户创建新的客户余额交易。您可以使用 balanceTransactions
方法检索这些交易记录,这对于提供客户可以查看的贷记和借记日志非常有用。
// Retrieve all transactions...$transactions = $user->balanceTransactions(); foreach ($transactions as $transaction) { // Transaction amount... $amount = $transaction->amount(); // $2.31 // Retrieve the related invoice when available... $invoice = $transaction->invoice();}
税号
Cashier 提供了一种简单的方法来管理客户的税务 ID。例如,taxIds
方法可用于检索作为集合分配给客户的所有税务 ID。
$taxIds = $user->taxIds();
您还可以通过其标识符检索客户的特定税务 ID。
$taxId = $user->findTaxId('txi_belgium');
您可以通过向 createTaxId
方法提供有效的类型和值来创建一个新的税务 ID。
$taxId = $user->createTaxId('eu_vat', 'BE0123456789');
createTaxId
方法会立即将增值税 ID 添加到客户的帐户。Stripe 也会验证增值税 ID;但是,这是一个异步过程。您可以通过订阅 customer.tax_id.updated
webhook 事件并检查增值税 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; /** * The "booted" method of the model. */protected static function booted(): void{ static::updated(queueable(function (User $customer) { if ($customer->hasStripeId()) { $customer->syncStripeCustomerDetails(); } }));}
现在,每次更新客户模型时,其信息都会与 Stripe 同步。为方便起见,Cashier 将在最初创建客户时自动将客户信息与 Stripe 同步。
您可以通过覆盖 Cashier 提供的多种方法来自定义用于将客户信息同步到 Stripe 的列。例如,您可以覆盖 stripeName
方法以自定义在 Cashier 将客户信息同步到 Stripe 时应视为客户“姓名”的属性。
/** * Get the customer name that should be synced to 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'));});
如果您想生成账单门户网站的 URL,而无需生成 HTTP 重定向响应,您可以调用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 Placeholder --><div id="card-element"></div> <button id="card-button" data-secret="{{ $intent->client_secret }}"> Update Payment Method</button>
接下来,可以使用 Stripe.js 库将Stripe 元素附加到表单,并安全地收集客户的支付详细信息。
<script src="https://js.stripe.com/v3/"></script> <script> const stripe = Stripe('stripe-public-key'); const elements = stripe.elements(); const cardElement = elements.create('card'); cardElement.mount('#card-element');</script>
接下来,可以使用Stripe 的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) { // Display "error.message" to the user... } else { // The card has been verified successfully... }});
Stripe 验证卡后,您可以将生成的setupIntent.payment_method
标识符传递到您的 Laravel 应用程序,在那里可以将其附加到客户。支付方式可以添加为新的支付方式或用于更新默认支付方式。您也可以立即使用支付方式标识符来创建新的订阅。
如果您想了解更多关于设置意图和收集客户支付详细信息的信息,请查看 Stripe 提供的此概述。
单次收费的支付方式
当然,当对客户的支付方式进行单次收费时,我们只需要使用一次支付方式标识符。由于 Stripe 的限制,您不能将客户的已存储默认支付方式用于单次收费。您必须允许客户使用 Stripe.js 库输入其支付方式详细信息。例如,考虑以下表单。
<input id="card-holder-name" type="text"> <!-- Stripe Elements Placeholder --><div id="card-element"></div> <button id="card-button"> Process Payment</button>
定义此类表单后,可以使用 Stripe.js 库将Stripe 元素附加到表单,并安全地收集客户的支付详细信息。
<script src="https://js.stripe.com/v3/"></script> <script> const stripe = Stripe('stripe-public-key'); const elements = stripe.elements(); const cardElement = elements.create('card'); cardElement.mount('#card-element');</script>
接下来,可以使用Stripe 的createPaymentMethod
方法验证卡并从 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) { // Display "error.message" to the user... } else { // The card has been verified successfully... }});
如果卡成功验证,您可以将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' => 'Some extra information.'],]);
优惠券
如果您想在创建订阅时应用优惠券,您可以使用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
方法。
// Find a promotion code ID by its customer facing code...$promotionCode = $user->findPromotionCode('SUMMERSALE'); // Find an active promotion code ID by its customer facing code...$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{ /** * Handle an incoming request. * * @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')) { // This user is not a paying customer... 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) }}"> Please confirm your payment.</a>
如果您希望订阅在past_due
或incomplete
状态下仍被视为活动状态,则可以使用 Cashier 提供的keepPastDueSubscriptionsActive
和keepIncompleteSubscriptionsActive
方法。通常,这些方法应该在您的App\Providers\AppServiceProvider
的register
方法中调用。
use Laravel\Cashier\Cashier; /** * Register any application services. */public function register(): void{ Cashier::keepPastDueSubscriptionsActive(); Cashier::keepIncompleteSubscriptionsActive();}
当订阅处于incomplete
状态时,在付款确认之前无法更改它。因此,当订阅处于incomplete
状态时,swap
和updateQuantity
方法将抛出异常。
订阅范围
大多数订阅状态也可用作查询范围,以便您可以轻松查询数据库中处于给定状态的订阅。
// Get all active subscriptions...$subscriptions = Subscription::query()->active()->get(); // Get all of the canceled subscriptions for a user...$subscriptions = $user->subscriptions()->canceled()->get();
下面列出了可用范围的完整列表。
Subscription::query()->active();Subscription::query()->canceled();Subscription::query()->ended();Subscription::query()->incomplete();Subscription::query()->notCanceled();Subscription::query()->notOnGracePeriod();Subscription::query()->notOnTrial();Subscription::query()->onGracePeriod();Subscription::query()->onTrial();Subscription::query()->pastDue();Subscription::query()->recurring();
更改价格
客户订阅您的应用程序后,他们有时可能希望更改为新的订阅价格。要将客户切换到新的价格,请将 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(); // Add five to the subscription's current quantity...$user->subscription('default')->incrementQuantity(5); $user->subscription('default')->decrementQuantity(); // Subtract five from the subscription's current quantity...$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_chat
附加产品的price_basic
订阅,并且您想将客户从price_basic
升级到price_pro
价格。
use App\Models\User; $user = User::find(1); $user->subscription('default')->swap(['price_pro', 'price_chat']);
执行上述示例时,将删除带有price_basic
的底层订阅项目,并保留带有price_chat
的订阅项目。此外,还将创建一个用于price_pro
的新订阅项目。
您还可以通过将键/值对数组传递给swap
方法来指定订阅项目选项。例如,您可能需要指定订阅价格数量。
$user = User::find(1); $user->subscription('default')->swap([ 'price_pro' => ['quantity' => 5], 'price_chat']);
如果您想交换订阅上的单个价格,可以使用订阅项目本身的swap
方法来实现。如果您想保留订阅其他价格上的所有现有元数据,此方法特别有用。
$user = User::find(1); $user->subscription('default') ->findItemOrFail('price_basic') ->swap('price_pro');
按比例计算
默认情况下,Stripe 会在向具有多个产品的订阅中添加或删除价格时按比例计算费用。如果您想在不按比例计算的情况下进行价格调整,则应将noProrate
方法链接到您的价格操作。
$user->subscription('default')->noProrate()->removePrice('price_chat');
数量
如果您想更新单个订阅价格的数量,可以使用现有数量方法,并将价格的 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');
当订阅具有多个价格时,Subscription
模型上的 stripe_price
和 quantity
属性将为 null
。要访问单个价格属性,应使用 Subscription
模型上可用的 items
关系。
订阅项目
当订阅具有多个价格时,它将在数据库的 subscription_items
表中存储多个订阅“项目”。您可以通过订阅上的 items
关系访问这些项目。
use App\Models\User; $user = User::find(1); $subscriptionItem = $user->subscription('default')->items->first(); // Retrieve the Stripe price and quantity for a specific item...$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 报告他们的用量,以便能够准确地向他们收费。要报告计量事件的用量,您可以在 Billable
模型上使用 reportMeterEvent
方法。
$user = User::find(1); $user->reportMeterEvent('emails-sent');
默认情况下,将向账单周期添加“用量数量”1。或者,您可以传递要添加到客户账单周期用量的特定“用量”金额。
$user = User::find(1); $user->reportMeterEvent('emails-sent', quantity: 15);
要检索客户的计量器的事件摘要,您可以使用 Billable
实例的 meterEventSummaries
方法。
$user = User::find(1); $meterUsage = $user->meterEventSummaries($meterId); $meterUsage->first()->aggregated_value // 10
有关计量器事件摘要的更多信息,请参阅 Stripe 的计量器事件摘要对象文档。
要列出所有计量器,您可以使用 Billable
实例的 meters
方法。
$user = User::find(1); $user->meters();
订阅税
无需手动计算税率,您可以使用 Stripe Tax 自动计算税款。
要指定用户在订阅上支付的税率,您应该在您的可计费模型上实现 taxRates
方法,并返回一个包含 Stripe 税率 ID 的数组。您可以在Stripe 仪表板中定义这些税率。
/** * The tax rates that should apply to the customer's subscriptions. * * @return array<int, string> */public function taxRates(): array{ return ['txr_id'];}
taxRates
方法使您可以按客户为基础应用税率,这对于跨越多个国家/地区和税率的用户群可能很有帮助。
如果您提供包含多个产品的订阅,您可以通过在您的可计费模型上实现 priceTaxRates
方法来为每个价格定义不同的税率。
/** * The tax rates that should apply to the customer's subscriptions. * * @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('first day of next month'); $request->user()->newSubscription('default', 'price_monthly') ->anchorBillingCycleOn($anchor->startOfDay()) ->create($request->paymentMethodId); // ...});
有关管理订阅账单周期的更多信息,请咨询Stripe 账单周期文档。
取消订阅
要取消订阅,请在用户的订阅上调用 cancel
方法。
$user->subscription('default')->cancel();
取消订阅后,Cashier 将自动设置数据库 subscriptions
表中的 ends_at
列。此列用于确定 subscribed
方法何时开始返回 false
。
例如,如果客户在 3 月 1 日取消订阅,但订阅计划在 3 月 5 日之前结束,则 subscribed
方法将继续返回 true
直到 3 月 5 日。这是因为用户通常可以继续使用应用程序,直到他们的账单周期结束。
您可以使用 onGracePeriod
方法确定用户是否已取消订阅,但仍在“宽限期”内。
if ($user->subscription('default')->onGracePeriod()) { // ...}
如果您想立即取消订阅,请在用户的订阅上调用 cancelNow
方法。
$user->subscription('default')->cancelNow();
如果您想立即取消订阅并为任何剩余未开具发票的计量用量或新的/待处理的按比例分配发票项目开具发票,请在用户的订阅上调用 cancelNowAndInvoice
方法。
$user->subscription('default')->cancelNowAndInvoice();
您也可以选择在特定时间取消订阅。
$user->subscription('default')->cancelAt( now()->addDays(10));
最后,您应始终在删除关联的用户模型之前取消用户订阅。
$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 将此类型的试用称为“通用试用”,因为它未附加到任何现有订阅。如果当前日期未超过 trial_ends_at
的值,则可计费模型实例上的 onTrial
方法将返回 true
。
if ($user->onTrial()) { // User is within their trial period...}
一旦准备好为用户创建实际的订阅,您可以照常使用 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()) { // User is within their "generic" trial period...}
延长试用期
extendTrial
方法允许您在订阅创建后延长订阅的试用期。如果试用期已过期,并且客户已开始为订阅付费,您仍然可以为他们提供延长的试用期。试用期内的时间将从客户的下一张发票中扣除。
use App\Models\User; $subscription = User::find(1)->subscription('default'); // End the trial 7 days from now...$subscription->extendTrial( now()->addDays(7)); // Add an additional 5 days to the trial...$subscription->extendTrial( $subscription->trial_ends_at->addDays(5));
处理 Stripe Webhook
您可以在本地开发期间使用Stripe CLI来帮助测试Webhook。
Stripe可以通过Webhook通知您的应用程序各种事件。默认情况下,Cashier 服务提供商会自动注册一个指向 Cashier Webhook 控制器 的路由。此控制器将处理所有传入的Webhook请求。
默认情况下,Cashier Webhook 控制器将自动处理因收费失败次数过多(由您的 Stripe 设置定义)而取消的订阅、客户更新、客户删除、订阅更新和付款方式更改;但是,正如我们很快就会发现的那样,您可以扩展此控制器来处理任何您喜欢的 Stripe Webhook 事件。
为确保您的应用程序可以处理 Stripe Webhook,请务必在 Stripe 控制面板中配置Webhook URL。默认情况下,Cashier 的Webhook 控制器响应/stripe/webhook
URL 路径。您应该在 Stripe 控制面板中启用的所有Webhook 的完整列表如下:
-
customer.subscription.created
-
customer.subscription.updated
-
customer.subscription.deleted
-
customer.updated
-
customer.deleted
-
payment_method.automatically_updated
-
invoice.payment_action_required
-
invoice.payment_succeeded
为了方便起见,Cashier 包含一个cashier:webhook
Artisan 命令。此命令将在 Stripe 中创建一个Webhook,该Webhook 监听 Cashier 需要的所有事件。
php artisan cashier:webhook
默认情况下,创建的Webhook 将指向由APP_URL
环境变量和 Cashier 附带的cashier.webhook
路由定义的 URL。如果您想使用不同的 URL,可以在调用命令时提供--url
选项。
php artisan cashier:webhook --url "https://example.com/stripe/webhook"
创建的Webhook 将使用您的 Cashier 版本兼容的 Stripe API 版本。如果您想使用不同的 Stripe 版本,可以提供--api-version
选项。
php artisan cashier:webhook --api-version="2019-12-03"
创建后,Webhook 将立即生效。如果您希望创建Webhook 但在准备好之前将其禁用,可以在调用命令时提供--disabled
选项。
php artisan cashier:webhook --disabled
确保使用 Cashier 附带的Webhook 签名验证中间件来保护传入的 Stripe Webhook 请求。
Webhook 和 CSRF 保护
由于 Stripe Webhook 需要绕过 Laravel 的CSRF 保护,您应确保 Laravel 不尝试验证传入 Stripe Webhook 的 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{ /** * Handle received Stripe webhooks. */ public function handle(WebhookReceived $event): void { if ($event->payload['type'] === 'invoice.payment_succeeded') { // Handle the incoming event... } }}
验证 Webhook 签名
为了保护您的 Webhook,您可以使用Stripe 的 Webhook 签名。为了方便起见,Cashier 自动包含一个中间件,用于验证传入的 Stripe Webhook 请求是否有效。
要启用 Webhook 验证,请确保在应用程序的.env
文件中设置STRIPE_WEBHOOK_SECRET
环境变量。Webhooksecret
可以从您的 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('One Time Fee', 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
方法接受以您的应用程序使用的货币的最小单位表示的付款金额。例如,如果客户使用美元支付,则金额应以美分表示。
退款
如果您需要退款,可以使用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 }}">Download</a></td> </tr> @endforeach</table>
即将到来的发票
要检索客户的即将到来的发票,可以使用upcomingInvoice
方法。
$invoice = $user->upcomingInvoice();
同样,如果客户有多个订阅,您也可以检索特定订阅的即将到来的发票。
$invoice = $user->subscription('default')->upcomingInvoice();
预览订阅发票
使用previewInvoice
方法,您可以在进行价格更改之前预览发票。这将允许您确定在进行给定的价格更改时,客户的发票将是什么样子。
$invoice = $user->subscription('default')->previewInvoice('price_yearly');
您可以将价格数组传递给previewInvoice
方法,以便预览具有多个新价格的发票。
$invoice = $user->subscription('default')->previewInvoice(['price_yearly', 'price_metered']);
生成发票 PDF
在生成发票 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' => 'Your Company', 'product' => 'Your Product', 'street' => 'Main Str. 1', 'location' => '2000 Antwerp, Belgium', 'phone' => '+32 499 00 00 00', '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
接口来使用任何您想要的渲染器。例如,您可能希望使用对第三方 PDF 渲染服务的 API 调用来渲染发票 PDF。
use Illuminate\Support\Facades\Http;use Laravel\Cashier\Contracts\InvoiceRenderer;use Laravel\Cashier\Invoice; class ApiInvoiceRenderer implements InvoiceRenderer{ /** * Render the given invoice and return the raw PDF bytes. */ public function render(Invoice $invoice, array $data = [], array $options = []): string { $html = $invoice->view($data)->render(); return Http::get('https://example.com/html-to-pdf', ['html' => $html])->get()->body(); }}
实现发票渲染器契约后,您应该更新应用程序 config/cashier.php
配置文件中的 cashier.invoices.renderer
配置值。此配置值应设置为自定义渲染器实现的类名。
结账
Cashier Stripe 还支持 Stripe Checkout。Stripe Checkout 通过提供预构建的托管支付页面,消除了实现自定义页面以接受支付的麻烦。
以下文档包含有关如何开始使用 Stripe Checkout 与 Cashier 的信息。要了解有关 Stripe Checkout 的更多信息,您还应该考虑查看 Stripe 自身的 Checkout 文档。
产品结账
您可以对已在 Stripe 仪表板中创建的现有产品执行结账,方法是在可计费模型上使用 checkout
方法。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 的结账页面。默认情况下,当用户成功完成或取消购买时,他们将被重定向到您的 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 添加为查询字符串参数。为此,请将文字字符串 {CHECKOUT_SESSION_ID}
添加到您的 success_url
查询字符串中。Stripe 将用实际的结账会话 ID 替换此占位符。
use Illuminate\Http\Request;use Stripe\Checkout\Session;use Stripe\Customer; Route::get('/product-checkout', function (Request $request) { return $request->user()->checkout(['price_tshirt' => 1], [ 'success_url' => route('checkout-success').'?session_id={CHECKOUT_SESSION_ID}', 'cancel_url' => route('checkout-cancel'), ]);}); Route::get('/checkout-success', function (Request $request) { $checkoutSession = $request->user()->stripe()->checkout->sessions->retrieve($request->get('session_id')); return view('checkout.success', ['checkoutSession' => $checkoutSession]);})->name('checkout-success');
促销代码
默认情况下,Stripe Checkout 不允许 用户兑换促销代码。幸运的是,有一种简单的方法可以为您的结账页面启用这些功能。为此,您可以调用 allowPromotionCodes
方法。
use Illuminate\Http\Request; Route::get('/product-checkout', function (Request $request) { return $request->user() ->allowPromotionCodes() ->checkout('price_tshirt');});
单次收费结账
您还可以对尚未在 Stripe 仪表板中创建的临时产品进行简单的收费。为此,您可以在可计费模型上使用 checkoutCharge
方法,并向其传递可收费金额、产品名称和可选数量。当客户访问此路由时,他们将被重定向到 Stripe 的结账页面。
use Illuminate\Http\Request; Route::get('/charge-checkout', function (Request $request) { return $request->user()->checkoutCharge(1200, 'T-Shirt', 5);});
使用 checkoutCharge
方法时,Stripe 将始终在您的 Stripe 仪表板中创建新的产品和价格。因此,我们建议您提前在 Stripe 仪表板中创建产品,并改用 checkout
方法。
订阅结账
使用 Stripe Checkout 进行订阅需要您在 Stripe 仪表板中启用 customer.subscription.created
webhook。此 webhook 将在您的数据库中创建订阅记录并存储所有相关的订阅项目。
您也可以使用 Stripe Checkout 来启动订阅。在使用 Cashier 的订阅构建器方法定义您的订阅后,您可以调用 checkout
方法。当客户访问此路由时,他们将被重定向到 Stripe 的结账页面。
use Illuminate\Http\Request; Route::get('/subscription-checkout', function (Request $request) { return $request->user() ->newSubscription('default', 'price_monthly') ->checkout();});
与产品结账一样,您可以自定义成功和取消 URL。
use Illuminate\Http\Request; Route::get('/subscription-checkout', function (Request $request) { return $request->user() ->newSubscription('default', 'price_monthly') ->checkout([ 'success_url' => route('your-success-route'), 'cancel_url' => route('your-cancel-route'), ]);});
当然,您也可以为订阅结账启用促销代码。
use Illuminate\Http\Request; Route::get('/subscription-checkout', function (Request $request) { return $request->user() ->newSubscription('default', 'price_monthly') ->allowPromotionCodes() ->checkout();});
不幸的是,Stripe Checkout 在启动订阅时并不支持所有订阅计费选项。在订阅构建器上使用 anchorBillingCycleOn
方法、设置比例行为或设置支付行为在 Stripe Checkout 会话期间不会有任何效果。请咨询 Stripe Checkout 会话 API 文档 以查看可用的参数。
Stripe Checkout 和试用期
当然,您可以在构建将使用 Stripe Checkout 完成的订阅时定义试用期。
$checkout = Auth::user()->newSubscription('default', 'price_monthly') ->trialDays(3) ->checkout();
但是,试用期必须至少为 48 小时,这是 Stripe Checkout 支持的最小试用时间。
订阅和 Webhook
请记住,Stripe 和 Cashier 通过 webhook 更新订阅状态,因此客户在输入支付信息后返回应用程序时,订阅可能尚未激活。为了处理这种情况,您可能希望显示一条消息,告知用户他们的付款或订阅正在等待处理。
收集税号
Checkout 还支持收集客户的税务 ID。要在结账会话中启用此功能,请在创建会话时调用 collectTaxIds
方法。
$checkout = $user->collectTaxIds()->checkout('price_tshirt');
调用此方法后,客户将可以使用一个新的复选框来指示他们是否以公司的名义购买。如果是,他们将有机会提供他们的税务 ID 号码。
如果您已经在应用程序的服务提供商中配置了 自动税收征收,则此功能将自动启用,无需调用 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 中。付款页面目前支持以下付款方式:
- 信用卡
- 支付宝
- Bancontact
- BECS 直接借记
- EPS
- Giropay
- iDEAL
- SEPA 直接借记
或者,您可以允许 Stripe 为您处理付款确认。在这种情况下,您可以 在 Stripe 仪表板中设置 Stripe 的自动账单电子邮件,而不是重定向到付款确认页面。但是,如果捕获到 IncompletePayment
异常,您仍然应该告知用户他们将收到包含进一步付款确认说明的电子邮件。
使用 Billable
特性处理模型的以下方法可能会抛出付款异常:charge
、invoiceFor
和 invoice
。在与订阅交互时,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) { // Get the payment intent status... $exception->payment->status; // Check specific conditions... 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
数据库列所示。一旦付款确认完成并且您的应用程序通过 webhook 从 Stripe 收到其完成的通知,Cashier 将自动激活客户的订阅。
有关 incomplete
和 past_due
状态的更多信息,请参阅 我们关于这些状态的其他文档。
非会话付款通知
由于 SCA 规定要求客户偶尔验证他们的付款详细信息(即使他们的订阅处于活动状态),Cashier 可以在需要非会话付款确认时向客户发送通知。例如,这可能发生在订阅续订时。可以通过将 CASHIER_PAYMENT_NOTIFICATION
环境变量设置为通知类来启用 Cashier 的付款通知。默认情况下,此通知已禁用。当然,Cashier 包含一个您可以为此目的使用的通知类,但您可以根据需要提供自己的通知类。
CASHIER_PAYMENT_NOTIFICATION=Laravel\Cashier\Notifications\ConfirmPayment
为了确保交付非会话付款确认通知,请验证 是否为您的应用程序配置了 Stripe webhook 并在您的 Stripe 仪表板中启用了 invoice.payment_action_required
webhook。此外,您的 Billable
模型还应该使用 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提供的各种测试卡号和令牌。