Laravel Cashier (Stripe)
- 简介
- 升级 Cashier
- 安装
- 配置
- 快速入门
- 客户
- 支付方式
- 订阅
- 订阅试用
- 处理 Stripe Webhooks
- 单次收费
- Checkout
- 发票
- 处理付款失败
- 强客户认证 (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 软件包
1composer require laravel/cashier
安装软件包后,使用 vendor:publish
Artisan 命令发布 Cashier 的迁移文件
1php artisan vendor:publish --tag="cashier-migrations"
然后,迁移您的数据库
1php artisan migrate
Cashier 的迁移将在您的 users
表中添加几个列。它们还将创建一个新的 subscriptions
表来保存您客户的所有订阅,以及一个 subscription_items
表用于包含多个价格的订阅。
如果您愿意,您还可以使用 vendor:publish
Artisan 命令发布 Cashier 的配置文件
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 假设您的可计费模型将是 Laravel 自带的 App\Models\User
类。如果您希望更改此设置,您可以通过 useCustomerModel
方法指定不同的模型。此方法通常应在您的 AppServiceProvider
类的 boot
方法中调用
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 密钥
接下来,您应该在应用程序的 .env
文件中配置您的 Stripe API 密钥。您可以从 Stripe 控制面板检索您的 Stripe API 密钥
1STRIPE_KEY=your-stripe-key2STRIPE_SECRET=your-stripe-secret3STRIPE_WEBHOOK_SECRET=your-stripe-webhook-secret
您应确保在应用程序的 .env
文件中定义了 STRIPE_WEBHOOK_SECRET
环境变量,因为此变量用于确保传入的 webhook 实际上来自 Stripe。
货币配置
默认的 Cashier 货币是美元 (USD)。您可以通过在应用程序的 .env
文件中设置 CASHIER_CURRENCY
环境变量来更改默认货币
1CASHIER_CURRENCY=eur
除了配置 Cashier 的货币之外,您还可以指定在发票上显示货币值时要使用的区域设置。在内部,Cashier 使用 PHP 的 NumberFormatter
类来设置货币区域设置
1CASHIER_CURRENCY_LOCALE=nl_BE
为了使用 en
以外的区域设置,请确保您的服务器上已安装并配置了 ext-intl
PHP 扩展。
税务配置
感谢 Stripe Tax,可以自动计算 Stripe 生成的所有发票的税费。您可以通过在应用程序的 App\Providers\AppServiceProvider
类的 boot
方法中调用 calculateTaxes
方法来启用自动税费计算
1use Laravel\Cashier\Cashier;2 3/**4 * Bootstrap any application services.5 */6public function boot(): void7{8 Cashier::calculateTaxes();9}
一旦启用税费计算,任何新订阅和生成的任何一次性发票都将自动进行税费计算。
为了使此功能正常工作,您客户的账单详细信息(例如客户姓名、地址和税务 ID)需要同步到 Stripe。您可以使用 Cashier 提供的客户数据同步和税务 ID方法来完成此操作。
日志
Cashier 允许您指定在记录致命 Stripe 错误时要使用的日志通道。您可以通过在应用程序的 .env
文件中定义 CASHIER_LOGGER
环境变量来指定日志通道
1CASHIER_LOGGER=stack
由对 Stripe 的 API 调用生成的异常将通过应用程序的默认日志通道记录。
使用自定义模型
您可以自由扩展 Cashier 内部使用的模型,方法是定义您自己的模型并扩展相应的 Cashier 模型
1use Laravel\Cashier\Subscription as CashierSubscription;2 3class Subscription extends CashierSubscription4{5 // ...6}
定义模型后,您可以指示 Cashier 通过 Laravel\Cashier\Cashier
类使用您的自定义模型。通常,您应该在应用程序的 App\Providers\AppServiceProvider
类的 boot
方法中告知 Cashier 您的自定义模型
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 客户记录连接到您的应用程序数据库中的相应用户。完成 checkout 会话后,客户将被重定向到专门的成功或取消页面,您可以在其中向客户显示信息性消息。
向 Stripe Checkout 提供元数据
在销售产品时,通常通过您自己的应用程序定义的 Cart
和 Order
模型来跟踪已完成的订单和购买的产品。当将客户重定向到 Stripe Checkout 以完成购买时,您可能需要提供现有的订单标识符,以便在客户被重定向回您的应用程序时,您可以将已完成的购买与相应的订单关联起来。
为了实现此目的,您可以向 checkout
方法提供一个 metadata
数组。让我们想象一下,当用户开始 checkout 流程时,在我们的应用程序中创建了一个待处理的 Order
。请记住,本示例中的 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');
正如您在上面的示例中看到的,当用户开始 checkout 流程时,我们将向 checkout
方法提供所有购物车/订单关联的 Stripe 价格标识符。当然,您的应用程序负责在客户添加这些商品时将它们与“购物车”或订单关联起来。我们还通过 metadata
数组将订单的 ID 提供给 Stripe Checkout 会话。最后,我们在 Checkout 成功路由中添加了 CHECKOUT_SESSION_ID
模板变量。当 Stripe 将客户重定向回您的应用程序时,此模板变量将自动填充 Checkout 会话 ID。
接下来,让我们构建 Checkout 成功路由。这是用户通过 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
) 下。此外,我们的订阅服务可能提供专家计划 pro_expert
。
首先,让我们了解客户如何订阅我们的服务。当然,您可以想象客户可能会在我们应用程序的定价页面上单击“订阅”按钮以选择基本计划。此按钮或链接应将用户定向到 Laravel 路由,该路由为其选择的计划创建 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 会话,这将允许他们订阅我们的基本计划。成功 checkout 或取消后,客户将被重定向回我们提供给 checkout
方法的 URL。要了解他们的订阅何时真正开始(因为某些付款方式需要几秒钟才能处理),我们还需要配置 Cashier 的 webhook 处理。
现在客户可以开始订阅了,我们需要限制我们应用程序的某些部分,以便只有订阅用户才能访问它们。当然,我们始终可以通过 Cashier 的 Billable
trait 提供的 subscribed
方法来确定用户的当前订阅状态
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
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 并在您的应用程序数据库中将订阅标记为“已取消”。
客户
检索客户
您可以使用 Cashier::findBillable
方法通过 Stripe ID 检索客户。此方法将返回可计费模型的实例
1use Laravel\Cashier\Cashier;2 3$user = Cashier::findBillable($stripeId);
创建客户
有时,您可能希望在不开始订阅的情况下创建 Stripe 客户。您可以使用 createAsStripeCustomer
方法来完成此操作
1$stripeCustomer = $user->createAsStripeCustomer();
在 Stripe 中创建客户后,您可以在稍后的日期开始订阅。您可以提供一个可选的 $options
数组,以传入 Stripe API 支持的任何其他客户创建参数
1$stripeCustomer = $user->createAsStripeCustomer($options);
如果您想返回可计费模型的 Stripe 客户对象,可以使用 asStripeCustomer
方法
1$stripeCustomer = $user->asStripeCustomer();
如果您想检索给定可计费模型的 Stripe 客户对象,但不确定该可计费模型是否已经是 Stripe 中的客户,则可以使用 createOrGetStripeCustomer
方法。如果 Stripe 中尚不存在客户,此方法将创建一个新客户
1$stripeCustomer = $user->createOrGetStripeCustomer();
更新客户
有时,您可能希望直接使用其他信息更新 Stripe 客户。您可以使用 updateStripeCustomer
方法来完成此操作。此方法接受 Stripe API 支持的客户更新选项数组
1$stripeCustomer = $user->updateStripeCustomer($options);
余额
Stripe 允许您贷记或借记客户的“余额”。稍后,此余额将在新的发票上贷记或借记。要检查客户的总余额,您可以使用可计费模型上可用的 balance
方法。balance
方法将返回客户货币中余额的格式化字符串表示形式
1$balance = $user->balance();
要贷记客户的余额,您可以向 creditBalance
方法提供一个值。如果您愿意,您还可以提供描述
1$user->creditBalance(500, 'Premium customer top-up.');
向 debitBalance
方法提供一个值将借记客户的余额
1$user->debitBalance(300, 'Bad usage penalty.');
applyBalance
方法将为客户创建新的客户余额交易。您可以使用 balanceTransactions
方法检索这些交易记录,这可能有助于为客户提供信用和借记的日志以供审查
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}
税务 ID
Cashier 提供了一种管理客户税务 ID 的简便方法。例如,taxIds
方法可用于检索分配给客户的所有 税务 ID 作为集合
1$taxIds = $user->taxIds();
您还可以按标识符检索客户的特定税务 ID
1$taxId = $user->findTaxId('txi_belgium');
您可以通过向 createTaxId
方法提供有效的 类型和值来创建新的税务 ID
1$taxId = $user->createTaxId('eu_vat', 'BE0123456789');
createTaxId
方法将立即将 VAT ID 添加到客户的帐户。VAT ID 的验证也由 Stripe 完成;但是,这是一个异步过程。您可以通过订阅 customer.tax_id.updated
webhook 事件并检查 VAT ID 的 verification
参数来收到验证更新的通知。有关处理 webhook 的更多信息,请参阅关于定义 webhook 处理程序的文档。
您可以使用 deleteTaxId
方法删除税务 ID
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
方法,将用户重定向到账单门户
1use Illuminate\Http\Request;2 3Route::get('/billing-portal', function (Request $request) {4 return $request->user()->redirectToBillingPortal();5});
默认情况下,当用户完成管理其订阅后,他们将能够通过 Stripe 账单门户内的链接返回到您应用程序的 home
路由。您可以通过将 URL 作为参数传递给 redirectToBillingPortal
方法来提供用户应返回的自定义 URL
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]);
创建 Setup Intent 并将其传递给视图后,您应将其密钥附加到将收集付款方式的元素。例如,考虑以下“更新付款方式”表单
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 Element 附加到表单,并安全地收集客户的付款详细信息
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 应用程序,在其中可以将其附加到客户。付款方式可以添加为新的付款方式或用于更新默认付款方式。您还可以立即使用付款方式标识符来创建新订阅。
如果您想了解有关 Setup Intents 和收集客户付款详细信息的更多信息,请查看 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 Element 附加到表单,并安全地收集客户的付款详细信息
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);
要了解如何检索支付方式标识符,请查看 支付方式存储文档。
删除支付方式
要删除支付方式,你可以在你想要删除的 Laravel\Cashier\PaymentMethod
实例上调用 delete
方法。
1$paymentMethod->delete();
deletePaymentMethod
方法将从可计费模型中删除特定的支付方式。
1$user->deletePaymentMethod('pm_visa');
deletePaymentMethods
方法将删除可计费模型的所有支付方式信息。
1$user->deletePaymentMethods();
默认情况下,此方法将删除所有类型的支付方式。要删除特定类型的支付方式,你可以将 type
作为参数传递给该方法。
1$user->deletePaymentMethods('sepa_debit');
如果用户有有效的订阅,你的应用程序不应允许他们删除其默认支付方式。
订阅
订阅提供了一种为你的客户设置定期付款的方式。由 Cashier 管理的 Stripe 订阅支持多种订阅价格、订阅数量、试用期等等。
创建订阅
要创建订阅,首先检索你的可计费模型的实例,这通常是 App\Models\User
的实例。一旦你检索到模型实例,你就可以使用 newSubscription
方法来创建模型的订阅。
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});
传递给 newSubscription
方法的第一个参数应该是订阅的内部类型。如果你的应用程序仅提供单个订阅,你可以将其称为 default
或 primary
。此订阅类型仅供内部应用程序使用,不应向用户显示。此外,它不应包含空格,并且在创建订阅后绝不应更改。第二个参数是用户订阅的具体价格。此值应与 Stripe 中价格的标识符相对应。
create
方法接受 Stripe 支付方式标识符 或 Stripe PaymentMethod
对象,它将开始订阅,并使用可计费模型的 Stripe 客户 ID 和其他相关的账单信息更新你的数据库。
直接将支付方式标识符传递给 create
订阅方法也会自动将其添加到用户存储的支付方式中。
通过发票邮件收取定期付款
你可以指示 Stripe 在客户的定期付款到期时通过电子邮件向客户发送发票,而不是自动收取客户的定期付款。然后,客户可以在收到发票后手动支付。通过发票收取定期付款时,客户无需预先提供支付方式。
1$user->newSubscription('default', 'price_monthly')->createAndSendInvoice();
客户在订阅被取消之前支付发票的时间量由 days_until_due
选项确定。默认情况下,这是 30 天;但是,如果你愿意,可以为此选项提供特定值。
1$user->newSubscription('default', 'price_monthly')->createAndSendInvoice([], [2 'days_until_due' => 303]);
数量
如果你想在创建订阅时为价格设置特定的 数量,你应该在创建订阅之前在订阅构建器上调用 quantity
方法。
1$user->newSubscription('default', 'price_monthly')2 ->quantity(5)3 ->create($paymentMethod);
额外细节
如果你想指定 Stripe 支持的其他 客户 或 订阅 选项,你可以通过将它们作为第二个和第三个参数传递给 create
方法来完成。
1$user->newSubscription('default', 'price_monthly')->create($paymentMethod, [2 'email' => $email,3], [4 'metadata' => ['note' => 'Some extra information.'],5]);
优惠券
如果你想在创建订阅时应用优惠券,你可以使用 withCoupon
方法。
1$user->newSubscription('default', 'price_monthly')2 ->withCoupon('code')3 ->create($paymentMethod);
或者,如果你想应用 Stripe 促销代码,你可以使用 withPromotionCode
方法。
1$user->newSubscription('default', 'price_monthly')2 ->withPromotionCode('promo_code_id')3 ->create($paymentMethod);
给定的促销代码 ID 应该是分配给促销代码的 Stripe API ID,而不是面向客户的促销代码。如果你需要根据给定的面向客户的促销代码查找促销代码 ID,你可以使用 findPromotionCode
方法。
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
订阅是否有效订阅了应用程序的“premium”产品。给定的 Stripe 产品标识符应与你在 Stripe 仪表板中的产品标识符之一相对应。
1if ($user->subscribedToProduct('prod_premium', 'default')) {2 // ...3}
通过将数组传递给 subscribedToProduct
方法,你可以确定用户的 default
订阅是否有效订阅了应用程序的“basic”或“premium”产品。
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
。订阅状态存储在 Cashier 的 subscriptions
数据库表的 stripe_status
列中。
同样,如果在交换价格时需要辅助付款操作,则订阅将被标记为 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
方法。通常,这些方法应在你的 App\Providers\AppServiceProvider
的 register
方法中调用。
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
状态时,在付款确认之前无法更改。因此,当订阅处于 incomplete
状态时,swap
和 updateQuantity
方法将抛出异常。
订阅范围
大多数订阅状态也可用作查询范围,以便你可以轻松地查询数据库中处于给定状态的订阅。
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 文档。
在 swapAndInvoice
方法之前执行 noProrate
方法对按比例收费没有影响。发票将始终发出。
订阅数量
有时订阅会受到“数量”的影响。例如,项目管理应用程序可能每个项目每月收费 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 作为第二个参数传递给 increment / decrement 方法。
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_chat
附加产品的 price_basic
订阅,并且你想将客户从 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');
当订阅有多个价格时,Subscription
模型上的 stripe_price
和 quantity
属性将为 null
。要访问单个价格属性,你应该使用 Subscription
模型上可用的 items
关系。
订阅项
当订阅有多个价格时,它将在数据库的 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 允许你的客户同时拥有多个订阅。例如,你可能经营一家健身房,提供游泳订阅和举重订阅,并且每个订阅可能有不同的定价。当然,客户应该能够订阅其中一项或两项计划。
当你的应用程序创建订阅时,你可以将订阅类型提供给 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 仪表板中使用 基于使用量计费模型 和 计量器 创建一个新产品。创建计量器后,存储关联的事件名称和计量器 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 报告他们的使用量,以便可以准确地向他们收费。要报告计量事件的使用量,你可以使用你的 Billable
模型上的 reportMeterEvent
方法。
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
方法,并返回一个包含 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 进行“一次性”收费,你需要在当时手动指定税率。
同步税率
更改 taxRates
方法返回的硬编码税率 ID 时,用户任何现有订阅的税费设置将保持不变。如果你希望使用新的 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 将自动设置你的 subscriptions
数据库表中的 ends_at
列。此列用于了解 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 将这种类型的试用称为“通用试用”,因为它未附加到任何现有订阅。如果当前日期未超过 trial_ends_at
的值,则可计费模型实例上的 onTrial
方法将返回 true
。
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 Webhooks
你可以使用 Stripe CLI 来帮助在本地开发期间测试 Webhook。
Stripe 可以通过 Webhook 通知你的应用程序各种事件。默认情况下,指向 Cashier 的 Webhook 控制器的路由由 Cashier 服务提供商自动注册。此控制器将处理所有传入的 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 所需的所有事件。
1php artisan cashier:webhook
默认情况下,创建的 Webhook 将指向由 APP_URL
环境变量和 Cashier 附带的 cashier.webhook
路由定义的 URL。如果你想使用其他 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 令牌。要实现此目的,你应该在应用程序的 bootstrap/app.php
文件中将 stripe/*
从 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 验证,请确保在应用程序的 .env
文件中设置了 STRIPE_WEBHOOK_SECRET
环境变量。Webhook secret
可以从你的 Stripe 帐户仪表板中检索。
单次收费
简单收费
如果你想对客户进行一次性收费,你可以使用可计费模型实例上的 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 个项目)创建一次性收费,方法是将它们添加到客户的“标签页”中,然后向客户开具发票。例如,我们可以为客户开具五件衬衫和两个马克杯的发票
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 关闭它们。
创建 Payment Intents
您可以通过在可计费模型实例上调用 pay
方法来创建一个新的 Stripe payment intent。调用此方法将创建一个 payment intent,该 intent 包装在 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});
创建 payment intent 后,您可以将客户端密钥返回到应用程序的前端,以便用户可以在其浏览器中完成付款。要了解更多关于使用 Stripe payment intents 构建完整付款流程的信息,请查阅 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 payment intent 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', 8 'url' => 'https://example.com', 9 'vendorVat' => 'BE123456789',10]);
downloadInvoice
方法还允许通过其第三个参数使用自定义文件名。 此文件名将自动添加 .pdf
后缀
1return $request->user()->downloadInvoice($invoiceId, [], 'my-invoice'); 2 3<a name="custom-invoice-render"></a> 4#### Custom Invoice Renderer 5 6Cashier also makes it possible to use a custom invoice renderer. By default, Cashier uses the `DompdfInvoiceRenderer` implementation, which utilizes the [dompdf](https://github.com/dompdf/dompdf) PHP library to generate Cashier's invoices. However, you may use any renderer you wish by implementing the `Laravel\Cashier\Contracts\InvoiceRenderer` interface. For example, you may wish to render an invoice PDF using an API call to a third-party PDF rendering service: 7 8use Illuminate\Support\Facades\Http; 9use Laravel\Cashier\Contracts\InvoiceRenderer;10use Laravel\Cashier\Invoice;11 12class ApiInvoiceRenderer implements InvoiceRenderer13{14 /**15 * Render the given invoice and return the raw PDF bytes.16 */17 public function render(Invoice $invoice, array $data = [], array $options = []): string18 {19 $html = $invoice->view($data)->render();20 21 return Http::get('https://example.com/html-to-pdf', ['html' => $html])->get()->body();22 }23}
一旦您实现了发票渲染器合同,您应该更新应用程序的 config/cashier.php
配置文件中的 cashier.invoices.renderer
配置值。此配置值应设置为您的自定义渲染器实现的类名。
Checkout
Cashier Stripe 还提供对 Stripe Checkout 的支持。 Stripe Checkout 通过提供预构建的托管支付页面,消除了实现自定义页面以接受付款的痛苦。
以下文档包含关于如何开始将 Stripe Checkout 与 Cashier 一起使用的信息。 要了解更多关于 Stripe Checkout 的信息,您还应该考虑查看 Stripe 关于 Checkout 的自身文档。
产品 Checkout
您可以对已在您的 Stripe 控制面板中创建的现有产品执行 checkout,方法是在可计费模型上使用 checkout
方法。 checkout
方法将启动一个新的 Stripe Checkout 会话。 默认情况下,您需要传递一个 Stripe Price 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 的 Checkout 页面。 默认情况下,当用户成功完成或取消购买时,他们将被重定向到您的 home
路由位置,但是您可以使用 success_url
和 cancel_url
选项指定自定义回调 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
checkout 选项时,您可以指示 Stripe 在调用您的 URL 时将 checkout 会话 ID 作为查询字符串参数添加。 为此,请将文字字符串 {CHECKOUT_SESSION_ID}
添加到您的 success_url
查询字符串。 Stripe 将使用实际的 checkout 会话 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});
单次收费 Checkout
您还可以为未在您的 Stripe 控制面板中创建的临时产品执行简单收费。 为此,您可以在可计费模型上使用 checkoutCharge
方法,并向其传递可收费金额、产品名称和可选的数量。 当客户访问此路由时,他们将被重定向到 Stripe 的 Checkout 页面
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
方法。
订阅 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});
与产品 checkout 一样,您可以自定义成功和取消 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});
当然,您也可以为订阅 checkout 启用促销代码
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 Session 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 更新订阅状态,因此当客户在输入其付款信息后返回应用程序时,订阅可能尚未处于活动状态。 为了处理这种情况,您可能希望显示一条消息,告知用户他们的付款或订阅正在等待处理。
收集税务 ID
Checkout 还支持收集客户的税务 ID。 要在 checkout 会话中启用此功能,请在创建会话时调用 collectTaxIds
方法
1$checkout = $user->collectTaxIds()->checkout('price_tshirt');
当调用此方法时,将为客户提供一个新的复选框,允许他们指示他们是否以公司身份购买。 如果是,他们将有机会提供他们的税务 ID 号码。
如果您已在应用程序的服务提供商中配置了 自动税务收集,则此功能将自动启用,无需调用 collectTaxIds
方法。
访客 Checkout
使用 Checkout::guest
方法,您可以为没有“帐户”的应用程序访客启动 checkout 会话
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});
与为现有用户创建 checkout 会话类似,您可以利用 Laravel\Cashier\CheckoutBuilder
实例上可用的其他方法来自定义访客 checkout 会话
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});
访客 checkout 完成后,Stripe 可以调度 checkout.session.completed
webhook 事件,因此请确保 配置您的 Stripe webhook 以实际将此事件发送到您的应用程序。 一旦 webhook 在 Stripe 控制面板中启用,您可以 使用 Cashier 处理 webhook。 webhook 有效负载中包含的对象将是一个 checkout
对象,您可以检查该对象以完成客户的订单。
处理付款失败
有时,订阅或单次收费的付款可能会失败。 发生这种情况时,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}
在付款确认页面上,将提示客户再次输入他们的信用卡信息,并执行 Stripe 要求的任何其他操作,例如 “3D Secure” 确认。 在确认付款后,用户将被重定向到上面指定的 redirect
参数提供的 URL。 重定向后,message
(字符串) 和 success
(整数) 查询字符串变量将被添加到 URL。 付款页面目前支持以下付款方式类型
- 信用卡
- 支付宝
- Bancontact
- BECS Direct Debit
- EPS
- Giropay
- iDEAL
- SEPA Direct Debit
或者,您可以允许 Stripe 为您处理付款确认。 在这种情况下,您可以 在您的 Stripe 控制面板中设置 Stripe 的自动账单电子邮件,而不是重定向到付款确认页面。 但是,如果捕获到 IncompletePayment
异常,您仍然应该告知用户他们将收到一封包含进一步付款确认说明的电子邮件。
付款异常可能会为以下方法抛出:使用 Billable
trait 的模型上的 charge
、invoiceFor
和 invoice
。 当与订阅交互时,SubscriptionBuilder
上的 create
方法,以及 Subscription
和 SubscriptionItem
模型上的 incrementAndInvoice
和 swapAndInvoice
方法可能会抛出不完整的付款异常。
可以使用可计费模型或订阅实例上的 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 付款方式在付款过程中需要额外的 “mandate” 数据。 您可以使用 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 提供的付款确认屏幕可以根据特定银行或发卡机构的付款流程进行定制,并且可以包括额外的卡片确认、临时小额收费、单独设备认证或其他形式的验证。
不完整和逾期状态
当付款需要额外确认时,订阅将保持 incomplete
或 past_due
状态,如其 stripe_status
数据库列所示。 一旦付款确认完成,并且您的应用程序通过 webhook 收到 Stripe 的完成通知,Cashier 将自动激活客户的订阅。
有关 incomplete
和 past_due
状态的更多信息,请参阅 我们关于这些状态的其他文档。
会话外付款通知
由于 SCA 法规要求客户偶尔验证其付款详细信息,即使他们的订阅处于活动状态,因此当需要会话外付款确认时,Cashier 可以向客户发送通知。 例如,这可能发生在订阅续订时。 可以通过将 CASHIER_PAYMENT_NOTIFICATION
环境变量设置为通知类来启用 Cashier 的付款通知。 默认情况下,此通知被禁用。 当然,Cashier 包含一个您可以用于此目的的通知类,但如果您愿意,您可以自由提供您自己的通知类
1CASHIER_PAYMENT_NOTIFICATION=Laravel\Cashier\Notifications\ConfirmPayment
为了确保会话外付款确认通知能够送达,请验证 Stripe webhook 是否已为您的应用程序配置,并且 invoice.payment_action_required
webhook 在您的 Stripe 控制面板中已启用。 此外,您的 Billable
模型也应使用 Laravel 的 Illuminate\Notifications\Notifiable
trait。
即使客户手动进行需要额外确认的付款,也会发送通知。 不幸的是,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\StripeClient
客户端,您可以在 Cashier
类上调用 stripe
方法。 例如,您可以使用此方法访问 StripeClient
实例并从您的 Stripe 帐户检索价格列表
1use Laravel\Cashier\Cashier;2 3$prices = Cashier::stripe()->prices->all();
测试
当测试使用 Cashier 的应用程序时,您可以模拟对 Stripe API 的实际 HTTP 请求; 但是,这需要您部分地重新实现 Cashier 自身的行为。 因此,我们建议允许您的测试访问实际的 Stripe API。 虽然这比较慢,但它提供了更大的信心,确保您的应用程序按预期工作,并且任何慢速测试都可以放在它们自己的 Pest / PHPUnit 测试组中。
测试时,请记住 Cashier 本身已经有一个很棒的测试套件,因此您应该只专注于测试您自己的应用程序的订阅和付款流程,而不是每个底层的 Cashier 行为。
要开始使用,请将您的 Stripe secret 的 testing 版本添加到您的 phpunit.xml
文件中
1<env name="STRIPE_SECRET" value="sk_test_<your-key>"/>
现在,每当您在测试时与 Cashier 交互时,它都会向您的 Stripe 测试环境发送实际的 API 请求。 为了方便起见,您应该预先填充您的 Stripe 测试帐户,其中包含您在测试期间可能使用的订阅/价格。
为了测试各种账单场景,例如信用卡拒绝和失败,您可以使用 Stripe 提供的各种 测试卡号和令牌。