Laravel Cashiers(Stripe)
- 介绍
- 升级 Cashier
- 安装
- 配置
- 快速入门
- 顾客
- 付款方式
- 订阅
- Subscription Trials
- Handling Stripe Webhooks
- Single Charges
- Checkout
- Invoices
- Handling Failed Payments
- Strong Customer Authentication (SCA)
- Stripe SDK
- Testing
介绍
Laravel Cashier Stripe为Stripe 的订阅计费服务提供了一个富有表现力且流畅的接口。它几乎可以处理所有您讨厌的订阅计费样板代码。除了基本的订阅管理之外,Cashier 还可以处理优惠券、订阅交换、订阅“数量”、取消宽限期,甚至生成发票 PDF。
升级 Cashier
升级到 Cashier 的新版本时,仔细阅读升级指南非常重要。
为了避免重大变更,Cashier 使用固定的 Stripe API 版本。Cashier 15 使用的 Stripe API 版本为2023-10-16
。为了充分利用 Stripe 的新功能和改进,Stripe API 版本会在小版本发布时进行更新。
安装
首先,使用 Composer 包管理器安装 Stripe 的 Cashier 包:
1composer require laravel/cashier
安装软件包后,使用vendor:publish
Artisan 命令发布 Cashier 的迁移:
1php artisan vendor:publish --tag="cashier-migrations"
然后,迁移数据库:
1php artisan migrate
Cashier 的迁移会在你的users
表中添加几列。它还会创建一个新subscriptions
表来保存你所有客户的订阅,以及一个subscription_items
包含多个价格的订阅的表。
如果您愿意,您还可以使用 Artisan 命令发布 Cashier 的配置文件vendor:publish
:
1php 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 提供了各种方法,允许您执行常见的计费任务,例如创建订阅、应用优惠券以及更新付款方式信息:
1use Laravel\Cashier\Billable;2 3class User extends Authenticatable4{5 use Billable;6}
Cashier 默认你的计费模型是App\Models\User
Laravel 自带的类。如果你想改变这一点,可以通过该方法指定一个不同的模型。该方法通常应该在你的类useCustomerModel
中调用:boot
AppServiceProvider
1use App\Models\Cashier\User; 2use Laravel\Cashier\Cashier; 3 4/** 5 * Bootstrap any application services. 6 */ 7public function boot(): void 8{ 9 Cashier::useCustomerModel(User::class);10}
如果您使用的模型不是 Laravel 提供的App\Models\User
模型,则需要发布并更改提供的Cashier 迁移以匹配您的替代模型的表名。
API 密钥
接下来,你应该在应用程序文件中配置 Stripe API 密钥.env
。你可以从 Stripe 控制面板获取你的 Stripe API 密钥:
1STRIPE_KEY=your-stripe-key2STRIPE_SECRET=your-stripe-secret3STRIPE_WEBHOOK_SECRET=your-stripe-webhook-secret
您应该确保STRIPE_WEBHOOK_SECRET
在应用程序的.env
文件中定义了环境变量,因为该变量用于确保传入的 webhook 确实来自 Stripe。
货币配置
Cashier 的默认货币是美元 (USD)。您可以通过CASHIER_CURRENCY
在应用程序.env
文件中设置环境变量来更改默认货币:
1CASHIER_CURRENCY=eur
除了配置 Cashier 的货币类型外,你还可以指定用于格式化发票上显示的金额的区域设置。Cashier 内部使用PHP 的NumberFormatter
类来设置货币区域:
1CASHIER_CURRENCY_LOCALE=nl_BE
为了使用除 之外的区域设置en
,请确保ext-intl
在您的服务器上安装并配置了 PHP 扩展。
税务配置
借助Stripe Tax,可以自动计算 Stripe 生成的所有发票的税费。您可以通过在应用程序类的方法calculateTaxes
中调用方法来启用自动税费计算:boot
App\Providers\AppServiceProvider
1use Laravel\Cashier\Cashier;2 3/**4 * Bootstrap any application services.5 */6public function boot(): void7{8 Cashier::calculateTaxes();9}
一旦启用税收计算,任何新订阅和生成的任何一次性发票都将自动进行税收计算。
为确保此功能正常运行,您需要将客户的账单信息(例如客户姓名、地址和税号)同步到 Stripe。您可以使用Cashier 提供的客户数据同步和税号方法来完成此操作。
日志记录
Cashier 允许您指定在记录 Stripe 致命错误时使用的日志通道。您可以通过CASHIER_LOGGER
在应用程序.env
文件中定义环境变量来指定日志通道:
1CASHIER_LOGGER=stack
对 Stripe 的 API 调用产生的异常将通过应用程序的默认日志通道进行记录。
使用自定义模型
你可以自由地扩展 Cashier 内部使用的模型,方法是定义你自己的模型并扩展相应的 Cashier 模型:
1use Laravel\Cashier\Subscription as CashierSubscription;2 3class Subscription extends CashierSubscription4{5 // ...6}
定义好模型后,你可以通过类来指示 Cashier 使用你的自定义模型。通常,你应该在应用类的方法Laravel\Cashier\Cashier
中告知 Cashier 你的自定义模型:boot
App\Providers\AppServiceProvider
1use App\Models\Cashier\Subscription; 2use App\Models\Cashier\SubscriptionItem; 3 4/** 5 * Bootstrap any application services. 6 */ 7public function boot(): void 8{ 9 Cashier::useSubscriptionModel(Subscription::class);10 Cashier::useSubscriptionItemModel(SubscriptionItem::class);11}
快速入门
销售产品
在使用 Stripe Checkout 之前,您需要在 Stripe 控制面板中定义固定价格的产品。此外,您还需要配置 Cashier 的 webhook 处理。
通过您的应用程序提供产品和订阅计费功能可能会令人望而生畏。但是,借助 Cashier 和Stripe Checkout,您可以轻松构建现代、强大的支付集成。
为了向客户收取非循环、单次付费产品的款项,我们将利用 Cashier 将客户引导至 Stripe Checkout,客户将在此处提供付款详情并确认购买。通过 Checkout 付款后,客户将被重定向到您在应用程序中指定的成功 URL:
1use Illuminate\Http\Request; 2 3Route::get('/checkout', function (Request $request) { 4 $stripePriceId = 'price_deluxe_album'; 5 6 $quantity = 1; 7 8 return $request->user()->checkout([$stripePriceId => $quantity], [ 9 'success_url' => route('checkout-success'),10 'cancel_url' => route('checkout-cancel'),11 ]);12})->name('checkout');13 14Route::view('/checkout/success', 'checkout.success')->name('checkout-success');15Route::view('/checkout/cancel', 'checkout.cancel')->name('checkout-cancel');
如上例所示,我们将利用 Cashier 提供的checkout
方法,根据指定的“价格标识符”将客户重定向到 Stripe Checkout。在使用 Stripe 时,“价格”指的是特定产品的指定价格。
如果需要,该checkout
方法会自动在 Stripe 中创建一个客户,并将该 Stripe 客户记录关联到您应用程序数据库中的相应用户。完成结账会话后,客户将被重定向到专门的成功或取消页面,您可以在该页面向客户显示一条信息消息。
向 Stripe Checkout 提供元数据
Cart
在销售产品时,通常会通过您自己应用程序定义的模型来跟踪已完成的订单和已购买的产品Order
。当将客户重定向到 Stripe Checkout 完成购买时,您可能需要提供一个现有的订单标识符,以便在客户重定向回您的应用程序时,能够将已完成的购买与相应的订单关联起来。
metadata
为了实现这一点,您可以为该方法提供一个数组checkout
。假设Order
当用户开始结账Processes时,我们的应用程序中会创建一个 pending 对象。请记住,此示例中的Cart
和Order
模型仅供参考,并非 Cashier 提供。您可以根据自己应用程序的需求自由实现这些概念:
1use App\Models\Cart; 2use App\Models\Order; 3use Illuminate\Http\Request; 4 5Route::get('/cart/{cart}/checkout', function (Request $request, Cart $cart) { 6 $order = Order::create([ 7 'cart_id' => $cart->id, 8 'price_ids' => $cart->price_ids, 9 'status' => 'incomplete',10 ]);11 12 return $request->user()->checkout($order->price_ids, [13 'success_url' => route('checkout-success').'?session_id={CHECKOUT_SESSION_ID}',14 'cancel_url' => route('checkout-cancel'),15 'metadata' => ['order_id' => $order->id],16 ]);17})->name('checkout');
如上例所示,当用户开始结账Processes时,我们会将所有与购物车/订单相关的 Stripe 价格标识符提供给该checkout
方法。当然,您的应用程序负责在客户添加这些商品时,将它们与“购物车”或订单关联起来。我们还通过metadata
数组将订单 ID 提供给 Stripe 的结账会话。最后,我们将模板变量添加CHECKOUT_SESSION_ID
到结账成功路由中。当 Stripe 将客户重定向回您的应用程序时,此模板变量将自动填充结账会话 ID。
接下来,我们来构建结账成功路由。用户通过 Stripe Checkout 完成购买后,将被重定向到此路由。在此路由中,我们可以检索 Stripe Checkout 会话 ID 和关联的 Stripe Checkout 实例,以便访问我们提供的元数据并相应地更新客户的订单:
1use App\Models\Order; 2use Illuminate\Http\Request; 3use Laravel\Cashier\Cashier; 4 5Route::get('/checkout/success', function (Request $request) { 6 $sessionId = $request->get('session_id'); 7 8 if ($sessionId === null) { 9 return;10 }11 12 $session = Cashier::stripe()->checkout->sessions->retrieve($sessionId);13 14 if ($session->payment_status !== 'paid') {15 return;16 }17 18 $orderId = $session['metadata']['order_id'] ?? null;19 20 $order = Order::findOrFail($orderId);21 22 $order->update(['status' => 'completed']);23 24 return view('checkout-success', ['order' => $order]);25})->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
) 下。此外,我们的订阅服务可能还会提供专家级Packagespro_expert
。
首先,让我们了解一下客户如何订阅我们的服务。当然,您可以想象客户可能会点击我们应用程序定价页面上的“订阅”按钮来订阅基础版Packages。此按钮或链接应将用户引导至 Laravel 路由,该路由会根据所选Packages创建 Stripe Checkout 会话:
1use Illuminate\Http\Request; 2 3Route::get('/subscription-checkout', function (Request $request) { 4 return $request->user() 5 ->newSubscription('default', 'price_basic_monthly') 6 ->trialDays(5) 7 ->allowPromotionCodes() 8 ->checkout([ 9 'success_url' => route('your-success-route'),10 'cancel_url' => route('your-cancel-route'),11 ]);12});
如上例所示,我们会将客户重定向到 Stripe Checkout 会话,以便他们订阅我们的 Basic Packages。成功结账或取消后,客户将被重定向回我们提供给该checkout
方法的 URL。为了了解他们的订阅何时真正开始(因为某些支付方式需要几秒钟才能处理),我们还需要配置 Cashier 的 webhook 处理。
subscribed
现在用户可以开始订阅了,我们需要限制应用程序的某些部分,以便只有订阅的用户才能访问它们。当然,我们可以通过Cashier trait 提供的方法来确定用户当前的订阅状态Billable
:
1@if ($user->subscribed())2 <p>You are subscribed.</p>3@endif
我们甚至可以轻松确定用户是否订阅了特定的产品或价格:
1@if ($user->subscribedToProduct('pro_basic'))2 <p>You are subscribed to our Basic product.</p>3@endif4 5@if ($user->subscribedToPrice('price_basic_monthly'))6 <p>You are subscribed to our monthly Basic plan.</p>7@endif
构建订阅中间件
为了方便起见,您可能希望创建一个中间件,用于判断传入的请求是否来自订阅用户。定义好此中间件后,您可以轻松地将其分配给路由,以防止未订阅的用户访问该路由:
1<?php 2 3namespace App\Http\Middleware; 4 5use Closure; 6use Illuminate\Http\Request; 7use Symfony\Component\HttpFoundation\Response; 8 9class Subscribed10{11 /**12 * Handle an incoming request.13 */14 public function handle(Request $request, Closure $next): Response15 {16 if (! $request->user()?->subscribed()) {17 // Redirect user to billing page and ask them to subscribe...18 return redirect('/billing');19 }20 21 return $next($request);22 }23}
一旦定义了中间件,就可以将其分配给路由:
1use App\Http\Middleware\Subscribed;2 3Route::get('/dashboard', function () {4 // ...5})->middleware([Subscribed::class]);
允许客户管理他们的计费计划
当然,客户可能希望将他们的订阅计划更改为其他产品或“层级”。最简单的方法是将客户引导至 Stripe 的客户账单门户,该门户提供了一个托管用户界面,允许客户下载发票、更新付款方式以及更改订阅计划。
首先,在您的应用程序中定义一个链接或按钮,将用户引导至 Laravel 路由,我们将使用该路由启动计费门户会话:
1<a href="{{ route('billing') }}">2 Billing3</a>
接下来,我们来定义一个路由,用于启动 Stripe 客户账单门户会话并将用户重定向到门户。该redirectToBillingPortal
方法接受一个 URL,用户退出门户时应返回到该 URL:
1use Illuminate\Http\Request;2 3Route::get('/billing', function (Request $request) {4 return $request->user()->redirectToBillingPortal(route('dashboard'));5})->middleware(['auth'])->name('billing');
只要您配置了 Cashier 的 webhook 处理,Cashier 就会通过检查来自 Stripe 的传入 webhook,自动同步您应用中与 Cashier 相关的数据库表。例如,当用户通过 Stripe 的客户账单门户取消订阅时,Cashier 会收到相应的 webhook,并在您应用的数据库中将该订阅标记为“已取消”。
顾客
检索客户
You can retrieve a customer by their Stripe ID using the Cashier::findBillable
method. This method will return an instance of the billable model:
1use Laravel\Cashier\Cashier;2 3$user = Cashier::findBillable($stripeId);
Creating Customers
Occasionally, you may wish to create a Stripe customer without beginning a subscription. You may accomplish this using the createAsStripeCustomer
method:
1$stripeCustomer = $user->createAsStripeCustomer();
Once the customer has been created in Stripe, you may begin a subscription at a later date. You may provide an optional $options
array to pass in any additional customer creation parameters that are supported by the Stripe API:
1$stripeCustomer = $user->createAsStripeCustomer($options);
You may use the asStripeCustomer
method if you want to return the Stripe customer object for a billable model:
1$stripeCustomer = $user->asStripeCustomer();
The createOrGetStripeCustomer
method may be used if you would like to retrieve the Stripe customer object for a given billable model but are not sure whether the billable model is already a customer within Stripe. This method will create a new customer in Stripe if one does not already exist:
1$stripeCustomer = $user->createOrGetStripeCustomer();
Updating Customers
Occasionally, you may wish to update the Stripe customer directly with additional information. You may accomplish this using the updateStripeCustomer
method. This method accepts an array of customer update options supported by the Stripe API:
1$stripeCustomer = $user->updateStripeCustomer($options);
Balances
Stripe allows you to credit or debit a customer's "balance". Later, this balance will be credited or debited on new invoices. To check the customer's total balance you may use the balance
method that is available on your billable model. The balance
method will return a formatted string representation of the balance in the customer's currency:
1$balance = $user->balance();
To credit a customer's balance, you may provide a value to the creditBalance
method. If you wish, you may also provide a description:
1$user->creditBalance(500, 'Premium customer top-up.');
Providing a value to the debitBalance
method will debit the customer's balance:
1$user->debitBalance(300, 'Bad usage penalty.');
The applyBalance
method will create new customer balance transactions for the customer. You may retrieve these transaction records using the balanceTransactions
method, which may be useful in order to provide a log of credits and debits for the customer to review:
1// Retrieve all transactions... 2$transactions = $user->balanceTransactions(); 3 4foreach ($transactions as $transaction) { 5 // Transaction amount... 6 $amount = $transaction->amount(); // $2.31 7 8 // Retrieve the related invoice when available... 9 $invoice = $transaction->invoice();10}
Tax IDs
Cashier offers an easy way to manage a customer's tax IDs. For example, the taxIds
method may be used to retrieve all of the tax IDs that are assigned to a customer as a collection:
1$taxIds = $user->taxIds();
You can also retrieve a specific tax ID for a customer by its identifier:
1$taxId = $user->findTaxId('txi_belgium');
You may create a new Tax ID by providing a valid type and value to the createTaxId
method:
1$taxId = $user->createTaxId('eu_vat', 'BE0123456789');
该createTaxId
方法会立即将增值税号添加到客户的账户。增值税号的验证也由 Stripe 完成;但是,这是一个异步过程。您可以通过订阅customer.tax_id.updated
webhook 事件并检查增值税号verification
参数来接收验证更新的通知。有关处理 webhook 的更多信息,请参阅定义 webhook 处理程序的文档。
您可以使用下列方法删除税号deleteTaxId
:
1$user->deleteTaxId('txi_belgium');
使用 Stripe 同步客户数据
通常,当您应用程序的用户更新其姓名、电子邮件地址或其他也由 Stripe 存储的信息时,您应该通知 Stripe 这些更新。这样,Stripe 的信息副本将与您的应用程序同步。
为了实现自动化,您可以在计费模型上定义一个事件监听器,用于响应模型的updated
事件。然后,在事件监听器中,您可以调用syncStripeCustomerDetails
模型上的方法:
1use App\Models\User; 2use function Illuminate\Events\queueable; 3 4/** 5 * The "booted" method of the model. 6 */ 7protected static function booted(): void 8{ 9 static::updated(queueable(function (User $customer) {10 if ($customer->hasStripeId()) {11 $customer->syncStripeCustomerDetails();12 }13 }));14}
现在,每次您的客户模型更新时,其信息都会与 Stripe 同步。为了方便起见,Cashier 会在客户首次创建时自动将其信息同步到 Stripe。
您可以通过重写 Cashier 提供的各种方法来自定义用于将客户信息同步到 Stripe 的列。例如,您可以重写以下stripeName
方法,自定义 Cashier 将客户信息同步到 Stripe 时应被视为客户“姓名”的属性:
1/**2 * Get the customer name that should be synced to Stripe.3 */4public function stripeName(): string|null5{6 return $this->company_name;7}
同样,您可以重写stripeEmail
、stripePhone
、stripeAddress
和stripePreferredLocales
方法。这些方法会在更新 Stripe 客户对象时将信息同步到其对应的客户参数中。如果您希望完全控制客户信息同步过程,可以重写syncStripeCustomerDetails
方法。
计费门户
Stripe 提供了一种简单的方法来设置计费门户,redirectToBillingPortal
以便您的客户可以管理他们的订阅、支付方式并查看他们的计费历史记录。您可以通过从控制器或路由调用 billable 模型上的方法,将用户重定向到计费门户:
1use Illuminate\Http\Request;2 3Route::get('/billing-portal', function (Request $request) {4 return $request->user()->redirectToBillingPortal();5});
默认情况下,当用户完成订阅管理后,他们将能够home
通过 Stripe 计费门户中的链接返回到你的应用程序路由。你可以提供一个自定义 URL,通过将 URL 作为参数传递给该方法来让用户返回到该 URL redirectToBillingPortal
:
1use Illuminate\Http\Request;2 3Route::get('/billing-portal', function (Request $request) {4 return $request->user()->redirectToBillingPortal(route('billing'));5});
如果您希望生成计费门户的 URL 而不生成 HTTP 重定向响应,您可以调用该billingPortalUrl
方法:
1$url = $request->user()->billingPortalUrl(route('billing'));
付款方式
存储付款方式
为了使用 Stripe 创建订阅或执行“一次性”扣款,您需要存储一种付款方式并从 Stripe 中检索其标识符。实现此操作的方法取决于您计划将付款方式用于订阅还是单次扣款,因此我们将在下文中分别进行讲解。
订阅付款方式
在存储客户的信用卡信息以供将来订阅使用时,必须使用 Stripe 的“Setup Intents”API 来安全地收集客户的付款方式详情。“Setup Intent”向 Stripe 表明了通过客户的付款方式进行扣款的意图。Cashier 的Billable
trait 包含一个createSetupIntent
可以轻松创建新的 Setup Intent 的方法。您应该从将呈现用于收集客户付款方式详情的表单的路由或控制器中调用此方法:
1return view('update-payment-method', [2 'intent' => $user->createSetupIntent()3]);
创建“设置意图”并将其传递给视图后,您应该将其密钥附加到用于收集付款方式的元素。例如,请考虑以下“更新付款方式”表单:
1<input id="card-holder-name" type="text">2 3<!-- Stripe Elements Placeholder -->4<div id="card-element"></div>5 6<button id="card-button" data-secret="{{ $intent->client_secret }}">7 Update Payment Method8</button>
接下来,可以使用 Stripe.js 库将Stripe 元素附加到表单并安全地收集客户的付款详细信息:
1<script src="https://js.stripe.com/v3/"></script> 2 3<script> 4 const stripe = Stripe('stripe-public-key'); 5 6 const elements = stripe.elements(); 7 const cardElement = elements.create('card'); 8 9 cardElement.mount('#card-element');10</script>
接下来,可以验证该卡,并使用Stripe 的confirmCardSetup
方法从 Stripe 检索安全的“付款方式标识符” :
1const cardHolderName = document.getElementById('card-holder-name'); 2const cardButton = document.getElementById('card-button'); 3const clientSecret = cardButton.dataset.secret; 4 5cardButton.addEventListener('click', async (e) => { 6 const { setupIntent, error } = await stripe.confirmCardSetup( 7 clientSecret, { 8 payment_method: { 9 card: cardElement,10 billing_details: { name: cardHolderName.value }11 }12 }13 );14 15 if (error) {16 // Display "error.message" to the user...17 } else {18 // The card has been verified successfully...19 }20});
Stripe 验证卡片后,您可以将生成的setupIntent.payment_method
标识符传递给 Laravel 应用程序,以便将其关联到客户。该付款方式可以添加为新的付款方式,也可以用于更新默认付款方式。您还可以立即使用该付款方式标识符创建新的订阅。
如果您想了解有关设置意图和收集客户付款详细信息的更多信息,请查看 Stripe 提供的概述。
单次收费的付款方式
当然,当使用客户的付款方式进行单笔收费时,我们只需使用一次付款方式标识符。由于 Stripe 的限制,您不能使用客户存储的默认付款方式进行单笔收费。您必须允许客户使用 Stripe.js 库输入其付款方式详细信息。例如,请考虑以下表单:
1<input id="card-holder-name" type="text">2 3<!-- Stripe Elements Placeholder -->4<div id="card-element"></div>5 6<button id="card-button">7 Process Payment8</button>
定义这样的表单后,可以使用 Stripe.js 库将Stripe 元素附加到表单并安全地收集客户的付款详细信息:
1<script src="https://js.stripe.com/v3/"></script> 2 3<script> 4 const stripe = Stripe('stripe-public-key'); 5 6 const elements = stripe.elements(); 7 const cardElement = elements.create('card'); 8 9 cardElement.mount('#card-element');10</script>
接下来,可以验证该卡,并使用Stripe 的createPaymentMethod
方法从 Stripe 检索安全的“付款方式标识符” :
1const cardHolderName = document.getElementById('card-holder-name'); 2const cardButton = document.getElementById('card-button'); 3 4cardButton.addEventListener('click', async (e) => { 5 const { paymentMethod, error } = await stripe.createPaymentMethod( 6 'card', cardElement, { 7 billing_details: { name: cardHolderName.value } 8 } 9 );10 11 if (error) {12 // Display "error.message" to the user...13 } else {14 // The card has been verified successfully...15 }16});
如果卡验证成功,您可以将其传递paymentMethod.id
给您的 Laravel 应用程序并处理单笔费用。
检索付款方式
可计费模型实例上的方法paymentMethods
返回实例集合Laravel\Cashier\PaymentMethod
:
1$paymentMethods = $user->paymentMethods();
默认情况下,此方法将返回所有类型的付款方式。要检索特定类型的付款方式,您可以将以下内容type
作为参数传递给该方法:
1$paymentMethods = $user->paymentMethods('sepa_debit');
要检索客户的默认付款方式,defaultPaymentMethod
可以使用该方法:
1$paymentMethod = $user->defaultPaymentMethod();
您可以使用下列方法检索与可计费模型关联的特定付款方式findPaymentMethod
:
1$paymentMethod = $user->findPaymentMethod($paymentMethodId);
付款方式存在
要确定可计费模型是否具有与其帐户关联的默认付款方式,请调用该hasDefaultPaymentMethod
方法:
1if ($user->hasDefaultPaymentMethod()) {2 // ...3}
您可以使用该hasPaymentMethod
方法来确定可计费模型是否至少有一种付款方式附加到其帐户:
1if ($user->hasPaymentMethod()) {2 // ...3}
此方法将确定可计费模型是否有任何付款方式。要确定该模型是否存在特定类型的付款方式,您可以将以下内容type
作为参数传递给该方法:
1if ($user->hasPaymentMethod('sepa_debit')) {2 // ...3}
更新默认付款方式
该updateDefaultPaymentMethod
方法可用于更新客户的默认付款方式信息。此方法接受 Stripe 付款方式标识符,并将新的付款方式指定为默认账单付款方式:
1$user->updateDefaultPaymentMethod($paymentMethod);
要将您的默认付款方式信息与 Stripe 中的客户默认付款方式信息同步,您可以使用该updateDefaultPaymentMethodFromStripe
方法:
1$user->updateDefaultPaymentMethodFromStripe();
客户的默认付款方式仅可用于开具发票和创建新订阅。由于 Stripe 的限制,该付款方式可能无法用于单笔付款。
添加付款方式
要添加新的付款方式,您可以调用addPaymentMethod
可计费模型上的方法,并传递付款方式标识符:
1$user->addPaymentMethod($paymentMethod);
要了解如何检索付款方式标识符,请查看付款方式存储文档。
删除付款方式
要删除付款方式,您可以在要删除的实例delete
上调用该方法:Laravel\Cashier\PaymentMethod
1$paymentMethod->delete();
该deletePaymentMethod
方法将从计费模型中删除特定的付款方式:
1$user->deletePaymentMethod('pm_visa');
该deletePaymentMethods
方法将删除计费模型的所有付款方式信息:
1$user->deletePaymentMethods();
默认情况下,此方法将删除所有类型的付款方式。要删除特定类型的付款方式,您可以将以下内容type
作为参数传递给该方法:
1$user->deletePaymentMethods('sepa_debit');
如果用户有有效订阅,您的应用程序不应允许他们删除其默认付款方式。
订阅
订阅为您的客户提供了一种设置定期付款的方式。由 Cashier 管理的 Stripe 订阅支持多种订阅价格、订阅数量、试用等功能。
创建订阅
To create a subscription, first retrieve an instance of your billable model, which typically will be an instance of App\Models\User
. Once you have retrieved the model instance, you may use the newSubscription
method to create the model's subscription:
1use Illuminate\Http\Request;2 3Route::post('/user/subscribe', function (Request $request) {4 $request->user()->newSubscription(5 'default', 'price_monthly'6 )->create($request->paymentMethodId);7 8 // ...9});
The first argument passed to the newSubscription
method should be the internal type of the subscription. If your application only offers a single subscription, you might call this default
or primary
. This subscription type is only for internal application usage and is not meant to be shown to users. In addition, it should not contain spaces and it should never be changed after creating the subscription. The second argument is the specific price the user is subscribing to. This value should correspond to the price's identifier in Stripe.
The create
method, which accepts a Stripe payment method identifier or Stripe PaymentMethod
object, will begin the subscription as well as update your database with the billable model's Stripe customer ID and other relevant billing information.
Passing a payment method identifier directly to the create
subscription method will also automatically add it to the user's stored payment methods.
Collecting Recurring Payments via Invoice Emails
Instead of collecting a customer's recurring payments automatically, you may instruct Stripe to email an invoice to the customer each time their recurring payment is due. Then, the customer may manually pay the invoice once they receive it. The customer does not need to provide a payment method up front when collecting recurring payments via invoices:
1$user->newSubscription('default', 'price_monthly')->createAndSendInvoice();
The amount of time a customer has to pay their invoice before their subscription is canceled is determined by the days_until_due
option. By default, this is 30 days; however, you may provide a specific value for this option if you wish:
1$user->newSubscription('default', 'price_monthly')->createAndSendInvoice([], [2 'days_until_due' => 303]);
Quantities
If you would like to set a specific quantity for the price when creating the subscription, you should invoke the quantity
method on the subscription builder before creating the subscription:
1$user->newSubscription('default', 'price_monthly')2 ->quantity(5)3 ->create($paymentMethod);
Additional Details
If you would like to specify additional customer or subscription options supported by Stripe, you may do so by passing them as the second and third arguments to the create
method:
1$user->newSubscription('default', 'price_monthly')->create($paymentMethod, [2 'email' => $email,3], [4 'metadata' => ['note' => 'Some extra information.'],5]);
Coupons
If you would like to apply a coupon when creating the subscription, you may use the withCoupon
method:
1$user->newSubscription('default', 'price_monthly')2 ->withCoupon('code')3 ->create($paymentMethod);
Or, if you would like to apply a Stripe promotion code, you may use the withPromotionCode
method:
1$user->newSubscription('default', 'price_monthly')2 ->withPromotionCode('promo_code_id')3 ->create($paymentMethod);
The given promotion code ID should be the Stripe API ID assigned to the promotion code and not the customer facing promotion code. If you need to find a promotion code ID based on a given customer facing promotion code, you may use the findPromotionCode
method:
1// Find a promotion code ID by its customer facing code...2$promotionCode = $user->findPromotionCode('SUMMERSALE');3 4// Find an active promotion code ID by its customer facing code...5$promotionCode = $user->findActivePromotionCode('SUMMERSALE');
在上面的示例中,返回的$promotionCode
对象是 的一个实例Laravel\Cashier\PromotionCode
。该类修饰了一个底层Stripe\PromotionCode
对象。您可以通过调用该方法来检索与促销代码相关的优惠券coupon
:
1$coupon = $user->findPromotionCode('SUMMERSALE')->coupon();
优惠券实例允许您确定折扣金额以及优惠券是否代表固定折扣或基于百分比的折扣:
1if ($coupon->isPercentage()) {2 return $coupon->percentOff().'%'; // 21.5%3} else {4 return $coupon->amountOff(); // $5.995}
您还可以检索当前应用于客户或订阅的折扣:
1$discount = $billable->discount();2 3$discount = $subscription->discount();
返回的Laravel\Cashier\Discount
实例装饰了底层Stripe\Discount
对象实例。您可以通过调用该方法获取与此折扣相关的优惠券coupon
:
1$coupon = $subscription->discount()->coupon();
如果您想将新的优惠券或促销代码应用于客户或订阅,您可以通过以下applyCoupon
或applyPromotionCode
方法进行操作:
1$billable->applyCoupon('coupon_id');2$billable->applyPromotionCode('promotion_code_id');3 4$subscription->applyCoupon('coupon_id');5$subscription->applyPromotionCode('promotion_code_id');
请记住,您应该使用分配给促销代码的 Stripe API ID,而不是面向客户的促销代码。同一时间,每位客户或订阅只能使用一张优惠券或促销代码。
有关此主题的更多信息,请参阅有关优惠券和促销代码的 Stripe 文档。
添加订阅
如果您想为已经有默认付款方式的客户添加订阅,您可以add
在订阅构建器上调用该方法:
1use App\Models\User;2 3$user = User::find(1);4 5$user->newSubscription('default', 'price_monthly')->add();
从 Stripe 控制面板创建订阅
您也可以从 Stripe 仪表板本身创建订阅。这样做时,Cashier 将同步新添加的订阅并为其分配一个 类型default
。要自定义分配给仪表板创建的订阅的订阅类型,请定义 webhook 事件处理程序。
此外,您只能通过 Stripe 控制面板创建一种订阅类型。如果您的应用提供多种不同类型的订阅,则您只能通过 Stripe 控制面板添加一种订阅类型。
最后,您应始终确保针对应用提供的每种订阅类型只添加一个有效订阅。如果客户有两个default
订阅,即使两个订阅都会同步到应用的数据库,Cashier 也只会使用最近添加的订阅。
检查订阅状态
一旦客户订阅了您的应用,您就可以使用各种便捷的方法轻松检查其订阅状态。首先,即使订阅目前处于试用期,该subscribed
方法也会返回true
客户是否拥有有效订阅。该subscribed
方法接受订阅类型作为其第一个参数:
1if ($user->subscribed('default')) {2 // ...3}
该subscribed
方法也非常适合用作路由中间件,允许您根据用户的订阅状态过滤对路由和控制器的访问:
1<?php 2 3namespace App\Http\Middleware; 4 5use Closure; 6use Illuminate\Http\Request; 7use Symfony\Component\HttpFoundation\Response; 8 9class EnsureUserIsSubscribed10{11 /**12 * Handle an incoming request.13 *14 * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next15 */16 public function handle(Request $request, Closure $next): Response17 {18 if ($request->user() && ! $request->user()->subscribed('default')) {19 // This user is not a paying customer...20 return redirect('/billing');21 }22 23 return $next($request);24 }25}
如果您想确定用户是否仍处于试用期,可以使用该onTrial
方法。此方法可用于确定是否应向用户显示其仍处于试用期的警告:
1if ($user->subscription('default')->onTrial()) {2 // ...3}
该subscribedToProduct
方法可用于根据给定的 Stripe 产品标识符判断用户是否订阅了指定产品。在 Stripe 中,产品是价格的集合。在本例中,我们将判断用户是否default
已订阅应用程序的“高级”产品。给定的 Stripe 产品标识符应与 Stripe 控制面板中您的某个产品标识符相对应:
1if ($user->subscribedToProduct('prod_premium', 'default')) {2 // ...3}
通过将数组传递给subscribedToProduct
方法,您可以确定用户的default
订阅是否主动订阅了应用程序的“基本”或“高级”产品:
1if ($user->subscribedToProduct(['prod_basic', 'prod_premium'], 'default')) {2 // ...3}
该subscribedToPrice
方法可用于确定客户的订阅是否对应于给定的价格 ID:
1if ($user->subscribedToPrice('price_basic_monthly', 'default')) {2 // ...3}
该recurring
方法可用于确定用户当前是否已订阅且不再处于试用期内:
1if ($user->subscription('default')->recurring()) {2 // ...3}
如果用户有两个相同类型的订阅,该方法将始终返回最新的订阅subscription
。例如,一个用户可能有两个类型为 的订阅记录default
;但是,其中一个订阅可能是旧的、已过期的订阅,而另一个是当前的、有效的订阅。该方法将始终返回最新的订阅,而较旧的订阅将保留在数据库中以供历史回顾。
取消订阅状态
要确定用户是否曾经是活跃订阅者但已取消订阅,您可以使用该canceled
方法:
1if ($user->subscription('default')->canceled()) {2 // ...3}
您还可以确定用户是否已取消订阅,但仍处于“宽限期”,直至订阅完全到期。例如,如果用户在 3 月 5 日取消了原定于 3 月 10 日到期的订阅,则该用户仍处于“宽限期”,直至 3 月 10 日。请注意,该方法在此期间subscribed
仍会返回:true
1if ($user->subscription('default')->onGracePeriod()) {2 // ...3}
要确定用户是否已取消订阅且不再处于“宽限期”,您可以使用该ended
方法:
1if ($user->subscription('default')->ended()) {2 // ...3}
不完整和逾期状态
如果订阅在创建后需要进行二次支付操作,则该订阅将被标记为incomplete
。订阅状态存储在stripe_status
Cashiersubscriptions
数据库表的相应列中。
同样,如果在交换价格时需要进行二次付款操作,则订阅将被标记为。当您的订阅处于以下任一状态时,它将在客户确认付款之前无法生效。您可以使用计费模型或订阅实例上的方法past_due
来确定订阅是否付款未完成:hasIncompletePayment
1if ($user->hasIncompletePayment('default')) {2 // ...3}4 5if ($user->subscription('default')->hasIncompletePayment()) {6 // ...7}
当订阅付款未完成时,您应该将用户引导至 Cashier 的付款确认页面,并传递latestPayment
标识符。您可以使用latestPayment
订阅实例上提供的方法来获取此标识符:
1<a href="{{ route('cashier.payment', $subscription->latestPayment()->id) }}">2 Please confirm your payment.3</a>
如果您希望订阅在past_due
或incomplete
状态下仍被视为有效,您可以使用Cashier 提供的keepPastDueSubscriptionsActive
和方法。通常,这些方法应该在您的 的方法keepIncompleteSubscriptionsActive
中调用:register
App\Providers\AppServiceProvider
1use Laravel\Cashier\Cashier; 2 3/** 4 * Register any application services. 5 */ 6public function register(): void 7{ 8 Cashier::keepPastDueSubscriptionsActive(); 9 Cashier::keepIncompleteSubscriptionsActive();10}
订阅处于某种incomplete
状态时,在付款确认之前无法更改。因此,当订阅处于某种状态时,swap
和方法将抛出异常。updateQuantity
incomplete
订阅范围
大多数订阅状态也可作为查询范围使用,以便您可以轻松地在数据库中查询处于给定状态的订阅:
1// Get all active subscriptions...2$subscriptions = Subscription::query()->active()->get();3 4// Get all of the canceled subscriptions for a user...5$subscriptions = $user->subscriptions()->canceled()->get();
可用范围的完整列表如下:
1Subscription::query()->active(); 2Subscription::query()->canceled(); 3Subscription::query()->ended(); 4Subscription::query()->incomplete(); 5Subscription::query()->notCanceled(); 6Subscription::query()->notOnGracePeriod(); 7Subscription::query()->notOnTrial(); 8Subscription::query()->onGracePeriod(); 9Subscription::query()->onTrial();10Subscription::query()->pastDue();11Subscription::query()->recurring();
价格变动
客户订阅您的应用后,可能偶尔会想要更改为新的订阅价格。要将客户切换到新价格,请将 Stripe 价格标识符传递给该swap
方法。切换价格时,系统会假设用户希望重新激活之前已取消的订阅。给定的价格标识符应与 Stripe 控制面板中可用的 Stripe 价格标识符相对应:
1use App\Models\User;2 3$user = App\Models\User::find(1);4 5$user->subscription('default')->swap('price_yearly');
如果客户处于试用期,则试用期将保持不变。此外,如果订阅存在“数量”,则该数量也将保持不变。
如果您想交换价格并取消客户当前的任何试用期,您可以调用该skipTrial
方法:
1$user->subscription('default')2 ->skipTrial()3 ->swap('price_yearly');
如果您想交换价格并立即向客户开具发票而不是等到他们的下一个结算周期,您可以使用该swapAndInvoice
方法:
1$user = User::find(1);2 3$user->subscription('default')->swapAndInvoice('price_yearly');
按比例分配
默认情况下,Stripe 在交换价格时会按比例计算费用。noProrate
可以使用该方法来更新订阅价格,而无需按比例计算费用:
1$user->subscription('default')->noProrate()->swap('price_yearly');
有关订阅按比例分配的更多信息,请参阅Stripe 文档。
noProrate
在方法之前
执行该方法swapAndInvoice
不会影响按比例分配。始终会开具发票。
订阅数量
有时订阅会受到“数量”的影响。例如,一个项目管理应用可能每个项目每月收费 10 美元。您可以使用incrementQuantity
和decrementQuantity
方法轻松增加或减少订阅数量:
1use App\Models\User; 2 3$user = User::find(1); 4 5$user->subscription('default')->incrementQuantity(); 6 7// Add five to the subscription's current quantity... 8$user->subscription('default')->incrementQuantity(5); 9 10$user->subscription('default')->decrementQuantity();11 12// Subtract five from the subscription's current quantity...13$user->subscription('default')->decrementQuantity(5);
或者,您可以使用该方法设置特定数量updateQuantity
:
1$user->subscription('default')->updateQuantity(10);
该noProrate
方法可用于更新订阅的数量而不按比例分配费用:
1$user->subscription('default')->noProrate()->updateQuantity(10);
有关订阅数量的更多信息,请参阅Stripe 文档。
多个产品订阅数量
如果您的订阅是包含多个产品的订阅,则应将希望增加或减少数量的价格 ID 作为第二个参数传递给增量/减量方法:
1$user->subscription('default')->incrementQuantity(1, 'price_chat');
多种产品订阅
多产品订阅允许您将多个计费产品分配给单个订阅。例如,假设您正在构建一个客户服务“帮助台”应用程序,其基本订阅价格为每月 10 美元,但提供实时聊天附加产品,每月需额外支付 15 美元。多产品订阅的信息存储在 Cashier 的subscription_items
数据库表中。
您可以通过将价格数组作为第二个参数传递给newSubscription
方法,为给定的订阅指定多个产品:
1use Illuminate\Http\Request; 2 3Route::post('/user/subscribe', function (Request $request) { 4 $request->user()->newSubscription('default', [ 5 'price_monthly', 6 'price_chat', 7 ])->create($request->paymentMethodId); 8 9 // ...10});
在上面的示例中,客户的订阅将包含两个价格default
。这两个价格将按各自的计费周期收取。如有必要,您可以使用该quantity
方法为每个价格指定具体数量:
1$user = User::find(1);2 3$user->newSubscription('default', ['price_monthly', 'price_chat'])4 ->quantity(5, 'price_chat')5 ->create($paymentMethod);
如果您想为现有订阅添加另一个价格,您可以调用订阅的addPrice
方法:
1$user = User::find(1);2 3$user->subscription('default')->addPrice('price_chat');
上面的示例将添加新价格,客户将在下一个计费周期支付相应费用。如果您想立即向客户计费,可以使用以下addPriceAndInvoice
方法:
1$user->subscription('default')->addPriceAndInvoice('price_chat');
如果您想添加具有特定数量的价格,您可以将数量作为addPrice
或addPriceAndInvoice
方法的第二个参数传递:
1$user = User::find(1);2 3$user->subscription('default')->addPrice('price_chat', 5);
您可以使用下列removePrice
方法从订阅中删除价格:
1$user->subscription('default')->removePrice('price_chat');
您无法移除订阅的最终价格。您只需取消订阅即可。
交换价格
您还可以更改包含多个产品的订阅价格。例如,假设一位客户订阅了price_basic
附加price_chat
产品,您想将其价格从以下价格升级到price_basic
以下price_pro
价格:
1use App\Models\User;2 3$user = User::find(1);4 5$user->subscription('default')->swap(['price_pro', 'price_chat']);
执行上述示例时,包含 的底层订阅项目price_basic
将被删除,包含 的订阅项目price_chat
将被保留。此外,还会创建一个新的 的订阅项目price_pro
。
您还可以通过向该方法传递一个键值对数组来指定订阅项选项swap
。例如,您可能需要指定订阅价格和数量:
1$user = User::find(1);2 3$user->subscription('default')->swap([4 'price_pro' => ['quantity' => 5],5 'price_chat'6]);
如果您想交换订阅中的单个价格,可以使用swap
订阅商品本身的方法。如果您希望保留订阅中其他价格的所有现有元数据,此方法尤其有用:
1$user = User::find(1);2 3$user->subscription('default')4 ->findItemOrFail('price_basic')5 ->swap('price_pro');
按比例分配
默认情况下,Stripe 会在为包含多个产品的订阅添加或移除价格时按比例收费。如果您不想按比例调整价格,则应将该noProrate
方法链接到您的价格操作中:
1$user->subscription('default')->noProrate()->removePrice('price_chat');
数量
如果您想要更新单个订阅价格的数量,您可以使用现有的数量方法,通过将价格 ID 作为附加参数传递给方法来实现:
1$user = User::find(1);2 3$user->subscription('default')->incrementQuantity(5, 'price_chat');4 5$user->subscription('default')->decrementQuantity(3, 'price_chat');6 7$user->subscription('default')->updateQuantity(10, 'price_chat');
当订阅包含多个价格时,模型中的stripe_price
和属性将为。要访问各个价格属性,您应该使用模型中可用的关系。quantity
Subscription
null
items
Subscription
订阅项目
当订阅包含多个价格时,它会在数据库subscription_items
表中存储多个订阅“项目”。您可以通过items
订阅上的关系访问这些项目:
1use App\Models\User;2 3$user = User::find(1);4 5$subscriptionItem = $user->subscription('default')->items->first();6 7// Retrieve the Stripe price and quantity for a specific item...8$stripePrice = $subscriptionItem->stripe_price;9$quantity = $subscriptionItem->quantity;
您还可以使用下列方法检索特定价格findItemOrFail
:
1$user = User::find(1);2 3$subscriptionItem = $user->subscription('default')->findItemOrFail('price_chat');
多个订阅
Stripe 允许您的客户同时订阅多个Packages。例如,您可能经营一家健身房,提供游泳Packages和举重Packages,每个Packages的价格可能不同。当然,客户应该可以同时订阅其中一个Packages或两个Packages。
当你的应用创建订阅时,你可以向该方法提供订阅的类型newSubscription
。该类型可以是表示用户正在发起的订阅类型的任何字符串:
1use Illuminate\Http\Request;2 3Route::post('/swimming/subscribe', function (Request $request) {4 $request->user()->newSubscription('swimming')5 ->price('price_swimming_monthly')6 ->create($request->paymentMethodId);7 8 // ...9});
在此示例中,我们为客户启动了按月订阅的游泳服务。但是,他们以后可能想切换为按年订阅。调整客户订阅时,我们只需调整订阅价格即可swimming
:
1$user->subscription('swimming')->swap('price_swimming_yearly');
当然,您也可以完全取消订阅:
1$user->subscription('swimming')->cancel();
基于使用情况的计费
基于使用量的计费允许您根据客户在一个计费周期内的产品使用情况向其收取费用。例如,您可以根据客户每月发送的短信或电子邮件数量来收费。
要开始使用按使用量计费,您首先需要在 Stripe 控制面板中创建一个新的产品,并采用基于使用量的计费模型和一个计量器 (meter)。创建计量器后,请存储关联的事件名称和计量器 ID,以便报告和检索使用情况。然后,使用以下meteredPrice
方法将计量价格 ID 添加到客户订阅:
1use Illuminate\Http\Request;2 3Route::post('/user/subscribe', function (Request $request) {4 $request->user()->newSubscription('default')5 ->meteredPrice('price_metered')6 ->create($request->paymentMethodId);7 8 // ...9});
您还可以通过Stripe Checkout开始计量订阅:
1$checkout = Auth::user()2 ->newSubscription('default', [])3 ->meteredPrice('price_metered')4 ->checkout();5 6return view('your-checkout-view', [7 'checkout' => $checkout,8]);
报告使用情况
当您的客户使用您的应用程序时,您需要向 Stripe 报告他们的使用情况,以便他们准确计费。要报告计量事件的使用情况,您可以在模型reportMeterEvent
上使用该方法Billable
:
1$user = User::find(1);2 3$user->reportMeterEvent('emails-sent');
默认情况下,计费周期会添加 1 个“使用量”。或者,您可以传递一个特定的“使用量”,将其添加到客户计费周期的使用量中:
1$user = User::find(1);2 3$user->reportMeterEvent('emails-sent', quantity: 15);
要检索电表的客户事件摘要,您可以使用Billable
实例的meterEventSummaries
方法:
1$user = User::find(1);2 3$meterUsage = $user->meterEventSummaries($meterId);4 5$meterUsage->first()->aggregated_value // 10
有关仪表事件摘要的更多信息,请参阅 Stripe 的仪表事件摘要对象文档。
要列出所有仪表,您可以使用Billable
实例的meters
方法:
1$user = User::find(1);2 3$user->meters();
订阅税
您无需手动计算税率,而是可以使用 Stripe Tax 自动计算税费
要指定用户在订阅中支付的税率,您应该taxRates
在 Billable 模型中实现该方法,并返回一个包含 Stripe 税率 ID 的数组。您可以在Stripe 控制面板中定义这些税率:
1/**2 * The tax rates that should apply to the customer's subscriptions.3 *4 * @return array<int, string>5 */6public function taxRates(): array7{8 return ['txr_id'];9}
该taxRates
方法使您能够根据每个客户应用税率,这对于跨越多个国家和税率的用户群可能很有帮助。
priceTaxRates
如果您提供多种产品的订阅,则可以通过在计费模型上实施方法为每种价格定义不同的税率:
1/** 2 * The tax rates that should apply to the customer's subscriptions. 3 * 4 * @return array<string, array<int, string>> 5 */ 6public function priceTaxRates(): array 7{ 8 return [ 9 'price_monthly' => ['txr_id'],10 ];11}
此taxRates
方法仅适用于订阅费用。如果您使用 Cashier 进行“一次性”收费,则需要手动指定税率。
同步税率
更改该方法返回的硬编码税率 ID 时taxRates
,用户所有现有订阅的税费设置将保持不变。如果您希望使用新taxRates
值更新现有订阅的税费值,则应syncTaxRates
在用户的订阅实例上调用该方法:
1$user->subscription('default')->syncTaxRates();
这还将同步包含多个产品的订阅的所有商品税率。如果您的应用提供包含多个产品的订阅,则应确保您的计费模型实现了上述priceTaxRates
方法。
免税
Cashier 还提供了isNotTaxExempt
、isTaxExempt
和reverseChargeApplies
方法来判断客户是否免税。这些方法会调用 Stripe API 来判断客户的免税状态:
1use App\Models\User;2 3$user = User::find(1);4 5$user->isTaxExempt();6$user->isNotTaxExempt();7$user->reverseChargeApplies();
这些方法也适用于任何Laravel\Cashier\Invoice
对象。但是,当在对象上调用时Invoice
,这些方法将确定发票创建时的免税状态。
订阅锚定日期
默认情况下,计费周期锚点是订阅的创建日期,或者如果使用了试用期,则为试用结束日期。如果您想修改计费锚点日期,可以使用以下anchorBillingCycleOn
方法:
1use Illuminate\Http\Request; 2 3Route::post('/user/subscribe', function (Request $request) { 4 $anchor = Carbon::parse('first day of next month'); 5 6 $request->user()->newSubscription('default', 'price_monthly') 7 ->anchorBillingCycleOn($anchor->startOfDay()) 8 ->create($request->paymentMethodId); 9 10 // ...11});
有关管理订阅计费周期的更多信息,请参阅Stripe 计费周期文档
取消订阅
要取消订阅,请调用cancel
用户订阅上的方法:
1$user->subscription('default')->cancel();
当订阅被取消时,Cashier 会自动设置数据库表ends_at
中的相应列subscriptions
。此列用于确定该subscribed
方法何时开始返回false
。
例如,如果客户在 3 月 1 日取消订阅,但订阅原定于 3 月 5 日结束,则该subscribed
方法将继续返回true
到 3 月 5 日。这样做是因为用户通常被允许继续使用应用程序直到其计费周期结束。
您可以使用该方法确定用户是否已取消订阅但仍处于“宽限期” onGracePeriod
:
1if ($user->subscription('default')->onGracePeriod()) {2 // ...3}
如果您希望立即取消订阅,请cancelNow
在用户的订阅上调用该方法:
1$user->subscription('default')->cancelNow();
如果您希望立即取消订阅并对任何剩余的未开票计量使用或新的/待处理的按比例计费发票项目开具发票,请cancelNowAndInvoice
在用户的订阅上调用该方法:
1$user->subscription('default')->cancelNowAndInvoice();
您也可以选择在特定时间点取消订阅:
1$user->subscription('default')->cancelAt(2 now()->addDays(10)3);
最后,您应该始终在删除关联的用户模型之前取消用户订阅:
1$user->subscription('default')->cancelNow();2 3$user->delete();
恢复订阅
如果客户已取消订阅,而您希望恢复订阅,则可以resume
在订阅上调用该方法。客户必须仍处于“宽限期”内才能恢复订阅:
1$user->subscription('default')->resume();
如果客户取消订阅,然后在订阅完全到期之前恢复订阅,则不会立即向客户收费。相反,他们的订阅将被重新激活,并按照原计费周期计费。
订阅试用
预付付款方式
如果您希望为客户提供试用期,同时仍提前收集付款方式信息,则应trialDays
在创建订阅时使用该方法:
1use Illuminate\Http\Request;2 3Route::post('/user/subscribe', function (Request $request) {4 $request->user()->newSubscription('default', 'price_monthly')5 ->trialDays(10)6 ->create($request->paymentMethodId);7 8 // ...9});
此方法将在数据库中的订阅记录中设置试用期结束日期,并指示 Stripe 在此日期之后才开始向客户计费。使用该trialDays
方法时,Cashier 将覆盖 Stripe 中为价格配置的任何默认试用期。
如果客户未在试用结束日期之前取消订阅,则试用期一过,他们就会被收费,因此您应该确保通知您的用户他们的试用结束日期。
该trialUntil
方法允许您提供一个DateTime
实例来指定试用期何时结束:
1use Carbon\Carbon;2 3$user->newSubscription('default', 'price_monthly')4 ->trialUntil(Carbon::now()->addDays(10))5 ->create($paymentMethod);
onTrial
您可以使用用户实例的方法或订阅实例的方法判断用户是否处于试用期内onTrial
。以下两个示例是等效的:
1if ($user->onTrial('default')) {2 // ...3}4 5if ($user->subscription('default')->onTrial()) {6 // ...7}
您可以使用该endTrial
方法立即结束订阅试用:
1$user->subscription('default')->endTrial();
要确定现有试用是否已过期,您可以使用以下hasExpiredTrial
方法:
1if ($user->hasExpiredTrial('default')) {2 // ...3}4 5if ($user->subscription('default')->hasExpiredTrial()) {6 // ...7}
在 Stripe / Cashier 中定义试用天数
您可以选择在 Stripe 控制面板中定义价格的试用天数,也可以始终使用 Cashier 明确传递。如果您选择在 Stripe 中定义价格的试用天数,则需要注意,除非您明确调用该方法,否则新订阅(包括过去已有订阅的客户的新订阅)将始终享有试用期skipTrial()
。
无需预付付款方式
如果您希望提供试用期,但又不想预先收集用户的付款方式信息,则可以将trial_ends_at
用户记录中的相应列设置为您希望的试用结束日期。这通常在用户注册时完成:
1use App\Models\User;2 3$user = User::create([4 // ...5 'trial_ends_at' => now()->addDays(10),6]);
请确保在可计费模型的类定义中为属性添加日期转换。trial_ends_at
Cashier 将这种试用类型称为“通用试用”,因为它不附加到任何现有订阅。如果当前日期不晚于 的值,则onTrial
billable 模型实例上的方法将返回:true
trial_ends_at
1if ($user->onTrial()) {2 // User is within their trial period...3}
一旦您准备好为用户创建实际订阅,您可以newSubscription
照常使用该方法:
1$user = User::find(1);2 3$user->newSubscription('default', 'price_monthly')->create($paymentMethod);
要获取用户的试用结束日期,您可以使用该trialEndsAt
方法。无论用户是否处于试用状态,此方法都会返回一个 Carbon 日期实例null
。如果您想获取特定订阅(而非默认订阅)的试用结束日期,还可以传递一个可选的订阅类型参数:
1if ($user->onTrial()) {2 $trialEndsAt = $user->trialEndsAt('main');3}
onGenericTrial
如果您希望具体了解用户是否处于“通用”试用期内并且尚未创建实际订阅,您也可以使用该方法:
1if ($user->onGenericTrial()) {2 // User is within their "generic" trial period...3}
延长试验
该extendTrial
方法允许您在订阅创建后延长其试用期。如果试用期已到期,且客户已支付订阅费用,您仍然可以为其提供延长试用。试用期内使用的时间将从客户的下一张账单中扣除:
1use App\Models\User; 2 3$subscription = User::find(1)->subscription('default'); 4 5// End the trial 7 days from now... 6$subscription->extendTrial( 7 now()->addDays(7) 8); 9 10// Add an additional 5 days to the trial...11$subscription->extendTrial(12 $subscription->trial_ends_at->addDays(5)13);
处理 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,用于监听 Cashier 所需的所有事件:
1php artisan cashier:webhook
默认情况下,创建的 webhook 将指向APP_URL
环境变量中定义的 URL 以及cashier.webhook
Cashier 自带的路由。--url
如果您想使用其他 URL,可以在调用命令时提供选项:
1php artisan cashier:webhook --url "https://example.com/stripe/webhook"
创建的 webhook 将使用与您的 Cashier 版本兼容的 Stripe API 版本。如果您想使用其他 Stripe 版本,您可以提供以下--api-version
选项:
1php artisan cashier:webhook --api-version="2019-12-03"
创建后,webhook 将立即生效。如果您希望创建 webhook 并希望在准备就绪之前将其禁用,则可以--disabled
在调用命令时提供以下选项:
1php artisan cashier:webhook --disabled
确保使用 Cashier 包含的webhook 签名验证中间件保护传入的 Stripe webhook 请求。
Webhook 和 CSRF 保护
由于 Stripe webhook 需要绕过 Laravel 的CSRF 保护,因此你应该确保 Laravel 不会尝试验证传入 Stripe webhook 的 CSRF 令牌。为此,你应该stripe/*
在应用程序的bootstrap/app.php
文件中将其排除在 CSRF 保护之外:
1->withMiddleware(function (Middleware $middleware) {2 $middleware->validateCsrfTokens(except: [3 'stripe/*',4 ]);5})
定义 Webhook 事件处理程序
Cashier 会自动处理因扣款失败和其他常见的 Stripe webhook 事件而导致的订阅取消。但是,如果您还有其他 webhook 事件需要处理,可以通过监听 Cashier 调度的以下事件来实现:
Laravel\Cashier\Events\WebhookReceived
Laravel\Cashier\Events\WebhookHandled
这两个事件都包含 Stripe webhook 的完整负载。例如,如果您希望处理invoice.payment_succeeded
webhook,可以注册一个监听器来处理该事件:
1<?php 2 3namespace App\Listeners; 4 5use Laravel\Cashier\Events\WebhookReceived; 6 7class StripeEventListener 8{ 9 /**10 * Handle received Stripe webhooks.11 */12 public function handle(WebhookReceived $event): void13 {14 if ($event->payload['type'] === 'invoice.payment_succeeded') {15 // Handle the incoming event...16 }17 }18}
验证 Webhook 签名
为了保护你的 webhook,你可以使用Stripe 的 webhook 签名。为了方便起见,Cashier 自动包含一个中间件,用于验证传入的 Stripe webhook 请求是否有效。
要启用 webhook 验证,请确保STRIPE_WEBHOOK_SECRET
在应用程序文件中设置了环境变量。您可以从 Stripe 帐户仪表板中检索.env
webhook 。secret
单次收费
简单充电
如果您想向客户进行一次性收费,您可以charge
在可计费模型实例上使用此方法。您需要提供一个付款方式标识符作为该方法的第二个参数charge
:
1use Illuminate\Http\Request;2 3Route::post('/purchase', function (Request $request) {4 $stripeCharge = $request->user()->charge(5 100, $request->paymentMethodId6 );7 8 // ...9});
该charge
方法接受一个数组作为第三个参数,允许您将任何所需的选项传递给底层 Stripe 费用创建过程。有关创建费用时可用选项的更多信息,请参阅Stripe 文档:
1$user->charge(100, $paymentMethod, [2 'custom_option' => $value,3]);
您也可以charge
在没有底层客户或用户的情况下使用该方法。为此,请charge
在应用程序的可计费模型的新实例上调用该方法:
1use App\Models\User;2 3$stripeCharge = (new User)->charge(100, $paymentMethod);
如果充值失败,该charge
方法将抛出异常。如果充值成功,Laravel\Cashier\Payment
该方法将返回一个实例:
1try {2 $payment = $user->charge(100, $paymentMethod);3} catch (Exception $e) {4 // ...5}
该charge
方法接受以您的应用程序所用货币的最低分母表示的付款金额。例如,如果客户使用美元付款,则金额应以美分表示。
凭发票收费
有时您可能需要一次性收费并向客户提供 PDF 发票。此invoicePrice
方法可以帮助您实现这一点。例如,我们为一位客户开具五件新衬衫的发票:
1$user->invoicePrice('price_tshirt', 5);
发票将立即从用户的默认付款方式中扣款。该invoicePrice
方法还接受一个数组作为其第三个参数。该数组包含发票项目的计费选项。该方法接受的第四个参数也是一个数组,其中包含发票本身的计费选项:
1$user->invoicePrice('price_tshirt', 5, [2 'discounts' => [3 ['coupon' => 'SUMMER21SALE']4 ],5], [6 'default_tax_rates' => ['txr_id'],7]);
与 类似invoicePrice
,您可以使用tabPrice
方法来创建一次性收费,涵盖多件商品(每张发票最多 250 件商品),只需将它们添加到客户的“标签”中,然后向客户开具发票即可。例如,我们可以为客户开具 5 件衬衫和 2 个马克杯的发票:
1$user->tabPrice('price_tshirt', 5);2$user->tabPrice('price_mug', 2);3$user->invoice();
或者,您可以使用该invoiceFor
方法根据客户的默认付款方式进行“一次性”收费:
1$user->invoiceFor('One Time Fee', 500);
虽然invoiceFor
您可以使用该方法,但建议您使用预定义价格的invoicePrice
和tabPrice
方法。这样,您可以在 Stripe 控制面板中更好地分析和获取每件产品的销售数据。
invoice
、invoicePrice
和
方法invoiceFor
将创建 Stripe 发票,并会重试失败的计费尝试。如果您不希望发票重试失败的收费,则需要在第一次收费失败后使用 Stripe API 关闭它们。
创建付款意向
您可以通过在可计费模型实例上调用该方法来创建新的 Stripe 支付意图pay
。调用此方法将创建一个包装在Laravel\Cashier\Payment
实例中的支付意图:
1use Illuminate\Http\Request;2 3Route::post('/pay', function (Request $request) {4 $payment = $request->user()->pay(5 $request->get('amount')6 );7 8 return $payment->client_secret;9});
创建付款意图后,您可以将客户端密钥返回到应用程序的前端,以便用户在浏览器中完成付款。要了解更多关于使用 Stripe 付款意图构建完整支付Processes的信息,请参阅Stripe 文档。
使用该pay
方法时,客户可以使用 Stripe 控制面板中启用的默认付款方式。或者,如果您只想允许使用某些特定的付款方式,则可以使用该payWith
方法:
1use Illuminate\Http\Request;2 3Route::post('/pay', function (Request $request) {4 $payment = $request->user()->payWith(5 $request->get('amount'), ['card', 'bancontact']6 );7 8 return $payment->client_secret;9});
和方法接受pay
以payWith
应用程序所用货币的最低分母表示的付款金额。例如,如果客户使用美元付款,则金额应以美分表示。
退还费用
如果您需要退还 Stripe 费用,可以使用该refund
方法。此方法接受 Stripe付款意图 ID作为其第一个参数:
1$payment = $user->charge(100, $paymentMethodId);2 3$user->refund($payment->id);
发票
检索发票
你可以使用 该方法轻松检索可计费模型的发票数组invoices
。该invoices
方法返回一个实例集合Laravel\Cashier\Invoice
:
1$invoices = $user->invoices();
如果您希望在结果中包含待处理的发票,您可以使用该invoicesIncludingPending
方法:
1$invoices = $user->invoicesIncludingPending();
您可以使用该findInvoice
方法通过其 ID 检索特定发票:
1$invoice = $user->findInvoice($invoiceId);
显示发票信息
列出客户的发票时,你可以使用发票的方法来显示相关的发票信息。例如,你可能希望在表格中列出每张发票,以便用户轻松下载其中任何一张:
1<table>2 @foreach ($invoices as $invoice)3 <tr>4 <td>{{ $invoice->date()->toFormattedDateString() }}</td>5 <td>{{ $invoice->total() }}</td>6 <td><a href="/user/invoice/{{ $invoice->id }}">Download</a></td>7 </tr>8 @endforeach9</table>
即将开具的发票
要检索客户即将开具的发票,您可以使用该upcomingInvoice
方法:
1$invoice = $user->upcomingInvoice();
同样,如果客户有多个订阅,您还可以检索特定订阅的即将开具的发票:
1$invoice = $user->subscription('default')->upcomingInvoice();
预览订阅发票
使用该previewInvoice
方法,您可以在价格变动之前预览发票。这样您就可以确定当价格发生变动时,客户的发票会是什么样子:
1$invoice = $user->subscription('default')->previewInvoice('price_yearly');
您可以将价格数组传递给该previewInvoice
方法,以便预览具有多个新价格的发票:
1$invoice = $user->subscription('default')->previewInvoice(['price_yearly', 'price_metered']);
生成发票 PDF
在生成发票 PDF 之前,您应该使用 Composer 安装 Dompdf 库,它是 Cashier 的默认发票渲染器:
1composer require dompdf/dompdf
在路由或控制器中,你可以使用该downloadInvoice
方法生成指定发票的 PDF 下载。此方法将自动生成下载发票所需的正确 HTTP 响应:
1use Illuminate\Http\Request;2 3Route::get('/user/invoice/{invoice}', function (Request $request, string $invoiceId) {4 return $request->user()->downloadInvoice($invoiceId);5});
默认情况下,发票上的所有数据均来自 Stripe 中存储的客户和发票数据。文件名取决于您的app.name
配置值。但是,您可以通过将数组作为该downloadInvoice
方法的第二个参数来自定义部分数据。此数组允许您自定义信息,例如您的公司和产品详情:
1return $request->user()->downloadInvoice($invoiceId, [ 2 'vendor' => 'Your Company', 3 'product' => 'Your Product', 4 'street' => 'Main Str. 1', 5 'location' => '2000 Antwerp, Belgium', 6 'phone' => '+32 499 00 00 00', 7 'email' => 'info@example.com', 8 'url' => 'https://example.com', 9 'vendorVat' => 'BE123456789',10]);
该downloadInvoice
方法还允许通过第三个参数指定自定义文件名。该文件名将自动添加后缀.pdf
:
1return $request->user()->downloadInvoice($invoiceId, [], 'my-invoice');
自定义发票渲染器
Cashier 还支持使用自定义发票渲染器。默认情况下,Cashier 使用dompdf PHP 库的DompdfInvoiceRenderer
实现来生成 Cashier 的发票。但是,您可以通过实现接口来使用任何您想要的渲染器。例如,您可能希望使用 API 调用第三方 PDF 渲染服务来渲染发票 PDF:Laravel\Cashier\Contracts\InvoiceRenderer
1use Illuminate\Support\Facades\Http; 2use Laravel\Cashier\Contracts\InvoiceRenderer; 3use Laravel\Cashier\Invoice; 4 5class ApiInvoiceRenderer implements InvoiceRenderer 6{ 7 /** 8 * Render the given invoice and return the raw PDF bytes. 9 */10 public function render(Invoice $invoice, array $data = [], array $options = []): string11 {12 $html = $invoice->view($data)->render();13 14 return Http::get('https://example.com/html-to-pdf', ['html' => $html])->get()->body();15 }16}
实现发票渲染器契约后,您应该更新cashier.invoices.renderer
应用程序config/cashier.php
配置文件中的配置值。此配置值应设置为自定义渲染器实现的类名。
查看
Cashier Stripe 也支持Stripe Checkout。Stripe Checkout 提供了一个预先构建的托管支付页面,免去了用户自定义页面接受付款的麻烦。
以下文档包含有关如何开始使用 Stripe Checkout 和 Cashier 的信息。要了解有关 Stripe Checkout 的更多信息,您还可以考虑查看Stripe 自己的 Checkout 文档。
产品结账
checkout
您可以使用可计费模型上的方法,对已在 Stripe 控制面板中创建的现有产品进行结账。该checkout
方法将启动一个新的 Stripe 结账会话。默认情况下,您需要传递 Stripe 价格 ID:
1use Illuminate\Http\Request;2 3Route::get('/product-checkout', function (Request $request) {4 return $request->user()->checkout('price_tshirt');5});
如果需要,您还可以指定产品数量:
1use Illuminate\Http\Request;2 3Route::get('/product-checkout', function (Request $request) {4 return $request->user()->checkout(['price_tshirt' => 15]);5});
当客户访问此路由时,他们将被重定向到 Stripe 的结账页面。默认情况下,当用户成功完成或取消购买时,他们将被重定向到您的路由位置,但您可以使用和选项home
指定自定义回调 URL :success_url
cancel_url
1use Illuminate\Http\Request;2 3Route::get('/product-checkout', function (Request $request) {4 return $request->user()->checkout(['price_tshirt' => 1], [5 'success_url' => route('your-success-route'),6 'cancel_url' => route('your-cancel-route'),7 ]);8});
定义success_url
结账选项时,您可以指示 Stripe 在调用 URL 时将结账会话 ID 添加为查询字符串参数。为此,请将文字字符串添加{CHECKOUT_SESSION_ID}
到success_url
查询字符串中。Stripe 会将此占位符替换为实际的结账会话 ID:
1use Illuminate\Http\Request; 2use Stripe\Checkout\Session; 3use Stripe\Customer; 4 5Route::get('/product-checkout', function (Request $request) { 6 return $request->user()->checkout(['price_tshirt' => 1], [ 7 'success_url' => route('checkout-success').'?session_id={CHECKOUT_SESSION_ID}', 8 'cancel_url' => route('checkout-cancel'), 9 ]);10});11 12Route::get('/checkout-success', function (Request $request) {13 $checkoutSession = $request->user()->stripe()->checkout->sessions->retrieve($request->get('session_id'));14 15 return view('checkout.success', ['checkoutSession' => $checkoutSession]);16})->name('checkout-success');
促销代码
默认情况下,Stripe Checkout 不允许用户兑换促销代码。幸运的是,有一种简单的方法可以在您的 Checkout 页面启用此功能。为此,您可以调用该allowPromotionCodes
方法:
1use Illuminate\Http\Request;2 3Route::get('/product-checkout', function (Request $request) {4 return $request->user()5 ->allowPromotionCodes()6 ->checkout('price_tshirt');7});
单次收费结账
您还可以对 Stripe 控制面板中尚未创建的临时产品执行简单的扣款。为此,您可以checkoutCharge
在可计费模型上使用此方法,并向其传递一个可收费金额、产品名称和一个可选数量。当客户访问此路由时,他们将被重定向到 Stripe 的结账页面:
1use Illuminate\Http\Request;2 3Route::get('/charge-checkout', function (Request $request) {4 return $request->user()->checkoutCharge(1200, 'T-Shirt', 5);5});
使用该checkoutCharge
方法时,Stripe 会始终在您的 Stripe 控制面板中创建新的产品和价格。因此,我们建议您预先在 Stripe 控制面板中创建产品,然后再使用该checkout
方法。
订阅结账
使用 Stripe Checkout 进行订阅需要您在 Stripe 控制面板中启用customer.subscription.created
Webhook。此 Webhook 将在您的数据库中创建订阅记录并存储所有相关的订阅项目。
您也可以使用 Stripe Checkout 来发起订阅。在使用 Cashier 的订阅构建器方法定义订阅后,您可以调用该checkout
方法。当客户访问此路由时,他们将被重定向到 Stripe 的 Checkout 页面:
1use Illuminate\Http\Request;2 3Route::get('/subscription-checkout', function (Request $request) {4 return $request->user()5 ->newSubscription('default', 'price_monthly')6 ->checkout();7});
与产品结账一样,您可以自定义成功和取消 URL:
1use Illuminate\Http\Request; 2 3Route::get('/subscription-checkout', function (Request $request) { 4 return $request->user() 5 ->newSubscription('default', 'price_monthly') 6 ->checkout([ 7 'success_url' => route('your-success-route'), 8 'cancel_url' => route('your-cancel-route'), 9 ]);10});
当然,您也可以为订阅结账启用促销代码:
1use Illuminate\Http\Request;2 3Route::get('/subscription-checkout', function (Request $request) {4 return $request->user()5 ->newSubscription('default', 'price_monthly')6 ->allowPromotionCodes()7 ->checkout();8});
很遗憾,Stripe Checkout 在启动订阅时不支持所有订阅计费选项。在anchorBillingCycleOn
Stripe Checkout 会话期间,使用订阅构建器中的方法、设置按比例计费行为或设置付款行为均不会产生任何效果。请参阅Stripe Checkout 会话 API 文档,了解哪些参数可用。
Stripe Checkout 和试用期
当然,您可以在构建使用 Stripe Checkout 完成的订阅时定义试用期:
1$checkout = Auth::user()->newSubscription('default', 'price_monthly')2 ->trialDays(3)3 ->checkout();
但是试用期必须至少为 48 小时,这是 Stripe Checkout 支持的最短试用时间。
订阅和 Webhook
请记住,Stripe 和 Cashier 通过 webhook 更新订阅状态,因此当客户输入付款信息后返响应用程序时,订阅可能尚未生效。为了处理这种情况,您可以显示一条消息,告知用户他们的付款或订阅处于待处理状态。
收集税号
Checkout 还支持收集客户的税号。要在结账会话中启用此功能,请collectTaxIds
在创建会话时调用该方法:
1$checkout = $user->collectTaxIds()->checkout('price_tshirt');
调用此方法时,客户将看到一个新的复选框,用于选择是否以公司名义购买。如果是,客户将有机会提供自己的税号。
如果您已经在应用程序的服务提供商中配置了自动税收征收,那么此功能将自动启用,无需调用该collectTaxIds
方法。
客人结账
使用该Checkout::guest
方法,您可以为没有“帐户”的应用程序访客启动结帐会话:
1use Illuminate\Http\Request;2use Laravel\Cashier\Checkout;3 4Route::get('/product-checkout', function (Request $request) {5 return Checkout::guest()->create('price_tshirt', [6 'success_url' => route('your-success-route'),7 'cancel_url' => route('your-cancel-route'),8 ]);9});
与为现有用户创建结帐会话类似,您可以利用Laravel\Cashier\CheckoutBuilder
实例上可用的其他方法来自定义访客结帐会话:
1use Illuminate\Http\Request; 2use Laravel\Cashier\Checkout; 3 4Route::get('/product-checkout', function (Request $request) { 5 return Checkout::guest() 6 ->withPromotionCode('promo-code') 7 ->create('price_tshirt', [ 8 'success_url' => route('your-success-route'), 9 'cancel_url' => route('your-cancel-route'),10 ]);11});
访客结账完成后,Stripe 会触发一个checkout.session.completed
webhook 事件,因此请确保配置好 Stripe webhook,以便将此事件真正发送到您的应用程序。在 Stripe 仪表盘中启用 webhook 后,您可以使用 Cashier 处理该 webhook。webhook负载中包含的对象是一个结账对象,您可以检查该对象以完成客户的订单。
处理付款失败
有时,订阅或单笔付款可能会失败。发生这种情况时,Cashier 会抛出Laravel\Cashier\Exceptions\IncompletePayment
异常,通知您发生了这种情况。捕获此异常后,您有两种处理方式。
首先,您可以将客户重定向到 Cashier 内置的专用付款确认页面。该页面已经关联了一个通过 Cashier 服务提供商注册的命名路由。因此,您可以捕获IncompletePayment
异常并将用户重定向到付款确认页面:
1use Laravel\Cashier\Exceptions\IncompletePayment; 2 3try { 4 $subscription = $user->newSubscription('default', 'price_monthly') 5 ->create($paymentMethod); 6} catch (IncompletePayment $exception) { 7 return redirect()->route( 8 'cashier.payment', 9 [$exception->payment->id, 'redirect' => route('home')]10 );11}
在付款确认页面,系统会Prompts客户再次输入信用卡信息,并执行 Stripe 要求的任何其他操作,例如“3D 安全”验证。确认付款后,用户将被重定向到redirect
上述参数提供的 URL。重定向后,message
(string) 和success
(integer) 查询字符串变量将添加到 URL 中。付款页面目前支持以下付款方式类型:
- 信用卡
- 支付宝
- 银行联系
- BECS直接借记
- 每股收益
- 吉罗支付
- 理想的
- SEPA 直接借记
或者,您可以让 Stripe 为您处理付款确认。在这种情况下,您可以在 Stripe 控制面板中设置 Stripe 的自动计费电子邮件IncompletePayment
,而不是重定向到付款确认页面。但是,如果发生异常,您仍然应该告知用户,他们将收到一封包含进一步付款确认说明的电子邮件。
以下方法可能会引发付款异常:使用 特征的模型上的charge
、invoiceFor
和。与订阅交互时,、 和模型上的和方法可能会引发付款不完整异常。invoice
Billable
create
SubscriptionBuilder
incrementAndInvoice
swapAndInvoice
Subscription
SubscriptionItem
hasIncompletePayment
可以使用计费模型或订阅实例上的方法来确定现有订阅是否有未完成的付款:
1if ($user->hasIncompletePayment('default')) {2 // ...3}4 5if ($user->subscription('default')->hasIncompletePayment()) {6 // ...7}
payment
您可以通过检查异常实例上的属性来获取未完成付款的具体状态:
1use Laravel\Cashier\Exceptions\IncompletePayment; 2 3try { 4 $user->charge(1000, 'pm_card_threeDSecure2Required'); 5} catch (IncompletePayment $exception) { 6 // Get the payment intent status... 7 $exception->payment->status; 8 9 // Check specific conditions...10 if ($exception->payment->requiresPaymentMethod()) {11 // ...12 } elseif ($exception->payment->requiresConfirmation()) {13 // ...14 }15}
确认付款
某些付款方式需要额外的数据才能确认付款。例如,SEPA 付款方式在付款过程中需要额外的“强制”数据。您可以使用以下withPaymentConfirmationOptions
方法向 Cashier 提供这些数据:
1$subscription->withPaymentConfirmationOptions([2 'mandate_data' => '...',3])->swap('price_xxx');
您可以查阅Stripe API 文档来查看确认付款时接受的所有选项。
强客户身份验证
如果您的企业或客户位于欧洲,则需要遵守欧盟的强客户身份验证 (SCA) 法规。该法规由欧盟于 2019 年 9 月实施,旨在防止支付欺诈。幸运的是,Stripe 和 Cashier 已准备好构建符合 SCA 规范的应用程序。
在开始之前,请查看Stripe 关于 PSD2 和 SCA 的指南以及有关新 SCA API 的文档。
需要额外确认的付款
根据强客户认证 (SCA) 规定,通常需要进行额外验证才能确认和处理付款。发生这种情况时,Cashier 会抛出异常,告知您需要进行额外验证。有关如何处理这些异常的更多信息,请参阅处理失败付款Laravel\Cashier\Exceptions\IncompletePayment
的文档。
Stripe 或 Cashier 呈现的付款确认屏幕可能针对特定银行或发卡机构的付款Processes进行定制,并且可以包括额外的卡确认、临时小额收费、单独的设备身份验证或其他形式的验证。
不完整和逾期状态
当付款需要额外确认时,订阅将保持incomplete
或past_due
状态,如其stripe_status
数据库列所示。Cashier 将在付款确认完成后立即自动激活客户的订阅,并且 Stripe 会通过 webhook 通知您的应用程序付款已完成。
有关incomplete
和past_due
状态的更多信息,请参阅有关这些状态的附加文档。
会话外付款通知
由于 SCA 法规要求客户即使在订阅有效期间也需要偶尔验证其支付信息,因此 Cashier 可以在需要会话外付款确认时向客户发送通知。例如,在订阅续订时可能会发生这种情况。您可以通过将CASHIER_PAYMENT_NOTIFICATION
环境变量设置为通知类来启用 Cashier 的付款通知。默认情况下,此通知是禁用的。当然,Cashier 包含一个您可以用于此目的的通知类,但您可以根据需要提供自己的通知类:
1CASHIER_PAYMENT_NOTIFICATION=Laravel\Cashier\Notifications\ConfirmPayment
为确保会话外付款确认通知能够送达,请确认您的应用程序已配置 Stripe Webhookinvoice.payment_action_required
,并且该Webhook 已在 Stripe 仪表板中启用。此外,您的Billable
模型也应使用 Laravel 的Illuminate\Notifications\Notifiable
trait。
即使客户手动付款需要额外确认,Stripe 也会发送通知。遗憾的是,Stripe 无法知道付款是手动完成还是“会话外”完成的。但是,如果客户在确认付款后访问付款页面,只会看到“付款成功”的消息。客户不会被允许意外确认同一笔付款两次,从而导致意外的第二次扣款。
Stripe SDK
Cashier 的许多对象都是 Stripe SDK 对象的包装器。如果您想直接与 Stripe 对象交互,可以使用以下asStripe
方法方便地获取它们:
1$stripeSubscription = $subscription->asStripeSubscription();2 3$stripeSubscription->application_fee_percent = 5;4 5$stripeSubscription->save();
您也可以使用该updateStripeSubscription
方法直接更新 Stripe 订阅:
1$subscription->updateStripeSubscription(['application_fee_percent' => 5]);
如果您想直接使用客户端,可以调用类stripe
中的方法。例如,您可以使用此方法访问实例并从您的 Stripe 帐户中获取价格列表:Cashier
Stripe\StripeClient
StripeClient
1use Laravel\Cashier\Cashier;2 3$prices = Cashier::stripe()->prices->all();
测试
在测试使用 Cashier 的应用程序时,您可以模拟对 Stripe API 的实际 HTTP 请求;但是,这需要您部分地重新实现 Cashier 自身的行为。因此,我们建议您的测试直接访问实际的 Stripe API。虽然这样做速度会比较慢,但它能更好地确保您的应用程序按预期运行,并且任何运行缓慢的测试都可以放在它们自己的 Pest / PHPUnit 测试组中。
测试时,请记住 Cashier 本身已经有一个很棒的测试套件,因此您应该只关注测试您自己的应用程序的订阅和支付Processes,而不是每个底层 Cashier 行为。
首先,将Stripe 机密的测试phpunit.xml
版本添加到您的文件中:
1<env name="STRIPE_SECRET" value="sk_test_<your-key>"/>
现在,每当您在测试期间与 Cashier 交互时,它都会向您的 Stripe 测试环境发送真实的 API 请求。为方便起见,您应该预先在 Stripe 测试帐户中填写测试期间可能用到的订阅 / 价格。
为了测试各种计费场景,例如信用卡被拒绝和失败,您可以使用Stripe 提供的 大量测试卡号和令牌。