交易工具包 (Stripe)
这是一篇协同翻译的文章,你可以点击『我来翻译』按钮来参与翻译。
Laravel Cashier (Stripe)
介绍(Introduction)
Laravel Cashier Stripe 为 Stripe 的订阅计费服务提供了一个简洁且流畅的接口。
它处理了几乎所有让你头疼的订阅计费样板代码。除了基本的订阅管理之外,Cashier 还可以处理优惠券、订阅切换、订阅“数量”、取消宽限期,甚至还能生成发票 PDF。
升级 Cashier(Upgrading Cashier)
当你升级到新版本的 Cashier 时,请务必仔细阅读 升级指南。
[!WARNING]
为了避免重大变更(Breaking Changes),Cashier 使用固定的 Stripe API 版本。
Cashier 15 使用的 Stripe API 版本为2023-10-16。
Stripe API 版本将在次要版本更新(minor releases)时升级,以便使用新的 Stripe 功能和改进。
安装(Installation)
首先,使用 Composer 包管理器安装用于 Stripe 的 Cashier 包:
composer require laravel/cashier
安装完成后,通过 vendor:publish Artisan 命令发布 Cashier 的数据库迁移文件:
php artisan vendor:publish --tag="cashier-migrations"
Then, migrate your database:
php artisan migrate
Cashier 的迁移会在你的 users 表中添加若干列。
同时,它还会创建一个新的 subscriptions 表,用于存储所有客户的订阅信息,
以及一个 subscription_items 表,用于存储包含多个价格项的订阅。
如果你愿意,也可以通过以下命令发布 Cashier 的配置文件:
php artisan vendor:publish --tag="cashier-config"
最后,为了确保 Cashier 能正确处理所有 Stripe 事件,请记得配置 Cashier 的 webhook 处理。
[!警告]
Stripe 建议所有用于存储 Stripe 标识符(如stripe_id)的数据库列都应区分大小写。
因此,在使用 MySQL 时,应确保stripe_id列的排序规则(collation)设置为utf8_bin。
有关更多信息,请参阅 Stripe 官方文档。
配置(Configuration)
可计费模型(Billable Model)
在使用 Cashier 之前,请在你的可计费模型定义中加入 Billable trait。
通常,这个模型是 App\Models\User。
该 trait 提供了多种方法,让你可以执行常见的计费任务,例如创建订阅、应用优惠券以及更新支付方式信息:
use Laravel\Cashier\Billable;
class User extends Authenticatable
{
use Billable;
}
Cashier 默认假设你的可计费模型是 Laravel 自带的 App\Models\User 类。
如果你希望更改这一点,可以通过 useCustomerModel 方法指定一个不同的模型。
通常,这个方法应在 AppServiceProvider 类的 boot 方法中调用:
use App\Models\Cashier\User;
use Laravel\Cashier\Cashier;
/**
* 启动任何应用服务。
*/
public function boot(): void
{
Cashier::useCustomerModel(User::class);
}
[!警告]
如果你使用的模型不是 Laravel 默认提供的App\Models\User模型,那么你需要发布并修改 Cashier 的迁移文件,以便与自定义模型的表名相匹配。
API 密钥(API Keys)
接下来,你需要在应用的 .env 文件中配置 Stripe 的 API 密钥。
你可以在 Stripe 控制面板中获取这些密钥:
STRIPE_KEY=your-stripe-key
STRIPE_SECRET=your-stripe-secret
STRIPE_WEBHOOK_SECRET=your-stripe-webhook-secret
[!警告]
请确保在应用的.env文件中定义了STRIPE_WEBHOOK_SECRET环境变量,因为该变量用于验证传入的 webhook 请求确实来自 Stripe。
货币配置(Currency Configuration)
Cashier 默认使用的货币是美元(USD)。
你可以在应用的 .env 文件中设置 CASHIER_CURRENCY 环境变量来更改默认货币:
CASHIER_CURRENCY=eur
除了配置 Cashier 的货币之外,你还可以指定用于在发票上显示货币数值时的 本地化设置(locale)。
在内部,Cashier 使用 PHP 的 NumberFormatter 类 来设置货币的本地化格式:
CASHIER_CURRENCY_LOCALE=nl_BE
[!警告]
如果要使用除en之外的其他语言环境,请确保服务器上已安装并配置ext-intlPHP 扩展。
税务配置(Tax Configuration)
借助 Stripe Tax,可以为 Stripe 生成的所有发票自动计算税额。
你可以在应用程序的 App\Providers\AppServiceProvider 类的 boot 方法中调用 calculateTaxes 方法来启用自动税额计算:
use Laravel\Cashier\Cashier;
/**
* 启动应用程序服务。
*/
public function boot(): void
{
Cashier::calculateTaxes();
}
启用税额计算后,任何新创建的订阅和一次性发票都将自动计算税额。
为了让此功能正常工作,你的客户账单信息(如客户姓名、地址和税号)需要与 Stripe 同步。
你可以使用 Cashier 提供的 客户数据同步 和 税号(Tax ID) 方法来实现这一点。
日志记录(Logging)
Cashier 允许你指定在记录 Stripe 严重错误日志时所使用的日志通道。
你可以在应用程序的 .env 文件中定义 CASHIER_LOGGER 环境变量来设置日志通道:
CASHIER_LOGGER=stack
通过对 Stripe 的 API 调用而产生的异常将通过应用程序的默认日志通道进行记录。
使用自定义模型(Using Custom Models)
你可以通过定义自己的模型并继承 Cashier 对应的模型,来自由扩展 Cashier 内部使用的模型:
use Laravel\Cashier\Subscription as CashierSubscription;
class Subscription extends CashierSubscription
{
// ...
}
定义好自定义模型后,你可以通过 Laravel\Cashier\Cashier 类指示 Cashier 使用你的自定义模型。
通常,你应该在应用程序的 App\Providers\AppServiceProvider 类的 boot 方法中告知 Cashier 使用这些自定义模型:
use App\Models\Cashier\Subscription;
use App\Models\Cashier\SubscriptionItem;
/**
* 启动应用程序服务。
*/
public function boot(): void
{
Cashier::useSubscriptionModel(Subscription::class);
Cashier::useSubscriptionItemModel(SubscriptionItem::class);
}
快速入门(Quickstart)
销售产品(Selling Products)
[!注意]
在使用 Stripe Checkout 之前,你应当在 Stripe 控制台中定义具有固定价格的产品。
此外,你还应当配置 Cashier 的 webhook 处理。
通过你的应用程序提供产品销售和订阅计费功能可能看起来很复杂。
然而,借助 Cashier 和 Stripe Checkout,你可以轻松构建现代化且稳定的支付集成。
若要向客户收取一次性、非循环的产品费用,我们可以使用 Cashier 将客户引导至 Stripe Checkout 页面,让他们在其中填写支付信息并确认购买。
支付完成后,客户将被重定向到你在应用程序中指定的成功页面 URL:
use Illuminate\Http\Request;
Route::get('/checkout', function (Request $request) {
$stripePriceId = 'price_deluxe_album';
$quantity = 1;
return $request->user()->checkout([$stripePriceId => $quantity], [
'success_url' => route('checkout-success'),
'cancel_url' => route('checkout-cancel'),
]);
})->name('checkout');
Route::view('/checkout/success', 'checkout.success')->name('checkout-success');
Route::view('/checkout/cancel', 'checkout.cancel')->name('checkout-cancel');
正如上面的示例所示,我们将使用 Cashier 提供的 checkout 方法,将客户根据指定的 “价格标识符(price identifier)” 重定向到 Stripe Checkout。
在 Stripe 中,“价格(price)” 是指为特定产品定义的价格。
如果有需要,checkout 方法会自动在 Stripe 中创建一个客户,并将该 Stripe 客户记录与应用程序数据库中的相应用户关联起来。
当结账会话完成后,客户会被重定向到一个专门的成功页面或取消页面,在那里你可以向客户显示相关的提示信息。
向 Stripe Checkout 提供元数据(Providing Meta Data to Stripe Checkout)
在销售产品时,通常会通过你自己应用程序中定义的 Cart(购物车)和 Order(订单)模型来跟踪已完成的订单和已购买的商品。
当你将客户重定向到 Stripe Checkout 以完成购买时,可能需要提供一个现有的订单标识符,以便在客户返回你的应用程序时,将完成的购买与相应订单关联起来。
为此,你可以向 checkout 方法提供一个包含 metadata(元数据)的数组。
假设当用户开始结账流程时,你的应用程序会创建一个待处理的 Order 记录。
请注意,这里的 Cart 和 Order 模型只是示例说明,Cashier 并未提供这些模型。
你可以根据自己应用程序的需求自由实现这些概念:
use App\Models\Cart;
use App\Models\Order;
use Illuminate\Http\Request;
Route::get('/cart/{cart}/checkout', function (Request $request, Cart $cart) {
$order = Order::create([
'cart_id' => $cart->id,
'price_ids' => $cart->price_ids,
'status' => 'incomplete',
]);
return $request->user()->checkout($order->price_ids, [
'success_url' => route('checkout-success').'?session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => route('checkout-cancel'),
'metadata' => ['order_id' => $order->id],
]);
})->name('checkout');
正如上面的示例所示,当用户开始结账流程时,我们会将购物车或订单中所有关联的 Stripe 价格标识符传递给 checkout 方法。
当然,当客户将商品加入购物车或订单时,你的应用程序需要负责将这些商品与“购物车”或订单进行关联。
此外,我们还通过 metadata 数组将订单的 ID 提供给 Stripe Checkout 会话。
最后,我们在 Checkout 成功回调路由中添加了 CHECKOUT_SESSION_ID 模板变量。当 Stripe 将客户重定向回你的应用程序时,该模板变量会自动被填充为对应的 Checkout 会话 ID。
接下来,让我们构建 Checkout 成功回调路由。
这是当用户通过 Stripe Checkout 完成购买后将被重定向到的路由。
在该路由中,我们可以获取 Stripe Checkout 会话 ID,并通过它检索对应的 Stripe Checkout 实例,从而访问我们之前提供的元数据,并据此更新客户的订单状态:
use App\Models\Order;
use Illuminate\Http\Request;
use Laravel\Cashier\Cashier;
Route::get('/checkout/success', function (Request $request) {
$sessionId = $request->get('session_id');
if ($sessionId === null) {
return;
}
$session = Cashier::stripe()->checkout->sessions->retrieve($sessionId);
if ($session->payment_status !== 'paid') {
return;
}
$orderId = $session['metadata']['order_id'] ?? null;
$order = Order::findOrFail($orderId);
$order->update(['status' => 'completed']);
return view('checkout-success', ['order' => $order]);
})->name('checkout-success');
有关 Checkout 会话对象中包含的数据的更多信息,请参阅 Stripe 的文档:Checkout session 对象数据说明。
销售订阅(Selling Subscriptions)
[!注意]
在使用 Stripe Checkout 之前,你应当在 Stripe 控制台中定义具有固定价格的产品。此外,你还应当配置 Cashier 的 webhook 处理。
通过你的应用程序提供产品和订阅计费功能可能看起来很复杂。
然而,借助 Cashier 和 Stripe Checkout,你可以轻松构建现代且稳定的支付集成。
为了学习如何使用 Cashier 和 Stripe Checkout 销售订阅,让我们来看一个简单的场景:
一个订阅服务提供基础月付计划(price_basic_monthly)和年付计划(price_basic_yearly)。
这两个价格可以在 Stripe 控制台中归类到一个名为 “Basic” 的产品(pro_basic)下。
此外,我们的订阅服务还可能提供一个名为 “Expert” 的高级计划(pro_expert)。
首先,让我们看看客户如何订阅我们的服务。
当然,你可以想象客户会在应用程序的定价页面上点击 “订阅” 按钮来选择 Basic 计划。
该按钮或链接应当引导用户访问一个 Laravel 路由,用于为他们选择的方案创建 Stripe Checkout 会话:
use Illuminate\Http\Request;
Route::get('/subscription-checkout', function (Request $request) {
return $request->user()
->newSubscription('default', 'price_basic_monthly')
->trialDays(5)
->allowPromotionCodes()
->checkout([
'success_url' => route('your-success-route'),
'cancel_url' => route('your-cancel-route'),
]);
});
正如上面的示例所示,我们会将客户重定向到一个 Stripe Checkout 会话,让他们能够订阅我们的 Basic 计划。
在成功结账或取消之后,客户会被重定向回我们通过 checkout 方法提供的 URL。
为了了解他们的订阅何时真正开始(因为某些支付方式可能需要几秒钟来处理),我们还需要配置 Cashier 的 webhook 处理。
现在客户已经可以开始订阅,我们需要限制应用程序的某些部分,使只有已订阅用户才能访问。
当然,我们可以通过 Cashier 的 Billable trait 提供的 subscribed 方法来确定用户当前的订阅状态:
@if ($user->subscribed())
<p>You are subscribed.</p>
@endif
我们甚至可以轻松地判断用户是否订阅了特定的产品或价格方案:
@if ($user->subscribedToProduct('pro_basic'))
<p>You are subscribed to our Basic product.</p>
@endif
@if ($user->subscribedToPrice('price_basic_monthly'))
<p>You are subscribed to our monthly Basic plan.</p>
@endif
构建 Subscribed 中间件
为了方便起见,您可能希望创建一个 中间件,用于判断传入的请求是否来自已订阅用户。
定义此中间件后,您可以轻松地将其分配给路由,从而防止未订阅用户访问该路由:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class Subscribed
{
/**
* 处理传入的请求。
*/
public function handle(Request $request, Closure $next): Response
{
if (! $request->user()?->subscribed()) {
// 将用户重定向到结算页面,并提示他们进行订阅...
return redirect('/billing');
}
return $next($request);
}
}
定义好中间件后,您可以将它分配给路由:
use App\Http\Middleware\Subscribed;
Route::get('/dashboard', function () {
// ...
})->middleware([Subscribed::class]);
允许客户管理他们的订阅计划
当然,客户可能希望将他们的订阅计划更改为其他产品或“等级”。
最简单的方式是将客户引导至 Stripe 的 客户结算门户 (Customer Billing Portal),
该门户提供一个托管的用户界面,使客户能够下载发票、更新付款方式以及更改订阅计划。
首先,在应用程序中定义一个链接或按钮,指向一个 Laravel 路由,我们将利用该路由来启动一个结算门户(Billing Portal)会话:
<a href="{{ route('billing') }}">
Billing
</a>
接下来,我们定义一个路由,用于启动一个 Stripe 客户结算门户(Customer Billing Portal)会话,并将用户重定向到该门户。
redirectToBillingPortal 方法接收一个 URL 参数,用于指定用户退出门户后应返回的页面地址:
use Illuminate\Http\Request;
Route::get('/billing', function (Request $request) {
return $request->user()->redirectToBillingPortal(route('dashboard'));
})->middleware(['auth'])->name('billing');
[!注意]
只要您配置了 Cashier 的 webhook 处理功能,Cashier 就会自动通过检查来自 Stripe 的入站 webhook 来保持应用程序中与 Cashier 相关的数据库表同步。
例如,当用户通过 Stripe 的客户结算门户取消订阅时,Cashier 会接收到相应的 webhook,并在您的应用数据库中将该订阅标记为“已取消”。
客户(Customers)
获取客户
您可以使用 Cashier::findBillable 方法通过 Stripe ID 检索客户。
此方法将返回一个可计费(billable)模型的实例:
use Laravel\Cashier\Cashier;
$user = Cashier::findBillable($stripeId);
创建客户
有时,您可能希望在未开始订阅的情况下创建一个 Stripe 客户。
您可以使用 createAsStripeCustomer 方法来实现:
$stripeCustomer = $user->createAsStripeCustomer();
一旦客户已在 Stripe 中创建,您可以在之后的任意时间开始订阅。
此外,您可以传递一个可选的 $options 数组,用于传入任何 Stripe API 所支持的客户创建参数:
$stripeCustomer = $user->createAsStripeCustomer($options);
如果您想为可计费(billable)模型返回对应的 Stripe 客户对象,可以使用 asStripeCustomer 方法:
$stripeCustomer = $user->asStripeCustomer();
如果您想获取某个可计费模型的 Stripe 客户对象,但不确定该模型是否已经是 Stripe 中的客户,可以使用 createOrGetStripeCustomer 方法。
该方法会在 Stripe 中创建一个新客户(如果尚不存在的话):
$stripeCustomer = $user->createOrGetStripeCustomer();
更新客户
有时,您可能希望直接在 Stripe 中更新客户的附加信息。
可以使用 updateStripeCustomer 方法来实现。
该方法接收一个数组,包含 Stripe API 支持的客户更新选项:
$stripeCustomer = $user->updateStripeCustomer($options);
余额(Balances)
Stripe 允许您为客户的“余额”进行充值(credit)或扣款(debit)。
之后,这个余额会在新的发票中进行抵扣或加收。
要检查客户的总余额,可以使用可计费模型上提供的 balance 方法。
该方法将返回一个以客户货币为单位的格式化字符串:
$balance = $user->balance();
要为客户账户充值(增加余额),可以向 creditBalance 方法传入一个数值。
如果需要,还可以提供一个描述说明:
$user->creditBalance(500, '高级客户充值。');
向 debitBalance 方法传入一个数值会从客户余额中扣除该金额:
$user->debitBalance(300, '违规使用处罚。');
applyBalance 方法会为客户创建新的余额交易记录。
您可以使用 balanceTransactions 方法来获取这些交易记录,
这对于向客户展示充值和扣款日志非常有用:
// 获取所有交易记录...
$transactions = $user->balanceTransactions();
foreach ($transactions as $transaction) {
// 交易金额...
$amount = $transaction->amount(); // $2.31
// 获取相关的发票(如果有的话)...
$invoice = $transaction->invoice();
}
税务 ID(Tax IDs)
Cashier 提供了一种简单的方式来管理客户的税务 ID。
例如,可以使用 taxIds 方法获取分配给某个客户的所有 税务 ID 集合:
$taxIds = $user->taxIds();
您还可以通过税务 ID 的标识符来获取特定客户的某个税务 ID:
$taxId = $user->findTaxId('txi_belgium');
您可以通过向 createTaxId 方法提供一个有效的 类型 和值来创建一个新的税务 ID:
$taxId = $user->createTaxId('eu_vat', 'BE0123456789');
createTaxId 方法会立即将增值税(VAT)ID 添加到客户账户中。
Stripe 还会对增值税 ID 进行验证,不过该验证过程是异步执行的。
您可以通过订阅 customer.tax_id.updated webhook 事件来获知验证状态的更新,并检查 税务 ID 的 verification 参数。
有关如何处理 webhook 的更多信息,请参阅定义 webhook 处理程序的文档。
您可以使用 deleteTaxId 方法删除税务 ID:
$user->deleteTaxId('txi_belgium');
同步客户数据与 Stripe
通常情况下,当您的应用用户更新了他们的姓名、电子邮件地址或其他同时存储在 Stripe 中的信息时,
您应当通知 Stripe 这些更新。
这样可以确保 Stripe 中保存的信息与您的应用数据保持同步。
为了实现自动同步,您可以在可计费(billable)模型上定义一个事件监听器,用于响应模型的 updated 事件。
然后,在事件监听器中调用模型的 syncStripeCustomerDetails 方法:
use App\Models\User;
use function Illuminate\Events\queueable;
/**
* 模型的 "booted" 方法。
*/
protected static function booted(): void
{
static::updated(queueable(function (User $customer) {
if ($customer->hasStripeId()) {
$customer->syncStripeCustomerDetails();
}
}));
}
现在,每当客户模型被更新时,其信息都会与 Stripe 同步。
为了方便起见,Cashier 会在客户首次创建时自动将客户信息与 Stripe 同步。
您可以通过重写 Cashier 提供的多个方法,来自定义同步客户信息到 Stripe 时所使用的字段。
例如,您可以重写 stripeName 方法,以自定义在 Cashier 同步客户信息到 Stripe 时,
应被视为客户“名称”的属性:
/**
* 获取应同步到 Stripe 的客户名称。
*/
public function stripeName(): string|null
{
return $this->company_name;
}
类似地,您可以重写 stripeEmail、stripePhone、stripeAddress 和 stripePreferredLocales 方法。
在更新 Stripe 客户对象时,这些方法会将信息同步到相应的客户参数。
如果您希望完全控制客户信息的同步过程,可以重写 syncStripeCustomerDetails 方法。
结算门户(Billing Portal)
Stripe 提供了一种简单的方式来设置结算门户,
以便客户可以管理他们的订阅、支付方式,并查看账单历史。
您可以通过在控制器或路由中调用可计费模型的 redirectToBillingPortal 方法,
将用户重定向到结算门户:
use Illuminate\Http\Request;
Route::get('/billing-portal', function (Request $request) {
return $request->user()->redirectToBillingPortal();
});
默认情况下,当用户在管理完他们的订阅后,他们可以通过 Stripe 账单门户中的一个链接返回到你应用程序的 home 路由。
你可以通过将自定义 URL 作为参数传递给 redirectToBillingPortal 方法,来指定用户返回的自定义地址:
use Illuminate\Http\Request;
Route::get('/billing-portal', function (Request $request) {
return $request->user()->redirectToBillingPortal(route('billing'));
});
如果你希望生成账单门户的 URL,而不是生成一个 HTTP 重定向响应,可以调用 billingPortalUrl 方法:
$url = $request->user()->billingPortalUrl(route('billing'));
支付方式
存储支付方式
为了使用 Stripe 创建订阅或执行“一次性”收费,你需要存储支付方式并从 Stripe 获取其标识符。
实现这一点的方法取决于你计划将该支付方式用于订阅还是单次收费。下面我们将分别进行说明。
用于订阅的支付方式
当存储客户的信用卡信息以供未来订阅使用时,必须使用 Stripe 的 “Setup Intents” API 来安全地收集客户的支付方式信息。
“Setup Intent” 表示你打算向客户的支付方式收费。
Cashier 的 Billable trait 提供了 createSetupIntent 方法,可以方便地创建一个新的 Setup Intent。
你应当在用于渲染客户填写支付方式信息表单的路由或控制器中调用此方法:
return view('update-payment-method', [
'intent' => $user->createSetupIntent()
]);
在你创建了 Setup Intent 并将其传递给视图之后,你应该将其密钥(secret)附加到用于收集支付方式信息的元素上。
例如,考虑下面这个“更新支付方式”的表单:
<input id="card-holder-name" type="text">
<!-- Stripe Elements 占位符 -->
<div id="card-element"></div>
<button id="card-button" data-secret="{{ $intent->client_secret }}">
Update Payment Method
</button>
接下来,可以使用 Stripe.js 库将一个 Stripe Element 附加到表单上,以安全地收集客户的支付信息:
<script src="https://js.stripe.com/v3/"></script>
<script>
const stripe = Stripe('stripe-public-key');
const elements = stripe.elements();
const cardElement = elements.create('card');
cardElement.mount('#card-element');
</script>
接下来,可以使用 Stripe 的 confirmCardSetup 方法 来验证银行卡并从 Stripe 获取一个安全的“支付方式标识符”:
const cardHolderName = document.getElementById('card-holder-name');
const cardButton = document.getElementById('card-button');
const clientSecret = cardButton.dataset.secret;
cardButton.addEventListener('click', async (e) => {
const { setupIntent, error } = await stripe.confirmCardSetup(
clientSecret, {
payment_method: {
card: cardElement,
billing_details: { name: cardHolderName.value }
}
}
);
if (error) {
// 向用户显示 “error.message” ...
} else {
// 卡片验证成功 ...
}
});
在卡片被 Stripe 验证之后,你可以将生成的 setupIntent.payment_method 标识符传递给你的 Laravel 应用程序,并将其附加到该客户。
该支付方式可以被 添加为新的支付方式,或者 用于更新默认支付方式。
你也可以立即使用该支付方式标识符来 创建一个新的订阅。
[!注意]
如果你想了解更多关于 Setup Intents 和收集客户支付信息的内容,请参阅 Stripe 提供的这篇概述。
用于单次收费的支付方式(Payment Methods for Single Charges)
当然,当对客户的支付方式进行单次收费时,我们只需要使用一次支付方式标识符。
由于 Stripe 的限制,你不能使用客户已存储的默认支付方式来执行单次收费。
你必须允许客户通过 Stripe.js 库输入他们的支付信息。
例如,考虑以下表单:
<input id="card-holder-name" type="text">
<!-- Stripe Elements 占位符 -->
<div id="card-element"></div>
<button id="card-button">
Process Payment
</button>
在定义了这样的表单后,可以使用 Stripe.js 库将一个 Stripe Element 附加到表单上,以安全地收集客户的支付信息:
<script src="https://js.stripe.com/v3/"></script>
<script>
const stripe = Stripe('stripe-public-key');
const elements = stripe.elements();
const cardElement = elements.create('card');
cardElement.mount('#card-element');
</script>
接下来,可以使用 Stripe 的 createPaymentMethod 方法 来验证银行卡,并从 Stripe 获取一个安全的“支付方式标识符”:
const cardHolderName = document.getElementById('card-holder-name');
const cardButton = document.getElementById('card-button');
cardButton.addEventListener('click', async (e) => {
const { paymentMethod, error } = await stripe.createPaymentMethod(
'card', cardElement, {
billing_details: { name: cardHolderName.value }
}
);
if (error) {
// 向用户显示 “error.message” ...
} else {
// 卡片验证成功 ...
}
});
如果银行卡验证成功,你可以将 paymentMethod.id 传递给你的 Laravel 应用程序,以便处理一次性单次收费。
获取支付方式(Retrieving Payment Methods)
在可计费(billable)模型实例上调用 paymentMethods 方法,会返回一个包含多个 Laravel\Cashier\PaymentMethod 实例的集合:
$paymentMethods = $user->paymentMethods();
默认情况下,此方法会返回所有类型的支付方式。
如果你想获取特定类型的支付方式,可以将 type 作为参数传递给该方法:
$paymentMethods = $user->paymentMethods('sepa_debit');
要获取客户的默认支付方式,可以使用 defaultPaymentMethod 方法:
$paymentMethod = $user->defaultPaymentMethod();
你还可以使用 findPaymentMethod 方法来获取附加到该可计费模型的特定支付方式:
$paymentMethod = $user->findPaymentMethod($paymentMethodId);
支付方式存在性(Payment Method Presence)
要判断一个可计费模型的账户中是否附加了默认支付方式,可以调用 hasDefaultPaymentMethod 方法:
if ($user->hasDefaultPaymentMethod()) {
// ...
}
你可以使用 hasPaymentMethod 方法来判断该模型是否至少附加了一个支付方式:
if ($user->hasPaymentMethod()) {
// ...
}
此方法会判断模型是否拥有任何支付方式。
若要判断模型是否拥有特定类型的支付方式,可以将 type 作为参数传递给该方法:
if ($user->hasPaymentMethod('sepa_debit')) {
// ...
}
更新默认支付方式(Updating the Default Payment Method)
可以使用 updateDefaultPaymentMethod 方法来更新客户的默认支付方式信息。
该方法接受一个 Stripe 支付方式标识符,并将其设置为新的默认账单支付方式:
$user->updateDefaultPaymentMethod($paymentMethod);
要将本地的默认支付方式信息与 Stripe 上客户的默认支付方式信息同步,可以使用 updateDefaultPaymentMethodFromStripe 方法:
$user->updateDefaultPaymentMethodFromStripe();
[!警告]
客户的默认支付方式只能用于开发票(invoicing)和创建新的订阅。
由于 Stripe 的限制,它不能用于单次收费(single charges)。
添加支付方式(Adding Payment Methods)
要添加新的支付方式,可以在可计费(billable)模型上调用 addPaymentMethod 方法,并传入支付方式标识符:
$user->addPaymentMethod($paymentMethod);
[!注意]
要了解如何获取支付方式标识符,请查看支付方式存储文档。
删除支付方式(Deleting Payment Methods)
要删除某个支付方式,可以在你想删除的 Laravel\Cashier\PaymentMethod 实例上调用 delete 方法:
$paymentMethod->delete();
deletePaymentMethod 方法可以从可计费模型中删除特定的支付方式:
$user->deletePaymentMethod('pm_visa');
deletePaymentMethods 方法会删除可计费模型的所有支付方式信息:
$user->deletePaymentMethods();
默认情况下,此方法会删除所有类型的支付方式。
若要删除特定类型的支付方式,可以将 type 作为参数传递给该方法:
$user->deletePaymentMethods('sepa_debit');
[!警告]
如果用户拥有一个活跃的订阅,你的应用程序不应允许他们删除其默认支付方式。
订阅(Subscriptions)
订阅提供了一种为客户设置定期付款的方式。
由 Cashier 管理的 Stripe 订阅支持多订阅价格、订阅数量、试用期等功能。
创建订阅(Creating Subscriptions)
要创建订阅,首先需要获取你的可计费模型实例(通常是 App\Models\User 的实例)。
在获取到模型实例后,你可以使用 newSubscription 方法来创建该模型的订阅:
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$request->user()->newSubscription(
'default', 'price_monthly'
)->create($request->paymentMethodId);
// ...
});
传递给 newSubscription 方法的第一个参数应该是订阅的内部类型。
如果你的应用程序只提供单一订阅,你可以将其命名为 default 或 primary。
这种订阅类型仅供应用程序内部使用,不用于展示给用户。
此外,它不应包含空格,并且在创建订阅之后绝不应该被更改。
第二个参数是用户正在订阅的具体价格。该值应当与 Stripe 中价格的标识符相对应。
create 方法,它接受 一个 Stripe 支付方式标识符 或 Stripe PaymentMethod 对象,将会启动订阅,并更新你的数据库,包括可计费模型的 Stripe 客户 ID 以及其他相关的计费信息。
[!警告]
直接将支付方式标识符传递给create订阅方法也会自动将其添加到用户已保存的支付方式中。
通过发票邮件收取循环付款
你可以不自动收取客户的循环付款,而是指示 Stripe 在每次循环付款到期时向客户发送一封发票邮件。然后客户可以在收到发票后手动支付。在通过发票收取循环付款时,客户不需要提前提供支付方式:
$user->newSubscription('default', 'price_monthly')->createAndSendInvoice();
客户必须在订阅被取消之前支付他们的发票,这个期限由 days_until_due 选项决定。
默认情况下是 30 天;不过,如果你愿意,你可以为此选项提供一个特定的值:
$user->newSubscription('default', 'price_monthly')->createAndSendInvoice([], [
'days_until_due' => 30
]);
数量(Quantities)
如果你想在创建订阅时为该价格设置一个特定的数量,你应该在创建订阅之前,在订阅构造器上调用 quantity 方法:
$user->newSubscription('default', 'price_monthly')
->quantity(5)
->create($paymentMethod);
其他详细信息(Additional Details)
如果你想指定 Stripe 所支持的额外客户或订阅选项,你可以将它们作为第二和第三个参数传递给 create 方法:
$user->newSubscription('default', 'price_monthly')->create($paymentMethod, [
'email' => $email,
], [
'metadata' => ['note' => 'Some extra information.'],
]);
优惠券(Coupons)
如果你想在创建订阅时应用一个优惠券,你可以使用 withCoupon 方法:
$user->newSubscription('default', 'price_monthly')
->withCoupon('code')
->create($paymentMethod);
或者,如果你想应用一个Stripe 促销代码,你可以使用 withPromotionCode 方法:
$user->newSubscription('default', 'price_monthly')
->withPromotionCode('promo_code_id')
->create($paymentMethod);
给定的促销代码 ID 应当是 Stripe 分配给该促销代码的 API ID,而不是面向客户的促销代码。
如果你需要根据一个面向客户的促销代码查找促销代码 ID,你可以使用 findPromotionCode 方法:
// 根据面向客户的代码查找促销代码 ID...
$promotionCode = $user->findPromotionCode('SUMMERSALE');
// 根据面向客户的代码查找一个“有效的”活动促销代码 ID...
$promotionCode = $user->findActivePromotionCode('SUMMERSALE');
在上面的示例中,返回的 $promotionCode 对象是 Laravel\Cashier\PromotionCode 的一个实例。
这个类包装了一个底层的 Stripe\PromotionCode 对象。
你可以通过调用 coupon 方法来获取与该促销代码相关联的优惠券:
$coupon = $user->findPromotionCode('SUMMERSALE')->coupon();
该优惠券实例允许你判断折扣金额,以及该优惠券表示的是固定金额折扣还是百分比折扣
if ($coupon->isPercentage()) {
return $coupon->percentOff().'%'; // 21.5%
} else {
return $coupon->amountOff(); // $5.99
}
你还可以检索当前应用于某个客户或订阅的折扣:
$discount = $billable->discount();
$discount = $subscription->discount();
返回的 Laravel\Cashier\Discount 实例包装了一个底层的 Stripe\Discount 对象实例。
你可以通过调用 coupon 方法来检索与该折扣相关的优惠券:
$coupon = $subscription->discount()->coupon();
如果你想将一个新的优惠券或促销代码应用到某个客户或订阅上,你可以通过 applyCoupon 或 applyPromotionCode 方法来实现:
$billable->applyCoupon('coupon_id');
$billable->applyPromotionCode('promotion_code_id');
$subscription->applyCoupon('coupon_id');
$subscription->applyPromotionCode('promotion_code_id');
请记住,你应该使用 Stripe 分配给促销代码的 API ID,而不是面向客户的促销代码。
在任意时刻,只有一个优惠券或促销代码可以被应用到某个客户或订阅上。
添加订阅
如果你想为一个已经拥有默认支付方式的客户添加订阅,你可以在订阅构造器上调用 add 方法:
use App\Models\User;
$user = User::find(1);
$user->newSubscription('default', 'price_monthly')->add();
从 Stripe 仪表盘创建订阅 (Creating Subscriptions From the Stripe Dashboard)
你也可以直接在 Stripe 仪表盘中创建订阅。
在这样做时,Cashier 会同步新添加的订阅,并将它们分配为 default 类型。
若要自定义分配给通过仪表盘创建的订阅的订阅类型,你可以定义 webhook 事件处理程序。
此外,你只能通过 Stripe 仪表盘创建一种类型的订阅。
如果你的应用提供多种使用不同类型的订阅,那么通过 Stripe 仪表盘只能添加其中一种订阅类型。
最后,你应该始终确保每种订阅类型在你的应用中只为客户添加一个活跃订阅。
如果一个客户拥有两个 default 订阅,即使两者都会与应用的数据库同步,Cashier 也只会使用最近添加的那个订阅。
检查订阅状态 (Checking Subscription Status)
一旦客户订阅了你的应用,你可以使用多种便捷方法轻松检查其订阅状态。
首先,如果客户拥有一个活跃订阅,即使该订阅当前处于试用期,subscribed 方法也会返回 true。
subscribed 方法将订阅的类型作为第一个参数:
if ($user->subscribed('default')) {
// ...
}
subscribed 方法也是一个很好的路由中间件候选,可以根据用户的订阅状态来过滤对路由和控制器的访问:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureUserIsSubscribed
{
/**
* 处理一个传入请求。
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if ($request->user() && ! $request->user()->subscribed('default')) {
// 该用户不是付费客户...
return redirect('/billing');
}
return $next($request);
}
}
如果你想判断用户是否仍处于试用期,你可以使用 onTrial 方法。
该方法可用于判断是否应向用户显示他们仍在试用期的提示:
if ($user->subscription('default')->onTrial()) {
// ...
}
subscribedToProduct 方法可用于判断用户是否订阅了指定产品,该产品基于给定的 Stripe 产品标识符。
在 Stripe 中,产品是一组价格的集合。
在此示例中,我们将判断用户的 default 订阅是否已主动订阅应用程序的 “premium” 产品。
给定的 Stripe 产品标识符应对应于 Stripe 仪表盘中你产品的某一个标识符:
if ($user->subscribedToProduct('prod_premium', 'default')) {
// ...
}
通过向 subscribedToProduct 方法传递一个数组,你可以判断用户的 default 订阅是否已主动订阅应用程序的 “basic” 或 “premium” 产品:
if ($user->subscribedToProduct(['prod_basic', 'prod_premium'], 'default')) {
// ...
}
subscribedToPrice 方法可用于判断某个客户的订阅是否对应给定的价格 ID:
if ($user->subscribedToPrice('price_basic_monthly', 'default')) {
// ...
}
recurring 方法可用于判断用户当前是否已订阅并且不再处于试用期内:
if ($user->subscription('default')->recurring()) {
// ...
}
[!警告]
如果一个用户拥有两个相同类型的订阅,subscription方法将始终返回最新的订阅。例如,一个用户可能拥有两个类型为default的订阅记录;然而,其中一个订阅可能是旧的、已过期的订阅,而另一个是当前的、有效的订阅。此时会始终返回最新的订阅,而较旧的订阅会保留在数据库中用于历史查看。
取消订阅状态
要确定用户是否曾经是活跃订阅者但已取消订阅,你可以使用 canceled 方法:
if ($user->subscription('default')->canceled()) {
// ...
}
你也可以判断用户是否已经取消订阅但仍处于他们的“宽限期”(grace period)直到订阅完全过期。例如,如果用户在 3 月 5 日取消了订阅,而该订阅原本计划在 3 月 10 日过期,那么用户将在 3 月 10 日之前处于“宽限期”。请注意,在这段时间内,subscribed 方法仍然返回 true:
if ($user->subscription('default')->onGracePeriod()) {
// ...
}
要判断用户是否已经取消订阅并且不再处于“宽限期”内,你可以使用 ended 方法:
if ($user->subscription('default')->ended()) {
// ...
}
未完成支付与逾期状态说明
如果一个订阅在创建之后需要执行二次支付操作,那么该订阅将被标记为 incomplete。订阅状态储存在 Cashier 的 subscriptions 数据表中的 stripe_status 列里。
类似地,如果在切换价格(swapping prices)时需要执行二次支付操作,那么该订阅将被标记为 past_due。当你的订阅处于这两种状态中的任一种时,在客户确认他们的付款之前,它都不会处于激活状态。要判断某个订阅是否有未完成的付款,可以在计费模型(billable model)或订阅实例上使用 hasIncompletePayment 方法:
if ($user->hasIncompletePayment('default')) {
// ...
}
if ($user->subscription('default')->hasIncompletePayment()) {
// ...
}
当一个订阅有未完成的付款时,你应该将用户引导到 Cashier 的付款确认页面,并传递 latestPayment 标识符。你可以使用订阅实例上可用的 latestPayment 方法来获取这个标识符:
<a href="{{ route('cashier.payment', $subscription->latestPayment()->id) }}">
Please confirm your payment.
</a>
如果你希望在订阅处于 past_due 或 incomplete 状态时仍将其视为已激活(active),你可以使用 Cashier 提供的 keepPastDueSubscriptionsActive 与 keepIncompleteSubscriptionsActive 方法。通常,这些方法应在 App\Providers\AppServiceProvider 的 register 方法中被调用:
use Laravel\Cashier\Cashier;
/**
* 注册应用程序所需的所有服务。
*/
public function register(): void
{
Cashier::keepPastDueSubscriptionsActive();
Cashier::keepIncompleteSubscriptionsActive();
}
[!警告]
当订阅处于incomplete状态时,在付款确认之前,它无法被更改。因此,当订阅处于incomplete状态时,swap和updateQuantity方法将抛出异常。
订阅作用域
大多数订阅状态也可作为查询作用域使用,这样你就可以轻松查询数据库中处于某一特定状态的订阅:
// 获取所有激活的订阅...
$subscriptions = Subscription::query()->active()->get();
// 获取某个用户所有已取消的订阅...
$subscriptions = $user->subscriptions()->canceled()->get();
A complete list of available scopes is available below:
Subscription::query()->active();
Subscription::query()->canceled();
Subscription::query()->ended();
Subscription::query()->incomplete();
Subscription::query()->notCanceled();
Subscription::query()->notOnGracePeriod();
Subscription::query()->notOnTrial();
Subscription::query()->onGracePeriod();
Subscription::query()->onTrial();
Subscription::query()->pastDue();
Subscription::query()->recurring();
更改价格
在客户订阅你的应用程序之后,他们可能偶尔会希望更改为新的订阅价格。要将客户切换到新的价格,将 Stripe 价格的标识符传递给 swap 方法。在切换价格时,假定用户希望在之前已取消的情况下重新激活他们的订阅。所给的价格标识符应对应 Stripe 控制台中可用的 Stripe 价格标识符:
use App\Models\User;
$user = App\Models\User::find(1);
$user->subscription('default')->swap('price_yearly');
如果客户处于试用期内,试用期将被保留。此外,如果订阅存在“数量”,该数量也会被保留。
如果你希望在切换价格的同时取消客户当前的任何试用期,你可以调用 skipTrial 方法:
$user->subscription('default')
->skipTrial()
->swap('price_yearly');
如果你希望在切换价格时立即向客户开具账单,而不是等待他们的下一个计费周期,你可以使用 swapAndInvoice 方法:
$user = User::find(1);
$user->subscription('default')->swapAndInvoice('price_yearly');
按比例分摊
默认情况下,在切换不同价格时,Stripe 会对费用进行按比例分摊。noProrate 方法可用于在不进行按比例分摊的情况下更新订阅的价格:
$user->subscription('default')->noProrate()->swap('price_yearly');
关于订阅按比例分摊的更多信息,请参阅 Stripe 文档。
[!警告]
在swapAndInvoice方法之前执行noProrate方法对按比例分摊不会产生任何影响。发票将始终被开具。
订阅数量
有时,订阅会受“数量”的影响。例如,一个项目管理应用可能会按每个项目每月 10 美元收费。你可以使用 incrementQuantity 和 decrementQuantity 方法来轻松增加或减少订阅数量:
use App\Models\User;
$user = User::find(1);
$user->subscription('default')->incrementQuantity();
// 在订阅当前数量上增加五个...
$user->subscription('default')->incrementQuantity(5);
$user->subscription('default')->decrementQuantity();
// 在订阅当前数量上减少五个...
$user->subscription('default')->decrementQuantity(5);
或者,你可以使用 updateQuantity 方法设置一个特定数量:
$user->subscription('default')->updateQuantity(10);
noProrate 方法可以用于在不进行按比例分摊的情况下更新订阅数量:
$user->subscription('default')->noProrate()->updateQuantity(10);
关于订阅数量的更多信息,请参阅 Stripe 文档。
包含多个产品的订阅的数量
如果你的订阅是带有多个产品的订阅,那么你应当将你希望递增或递减数量的价格 ID 作为第二个参数传递给 increment / decrement 方法:
$user->subscription('default')->incrementQuantity(1, 'price_chat');
包含多个产品的订阅
带有多个产品的订阅允许你将多个计费产品分配给一个单一的订阅。例如,假设你正在构建一个客户服务“帮助台”应用程序,它的基础订阅价格是每月 10 美元,但提供一个额外的实时聊天附加产品,每月额外收费 15 美元。有关带有多个产品的订阅的信息存储在 Cashier 的 subscription_items 数据表中。
你可以通过将一个价格数组作为第二个参数传递给 newSubscription 方法,为某个订阅指定多个产品:
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$request->user()->newSubscription('default', [
'price_monthly',
'price_chat',
])->create($request->paymentMethodId);
// ...
});
在上述示例中,客户将有两个价格附加到他们的 default 订阅上。这两个价格都会根据它们各自的计费周期进行收费。如有必要,你可以使用 quantity 方法为每个价格指定特定数量:
$user = User::find(1);
$user->newSubscription('default', ['price_monthly', 'price_chat'])
->quantity(5, 'price_chat')
->create($paymentMethod);
如果你希望向一个已有订阅添加另一个价格,你可以调用订阅的 addPrice 方法:
$user = User::find(1);
$user->subscription('default')->addPrice('price_chat');
上述示例会添加新的价格,并且客户会在他们的下一个计费周期被收取费用。
如果你想立即向客户收费,你可以使用 addPriceAndInvoice 方法:
$user->subscription('default')->addPriceAndInvoice('price_chat');
如果你想以特定数量添加一个价格,你可以将数量作为第二个参数传递给 addPrice 或 addPriceAndInvoice 方法:
$user = User::find(1);
$user->subscription('default')->addPrice('price_chat', 5);
你可以使用 removePrice 方法从订阅中移除价格:
$user->subscription('default')->removePrice('price_chat');
[!警告]
你不能移除订阅中的最后一个价格。相反,你应该直接取消订阅。
交换价格(Swapping Prices)
你也可以更改附加在包含多个产品的订阅上的价格。
例如,想象一个客户有一个带有 price_chat 附加产品的 price_basic 订阅,而你想将该客户从 price_basic 升级到 price_pro:
use App\Models\User;
$user = User::find(1);
$user->subscription('default')->swap(['price_pro', 'price_chat']);
执行上述示例时,带有 price_basic 的底层订阅项会被删除,而带有 price_chat 的订阅项会被保留。并且,会创建一个新的 price_pro 订阅项。
你也可以通过向 swap 方法传递一个键/值对数组来指定订阅项选项。例如,你可能需要指定订阅价格的数量:
$user = User::find(1);
$user->subscription('default')->swap([
'price_pro' => ['quantity' => 5],
'price_chat'
]);
如果你想在订阅上交换单个价格,你可以在订阅项本身上使用 swap 方法来完成。
这种方法在你希望保留订阅中其他价格的所有现有元数据时特别有用:
$user = User::find(1);
$user->subscription('default')
->findItemOrFail('price_basic')
->swap('price_pro');
按比例分摊(Proration)
默认情况下,当在包含多个产品的订阅中添加或移除价格时,Stripe 会按比例分摊费用。
如果你想在不进行按比例分摊的情况下进行价格调整,你应该将 noProrate 方法链式调用到你的价格操作上:
$user->subscription('default')->noProrate()->removePrice('price_chat');
数量(Quantities)
如果你想更新单个订阅价格的数量,你可以使用现有的数量方法(existing quantity methods),并将价格的 ID 作为额外参数传递给该方法:
$user = User::find(1);
$user->subscription('default')->incrementQuantity(5, 'price_chat');
$user->subscription('default')->decrementQuantity(3, 'price_chat');
$user->subscription('default')->updateQuantity(10, 'price_chat');
[!警告]
当一个订阅有多个价格时,Subscription模型上的stripe_price和quantity属性将为null。要访问单个价格的属性,你应该使用Subscription模型提供的items关系。
订阅项(Subscription Items)
当一个订阅有多个价格时,它会在数据库的 subscription_items 表中拥有多个订阅 “items”。你可以通过订阅上的 items 关系来访问这些项:
use App\Models\User;
$user = User::find(1);
$subscriptionItem = $user->subscription('default')->items->first();
// 获取特定项的 Stripe 价格和数量...
$stripePrice = $subscriptionItem->stripe_price;
$quantity = $subscriptionItem->quantity;
你也可以使用 findItemOrFail 方法检索特定的价格:
$user = User::find(1);
$subscriptionItem = $user->subscription('default')->findItemOrFail('price_chat');
多个订阅(Multiple Subscriptions)
Stripe 允许你的客户同时拥有多个订阅。
例如,你可能经营一家健身房,提供游泳订阅和举重订阅,并且每个订阅可能有不同的定价。
当然,客户应该能够订阅其中一个或两个计划。
当你的应用程序创建订阅时,你可以将订阅的类型提供给 newSubscription 方法。
该类型可以是任何字符串,用来表示用户正在启动的订阅类型:
use Illuminate\Http\Request;
Route::post('/swimming/subscribe', function (Request $request) {
$request->user()->newSubscription('swimming')
->price('price_swimming_monthly')
->create($request->paymentMethodId);
// ...
});
在这个示例中,我们为客户启动了一个按月计费的游泳订阅。
然而,他们可能会在稍后时间希望切换到按年计费的订阅。
在调整客户的订阅时,我们只需交换 swimming 订阅上的价格即可:
$user->subscription('swimming')->swap('price_swimming_yearly');
当然,你也可以完全取消该订阅:
$user->subscription('swimming')->cancel();
基于用量计费(Usage Based Billing)
基于用量计费 允许你根据客户在计费周期内的产品使用量向他们收费。
例如,你可能会根据客户每月发送的短信数或电子邮件数量进行收费。
要开始使用用量计费,你首先需要在 Stripe 控制台中创建一个带有
基于用量的计费模型 和一个
meter(计量器) 的新产品。
在创建计量器之后,存储相关的事件名称和计量器 ID,因为你需要它们来报告和检索用量。
然后,使用 meteredPrice 方法将按用量计费的价格 ID 添加到客户订阅中:
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$request->user()->newSubscription('default')
->meteredPrice('price_metered')
->create($request->paymentMethodId);
// ...
});
你也可以通过 Stripe Checkout 启动一个按用量计费的订阅:
$checkout = Auth::user()
->newSubscription('default', [])
->meteredPrice('price_metered')
->checkout();
return view('your-checkout-view', [
'checkout' => $checkout,
]);
报告用量(Reporting Usage)
当你的客户使用你的应用程序时,你将向 Stripe 报告他们的使用量,以便他们能够被准确计费。
要报告一个按用量计费事件的使用量,你可以在你的 Billable 模型上使用 reportMeterEvent 方法:
$user = User::find(1);
$user->reportMeterEvent('emails-sent');
默认情况下,会将一个“使用数量”(usage quantity)1 添加到计费周期中。
或者,你可以传递一个特定数量的 “usage”,将其添加到该计费周期内用户的使用量中:
$user = User::find(1);
$user->reportMeterEvent('emails-sent', quantity: 15);
要检索客户某个 meter 的事件摘要,你可以使用 Billable 实例的 meterEventSummaries 方法:
$user = User::find(1);
$meterUsage = $user->meterEventSummaries($meterId);
$meterUsage->first()->aggregated_value // 10
关于 meter 事件摘要的更多信息,请参考 Stripe 的
Meter Event Summary 对象文档。
要列出所有 meters,你可以使用 Billable 实例的 meters 方法:
$user = User::find(1);
$user->meters();
订阅税(Subscription Taxes)
[!警告]
与其手动计算税率,你可以使用 Stripe Tax 自动计算税费
要指定用户在订阅上支付的税率,你应该在你的 billable 模型上实现 taxRates 方法,并返回一个包含 Stripe 税率 ID 的数组。
你可以在你的 Stripe 控制台中定义这些税率:
/**
* 应用于客户订阅的税率。
*
* @return array<int, string>
*/
public function taxRates(): array
{
return ['txr_id'];
}
taxRates 方法使你能够基于每个客户来应用税率,这对于拥有跨多个国家和税率的用户群体可能会很有帮助。
如果你提供包含多个产品的订阅,你可以通过在 billable 模型上实现 priceTaxRates 方法,为每个价格定义不同的税率:
/**
* 应用于客户订阅的税率。
*
* @return array<string, array<int, string>>
*/
public function priceTaxRates(): array
{
return [
'price_monthly' => ['txr_id'],
];
}
[!警告]
taxRates方法仅适用于订阅收费。如果你使用 Cashier 进行“一次性”收费,你需要在当时手动指定税率。
同步税率(Syncing Tax Rates)
当更改 taxRates 方法返回的硬编码税率 ID 时,该用户的任何现有订阅上的税务设置将保持不变。
如果你希望使用新的 taxRates 值更新现有订阅的税值,你应该在用户的订阅实例上调用 syncTaxRates 方法:
$user->subscription('default')->syncTaxRates();
这也会同步具有多个产品的订阅的任何项目税率。
如果你的应用程序提供包含多个产品的订阅,你应该确保你的 billable 模型实现了前文讨论的 priceTaxRates 方法(见上方的 订阅税 部分)。
税务豁免(Tax Exemption)
Cashier 还提供了 isNotTaxExempt、isTaxExempt 和 reverseChargeApplies 方法,用于确定客户是否免税。
这些方法将调用 Stripe API 以确定客户的税务豁免状态:
use App\Models\User;
$user = User::find(1);
$user->isTaxExempt();
$user->isNotTaxExempt();
$user->reverseChargeApplies();
[!警告]
这些方法在任何Laravel\Cashier\Invoice对象上也可用。
然而,当在Invoice对象上调用时,这些方法将根据发票创建时的状态来确定豁免情况。
订阅锚定日期(Subscription Anchor Date)
默认情况下,计费周期锚点是订阅创建的日期;如果使用了试用期,则为试用期结束的日期。如果你希望修改计费锚定日期,可以使用 anchorBillingCycleOn 方法:
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$anchor = Carbon::parse('first day of next month');
$request->user()->newSubscription('default', 'price_monthly')
->anchorBillingCycleOn($anchor->startOfDay())
->create($request->paymentMethodId);
// ...
});
有关管理订阅计费周期的更多信息,请参阅 Stripe 计费周期文档。
取消订阅(Cancelling Subscriptions)
要取消订阅,请在用户的订阅上调用 cancel 方法:
$user->subscription('default')->cancel();
当订阅被取消时,Cashier 会自动设置你 subscriptions 数据库表中的 ends_at 列。该列用于判断 subscribed 方法何时应开始返回 false。
例如,如果客户在 3 月 1 日取消了订阅,但该订阅原本计划在 3 月 5 日才结束,那么在 3 月 5 日之前,subscribed 方法仍会返回 true。这样做是因为通常允许用户在其计费周期结束之前继续使用应用程序。
你可以使用 onGracePeriod 方法来判断用户是否已经取消订阅但仍处于“宽限期”内:
if ($user->subscription('default')->onGracePeriod()) {
// ...
}
如果你希望立即取消订阅,请在用户的订阅上调用 cancelNow 方法:
$user->subscription('default')->cancelNow();
如果你希望立即取消订阅,并为任何尚未开具发票的计量使用量或新的 / 待处理的按比例计费发票项目开具发票,可以在用户的订阅上调用 cancelNowAndInvoice 方法:
$user->subscription('default')->cancelNowAndInvoice();
你也可以选择在某个特定时间点取消订阅:
$user->subscription('default')->cancelAt(
now()->addDays(10)
);
最后,在删除关联的用户模型之前,你应当始终先取消用户的订阅:
$user->subscription('default')->cancelNow();
$user->delete();
恢复订阅(Resuming Subscriptions)
如果客户已经取消了订阅,而你希望恢复该订阅,你可以在订阅上调用 resume 方法。客户必须仍然处于其“宽限期”内,才能恢复订阅:
$user->subscription('default')->resume();
如果客户取消订阅后,在订阅完全过期之前恢复了该订阅,则客户不会被立即扣费。相反,订阅将被重新激活,并且客户将按照原始的计费周期进行计费。
订阅试用(Subscription Trials)
预先提供付款方式(With Payment Method Up Front)
如果你希望在仍然提前收集付款方式信息的情况下向客户提供试用期,那么在创建订阅时应使用 trialDays 方法:
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$request->user()->newSubscription('default', 'price_monthly')
->trialDays(10)
->create($request->paymentMethodId);
// ...
});
该方法会在数据库中的订阅记录上设置试用期结束日期,并指示 Stripe 在此日期之后才开始向客户计费。使用 trialDays 方法时,Cashier 会覆盖在 Stripe 中为该价格配置的任何默认试用期。
[!警告]
如果在试用期结束日期之前未取消客户的订阅,那么试用期一结束就会立即向客户收费,因此你应当确保通知用户其试用期的结束日期。
trialUntil 方法允许你提供一个 DateTime 实例,用于指定试用期应在何时结束:
use Carbon\Carbon;
$user->newSubscription('default', 'price_monthly')
->trialUntil(Carbon::now()->addDays(10))
->create($paymentMethod);
你可以使用用户实例的 onTrial 方法,或订阅实例的 onTrial 方法来判断用户是否处于试用期内。下面两个示例是等价的:
if ($user->onTrial('default')) {
// ...
}
if ($user->subscription('default')->onTrial()) {
// ...
}
你可以使用 endTrial 方法来立即结束订阅试用期:
$user->subscription('default')->endTrial();
要判断一个现有的试用期是否已经过期,可以使用 hasExpiredTrial 方法:
if ($user->hasExpiredTrial('default')) {
// ...
}
if ($user->subscription('default')->hasExpiredTrial()) {
// ...
}
在 Stripe / Cashier 中定义试用天数(Defining Trial Days in Stripe / Cashier)
你可以选择在 Stripe 仪表盘中定义你的价格所包含的试用天数,或者始终通过 Cashier 显式传递试用天数。如果你选择在 Stripe 中定义价格的试用天数,需要注意的是:新的订阅(包括曾经订阅过的客户再次创建的新订阅)都会自动获得试用期,除非你显式调用 skipTrial() 方法。
不预先提供付款方式(Without Payment Method Up Front)
如果你希望在不提前收集用户付款方式信息的情况下提供试用期,你可以在用户记录上将 trial_ends_at 列设置为你期望的试用结束日期。这通常在用户注册期间完成:
use App\Models\User;
$user = User::create([
// ...
'trial_ends_at' => now()->addDays(10),
]);
[!警告]
请务必在你的可计费模型(billable model)类定义中,为trial_ends_at属性添加一个 日期类型转换(date cast)。
Cashier 将这种类型的试用称为“通用试用(generic trial)”,因为它并未关联到任何现有的订阅。如果当前日期尚未超过 trial_ends_at 的值,那么在可计费模型实例上调用 onTrial 方法将返回 true:
if ($user->onTrial()) {
// 用户正处于试用期内……
}
当你准备为用户创建一个实际的订阅时,可以像往常一样使用 newSubscription 方法:
$user = User::find(1);
$user->newSubscription('default', 'price_monthly')->create($paymentMethod);
要获取用户的试用结束日期,可以使用 trialEndsAt 方法。如果用户正处于试用期,该方法将返回一个 Carbon 日期实例;如果用户未处于试用期,则返回 null。如果你希望获取非默认订阅类型的试用结束日期,还可以传递一个可选的订阅类型参数:
if ($user->onTrial()) {
$trialEndsAt = $user->trialEndsAt('main');
}
如果你希望明确判断用户是否正处于其“通用试用”期内,并且尚未创建任何实际订阅,也可以使用 onGenericTrial 方法:
if ($user->onGenericTrial()) {
// 用户正处于其“通用试用”期内……
}
延长试用期(Extending Trials)
extendTrial 方法允许你在订阅创建之后延长订阅的试用期。如果试用期已经结束,并且客户已经开始为该订阅付费,你仍然可以为其提供延长的试用期。客户在试用期内所花费的时间将从其下一张发票中扣除:
use App\Models\User;
$subscription = User::find(1)->subscription('default');
// 将试用期设置为从现在起 7 天后结束……
$subscription->extendTrial(
now()->addDays(7)
);
// 在现有试用期基础上再增加 5 天……
$subscription->extendTrial(
$subscription->trial_ends_at->addDays(5)
);
处理 Stripe Webhooks(Handling Stripe Webhooks)
[!注意]
你可以使用 Stripe CLI 来帮助在本地开发期间测试 webhooks。
Stripe 可以通过 webhooks 向你的应用程序通知各种事件。默认情况下,Cashier 服务提供者会自动注册一个指向 Cashier webhook 控制器的路由。该控制器将处理所有传入的 webhook 请求。
默认情况下,Cashier 的 webhook 控制器会自动处理因失败扣款次数过多而被取消的订阅(由你的 Stripe 设置定义)、客户更新、客户删除、订阅更新以及付款方式更改;不过,正如我们接下来将看到的那样,你可以扩展该控制器来处理任何你需要的 Stripe webhook 事件。
为确保你的应用程序能够处理 Stripe webhooks,请务必在 Stripe 控制面板中配置 webhook URL。默认情况下,Cashier 的 webhook 控制器会响应 /stripe/webhook URL 路径。在 Stripe 控制面板中你应当启用的所有 webhook 事件完整列表如下:
customer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedcustomer.updatedcustomer.deletedpayment_method.automatically_updatedinvoice.payment_action_requiredinvoice.payment_succeeded
为方便起见,Cashier 提供了一个 cashier:webhook Artisan 命令。该命令会在 Stripe 中创建一个 webhook,用于监听 Cashier 所需的所有事件:
php artisan cashier:webhook
默认情况下,创建的 webhook 会指向由 APP_URL 环境变量以及 Cashier 自带的 cashier.webhook 路由所定义的 URL。如果你希望使用不同的 URL,可以在调用该命令时提供 --url 选项:
php artisan cashier:webhook --url "https://example.com/stripe/webhook"
The webhook that is created will use the Stripe API version that your version of Cashier is compatible with. If you would like to use a different Stripe version, you may provide the --api-version option:
php artisan cashier:webhook --api-version="2019-12-03"
After creation, the webhook will be immediately active. If you wish to create the webhook but have it disabled until you're ready, you may provide the --disabled option when invoking the command:
php artisan cashier:webhook --disabled
[!WARNING]
Make sure you protect incoming Stripe webhook requests with Cashier's included webhook signature verification middleware.
Webhooks and CSRF Protection
Since Stripe webhooks need to bypass Laravel's CSRF protection, you should ensure that Laravel does not attempt to validate the CSRF token for incoming Stripe webhooks. To accomplish this, you should exclude stripe/* from CSRF protection in your application's bootstrap/app.php file:
->withMiddleware(function (Middleware $middleware) {
$middleware->validateCsrfTokens(except: [
'stripe/*',
]);
})
Defining Webhook Event Handlers
Cashier automatically handles subscription cancellations for failed charges and other common Stripe webhook events. However, if you have additional webhook events you would like to handle, you may do so by listening to the following events that are dispatched by Cashier:
Laravel\Cashier\Events\WebhookReceivedLaravel\Cashier\Events\WebhookHandled
Both events contain the full payload of the Stripe webhook. For example, if you wish to handle the invoice.payment_succeeded webhook, you may register a listener that will handle the event:
<?php
namespace App\Listeners;
use Laravel\Cashier\Events\WebhookReceived;
class StripeEventListener
{
/**
* Handle received Stripe webhooks.
*/
public function handle(WebhookReceived $event): void
{
if ($event->payload['type'] === 'invoice.payment_succeeded') {
// Handle the incoming event...
}
}
}
Verifying Webhook Signatures
To secure your webhooks, you may use Stripe's webhook signatures. For convenience, Cashier automatically includes a middleware which validates that the incoming Stripe webhook request is valid.
To enable webhook verification, ensure that the STRIPE_WEBHOOK_SECRET environment variable is set in your application's .env file. The webhook secret may be retrieved from your Stripe account dashboard.
Single Charges
Simple Charge
If you would like to make a one-time charge against a customer, you may use the charge method on a billable model instance. You will need to provide a payment method identifier as the second argument to the charge method:
use Illuminate\Http\Request;
Route::post('/purchase', function (Request $request) {
$stripeCharge = $request->user()->charge(
100, $request->paymentMethodId
);
// ...
});
The charge method accepts an array as its third argument, allowing you to pass any options you wish to the underlying Stripe charge creation. More information regarding the options available to you when creating charges may be found in the Stripe documentation:
$user->charge(100, $paymentMethod, [
'custom_option' => $value,
]);
You may also use the charge method without an underlying customer or user. To accomplish this, invoke the charge method on a new instance of your application's billable model:
use App\Models\User;
$stripeCharge = (new User)->charge(100, $paymentMethod);
The charge method will throw an exception if the charge fails. If the charge is successful, an instance of Laravel\Cashier\Payment will be returned from the method:
try {
$payment = $user->charge(100, $paymentMethod);
} catch (Exception $e) {
// ...
}
[!WARNING]
Thechargemethod accepts the payment amount in the lowest denominator of the currency used by your application. For example, if customers are paying in United States Dollars, amounts should be specified in pennies.
Charge With Invoice
Sometimes you may need to make a one-time charge and offer a PDF invoice to your customer. The invoicePrice method lets you do just that. For example, let's invoice a customer for five new shirts:
$user->invoicePrice('price_tshirt', 5);
The invoice will be immediately charged against the user's default payment method. The invoicePrice method also accepts an array as its third argument. This array contains the billing options for the invoice item. The fourth argument accepted by the method is also an array which should contain the billing options for the invoice itself:
$user->invoicePrice('price_tshirt', 5, [
'discounts' => [
['coupon' => 'SUMMER21SALE']
],
], [
'default_tax_rates' => ['txr_id'],
]);
Similarly to invoicePrice, you may use the tabPrice method to create a one-time charge for multiple items (up to 250 items per invoice) by adding them to the customer's "tab" and then invoicing the customer. For example, we may invoice a customer for five shirts and two mugs:
$user->tabPrice('price_tshirt', 5);
$user->tabPrice('price_mug', 2);
$user->invoice();
Alternatively, you may use the invoiceFor method to make a "one-off" charge against the customer's default payment method:
$user->invoiceFor('One Time Fee', 500);
Although the invoiceFor method is available for you to use, it is recommended that you use the invoicePrice and tabPrice methods with pre-defined prices. By doing so, you will have access to better analytics and data within your Stripe dashboard regarding your sales on a per-product basis.
[!WARNING]
Theinvoice,invoicePrice, andinvoiceFormethods will create a Stripe invoice which will retry failed billing attempts. If you do not want invoices to retry failed charges, you will need to close them using the Stripe API after the first failed charge.
Creating Payment Intents
You can create a new Stripe payment intent by invoking the pay method on a billable model instance. Calling this method will create a payment intent that is wrapped in a Laravel\Cashier\Payment instance:
use Illuminate\Http\Request;
Route::post('/pay', function (Request $request) {
$payment = $request->user()->pay(
$request->get('amount')
);
return $payment->client_secret;
});
After creating the payment intent, you can return the client secret to your application's frontend so that the user can complete the payment in their browser. To read more about building entire payment flows using Stripe payment intents, please consult the Stripe documentation.
When using the pay method, the default payment methods that are enabled within your Stripe dashboard will be available to the customer. Alternatively, if you only want to allow for some specific payment methods to be used, you may use the payWith method:
use Illuminate\Http\Request;
Route::post('/pay', function (Request $request) {
$payment = $request->user()->payWith(
$request->get('amount'), ['card', 'bancontact']
);
return $payment->client_secret;
});
[!WARNING]
ThepayandpayWithmethods accept the payment amount in the lowest denominator of the currency used by your application. For example, if customers are paying in United States Dollars, amounts should be specified in pennies.
Refunding Charges
If you need to refund a Stripe charge, you may use the refund method. This method accepts the Stripe payment intent ID as its first argument:
$payment = $user->charge(100, $paymentMethodId);
$user->refund($payment->id);
Invoices
Retrieving Invoices
You may easily retrieve an array of a billable model's invoices using the invoices method. The invoices method returns a collection of Laravel\Cashier\Invoice instances:
$invoices = $user->invoices();
If you would like to include pending invoices in the results, you may use the invoicesIncludingPending method:
$invoices = $user->invoicesIncludingPending();
You may use the findInvoice method to retrieve a specific invoice by its ID:
$invoice = $user->findInvoice($invoiceId);
Displaying Invoice Information
When listing the invoices for the customer, you may use the invoice's methods to display the relevant invoice information. For example, you may wish to list every invoice in a table, allowing the user to easily download any of them:
<table>
@foreach ($invoices as $invoice)
<tr>
<td>{{ $invoice->date()->toFormattedDateString() }}</td>
<td>{{ $invoice->total() }}</td>
<td><a href="/user/invoice/{{ $invoice->id }}">Download</a></td>
</tr>
@endforeach
</table>
Upcoming Invoices
To retrieve the upcoming invoice for a customer, you may use the upcomingInvoice method:
$invoice = $user->upcomingInvoice();
Similarly, if the customer has multiple subscriptions, you can also retrieve the upcoming invoice for a specific subscription:
$invoice = $user->subscription('default')->upcomingInvoice();
Previewing Subscription Invoices
Using the previewInvoice method, you can preview an invoice before making price changes. This will allow you to determine what your customer's invoice will look like when a given price change is made:
$invoice = $user->subscription('default')->previewInvoice('price_yearly');
You may pass an array of prices to the previewInvoice method in order to preview invoices with multiple new prices:
$invoice = $user->subscription('default')->previewInvoice(['price_yearly', 'price_metered']);
Generating Invoice PDFs
Before generating invoice PDFs, you should use Composer to install the Dompdf library, which is the default invoice renderer for Cashier:
composer require dompdf/dompdf
From within a route or controller, you may use the downloadInvoice method to generate a PDF download of a given invoice. This method will automatically generate the proper HTTP response needed to download the invoice:
use Illuminate\Http\Request;
Route::get('/user/invoice/{invoice}', function (Request $request, string $invoiceId) {
return $request->user()->downloadInvoice($invoiceId);
});
By default, all data on the invoice is derived from the customer and invoice data stored in Stripe. The filename is based on your app.name config value. However, you can customize some of this data by providing an array as the second argument to the downloadInvoice method. This array allows you to customize information such as your company and product details:
return $request->user()->downloadInvoice($invoiceId, [
'vendor' => 'Your Company',
'product' => 'Your Product',
'street' => 'Main Str. 1',
'location' => '2000 Antwerp, Belgium',
'phone' => '+32 499 00 00 00',
'email' => 'info@example.com',
'url' => 'https://example.com',
'vendorVat' => 'BE123456789',
]);
The downloadInvoice method also allows for a custom filename via its third argument. This filename will automatically be suffixed with .pdf:
return $request->user()->downloadInvoice($invoiceId, [], 'my-invoice');
Custom Invoice Renderer
Cashier also makes it possible to use a custom invoice renderer. By default, Cashier uses the DompdfInvoiceRenderer implementation, which utilizes the 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:
use Illuminate\Support\Facades\Http;
use Laravel\Cashier\Contracts\InvoiceRenderer;
use Laravel\Cashier\Invoice;
class ApiInvoiceRenderer implements InvoiceRenderer
{
/**
* Render the given invoice and return the raw PDF bytes.
*/
public function render(Invoice $invoice, array $data = [], array $options = []): string
{
$html = $invoice->view($data)->render();
return Http::get('https://example.com/html-to-pdf', ['html' => $html])->get()->body();
}
}
Once you have implemented the invoice renderer contract, you should update the cashier.invoices.renderer configuration value in your application's config/cashier.php configuration file. This configuration value should be set to the class name of your custom renderer implementation.
Checkout
Cashier Stripe also provides support for Stripe Checkout. Stripe Checkout takes the pain out of implementing custom pages to accept payments by providing a pre-built, hosted payment page.
The following documentation contains information on how to get started using Stripe Checkout with Cashier. To learn more about Stripe Checkout, you should also consider reviewing Stripe's own documentation on Checkout.
Product Checkouts
You may perform a checkout for an existing product that has been created within your Stripe dashboard using the checkout method on a billable model. The checkout method will initiate a new Stripe Checkout session. By default, you're required to pass a Stripe Price ID:
use Illuminate\Http\Request;
Route::get('/product-checkout', function (Request $request) {
return $request->user()->checkout('price_tshirt');
});
If needed, you may also specify a product quantity:
use Illuminate\Http\Request;
Route::get('/product-checkout', function (Request $request) {
return $request->user()->checkout(['price_tshirt' => 15]);
});
When a customer visits this route they will be redirected to Stripe's Checkout page. By default, when a user successfully completes or cancels a purchase they will be redirected to your home route location, but you may specify custom callback URLs using the success_url and cancel_url options:
use Illuminate\Http\Request;
Route::get('/product-checkout', function (Request $request) {
return $request->user()->checkout(['price_tshirt' => 1], [
'success_url' => route('your-success-route'),
'cancel_url' => route('your-cancel-route'),
]);
});
When defining your success_url checkout option, you may instruct Stripe to add the checkout session ID as a query string parameter when invoking your URL. To do so, add the literal string {CHECKOUT_SESSION_ID} to your success_url query string. Stripe will replace this placeholder with the actual checkout session ID:
use Illuminate\Http\Request;
use Stripe\Checkout\Session;
use Stripe\Customer;
Route::get('/product-checkout', function (Request $request) {
return $request->user()->checkout(['price_tshirt' => 1], [
'success_url' => route('checkout-success').'?session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => route('checkout-cancel'),
]);
});
Route::get('/checkout-success', function (Request $request) {
$checkoutSession = $request->user()->stripe()->checkout->sessions->retrieve($request->get('session_id'));
return view('checkout.success', ['checkoutSession' => $checkoutSession]);
})->name('checkout-success');
Promotion Codes
By default, Stripe Checkout does not allow user redeemable promotion codes. Luckily, there's an easy way to enable these for your Checkout page. To do so, you may invoke the allowPromotionCodes method:
use Illuminate\Http\Request;
Route::get('/product-checkout', function (Request $request) {
return $request->user()
->allowPromotionCodes()
->checkout('price_tshirt');
});
Single Charge Checkouts
You can also perform a simple charge for an ad-hoc product that has not been created in your Stripe dashboard. To do so you may use the checkoutCharge method on a billable model and pass it a chargeable amount, a product name, and an optional quantity. When a customer visits this route they will be redirected to Stripe's Checkout page:
use Illuminate\Http\Request;
Route::get('/charge-checkout', function (Request $request) {
return $request->user()->checkoutCharge(1200, 'T-Shirt', 5);
});
[!WARNING]
When using thecheckoutChargemethod, Stripe will always create a new product and price in your Stripe dashboard. Therefore, we recommend that you create the products up front in your Stripe dashboard and use thecheckoutmethod instead.
Subscription Checkouts
[!WARNING]
Using Stripe Checkout for subscriptions requires you to enable thecustomer.subscription.createdwebhook in your Stripe dashboard. This webhook will create the subscription record in your database and store all of the relevant subscription items.
You may also use Stripe Checkout to initiate subscriptions. After defining your subscription with Cashier's subscription builder methods, you may call the checkoutmethod. When a customer visits this route they will be redirected to Stripe's Checkout page:
use Illuminate\Http\Request;
Route::get('/subscription-checkout', function (Request $request) {
return $request->user()
->newSubscription('default', 'price_monthly')
->checkout();
});
Just as with product checkouts, you may customize the success and cancellation URLs:
use Illuminate\Http\Request;
Route::get('/subscription-checkout', function (Request $request) {
return $request->user()
->newSubscription('default', 'price_monthly')
->checkout([
'success_url' => route('your-success-route'),
'cancel_url' => route('your-cancel-route'),
]);
});
Of course, you can also enable promotion codes for subscription checkouts:
use Illuminate\Http\Request;
Route::get('/subscription-checkout', function (Request $request) {
return $request->user()
->newSubscription('default', 'price_monthly')
->allowPromotionCodes()
->checkout();
});
[!WARNING]
Unfortunately Stripe Checkout does not support all subscription billing options when starting subscriptions. Using theanchorBillingCycleOnmethod on the subscription builder, setting proration behavior, or setting payment behavior will not have any effect during Stripe Checkout sessions. Please consult the Stripe Checkout Session API documentation to review which parameters are available.
Stripe Checkout and Trial Periods
Of course, you can define a trial period when building a subscription that will be completed using Stripe Checkout:
$checkout = Auth::user()->newSubscription('default', 'price_monthly')
->trialDays(3)
->checkout();
However, the trial period must be at least 48 hours, which is the minimum amount of trial time supported by Stripe Checkout.
Subscriptions and Webhooks
Remember, Stripe and Cashier update subscription statuses via webhooks, so there's a possibility a subscription might not yet be active when the customer returns to the application after entering their payment information. To handle this scenario, you may wish to display a message informing the user that their payment or subscription is pending.
Collecting Tax IDs
Checkout also supports collecting a customer's Tax ID. To enable this on a checkout session, invoke the collectTaxIds method when creating the session:
$checkout = $user->collectTaxIds()->checkout('price_tshirt');
When this method is invoked, a new checkbox will be available to the customer that allows them to indicate if they're purchasing as a company. If so, they will have the opportunity to provide their Tax ID number.
[!WARNING]
If you have already configured automatic tax collection in your application's service provider then this feature will be enabled automatically and there is no need to invoke thecollectTaxIdsmethod.
Guest Checkouts
Using the Checkout::guest method, you may initiate checkout sessions for guests of your application that do not have an "account":
use Illuminate\Http\Request;
use Laravel\Cashier\Checkout;
Route::get('/product-checkout', function (Request $request) {
return Checkout::guest()->create('price_tshirt', [
'success_url' => route('your-success-route'),
'cancel_url' => route('your-cancel-route'),
]);
});
Similarly to when creating checkout sessions for existing users, you may utilize additional methods available on the Laravel\Cashier\CheckoutBuilder instance to customize the guest checkout session:
use Illuminate\Http\Request;
use Laravel\Cashier\Checkout;
Route::get('/product-checkout', function (Request $request) {
return Checkout::guest()
->withPromotionCode('promo-code')
->create('price_tshirt', [
'success_url' => route('your-success-route'),
'cancel_url' => route('your-cancel-route'),
]);
});
After a guest checkout has been completed, Stripe can dispatch a checkout.session.completed webhook event, so make sure to configure your Stripe webhook to actually send this event to your application. Once the webhook has been enabled within the Stripe dashboard, you may handle the webhook with Cashier. The object contained in the webhook payload will be a checkout object that you may inspect in order to fulfill your customer's order.
Handling Failed Payments
Sometimes, payments for subscriptions or single charges can fail. When this happens, Cashier will throw an Laravel\Cashier\Exceptions\IncompletePayment exception that informs you that this happened. After catching this exception, you have two options on how to proceed.
First, you could redirect your customer to the dedicated payment confirmation page which is included with Cashier. This page already has an associated named route that is registered via Cashier's service provider. So, you may catch the IncompletePayment exception and redirect the user to the payment confirmation page:
use Laravel\Cashier\Exceptions\IncompletePayment;
try {
$subscription = $user->newSubscription('default', 'price_monthly')
->create($paymentMethod);
} catch (IncompletePayment $exception) {
return redirect()->route(
'cashier.payment',
[$exception->payment->id, 'redirect' => route('home')]
);
}
On the payment confirmation page, the customer will be prompted to enter their credit card information again and perform any additional actions required by Stripe, such as "3D Secure" confirmation. After confirming their payment, the user will be redirected to the URL provided by the redirect parameter specified above. Upon redirection, message (string) and success (integer) query string variables will be added to the URL. The payment page currently supports the following payment method types:
- Credit Cards
- Alipay
- Bancontact
- BECS Direct Debit
- EPS
- Giropay
- iDEAL
- SEPA Direct Debit
Alternatively, you could allow Stripe to handle the payment confirmation for you. In this case, instead of redirecting to the payment confirmation page, you may setup Stripe's automatic billing emails in your Stripe dashboard. However, if an IncompletePayment exception is caught, you should still inform the user they will receive an email with further payment confirmation instructions.
Payment exceptions may be thrown for the following methods: charge, invoiceFor, and invoice on models using the Billable trait. When interacting with subscriptions, the create method on the SubscriptionBuilder, and the incrementAndInvoice and swapAndInvoice methods on the Subscription and SubscriptionItem models may throw incomplete payment exceptions.
Determining if an existing subscription has an incomplete payment may be accomplished using the hasIncompletePayment method on the billable model or a subscription instance:
if ($user->hasIncompletePayment('default')) {
// ...
}
if ($user->subscription('default')->hasIncompletePayment()) {
// ...
}
You can derive the specific status of an incomplete payment by inspecting the payment property on the exception instance:
use Laravel\Cashier\Exceptions\IncompletePayment;
try {
$user->charge(1000, 'pm_card_threeDSecure2Required');
} catch (IncompletePayment $exception) {
// Get the payment intent status...
$exception->payment->status;
// Check specific conditions...
if ($exception->payment->requiresPaymentMethod()) {
// ...
} elseif ($exception->payment->requiresConfirmation()) {
// ...
}
}
Confirming Payments
Some payment methods require additional data in order to confirm payments. For example, SEPA payment methods require additional "mandate" data during the payment process. You may provide this data to Cashier using the withPaymentConfirmationOptions method:
$subscription->withPaymentConfirmationOptions([
'mandate_data' => '...',
])->swap('price_xxx');
You may consult the Stripe API documentation to review all of the options accepted when confirming payments.
Strong Customer Authentication
If your business or one of your customers is based in Europe you will need to abide by the EU's Strong Customer Authentication (SCA) regulations. These regulations were imposed in September 2019 by the European Union to prevent payment fraud. Luckily, Stripe and Cashier are prepared for building SCA compliant applications.
[!WARNING]
Before getting started, review Stripe's guide on PSD2 and SCA as well as their documentation on the new SCA APIs.
Payments Requiring Additional Confirmation
SCA regulations often require extra verification in order to confirm and process a payment. When this happens, Cashier will throw a Laravel\Cashier\Exceptions\IncompletePayment exception that informs you that extra verification is needed. More information on how to handle these exceptions be found can be found in the documentation on handling failed payments.
Payment confirmation screens presented by Stripe or Cashier may be tailored to a specific bank or card issuer's payment flow and can include additional card confirmation, a temporary small charge, separate device authentication, or other forms of verification.
Incomplete and Past Due State
When a payment needs additional confirmation, the subscription will remain in an incomplete or past_due state as indicated by its stripe_status database column. Cashier will automatically activate the customer's subscription as soon as payment confirmation is complete and your application is notified by Stripe via webhook of its completion.
For more information on incomplete and past_due states, please refer to our additional documentation on these states.
Off-Session Payment Notifications
Since SCA regulations require customers to occasionally verify their payment details even while their subscription is active, Cashier can send a notification to the customer when off-session payment confirmation is required. For example, this may occur when a subscription is renewing. Cashier's payment notification can be enabled by setting the CASHIER_PAYMENT_NOTIFICATION environment variable to a notification class. By default, this notification is disabled. Of course, Cashier includes a notification class you may use for this purpose, but you are free to provide your own notification class if desired:
CASHIER_PAYMENT_NOTIFICATION=Laravel\Cashier\Notifications\ConfirmPayment
To ensure that off-session payment confirmation notifications are delivered, verify that Stripe webhooks are configured for your application and the invoice.payment_action_required webhook is enabled in your Stripe dashboard. In addition, your Billable model should also use Laravel's Illuminate\Notifications\Notifiable trait.
[!WARNING]
Notifications will be sent even when customers are manually making a payment that requires additional confirmation. Unfortunately, there is no way for Stripe to know that the payment was done manually or "off-session". But, a customer will simply see a "Payment Successful" message if they visit the payment page after already confirming their payment. The customer will not be allowed to accidentally confirm the same payment twice and incur an accidental second charge.
Stripe SDK
Many of Cashier's objects are wrappers around Stripe SDK objects. If you would like to interact with the Stripe objects directly, you may conveniently retrieve them using the asStripe method:
$stripeSubscription = $subscription->asStripeSubscription();
$stripeSubscription->application_fee_percent = 5;
$stripeSubscription->save();
You may also use the updateStripeSubscription method to update a Stripe subscription directly:
$subscription->updateStripeSubscription(['application_fee_percent' => 5]);
You may invoke the stripe method on the Cashier class if you would like to use the Stripe\StripeClient client directly. For example, you could use this method to access the StripeClient instance and retrieve a list of prices from your Stripe account:
use Laravel\Cashier\Cashier;
$prices = Cashier::stripe()->prices->all();
Testing
When testing an application that uses Cashier, you may mock the actual HTTP requests to the Stripe API; however, this requires you to partially re-implement Cashier's own behavior. Therefore, we recommend allowing your tests to hit the actual Stripe API. While this is slower, it provides more confidence that your application is working as expected and any slow tests may be placed within their own Pest / PHPUnit testing group.
When testing, remember that Cashier itself already has a great test suite, so you should only focus on testing the subscription and payment flow of your own application and not every underlying Cashier behavior.
To get started, add the testing version of your Stripe secret to your phpunit.xml file:
<env name="STRIPE_SECRET" value="sk_test_<your-key>"/>
Now, whenever you interact with Cashier while testing, it will send actual API requests to your Stripe testing environment. For convenience, you should pre-fill your Stripe testing account with subscriptions / prices that you may use during testing.
[!NOTE]
In order to test a variety of billing scenarios, such as credit card denials and failures, you may use the vast range of testing card numbers and tokens provided by Stripe.
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
Laravel 12 中文文档
关于 LearnKu
推荐文章: