Laravel Cashier (Stripe)
介绍
Laravel Cashier Stripe 提供了一个对 Stripe 订阅计费服务的表达性、流畅的接口。它几乎处理了所有你不想编写的订阅计费样板代码。除了基本的订阅管理,Cashier 还可以处理优惠券、交换订阅、订阅“数量”、取消宽限期,甚至生成发票 PDF。
升级 Cashier
在升级到新版本的 Cashier 时,务必仔细查看升级指南。
为了防止破坏性更改,Cashier 使用固定的 Stripe API 版本。Cashier 13 使用 Stripe API 版本 2020-08-27
。Stripe API 版本将在小版本更新中更新,以便利用新的 Stripe 功能和改进。
安装
首先,使用 Composer 包管理器安装 Stripe 的 Cashier 包:
composer require laravel/cashier
为确保 Cashier 正确处理所有 Stripe 事件,请记得设置 Cashier 的 webhook 处理。
数据库迁移
Cashier 的服务提供者注册了自己的数据库迁移目录,因此在安装包后请记得迁移数据库。Cashier 迁移将向你的 users
表添加几个列,并创建一个新的 subscriptions
表来保存所有客户的订阅:
php artisan migrate
如果你需要覆盖 Cashier 附带的迁移,可以使用 vendor:publish
Artisan 命令发布它们:
php artisan vendor:publish --tag="cashier-migrations"
如果你想完全阻止 Cashier 的迁移运行,可以使用 Cashier 提供的 ignoreMigrations
方法。通常,此方法应在 AppServiceProvider
的 register
方法中调用:
use Laravel\Cashier\Cashier;
/**
* 注册任何应用程序服务。
*
* @return void
*/
public function register()
{
Cashier::ignoreMigrations();
}
Stripe 建议用于存储 Stripe 标识符的任何列都应区分大小写。因此,在使用 MySQL 时,应确保 stripe_id
列的排序规则设置为 utf8_bin
。有关更多信息,请参阅 Stripe 文档。
配置
可计费模型
在使用 Cashier 之前,将 Billable
trait 添加到你的可计费模型定义中。通常,这将是 App\Models\User
模型。此 trait 提供了各种方法,允许你执行常见的计费任务,例如创建订阅、应用优惠券和更新支付方式信息:
use Laravel\Cashier\Billable;
class User extends Authenticatable
{
use Billable;
}
Cashier 假定你的可计费模型将是 Laravel 附带的 App\Models\User
类。如果你希望更改此设置,可以通过 useCustomerModel
方法指定不同的模型。此方法通常应在 AppServiceProvider
类的 boot
方法中调用:
use App\Models\Cashier\User;
use Laravel\Cashier\Cashier;
/**
* 启动任何应用程序服务。
*
* @return void
*/
public function boot()
{
Cashier::useCustomerModel(User::class);
}
如果你使用的是 Laravel 提供的 App\Models\User
模型以外的模型,则需要发布并更改提供的 Cashier 迁移 以匹配你的替代模型的表名。
API 密钥
接下来,你应该在应用程序的 .env
文件中配置你的 Stripe API 密钥。你可以从 Stripe 控制面板中检索你的 Stripe API 密钥:
STRIPE_KEY=your-stripe-key
STRIPE_SECRET=your-stripe-secret
货币配置
默认的 Cashier 货币是美元 (USD)。你可以通过在应用程序的 .env
文件中设置 CASHIER_CURRENCY
环境变量来更改默认货币:
CASHIER_CURRENCY=eur
除了配置 Cashier 的货币外,你还可以指定一个用于在发票上显示货币值时使用的区域设置。Cashier 内部使用 PHP 的 NumberFormatter
类 来设置货币区域设置:
CASHIER_CURRENCY_LOCALE=nl_BE
为了使用 en
以外的区域设置,请确保在服务器上安装并配置了 ext-intl
PHP 扩展。
税务配置
感谢 Stripe Tax,可以自动计算 Stripe 生成的所有发票的税款。你可以通过在应用程序的 App\Providers\AppServiceProvider
类的 boot
方法中调用 calculateTaxes
方法来启用自动税款计算:
use Laravel\Cashier\Cashier;
/**
* 启动任何应用程序服务。
*
* @return void
*/
public function boot()
{
Cashier::calculateTaxes();
}
一旦启用了税款计算,任何新订阅和生成的任何一次性发票都将收到自动税款计算。
为了使此功能正常工作,你的客户的账单详细信息(例如客户的姓名、地址和税号)需要同步到 Stripe。你可以使用 Cashier 提供的 客户数据同步 和 税号 方法来完成此操作。
不幸的是,目前没有为 单次收费 或 单次收费结账 计算税款。此外,Stripe Tax 目前在其测试期间是“仅限邀请”的。你可以通过 Stripe Tax 网站 请求访问 Stripe Tax。
日志记录
Cashier 允许你指定在记录严重 Stripe 错误时使用的日志通道。你可以通过在应用程序的 .env
文件中定义 CASHIER_LOGGER
环境变量来指定日志通道:
CASHIER_LOGGER=stack
通过对 Stripe 的 API 调用生成的异常将通过应用程序的默认日志通道记录。
使用自定义模型
你可以通过定义自己的模型并扩展相应的 Cashier 模型来自由扩展 Cashier 内部使用的模型:
use Laravel\Cashier\Subscription as CashierSubscription;
class Subscription extends CashierSubscription
{
// ...
}
定义模型后,你可以通过 Laravel\Cashier\Cashier
类指示 Cashier 使用你的自定义模型。通常,你应该在应用程序的 App\Providers\AppServiceProvider
类的 boot
方法中通知 Cashier 你的自定义模型:
use App\Models\Cashier\Subscription;
use App\Models\Cashier\SubscriptionItem;
/**
* 启动任何应用程序服务。
*
* @return void
*/
public function boot()
{
Cashier::useSubscriptionModel(Subscription::class);
Cashier::useSubscriptionItemModel(SubscriptionItem::class);
}
客户
检索客户
你可以使用 Cashier::findBillable
方法通过其 Stripe ID 检索客户。此方法将返回可计费模型的实例:
use Laravel\Cashier\Cashier;
$user = Cashier::findBillable($stripeId);
创建客户
有时,你可能希望在不开始订阅的情况下创建 Stripe 客户。你可以使用 createAsStripeCustomer
方法实现此目的:
$stripeCustomer = $user->createAsStripeCustomer();
一旦客户在 Stripe 中创建,你可以在稍后开始订阅。你可以提供一个可选的 $options
数组来传递任何其他 Stripe API 支持的客户创建参数:
$stripeCustomer = $user->createAsStripeCustomer($options);
如果你想返回可计费模型的 Stripe 客户对象,可以使用 asStripeCustomer
方法:
$stripeCustomer = $user->asStripeCustomer();
如果你不确定可计费模型是否已经是 Stripe 中的客户,但希望检索 Stripe 客户对象,可以使用 createOrGetStripeCustomer
方法。此方法将在 Stripe 中创建一个新客户(如果尚不存在):
$stripeCustomer = $user->createOrGetStripeCustomer();
更新客户
有时,你可能希望直接使用附加信息更新 Stripe 客户。你可以使用 updateStripeCustomer
方法实现此目的。此方法接受一个 Stripe API 支持的客户更新选项 数组:
$stripeCustomer = $user->updateStripeCustomer($options);
余额
Stripe 允许你贷记或借记客户的“余额”。稍后,此余额将在新发票上贷记或借记。要检查客户的总余额,可以使用可计费模型上可用的 balance
方法。balance
方法将返回客户货币中余额的格式化字符串表示:
$balance = $user->balance();
要贷记客户的余额,可以向 applyBalance
方法提供负值。如果愿意,还可以提供描述:
$user->applyBalance(-500, 'Premium customer top-up.');
向 applyBalance
方法提供正值将借记客户的余额:
$user->applyBalance(300, 'Bad usage penalty.');
applyBalance
方法将为客户创建新的客户余额交易。你可以使用 balanceTransactions
方法检索这些交易记录,这可能有助于提供客户查看的贷记和借记日志:
// 检索所有交易...
$transactions = $user->balanceTransactions();
foreach ($transactions as $transaction) {
// 交易金额...
$amount = $transaction->amount(); // $2.31
// 检索相关发票(如果有)...
$invoice = $transaction->invoice();
}
税号
Cashier 提供了一种简单的方法来管理客户的税号。例如,可以使用 taxIds
方法检索分配给客户的所有 税号 作为集合:
$taxIds = $user->taxIds();
你还可以通过其标识符检索客户的特定税号:
$taxId = $user->findTaxId('txi_belgium');
你可以通过向 createTaxId
方法提供有效的 类型 和值来创建新的税号:
$taxId = $user->createTaxId('eu_vat', 'BE0123456789');
createTaxId
方法将立即将 VAT ID 添加到客户的帐户中。Stripe 也会验证 VAT ID;然而,这是一个异步过程。你可以通过订阅 customer.tax_id.updated
webhook 事件并检查 VAT ID 的 verification
参数 来接收验证更新的通知。有关处理 webhooks 的更多信息,请查阅 定义 webhook 处理程序的文档。
你可以使用 deleteTaxId
方法删除税号:
$user->deleteTaxId('txi_belgium');
与 Stripe 同步客户数据
通常,当你的应用程序用户更新其姓名、电子邮件地址或其他信息(Stripe 也存储这些信息)时,你应该通知 Stripe 这些更新。通过这样做,Stripe 的信息副本将与应用程序同步。
要自动化此过程,你可以在可计费模型上定义一个事件监听器,该监听器对模型的 updated
事件做出反应。然后,在事件监听器中,可以在模型上调用 syncStripeCustomerDetails
方法:
use function Illuminate\Events\queueable;
/**
* 模型的 "booted" 方法。
*
* @return void
*/
protected static function booted()
{
static::updated(queueable(function ($customer) {
if ($customer->hasStripeId()) {
$customer->syncStripeCustomerDetails();
}
}));
}
现在,每次更新客户模型时,其信息将与 Stripe 同步。为了方便起见,Cashier 将在客户初次创建时自动同步客户的信息。
你可以通过覆盖 Cashier 提供的各种方法来自定义用于同步客户信息到 Stripe 的列。例如,你可以覆盖 stripeName
方法,以自定义在 Cashier 将客户信息同步到 Stripe 时应视为客户“姓名”的属性:
/**
* 获取应同步到 Stripe 的客户姓名。
*
* @return string|null
*/
public function stripeName()
{
return $this->company_name;
}
同样,你可以覆盖 stripeEmail
、stripePhone
和 stripeAddress
方法。这些方法将在 更新 Stripe 客户对象 时将信息同步到其对应的客户参数。如果你希望完全控制客户信息同步过程,可以覆盖 syncStripeCustomerDetails
方法。
账单门户
Stripe 提供了一种简单的方法来设置账单门户,以便你的客户可以管理其订阅、支付方式并查看其账单历史记录。你可以通过从控制器或路由调用可计费模型上的 redirectToBillingPortal
方法将用户重定向到账单门户:
use Illuminate\Http\Request;
Route::get('/billing-portal', function (Request $request) {
return $request->user()->redirectToBillingPortal();
});
默认情况下,当用户完成管理其订阅时,他们将能够通过 Stripe 账单门户中的链接返回到应用程序的 home
路由。你可以通过将 URL 作为参数传递给 redirectToBillingPortal
方法来提供用户应返回的自定义 URL:
use Illuminate\Http\Request;
Route::get('/billing-portal', function (Request $request) {
return $request->user()->redirectToBillingPortal(route('billing'));
});
如果你希望生成账单门户的 URL 而不生成 HTTP 重定向响应,可以调用 billingPortalUrl
方法:
$url = $request->user()->billingPortalUrl(route('billing'));
支付方式
存储支付方式
为了使用 Stripe 创建订阅或执行“一次性”收费,你需要存储支付方式并从 Stripe 检索其标识符。实现此目的的方法取决于你计划将支付方式用于订阅还是单次收费,因此我们将在下面分别进行检查。
订阅的支付方式
在存储客户的信用卡信息以供订阅将来使用时,必须使用 Stripe 的“设置意图”API 来安全地收集客户的支付方式详细信息。“设置意图”表示向 Stripe 表示收取客户支付方式的意图。Cashier 的 Billable
trait 包含 createSetupIntent
方法,以便轻松创建新的设置意图。你应该从将呈现收集客户支付方式详细信息的表单的路由或控制器调用此方法:
return view('update-payment-method', [
'intent' => $user->createSetupIntent()
]);
在创建设置意图并将其传递给视图后,你应该将其秘密附加到将收集支付方式的元素上。例如,考虑此“更新支付方式”表单:
<input id="card-holder-name" type="text">
<!-- Stripe Elements 占位符 -->
<div id="card-element"></div>
<button id="card-button" data-secret="{{ $intent->client_secret }}">
更新支付方式
</button>
接下来,可以使用 Stripe.js 库将 Stripe 元素 附加到表单并安全地收集客户的支付详细信息:
<script src="https://js.stripe.com/v3/"></script>
<script>
const stripe = Stripe('stripe-public-key');
const elements = stripe.elements();
const cardElement = elements.create('card');
cardElement.mount('#card-element');
</script>
接下来,可以使用 Stripe 的 confirmCardSetup
方法 验证卡并从 Stripe 检索安全的“支付方式标识符”:
const cardHolderName = document.getElementById('card-holder-name');
const cardButton = document.getElementById('card-button');
const clientSecret = cardButton.dataset.secret;
cardButton.addEventListener('click', async (e) => {
const { setupIntent, error } = await stripe.confirmCardSetup(
clientSecret, {
payment_method: {
card: cardElement,
billing_details: { name: cardHolderName.value }
}
}
);
if (error) {
// 向用户显示 "error.message"...
} else {
// 卡已成功验证...
}
});
在 Stripe 验证卡后,你可以将生成的 setupIntent.payment_method
标识符传递给你的 Laravel 应用程序,在那里可以将其附加到客户。支付方式可以作为新的支付方式 添加 或用于 更新默认支付方式。你还可以立即使用支付方式标识符 创建新订阅。
如果你想了解有关设置意图和收集客户支付详细信息的更多信息,请查看 Stripe 提供的此概述。
单次收费的支付方式
当然,在对客户的支付方式进行单次收费时,我们只需要使用一次支付方式标识符。由于 Stripe 的限制,你不能将客户的存储默认支付方式用于单次收费。你必须允许客户使用 Stripe.js 库输入其支付方式详细信息。例如,考虑以下表单:
<input id="card-holder-name" type="text">
<!-- Stripe Elements 占位符 -->
<div id="card-element"></div>
<button id="card-button">
处理支付
</button>
在定义此类表单后,可以使用 Stripe.js 库将 Stripe 元素 附加到表单并安全地收集客户的支付详细信息:
<script src="https://js.stripe.com/v3/"></script>
<script>
const stripe = Stripe('stripe-public-key');
const elements = stripe.elements();
const cardElement = elements.create('card');
cardElement.mount('#card-element');
</script>
接下来,可以使用 Stripe 的 createPaymentMethod
方法 验证卡并从 Stripe 检索安全的“支付方式标识符”:
const cardHolderName = document.getElementById('card-holder-name');
const cardButton = document.getElementById('card-button');
cardButton.addEventListener('click', async (e) => {
const { paymentMethod, error } = await stripe.createPaymentMethod(
'card', cardElement, {
billing_details: { name: cardHolderName.value }
}
);
if (error) {
// 向用户显示 "error.message"...
} else {
// 卡已成功验证...
}
});
如果卡验证成功,你可以将 paymentMethod.id
传递给你的 Laravel 应用程序并处理 单次收费。
检索支付方式
可计费模型实例上的 paymentMethods
方法返回 Laravel\Cashier\PaymentMethod
实例的集合:
$paymentMethods = $user->paymentMethods();
默认情况下,此方法将返回 card
类型的支付方式。要检索其他类型的支付方式,可以将 type
作为参数传递给方法:
$paymentMethods = $user->paymentMethods('sepa_debit');
要检索客户的默认支付方式,可以使用 defaultPaymentMethod
方法:
$paymentMethod = $user->defaultPaymentMethod();
你可以使用 findPaymentMethod
方法检索附加到可计费模型的特定支付方式:
$paymentMethod = $user->findPaymentMethod($paymentMethodId);
确定用户是否有支付方式
要确定可计费模型是否有默认支付方式附加到其帐户,可以调用 hasDefaultPaymentMethod
方法:
if ($user->hasDefaultPaymentMethod()) {
//
}
你可以使用 hasPaymentMethod
方法确定可计费模型是否至少有一种支付方式附加到其帐户:
if ($user->hasPaymentMethod()) {
//
}
此方法将确定可计费模型是否具有 card
类型的支付方式。要确定模型是否存在其他类型的支付方式,可以将 type
作为参数传递给方法:
if ($user->hasPaymentMethod('sepa_debit')) {
//
}
更新默认支付方式
updateDefaultPaymentMethod
方法可用于更新客户的默认支付方式信息。此方法接受 Stripe 支付方式标识符,并将新支付方式指定为默认计费支付方式:
$user->updateDefaultPaymentMethod($paymentMethod);
要将默认支付方式信息与 Stripe 中的客户默认支付方式信息同步,可以使用 updateDefaultPaymentMethodFromStripe
方法:
$user->updateDefaultPaymentMethodFromStripe();
客户的默认支付方式只能用于开票和创建新订阅。由于 Stripe 施加的限制,它不能用于单次收费。
添加支付方式
要添加新的支付方式,可以在可计费模型上调用 addPaymentMethod
方法,传递支付方式标识符:
$user->addPaymentMethod($paymentMethod);
要了解如何检索支付方式标识符,请查看 支付方式存储文档。
删除支付方式
要删除支付方式,可以在要删除的 Laravel\Cashier\PaymentMethod
实例上调用 delete
方法:
$paymentMethod->delete();
deletePaymentMethod
方法将从可计费模型中删除特定支付方式:
$user->deletePaymentMethod('pm_visa');
deletePaymentMethods
方法将删除可计费模型的所有支付方式信息:
$user->deletePaymentMethods();
默认情况下,此方法将删除 card
类型的支付方式。要删除其他类型的支付方式,可以将 type
作为参数传递给方法:
$user->deletePaymentMethods('sepa_debit');
如果用户有一个活动的订阅,你的应用程序不应允许他们删除其默认支付方式。
订阅
订阅提供了一种为客户设置定期付款的方法。Cashier 管理的 Stripe 订阅支持多个订阅价格、订阅数量、试用等。
创建订阅
要创建订阅,首先检索可计费模型的实例,通常是 App\Models\User
的实例。一旦检索到模型实例,可以使用 newSubscription
方法创建模型的订阅:
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$request->user()->newSubscription(
'default', 'price_monthly'
)->create($request->paymentMethodId);
// ...
});
传递给 newSubscription
方法的第一个参数应为订阅的内部名称。如果你的应用程序仅提供单个订阅,可以将其命名为 default
或 primary
。此订阅名称仅供内部应用程序使用,不应显示给用户。此外,它不应包含空格,并且在创建订阅后不应更改。第二个参数是用户订阅的特定价格。此值应对应于 Stripe 中价格的标识符。
create
方法接受 Stripe 支付方式标识符 或 Stripe PaymentMethod
对象,将开始订阅并更新数据库中的可计费模型的 Stripe 客户 ID 和其他相关计费信息。
直接将支付方式标识符传递给 create
订阅方法也会自动将其添加到用户的存储支付方式中。
通过发票电子邮件收取定期付款
你可以指示 Stripe 在每次客户的定期付款到期时向客户发送发票电子邮件,而不是自动收取客户的定期付款。然后,客户可以在收到发票后手动支付发票。通过发票收取定期付款时,客户不需要提前提供支付方式:
$user->newSubscription('default', 'price_monthly')->createAndSendInvoice();
客户支付发票的时间由 Stripe 仪表板 中的订阅和发票设置决定。
数量
如果你希望在创建订阅时为价格设置特定的 数量,应在创建订阅之前在订阅构建器上调用 quantity
方法:
$user->newSubscription('default', 'price_monthly')
->quantity(5)
->create($paymentMethod);
其他详细信息
如果你希望指定 Stripe 支持的其他 客户 或 订阅 选项,可以通过将它们作为第二个和第三个参数传递给 create
方法来实现:
$user->newSubscription('default', 'price_monthly')->create($paymentMethod, [
'email' => $email,
], [
'metadata' => ['note' => 'Some extra information.'],
]);
优惠券
如果你希望在创建订阅时应用优惠券,可以使用 withCoupon
方法:
$user->newSubscription('default', 'price_monthly')
->withCoupon('code')
->create($paymentMethod);
或者,如果你希望应用 Stripe 促销代码,可以使用 withPromotionCode
方法。给定的促销代码 ID 应为分配给促销代码的 Stripe API ID,而不是客户面向的促销代码:
$user->newSubscription('default', 'price_monthly')
->withPromotionCode('promo_code')
->create($paymentMethod);
添加订阅
如果你希望为已经有默认支付方式的客户添加订阅,可以在订阅构建器上调用 add
方法:
use App\Models\User;
$user = User::find(1);
$user->newSubscription('default', 'price_monthly')->add();
从 Stripe 仪表板创建订阅
你还可以从 Stripe 仪表板本身创建订阅。在这样做时,Cashier 将同步新添加的订阅并为其分配名称 default
。要自定义分配给仪表板创建的订阅的订阅名称,请扩展 WebhookController
并覆盖 newSubscriptionName
方法。
此外,你只能通过 Stripe 仪表板创建一种类型的订阅。如果你的应用程序提供多种使用不同名称的订阅,则只能通过 Stripe 仪表板添加一种类型的订阅。
最后,你应该始终确保只为应用程序提供的每种类型的订阅添加一个活动订阅。如果客户有两个 default
订阅,Cashier 只会使用最近添加的订阅,即使两个订阅都与应用程序的数据库同步。
检查订阅状态
一旦客户订阅了你的应用程序,你可以使用各种方便的方法轻松检查其订阅状态。首先,如果客户有一个活动的订阅,即使订阅当前处于试用期,subscribed
方法也会返回 true
。subscribed
方法接受订阅名称作为其第一个参数:
if ($user->subscribed('default')) {
//
}
subscribed
方法也是 路由中间件 的一个很好的候选者,允许你根据用户的订阅状态过滤对路由和控制器的访问:
<?php
namespace App\Http\Middleware;
use Closure;
class EnsureUserIsSubscribed
{
/**
* 处理传入请求。
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if ($request->user() && ! $request->user()->subscribed('default')) {
// 此用户不是付费客户...
return redirect('billing');
}
return $next($request);
}
}
如果你想确定用户是否仍在试用期内,可以使用 onTrial
方法。此方法可用于确定是否应向用户显示警告,告知他们仍在试用期内:
if ($user->subscription('default')->onTrial()) {
//
}
subscribedToProduct
方法可用于确定用户是否订阅了给定产品,基于给定的 Stripe 产品标识符。在 Stripe 中,产品是价格的集合。在此示例中,我们将确定用户的 default
订阅是否积极订阅了应用程序的“高级”产品。给定的 Stripe 产品标识符应对应于 Stripe 仪表板中产品的标识符之一:
if ($user->subscribedToProduct('prod_premium', 'default')) {
//
}
通过将数组传递给 subscribedToProduct
方法,可以确定用户的 default
订阅是否积极订阅了应用程序的“基本”或“高级”产品:
if ($user->subscribedToProduct(['prod_basic', 'prod_premium'], 'default')) {
//
}
subscribedToPrice
方法可用于确定客户的订阅是否对应于给定的价格 ID:
if ($user->subscribedToPrice('price_basic_monthly', 'default')) {
//
}
recurring
方法可用于确定用户当前是否订阅并且不再处于试用期:
if ($user->subscription('default')->recurring()) {
//
}
如果用户有两个具有相同名称的订阅,subscription
方法将始终返回最新的订阅。例如,用户可能有两个名为 default
的订阅记录;然而,其中一个订阅可能是旧的、过期的订阅,而另一个是当前的、活动的订阅。最新的订阅将始终返回,而旧的订阅将保留在数据库中以供历史审查。
已取消的订阅状态
要确定用户是否曾经是活跃订阅者但已取消其订阅,可以使用 canceled
方法:
if ($user->subscription('default')->canceled()) {
//
}
你还可以确定用户是否已取消其订阅但仍在其“宽限期”内,直到订阅完全过期。例如,如果用户在 3 月 5 日取消订阅,而订阅原定于 3 月 10 日到期,则用户在 3 月 10 日之前处于“宽限期”。请注意,在此期间,subscribed
方法仍返回 true
:
if ($user->subscription('default')->onGracePeriod()) {
//
}
要确定用户是否已取消其订阅并且不再处于“宽限期”,可以使用 ended
方法:
if ($user->subscription('default')->ended()) {
//
}
未完成和逾期状态
如果订阅在创建后需要二次支付操作,则订阅将标记为 incomplete
。订阅状态存储在 Cashier 的 subscriptions
数据库表的 stripe_status
列中。
同样,如果在交换价格时需要二次支付操作,则订阅将标记为 past_due
。当你的订阅处于这些状态之一时,它将不会激活,直到客户确认其支付。可以使用可计费模型或订阅实例上的 hasIncompletePayment
方法确定订阅是否有未完成的支付:
if ($user->hasIncompletePayment('default')) {
//
}
if ($user->subscription('default')->hasIncompletePayment()) {
//
}
当订阅有未完成的支付时,你应该将用户引导到 Cashier 的支付确认页面,传递 latestPayment
标识符。可以使用订阅实例上可用的 latestPayment
方法检索此标识符:
<a href="{{ route('cashier.payment', $subscription->latestPayment()->id) }}">
请确认您的支付。
</a>
如果你希望订阅在 past_due
状态时仍被视为活动,可以使用 Cashier 提供的 keepPastDueSubscriptionsActive
方法。通常,此方法应在 App\Providers\AppServiceProvider
的 register
方法中调用:
use Laravel\Cashier\Cashier;
/**
* 注册任何应用程序服务。
*
* @return void
*/
public function register()
{
Cashier::keepPastDueSubscriptionsActive();
}
当订阅处于 incomplete
状态时,无法更改,直到支付得到确认。因此,当订阅处于 incomplete
状态时,swap
和 updateQuantity
方法将抛出异常。
订阅范围
大多数订阅状态也可作为查询范围使用,以便你可以轻松查询数据库中处于给定状态的订阅:
// 获取所有活动的订阅...
$subscriptions = Subscription::query()->active()->get();
// 获取用户的所有已取消订阅...
$subscriptions = $user->subscriptions()->canceled()->get();
以下是可用范围的完整列表:
Subscription::query()->active();
Subscription::query()->canceled();
Subscription::query()->ended();
Subscription::query()->incomplete();
Subscription::query()->notCanceled();
Subscription::query()->notOnGracePeriod();
Subscription::query()->notOnTrial();
Subscription::query()->onGracePeriod();
Subscription::query()->onTrial();
Subscription::query()->pastDue();
Subscription::query()->recurring();
更改价格
在客户订阅您的应用程序后,他们可能偶尔会想要更改为新的订阅价格。要将客户切换到新的价格,请将 Stripe 价格的标识符传递给 swap
方法。在更换价格时,假设用户希望重新激活其先前取消的订阅。给定的价格标识符应对应于 Stripe 仪表板中可用的 Stripe 价格标识符:
use App\Models\User;
$user = App\Models\User::find(1);
$user->subscription('default')->swap('price_yearly');
如果客户正在试用期内,试用期将被保留。此外,如果订阅存在“数量”,该数量也将被保留。
如果您想更换价格并取消客户当前的任何试用期,可以调用 skipTrial
方法:
$user->subscription('default')
->skipTrial()
->swap('price_yearly');
如果您想更换价格并立即向客户开具发票,而不是等待他们的下一个计费周期,可以使用 swapAndInvoice
方法:
$user = User::find(1);
$user->subscription('default')->swapAndInvoice('price_yearly');
按比例分配
默认情况下,Stripe 在价格之间切换时会按比例分配费用。可以使用 noProrate
方法在不按比例分配费用的情况下更新订阅的价格:
$user->subscription('default')->noProrate()->swap('price_yearly');
有关订阅按比例分配的更多信息,请参阅 Stripe 文档。
在 swapAndInvoice
方法之前执行 noProrate
方法对按比例分配没有影响。发票将始终被开具。
订阅数量
有时订阅会受到“数量”的影响。例如,项目管理应用程序可能每月每个项目收费 10 美元。您可以使用 incrementQuantity
和 decrementQuantity
方法轻松增加或减少订阅数量:
use App\Models\User;
$user = User::find(1);
$user->subscription('default')->incrementQuantity();
// 在订阅的当前数量上增加五个...
$user->subscription('default')->incrementQuantity(5);
$user->subscription('default')->decrementQuantity();
// 从订阅的当前数量中减去五个...
$user->subscription('default')->decrementQuantity(5);
或者,您可以使用 updateQuantity
方法设置特定数量:
$user->subscription('default')->updateQuantity(10);
可以使用 noProrate
方法在不按比例分配费用的情况下更新订阅的数量:
$user->subscription('default')->noProrate()->updateQuantity(10);
有关订阅数量的更多信息,请参阅 Stripe 文档。
多价格订阅数量
如果您的订阅是 多价格订阅,您应该将要增加或减少数量的价格名称作为第二个参数传递给增量/减量方法:
$user->subscription('default')->incrementQuantity(1, 'price_chat');
多价格订阅
多价格订阅允许您为单个订阅分配多个计费价格。例如,假设您正在构建一个客户服务“帮助台”应用程序,该应用程序的基本订阅价格为每月 10 美元,但提供一个实时聊天附加价格,每月额外收费 15 美元。多价格订阅信息存储在 Cashier 的 subscription_items
数据库表中。
您可以通过将价格数组作为第二个参数传递给 newSubscription
方法来为给定订阅指定多个价格:
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$request->user()->newSubscription('default', [
'price_monthly',
'price_chat',
])->create($request->paymentMethodId);
// ...
});
在上面的示例中,客户将有两个价格附加到他们的 default
订阅。两个价格将在各自的计费间隔中收费。如果需要,您可以使用 quantity
方法为每个价格指定特定数量:
$user = User::find(1);
$user->newSubscription('default', ['price_monthly', 'price_chat'])
->quantity(5, 'price_chat')
->create($paymentMethod);
如果您想向现有订阅添加另一个价格,可以调用订阅的 addPrice
方法:
$user = User::find(1);
$user->subscription('default')->addPrice('price_chat');
上面的示例将添加新价格,客户将在下一个计费周期中为其开具发票。如果您想立即向客户开具发票,可以使用 addPriceAndInvoice
方法:
$user->subscription('default')->addPriceAndInvoice('price_chat');
如果您想以特定数量添加价格,可以将数量作为 addPrice
或 addPriceAndInvoice
方法的第二个参数传递:
$user = User::find(1);
$user->subscription('default')->addPrice('price_chat', 5);
您可以使用 removePrice
方法从订阅中删除价格:
$user->subscription('default')->removePrice('price_chat');
您不能删除订阅上的最后一个价格。相反,您应该简单地取消订阅。
更换价格
您还可以更改附加到多价格订阅的价格。例如,假设客户有一个 price_basic
订阅,并且有一个 price_chat
附加价格,您想将客户从 price_basic
升级到 price_pro
价格:
use App\Models\User;
$user = User::find(1);
$user->subscription('default')->swap(['price_pro', 'price_chat']);
在执行上面的示例时,具有 price_basic
的底层订阅项将被删除,并保留具有 price_chat
的订阅项。此外,将为 price_pro
创建一个新的订阅项。
您还可以通过将键/值对数组传递给 swap
方法来指定订阅项选项。例如,您可能需要指定订阅价格数量:
$user = User::find(1);
$user->subscription('default')->swap([
'price_pro' => ['quantity' => 5],
'price_chat'
]);
如果您想在订阅上更换单个价格,可以在订阅项本身上使用 swap
方法。这种方法特别有用,如果您想保留订阅的其他价格上的所有现有元数据:
$user = User::find(1);
$user->subscription('default')
->findItemOrFail('price_basic')
->swap('price_pro');
按比例分配
默认情况下,Stripe 在从多价格订阅中添加或删除价格时会按比例分配费用。如果您想在不按比例分配的情况下进行价格调整,您应该将 noProrate
方法链接到您的价格操作上:
$user->subscription('default')->noProrate()->removePrice('price_chat');
数量
如果您想更新单个订阅价格的数量,可以通过将价格名称作为方法的附加参数传递来使用 现有数量方法:
$user = User::find(1);
$user->subscription('default')->incrementQuantity(5, 'price_chat');
$user->subscription('default')->decrementQuantity(3, 'price_chat');
$user->subscription('default')->updateQuantity(10, 'price_chat');
当订阅有多个价格时,Subscription
模型上的 stripe_price
和 quantity
属性将为 null
。要访问单个价格属性,您应该使用 Subscription
模型上可用的 items
关系。
订阅项
当订阅有多个价格时,它将在数据库的 subscription_items
表中存储多个订阅“项”。您可以通过订阅上的 items
关系访问这些项:
use App\Models\User;
$user = User::find(1);
$subscriptionItem = $user->subscription('default')->items->first();
// 检索特定项的 Stripe 价格和数量...
$stripePrice = $subscriptionItem->stripe_price;
$quantity = $subscriptionItem->quantity;
您还可以使用 findItemOrFail
方法检索特定价格:
$user = User::find(1);
$subscriptionItem = $user->subscription('default')->findItemOrFail('price_chat');
计量计费
计量计费允许您根据客户在计费周期内的产品使用情况向其收费。例如,您可以根据客户每月发送的短信或电子邮件数量向其收费。
要开始使用计量计费,您首先需要在 Stripe 仪表板中创建一个具有计量价格的新产品。然后,使用 meteredPrice
将计量价格 ID 添加到客户订阅中:
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$request->user()->newSubscription('default')
->meteredPrice('price_metered')
->create($request->paymentMethodId);
// ...
});
您还可以通过 Stripe Checkout 启动计量订阅:
$checkout = Auth::user()
->newSubscription('default', [])
->meteredPrice('price_metered')
->checkout();
return view('your-checkout-view', [
'checkout' => $checkout,
]);
报告使用情况
随着客户使用您的应用程序,您将向 Stripe 报告其使用情况,以便准确计费。要增加计量订阅的使用量,可以使用 reportUsage
方法:
$user = User::find(1);
$user->subscription('default')->reportUsage();
默认情况下,将向计费周期添加“使用量”1。或者,您可以传递特定的“使用量”以添加到客户的计费周期使用量中:
$user = User::find(1);
$user->subscription('default')->reportUsage(15);
如果您的应用程序在单个订阅上提供多个价格,您将需要使用 reportUsageFor
方法来指定要报告使用情况的计量价格:
$user = User::find(1);
$user->subscription('default')->reportUsageFor('price_metered', 15);
有时,您可能需要更新之前报告的使用情况。为此,您可以将时间戳或 DateTimeInterface
实例作为 reportUsage
的第二个参数传递。这样做时,Stripe 将更新在给定时间报告的使用情况。只要给定的日期和时间仍在当前计费周期内,您就可以继续更新以前的使用记录:
$user = User::find(1);
$user->subscription('default')->reportUsage(5, $timestamp);
检索使用记录
要检索客户的过去使用情况,可以使用订阅实例的 usageRecords
方法:
$user = User::find(1);
$usageRecords = $user->subscription('default')->usageRecords();
如果您的应用程序在单个订阅上提供多个价格,可以使用 usageRecordsFor
方法来指定要检索使用记录的计量价格:
$user = User::find(1);
$usageRecords = $user->subscription('default')->usageRecordsFor('price_metered');
usageRecords
和 usageRecordsFor
方法返回一个包含使用记录关联数组的 Collection 实例。您可以遍历此数组以显示客户的总使用量:
@foreach ($usageRecords as $usageRecord)
- 开始时间:{{ $usageRecord['period']['start'] }}
- 结束时间:{{ $usageRecord['period']['end'] }}
- 总使用量:{{ $usageRecord['total_usage'] }}
@endforeach
有关返回的所有使用数据的完整参考以及如何使用 Stripe 的基于游标的分页,请参阅 官方 Stripe API 文档。
订阅税
您可以使用 Stripe Tax 自动计算税费,而不是手动计算税率
要指定用户在订阅上支付的税率,您应该在可计费模型上实现 taxRates
方法,并返回一个包含 Stripe 税率 ID 的数组。您可以在 Stripe 仪表板 中定义这些税率:
/**
* 应用于客户订阅的税率。
*
* @return array
*/
public function taxRates()
{
return ['txr_id'];
}
taxRates
方法使您能够在客户基础上应用税率,这对于跨多个国家和税率的用户群可能很有帮助。
如果您提供多价格订阅,可以通过在可计费模型上实现 priceTaxRates
方法为每个价格定义不同的税率:
/**
* 应用于客户订阅的税率。
*
* @return array
*/
public function priceTaxRates()
{
return [
'price_monthly' => ['txr_id'],
];
}
taxRates
方法仅适用于订阅费用。如果您使用 Cashier 进行“一次性”收费,您将需要在那时手动指定税率。
同步税率
在更改 taxRates
方法返回的硬编码税率 ID 时,用户的任何现有订阅的税设置将保持不变。如果您希望使用新的 taxRates
值更新现有订阅的税值,您应该调用用户订阅实例的 syncTaxRates
方法:
$user->subscription('default')->syncTaxRates();
这也将同步任何多价格订阅项税率。如果您的应用程序提供多价格订阅,您应该确保您的可计费模型实现了 上面讨论的 priceTaxRates
方法。
税收豁免
Cashier 还提供了 isNotTaxExempt
、isTaxExempt
和 reverseChargeApplies
方法来确定客户是否免税。这些方法将调用 Stripe API 以确定客户的税收豁免状态:
use App\Models\User;
$user = User::find(1);
$user->isTaxExempt();
$user->isNotTaxExempt();
$user->reverseChargeApplies();
这些方法也可用于任何 Laravel\Cashier\Invoice
对象。但是,当在 Invoice
对象上调用时,这些方法将确定发票创建时的豁免状态。
订阅锚定日期
默认情况下,计费周期锚点是订阅创建的日期,或者如果使用了试用期,则是试用期结束的日期。如果您想修改计费锚定日期,可以使用 anchorBillingCycleOn
方法:
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$anchor = Carbon::parse('first day of next month');
$request->user()->newSubscription('default', 'price_monthly')
->anchorBillingCycleOn($anchor->startOfDay())
->create($request->paymentMethodId);
// ...
});
有关管理订阅计费周期的更多信息,请参阅 Stripe 计费周期文档
取消订阅
要取消订阅,请在用户的订阅上调用 cancel
方法:
$user->subscription('default')->cancel();
当订阅被取消时,Cashier 将自动在您的 subscriptions
数据库表中设置 ends_at
列。此列用于知道何时 subscribed
方法应开始返回 false
。
例如,如果客户在 3 月 1 日取消订阅,但订阅计划在 3 月 5 日结束,则 subscribed
方法将继续返回 true
,直到 3 月 5 日。这是因为通常允许用户在其计费周期结束之前继续使用应用程序。
您可以使用 onGracePeriod
方法确定用户是否已取消其订阅但仍处于“宽限期”:
if ($user->subscription('default')->onGracePeriod()) {
//
}
如果您希望立即取消订阅,请在用户的订阅上调用 cancelNow
方法:
$user->subscription('default')->cancelNow();
如果您希望立即取消订阅并开具任何剩余的未开具发票的计量使用或新的/待处理的按比例分配发票项目,请在用户的订阅上调用 cancelNowAndInvoice
方法:
$user->subscription('default')->cancelNowAndInvoice();
您还可以选择在特定时间取消订阅:
$user->subscription('default')->cancelAt(
now()->addDays(10)
);
恢复订阅
如果客户已取消其订阅并且您希望恢复它,可以在订阅上调用 resume
方法。客户必须仍在其“宽限期”内才能恢复订阅:
$user->subscription('default')->resume();
如果客户取消订阅,然后在订阅完全过期之前恢复该订阅,客户将不会立即被计费。相反,他们的订阅将被重新激活,并将在原始计费周期上计费。
订阅试用
预先提供付款方式
如果您希望在仍然收集付款方式信息的同时向客户提供试用期,您应该在创建订阅时使用 trialDays
方法:
use Illuminate\Http\Request;
Route::post('/user/subscribe', function (Request $request) {
$request->user()->newSubscription('default', 'price_monthly')
->trialDays(10)
->create($request->paymentMethodId);
// ...
});
此方法将在数据库中的订阅记录上设置试用期结束日期,并指示 Stripe 在此日期之后才开始向客户计费。使用 trialDays
方法时,Cashier 将覆盖 Stripe 中为价格配置的任何默认试用期。
如果客户的订阅在试用期结束日期之前未被取消,他们将在试用期到期后立即被收费,因此您应该确保通知用户其试用期结束日期。
trialUntil
方法允许您提供一个 DateTime
实例,指定试用期应结束的时间:
use Carbon\Carbon;
$user->newSubscription('default', 'price_monthly')
->trialUntil(Carbon::now()->addDays(10))
->create($paymentMethod);
您可以使用用户实例的 onTrial
方法或订阅实例的 onTrial
方法来确定用户是否在其试用期内。以下两个示例是等效的:
if ($user->onTrial('default')) {
//
}
if ($user->subscription('default')->onTrial()) {
//
}
您可以使用 endTrial
方法立即结束订阅试用:
$user->subscription('default')->endTrial();
在 Stripe / Cashier 中定义试用天数
您可以选择在 Stripe 仪表板中定义价格的试用天数,或者始终使用 Cashier 显式传递它们。如果您选择在 Stripe 中定义价格的试用天数,您应该知道新订阅,包括过去曾有订阅的客户的新订阅,将始终获得试用期,除非您显式调用 skipTrial()
方法。
无需预先提供付款方式
如果您希望在不收集用户付款方式信息的情况下提供试用期,您可以在用户记录上设置 trial_ends_at
列为您想要的试用结束日期。这通常在用户注册期间完成:
use App\Models\User;
$user = User::create([
// ...
'trial_ends_at' => now()->addDays(10),
]);
确保在可计费模型的类定义中为 trial_ends_at
属性添加 日期转换。
Cashier 将此类型的试用称为“通用试用”,因为它不附加到任何现有订阅。可计费模型实例上的 onTrial
方法将返回 true
,如果当前日期未超过 trial_ends_at
的值:
if ($user->onTrial()) {
// 用户在其试用期内...
}
一旦您准备好为用户创建实际订阅,可以像往常一样使用 newSubscription
方法:
$user = User::find(1);
$user->newSubscription('default', 'price_monthly')->create($paymentMethod);
要检索用户的试用结束日期,可以使用 trialEndsAt
方法。如果用户在试用期内,此方法将返回一个 Carbon 日期实例,如果他们不在试用期内,则返回 null
。如果您想获取特定订阅(而不是默认订阅)的试用结束日期,还可以传递一个可选的订阅名称参数:
if ($user->onTrial()) {
$trialEndsAt = $user->trialEndsAt('main');
}
如果您希望具体知道用户在其“通用”试用期内且尚未创建实际订阅,可以使用 onGenericTrial
方法:
if ($user->onGenericTrial()) {
// 用户在其“通用”试用期内...
}
延长试用期
extendTrial
方法允许您在创建订阅后延长订阅的试用期。如果试用期已过期且客户已为订阅付费,您仍然可以为他们提供延长的试用期。试用期内的时间将从客户的下一张发票中扣除:
use App\Models\User;
$subscription = User::find(1)->subscription('default');
// 试用期在 7 天后结束...
$subscription->extendTrial(
now()->addDays(7)
);
// 在试用期中再增加 5 天...
$subscription->extendTrial(
$subscription->trial_ends_at->addDays(5)
);
处理 Stripe Webhooks
您可以使用 Stripe CLI 来帮助在本地开发期间测试 webhooks。
Stripe 可以通过 webhooks 通知您的应用程序各种事件。默认情况下,Cashier 服务提供者会自动注册一个指向 Cashier 的 webhook 控制器的路由。此控制器将处理所有传入的 webhook 请求。
默认情况下,Cashier 的 webhook 控制器将自动处理因过多失败的收费(根据您的 Stripe 设置定义)而取消的订阅、客户更新、客户删除、订阅更新和付款方式更改;然而,正如我们将很快发现的,您可以扩展此控制器以处理您喜欢的任何 Stripe webhook 事件。
要确保您的应用程序可以处理 Stripe webhooks,请确保在 Stripe 控制面板中配置 webhook URL。默认情况下,Cashier 的 webhook 控制器响应 /stripe/webhook
URL 路径。您应该在 Stripe 控制面板中启用的所有 webhooks 的完整列表是:
customer.subscription.created
customer.subscription.updated
customer.subscription.deleted
customer.updated
customer.deleted
invoice.payment_action_required
为了方便起见,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"
创建的 webhook 将使用与您的 Cashier 版本兼容的 Stripe API 版本。如果您想使用不同的 Stripe 版本,可以提供 --api-version
选项:
php artisan cashier:webhook --api-version="2019-12-03"
创建后,webhook 将立即激活。如果您希望创建 webhook 但在准备好之前将其禁用,可以在调用命令时提供 --disabled
选项:
php artisan cashier:webhook --disabled
确保使用 Cashier 附带的 webhook 签名验证 中间件保护传入的 Stripe webhook 请求。
Webhooks 和 CSRF 保护
由于 Stripe webhooks 需要绕过 Laravel 的 CSRF 保护,请确保在应用程序的 App\Http\Middleware\VerifyCsrfToken
中间件中将 URI 列为例外,或将路由列在 web
中间件组之外:
protected $except = [
'stripe/*',
];
定义 Webhook 事件处理程序
Cashier 自动处理因失败的收费和其他常见的 Stripe webhook 事件而取消的订阅。但是,如果您有其他想要处理的 webhook 事件,可以通过监听 Cashier 触发的以下事件来实现:
Laravel\Cashier\Events\WebhookReceived
Laravel\Cashier\Events\WebhookHandled
这两个事件都包含 Stripe webhook 的完整负载。例如,如果您希望处理 invoice.payment_succeeded
webhook,可以注册一个 监听器 来处理该事件:
<?php
namespace App\Listeners;
use Laravel\Cashier\Events\WebhookReceived;
class StripeEventListener
{
/**
* 处理接收到的 Stripe webhooks。
*
* @param \Laravel\Cashier\Events\WebhookReceived $event
* @return void
*/
public function handle(WebhookReceived $event)
{
if ($event->payload['type'] === 'invoice.payment_succeeded') {
// 处理传入的事件...
}
}
}
定义监听器后,可以在应用程序的 EventServiceProvider
中注册它:
<?php
namespace App\Providers;
use App\Listeners\StripeEventListener;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Laravel\Cashier\Events\WebhookReceived;
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
WebhookReceived::class => [
StripeEventListener::class,
],
];
}
验证 Webhook 签名
为了保护您的 webhooks,您可以使用 Stripe 的 webhook 签名。为了方便起见,Cashier 自动包含一个中间件,用于验证传入的 Stripe webhook 请求是否有效。
要启用 webhook 验证,请确保在应用程序的 .env
文件中设置 STRIPE_WEBHOOK_SECRET
环境变量。webhook secret
可以从您的 Stripe 账户仪表板中检索。
单次收费
简单收费
charge
方法接受您希望以应用程序使用的货币的最低单位进行收费的金额。例如,使用美元时,金额应以美分为单位指定。
如果您想对客户进行一次性收费,可以在可计费模型实例上使用 charge
方法。您需要 提供付款方式标识符 作为 charge
方法的第二个参数:
use Illuminate\Http\Request;
Route::post('/purchase', function (Request $request) {
$stripeCharge = $request->user()->charge(
100, $request->paymentMethodId
);
// ...
});
charge
方法接受一个数组作为其第三个参数,允许您将任何选项传递给底层 Stripe 收费创建。有关创建收费时可用选项的更多信息,请参阅 Stripe 文档:
$user->charge(100, $paymentMethod, [
'custom_option' => $value,
]);
您还可以在没有底层客户或用户的情况下使用 charge
方法。为此,请在应用程序的可计费模型的新实例上调用 charge
方法:
use App\Models\User;
$stripeCharge = (new User)->charge(100, $paymentMethod);
如果收费失败,charge
方法将抛出异常。如果收费成功,将从方法中返回一个 Laravel\Cashier\Payment
实例:
try {
$payment = $user->charge(100, $paymentMethod);
} catch (Exception $e) {
//
}
带发票的收费
有时您可能需要进行一次性收费并向客户提供 PDF 收据。invoicePrice
方法可以让您做到这一点。例如,让我们为客户开具五件新衬衫的发票:
$user->invoicePrice('price_tshirt', 5);
发票将立即从用户的默认付款方式中扣款。invoicePrice
方法还接受一个数组作为其第三个参数。此数组包含发票项目的计费选项。方法接受的第四个参数也是一个数组,其中应包含发票本身的计费选项:
$user->invoicePrice('price_tshirt', 5, [
'discounts' => [
['coupon' => 'SUMMER21SALE']
],
], [
'default_tax_rates' => ['txr_id'],
]);
或者,您可以使用 invoiceFor
方法对客户的默认付款方式进行“一次性”收费:
$user->invoiceFor('One Time Fee', 500);
尽管 invoiceFor
方法可供您使用,但建议您使用具有预定义价格的 invoicePrice
方法。通过这样做,您将能够在 Stripe 仪表板中获得更好的分析和数据,了解您的销售情况。
invoicePrice
和 invoiceFor
方法将创建一个 Stripe 发票,该发票将重试失败的计费尝试。如果您不希望发票重试失败的收费,您将需要在第一次失败的收费后使用 Stripe API 关闭它们。
退款
如果您需要退款 Stripe 收费,可以使用 refund
方法。此方法接受 Stripe 付款意图 ID 作为其第一个参数:
$payment = $user->charge(100, $paymentMethodId);
$user->refund($payment->id);
发票
检索发票
您可以使用 invoices
方法轻松检索可计费模型的发票数组。invoices
方法返回一个 Laravel\Cashier\Invoice
实例的集合:
$invoices = $user->invoices();
如果您希望在结果中包含待处理的发票,可以使用 invoicesIncludingPending
方法:
$invoices = $user->invoicesIncludingPending();
您可以使用 findInvoice
方法通过其 ID 检索特定发票:
$invoice = $user->findInvoice($invoiceId);
显示发票信息
在为客户列出发票时,您可以使用发票的方法来显示相关的发票信息。例如,您可能希望在表格中列出每张发票,以便用户可以轻松下载其中的任何一张:
<table>
@foreach ($invoices as $invoice)
<tr>
<td>{{ $invoice->date()->toFormattedDateString() }}</td>
<td>{{ $invoice->total() }}</td>
<td><a href="/user/invoice/{{ $invoice->id }}">下载</a></td>
</tr>
@endforeach
</table>
即将到来的发票
要检索客户的即将到来的发票,可以使用 upcomingInvoice
方法:
$invoice = $user->upcomingInvoice();
同样,如果客户有多个订阅,您还可以检索特定订阅的即将到来的发票:
$invoice = $user->subscription('default')->upcomingInvoice();
预览订阅发票
使用 previewInvoice
方法,您可以在进行价格更改之前预览发票。这将允许您确定在进行给定价格更改时客户的发票将是什么样子:
$invoice = $user->subscription('default')->previewInvoice('price_yearly');
您可以将价格数组传递给 previewInvoice
方法,以便预览具有多个新价格的发票:
$invoice = $user->subscription('default')->previewInvoice(['price_yearly', 'price_metered']);
生成发票 PDF
在路由或控制器中,您可以使用 downloadInvoice
方法生成给定发票的 PDF 下载。此方法将自动生成下载发票所需的正确 HTTP 响应:
use Illuminate\Http\Request;
Route::get('/user/invoice/{invoice}', function (Request $request, $invoiceId) {
return $request->user()->downloadInvoice($invoiceId, [
'vendor' => 'Your Company',
'product' => 'Your Product',
]);
});
默认情况下,发票上的所有数据都来自 Stripe 中存储的客户和发票数据。但是,您可以通过向 downloadInvoice
方法提供一个数组作为第二个参数来自定义其中的一些数据。此数组允许您自定义信息,例如您的公司和产品详细信息:
return $request->user()->downloadInvoice($invoiceId, [
'vendor' => 'Your Company',
'product' => 'Your Product',
'street' => 'Main Str. 1',
'location' => '2000 Antwerp, Belgium',
'phone' => '+32 499 00 00 00',
'email' => 'info@example.com',
'url' => 'https://example.com',
'vendorVat' => 'BE123456789',
], 'my-invoice');
downloadInvoice
方法还允许通过其第三个参数自定义文件名。此文件名将自动后缀为 .pdf
:
return $request->user()->downloadInvoice($invoiceId, [], 'my-invoice');
自定义发票渲染器
Cashier 还可以使用自定义发票渲染器。默认情况下,Cashier 使用 DompdfInvoiceRenderer
实现,该实现利用 dompdf PHP 库生成 Cashier 的发票。但是,您可以通过实现 Laravel\Cashier\Contracts\InvoiceRenderer
接口来使用任何渲染器。例如,您可能希望通过调用第三方 PDF 渲染服务的 API 来渲染发票 PDF:
use Illuminate\Support\Facades\Http;
use Laravel\Cashier\Contracts\InvoiceRenderer;
use Laravel\Cashier\Invoice;
class ApiInvoiceRenderer implements InvoiceRenderer
{
/**
* 渲染给定发票并返回原始 PDF 字节。
*
* @param \Laravel\Cashier\Invoice. $invoice
* @param array $data
* @param array $options
* @return string
*/
public function render(Invoice $invoice, array $data = [], array $options = []): string
{
$html = $invoice->view($data)->render();
return Http::get('https://example.com/html-to-pdf', ['html' => $html])->get()->body();
}
}
一旦您实现了发票渲染器合同,您应该在应用程序的 config/cashier.php
配置文件中更新 cashier.invoices.renderer
配置值。此配置值应设置为自定义渲染器实现的类名。
结账
Cashier Stripe 还提供对 Stripe Checkout 的支持。Stripe Checkout 通过提供一个预构建的托管支付页面,消除了实现自定义页面以接受付款的痛苦。
以下文档包含有关如何开始使用 Cashier 的 Stripe Checkout 的信息。要了解有关 Stripe Checkout 的更多信息,您还应该考虑查看 Stripe 自己的 Checkout 文档。
产品结账
您可以使用可计费模型上的 checkout
方法为已在 Stripe 仪表板中创建的现有产品执行结账。checkout
方法将启动一个新的 Stripe Checkout 会话。默认情况下,您需要传递一个 Stripe 价格 ID:
use Illuminate\Http\Request;
Route::get('/product-checkout', function (Request $request) {
return $request->user()->checkout('price_tshirt');
});
如果需要,您还可以指定产品数量:
use Illuminate\Http\Request;
Route::get('/product-checkout', function (Request $request) {
return $request->user()->checkout(['price_tshirt' => 15]);
});
当客户访问此路由时,他们将被重定向到 Stripe 的结账页面。默认情况下,当用户成功完成或取消购买时,他们将被重定向到您的 home
路由位置,但您可以使用 success_url
和 cancel_url
选项指定自定义回调 URL:
use Illuminate\Http\Request;
Route::get('/product-checkout', function (Request $request) {
return $request->user()->checkout(['price_tshirt' => 1], [
'success_url' => route('your-success-route'),
'cancel_url' => route('your-cancel-route'),
]);
});
在定义 success_url
结账选项时,您可以指示 Stripe 在调用您的 URL 时将结账会话 ID 添加为查询字符串参数。为此,请将字面字符串 {CHECKOUT_SESSION_ID}
添加到 success_url
查询字符串中。Stripe 将用实际的结账会话 ID 替换此占位符:
use Illuminate\Http\Request;
use Stripe\Checkout\Session;
use Stripe\Customer;
Route::get('/product-checkout', function (Request $request) {
return $request->user()->checkout(['price_tshirt' => 1], [
'success_url' => route('checkout-success') . '?session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => route('checkout-cancel'),
]);
});
Route::get('/checkout-success', function (Request $request) {
$checkoutSession = $request->user()->stripe()->checkout->sessions->retrieve($request->get('session_id'));
return view('checkout.success', ['checkoutSession' => $checkoutSession]);
})->name('checkout-success');
促销代码
默认情况下,Stripe Checkout 不允许用户兑换促销代码。幸运的是,有一种简单的方法可以为您的结账页面启用这些功能。为此,您可以调用 allowPromotionCodes
方法:
use Illuminate\Http\Request;
Route::get('/product-checkout', function (Request $request) {
return $request->user()
->allowPromotionCodes()
->checkout('price_tshirt');
});
单次收费结账
您还可以为尚未在您的 Stripe 仪表板中创建的临时产品执行简单的收费。为此,您可以在可计费模型上使用 checkoutCharge
方法,并传递一个可收费金额、产品名称和可选数量。当客户访问此路由时,他们将被重定向到 Stripe 的结账页面:
use Illuminate\Http\Request;
Route::get('/charge-checkout', function (Request $request) {
return $request->user()->checkoutCharge(1200, 'T-Shirt', 5);
});
使用 checkoutCharge
方法时,Stripe 将始终在您的 Stripe 仪表板中创建一个新产品和价格。因此,我们建议您在 Stripe 仪表板中预先创建产品并使用 checkout
方法。
订阅结账
使用 Stripe Checkout 进行订阅需要您在 Stripe 仪表板中启用 customer.subscription.created
webhook。此 webhook 将在您的数据库中创建订阅记录并存储所有相关的订阅项。
您还可以使用 Stripe Checkout 启动订阅。在使用 Cashier 的订阅构建器方法定义订阅后,您可以调用 checkout
方法。当客户访问此路由时,他们将被重定向到 Stripe 的结账页面:
use Illuminate\Http\Request;
Route::get('/subscription-checkout', function (Request $request) {
return $request->user()
->newSubscription('default', 'price_monthly')
->checkout();
});
与产品结账一样,您可以自定义成功和取消 URL:
use Illuminate\Http\Request;
Route::get('/subscription-checkout', function (Request $request) {
return $request->user()
->newSubscription('default', 'price_monthly')
->checkout([
'success_url' => route('your-success-route'),
'cancel_url' => route('your-cancel-route'),
]);
});
当然,您还可以为订阅结账启用促销代码:
use Illuminate\Http\Request;
Route::get('/subscription-checkout', function (Request $request) {
return $request->user()
->newSubscription('default', 'price_monthly')
->allowPromotionCodes()
->checkout();
});
不幸的是,Stripe Checkout 在启动订阅时不支持所有订阅计费选项。在订阅构建器上使用 anchorBillingCycleOn
方法、设置分摊行为或设置支付行为在 Stripe Checkout 会话期间将不起作用。请查阅 Stripe Checkout Session API 文档 以查看可用的参数。
Stripe Checkout & 试用期
当然,您可以在使用 Stripe Checkout 完成订阅时定义试用期:
$checkout = Auth::user()->newSubscription('default', 'price_monthly')
->trialDays(3)
->checkout();
然而,试用期必须至少为 48 小时,这是 Stripe Checkout 支持的最短试用时间。
订阅 & Webhooks
请记住,Stripe 和 Cashier 通过 webhooks 更新订阅状态,因此当客户在输入付款信息后返回应用程序时,订阅可能尚未激活。为处理这种情况,您可能希望显示一条消息,告知用户他们的付款或订阅正在处理中。
收集税号
结账还支持收集客户的税号。要在结账会话中启用此功能,请在创建会话时调用 collectTaxIds
方法:
$checkout = $user->collectTaxIds()->checkout('price_tshirt');
调用此方法时,客户将看到一个新复选框,允许他们指明是否以公司身份购买。如果是,他们将有机会提供他们的税号。
如果您已经在应用程序的服务提供者中配置了自动税收收集,则此功能将自动启用,无需调用 collectTaxIds
方法。
处理失败的付款
有时,订阅或单次收费的付款可能会失败。当这种情况发生时,Cashier 将抛出一个 Laravel\Cashier\Exceptions\IncompletePayment
异常,通知您发生了这种情况。捕获此异常后,您有两种选择来继续。
首先,您可以将客户重定向到 Cashier 附带的专用付款确认页面。此页面已经有一个通过 Cashier 的服务提供者注册的关联命名路由。因此,您可以捕获 IncompletePayment
异常并将用户重定向到付款确认页面:
use Laravel\Cashier\Exceptions\IncompletePayment;
try {
$subscription = $user->newSubscription('default', 'price_monthly')
->create($paymentMethod);
} catch (IncompletePayment $exception) {
return redirect()->route(
'cashier.payment',
[$exception->payment->id, 'redirect' => route('home')]
);
}
在付款确认页面上,客户将被提示再次输入他们的信用卡信息并执行 Stripe 要求的任何其他操作,例如“3D Secure”确认。确认付款后,用户将被重定向到由上面指定的 redirect
参数提供的 URL。重定向后,message
(字符串)和 success
(整数)查询字符串变量将被添加到 URL。付款页面目前支持以下付款方式类型:
- 信用卡
- 支付宝
- Bancontact
- BECS 直接借记
- EPS
- Giropay
- iDEAL
- SEPA 直接借记
或者,您可以允许 Stripe 为您处理付款确认。在这种情况下,您可以在 Stripe 仪表板中设置 Stripe 的自动账单电子邮件,而不是重定向到付款确认页面。但是,如果捕获到 IncompletePayment
异常,您仍应通知用户他们将收到一封包含进一步付款确认说明的电子邮件。
付款异常可能会在使用 Billable
特性的模型上的以下方法中抛出:charge
、invoiceFor
和 invoice
。在与订阅交互时,SubscriptionBuilder
上的 create
方法,以及 Subscription
和 SubscriptionItem
模型上的 incrementAndInvoice
和 swapAndInvoice
方法可能会抛出不完整的付款异常。
可以使用可计费模型或订阅实例上的 hasIncompletePayment
方法确定现有订阅是否有不完整的付款:
if ($user->hasIncompletePayment('default')) {
//
}
if ($user->subscription('default')->hasIncompletePayment()) {
//
}
您可以通过检查异常实例上的 payment
属性来推导不完整付款的具体状态:
use Laravel\Cashier\Exceptions\IncompletePayment;
try {
$user->charge(1000, 'pm_card_threeDSecure2Required');
} catch (IncompletePayment $exception) {
// 获取付款意图状态...
$exception->payment->status;
// 检查特定条件...
if ($exception->payment->requiresPaymentMethod()) {
// ...
} elseif ($exception->payment->requiresConfirmation()) {
// ...
}
}
强客户认证
如果您的业务或客户之一位于欧洲,您将需要遵守欧盟的强客户认证(SCA)法规。这些法规由欧盟于 2019 年 9 月实施,以防止支付欺诈。幸运的是,Stripe 和 Cashier 已为构建符合 SCA 的应用程序做好准备。
在开始之前,请查看 Stripe 关于 PSD2 和 SCA 的指南 以及他们关于新 SCA API 的文档。
需要额外确认的付款
SCA 法规通常需要额外的验证才能确认和处理付款。当这种情况发生时,Cashier 将抛出一个 Laravel\Cashier\Exceptions\IncompletePayment
异常,通知您需要额外的验证。有关如何处理这些异常的更多信息,请参阅处理失败付款的文档。
Stripe 或 Cashier 提供的付款确认屏幕可以根据特定银行或卡发行商的付款流程进行定制,可能包括额外的卡确认、临时小额收费、单独的设备认证或其他形式的验证。
不完整和逾期状态
当付款需要额外确认时,订阅将保持在 incomplete
或 past_due
状态,如其 stripe_status
数据库列所示。Cashier 将在付款确认完成后自动激活客户的订阅,并在 Stripe 通过 webhook 通知您的应用程序其完成。
有关 incomplete
和 past_due
状态的更多信息,请参阅我们关于这些状态的附加文档。
会话外付款通知
由于 SCA 法规要求客户偶尔验证他们的付款详细信息,即使他们的订阅处于活动状态,Cashier 可以在需要会话外付款确认时向客户发送通知。例如,这可能发生在订阅续订时。可以通过将 CASHIER_PAYMENT_NOTIFICATION
环境变量设置为通知类来启用 Cashier 的付款通知。默认情况下,此通知被禁用。当然,Cashier 包含一个您可以用于此目的的通知类,但如果需要,您可以自由提供自己的通知类:
CASHIER_PAYMENT_NOTIFICATION=Laravel\Cashier\Notifications\ConfirmPayment
为了确保会话外付款确认通知被传递,请验证您的应用程序是否已配置 Stripe webhooks,并在您的 Stripe 仪表板中启用了 invoice.payment_action_required
webhook。此外,您的 Billable
模型还应使用 Laravel 的 Illuminate\Notifications\Notifiable
特性。
即使客户手动进行需要额外确认的付款时,也会发送通知。不幸的是,Stripe 无法知道付款是手动完成的还是“会话外”的。但是,如果客户在确认付款后访问付款页面,他们只会看到“付款成功”消息。客户将无法意外地确认相同的付款两次并产生意外的第二次收费。
Stripe SDK
Cashier 的许多对象都是 Stripe SDK 对象的包装器。如果您想直接与 Stripe 对象交互,可以使用 asStripe
方法方便地检索它们:
$stripeSubscription = $subscription->asStripeSubscription();
$stripeSubscription->application_fee_percent = 5;
$stripeSubscription->save();
您还可以使用 updateStripeSubscription
方法直接更新 Stripe 订阅:
$subscription->updateStripeSubscription(['application_fee_percent' => 5]);
如果您想直接使用 Stripe\StripeClient
客户端,可以在 Cashier
类上调用 stripe
方法。例如,您可以使用此方法访问 StripeClient
实例并从您的 Stripe 帐户中检索价格列表:
use Laravel\Cashier\Cashier;
$prices = Cashier::stripe()->prices->all();
测试
在测试使用 Cashier 的应用程序时,您可以模拟对 Stripe API 的实际 HTTP 请求;然而,这需要您部分重新实现 Cashier 自己的行为。因此,我们建议允许您的测试访问实际的 Stripe API。虽然这比较慢,但它提供了更多的信心,确保您的应用程序按预期工作,并且任何慢速测试都可以放在它们自己的 PHPUnit 测试组中。
在测试时,请记住 Cashier 本身已经有一个很好的测试套件,因此您只需专注于测试您自己应用程序的订阅和付款流程,而不是每个底层的 Cashier 行为。
要开始,请将 Stripe 密钥的测试版本添加到您的 phpunit.xml
文件中:
<env name="STRIPE_SECRET" value="sk_test_<your-key>"/>
现在,每当您在测试时与 Cashier 交互时,它将向您的 Stripe 测试环境发送实际的 API 请求。为了方便起见,您应该预先填充您的 Stripe 测试帐户,以便在测试期间使用的订阅/价格。
为了测试各种计费场景,例如信用卡拒绝和失败,您可以使用 Stripe 提供的广泛的测试卡号和令牌。