Laravel 收银员(条纹)

Introduction

Laravel 收银员条纹 提供一个富有表现力的、流畅的界面Stripe's 订阅计费服务。它几乎可以处理您害怕编写的所有样板订阅计费代码。除了基本的订阅管理外,Cashier 还可以处理优惠券、交换订阅、订阅“数量”、取消宽限期,甚至生成发票 PDF。

升级收银台

升级到新版本的 Cashier 时,请务必仔细查看升级指南.

Warning
为防止破坏性更改,Cashier 使用固定的 Stripe API 版本。 Cashier 14 使用 Stripe API 版本2022-11-15. Stripe API 版本将在次要版本上更新,以便使用新的 Stripe 功能和改进。

Installation

首先,使用 Composer 包管理器为 Stripe 安装 Cashier 包:

composer require laravel/cashier

Warning
为确保 Cashier 正确处理所有 Stripe 事件,请记住设置 Cashier 的 webhook 处理.

数据库迁移

Cashier 的服务提供者注册了自己的数据库迁移目录,所以记得在安装包后迁移你的数据库。收银员迁移将为您的users 表以及创建一个新的subscriptions 保存所有客户订阅的表:

php artisan migrate

如果您需要覆盖 Cashier 附带的迁移,您可以使用vendor:publish工匠命令:

php artisan vendor:publish --tag="cashier-migrations"

如果你想阻止 Cashier 的迁移完全运行,你可以使用ignoreMigrations Cashier 提供的方法。通常,应在register 你的方法AppServiceProvider:

use Laravel\Cashier\Cashier;

/**
 * Register any application services.
 */
public function register(): void
{
    Cashier::ignoreMigrations();
}

Warning
Stripe 建议任何用于存储 Stripe 标识符的列都应该区分大小写。因此,您应该确保列排序规则stripe_id 列设置为utf8_bin 使用 MySQL 时。有关此的更多信息,请参见条纹文档.

Configuration

计费模型

在使用 Cashier 之前,添加Billable 计费模型定义的特征。通常,这将是App\Models\User 模型。该特征提供了多种方法来允许您执行常见的计费任务,例如创建订阅、应用优惠券和更新支付方式信息:

use Laravel\Cashier\Billable;

class User extends Authenticatable
{
    use Billable;
}

Cashier 假设您的计费模型将是App\Models\User Laravel 附带的类。如果你想改变这个,你可以通过指定一个不同的模型useCustomerModel 方法。此方法通常应在boot 你的方法AppServiceProvider 班级:

use App\Models\Cashier\User;
use Laravel\Cashier\Cashier;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Cashier::useCustomerModel(User::class);
}

Warning
如果你使用的不是 Laravel 提供的模型App\Models\User 模型,您需要发布和更改出纳迁移 提供以匹配您的替代模型的表名。

API 密钥

接下来,您应该在应用程序的.env 文件。您可以从 Stripe 控制面板检索您的 Stripe API 密钥:

STRIPE_KEY=your-stripe-key
STRIPE_SECRET=your-stripe-secret
STRIPE_WEBHOOK_SECRET=your-stripe-webhook-secret

Warning
你应该确保STRIPE_WEBHOOK_SECRET 环境变量在您的应用程序中定义.env 文件,因为此变量用于确保传入的 webhook 实际上来自 Stripe。

货币配置

默认出纳货币为美元 (USD)。您可以通过设置更改默认货币CASHIER_CURRENCY 应用程序中的环境变量.env 文件:

CASHIER_CURRENCY=eur

除了配置 Cashier 的货币外,您还可以指定在格式化货币值以显示在发票上时使用的区域设置。在内部,收银员利用PHP的NumberFormatter 班级 设置货币区域:

CASHIER_CURRENCY_LOCALE=nl_BE

Warning
为了使用除en, 确保ext-intl PHP 扩展已在您的服务器上安装和配置。

税收配置

谢谢条纹税,可以自动计算 Stripe 生成的所有发票的税费。您可以通过调用calculateTaxes 中的方法boot 你的应用程序的方法App\Providers\AppServiceProvider 班级:

use Laravel\Cashier\Cashier;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Cashier::calculateTaxes();
}

启用税务计算后,任何新订阅和生成的任何一次性发票都将自动进行税务计算。

为了使此功能正常运行,您的客户的账单详细信息(例如客户的姓名、地址和税号)需要同步到 Stripe。您可以使用客户资料同步税号 Cashier 提供的方法来完成此操作。

Warning
不计税单次收费 或者单次收费结账.

Logging

Cashier 允许您指定在记录 Stripe 致命错误时要使用的日志通道。您可以通过定义CASHIER_LOGGER 应用程序中的环境变量.env 文件:

CASHIER_LOGGER=stack

由对 Stripe 的 API 调用生成的异常将通过应用程序的默认日志通道记录。

使用自定义模型

您可以通过定义自己的模型并扩展相应的 Cashier 模型来自由扩展 Cashier 内部使用的模型:

use Laravel\Cashier\Subscription as CashierSubscription;

class Subscription extends CashierSubscription
{
    // ...
}

定义模型后,您可以指示 Cashier 通过Laravel\Cashier\Cashier 班级。通常,您应该在 Cashier 中告知您的自定义模型boot 你的应用程序的方法App\Providers\AppServiceProvider 班级:

use App\Models\Cashier\Subscription;
use App\Models\Cashier\SubscriptionItem;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Cashier::useSubscriptionModel(Subscription::class);
    Cashier::useSubscriptionItemModel(SubscriptionItem::class);
}

Customers

检索客户

您可以使用Cashier::findBillable 方法。此方法将返回计费模型的实例:

use Laravel\Cashier\Cashier;

$user = Cashier::findBillable($stripeId);

创造客户

有时,您可能希望在不开始订阅的情况下创建一个 Stripe 客户。您可以使用createAsStripeCustomer 方法:

$stripeCustomer = $user->createAsStripeCustomer();

在 Stripe 中创建客户后,您可以在以后开始订阅。你可以提供一个可选的$options 数组传递任何额外的Stripe API 支持的客户创建参数:

$stripeCustomer = $user->createAsStripeCustomer($options);

您可以使用asStripeCustomer方法,如果你想为计费模型返回 Stripe 客户对象:

$stripeCustomer = $user->asStripeCustomer();

createOrGetStripeCustomer 如果您想检索给定计费模型的 Stripe 客户对象,但不确定该计费模型是否已经是 Stripe 中的客户,则可以使用该方法。如果尚不存在,此方法将在 Stripe 中创建一个新客户:

$stripeCustomer = $user->createOrGetStripeCustomer();

更新客户

有时,您可能希望直接向 Stripe 客户更新其他信息。您可以使用updateStripeCustomer 方法。这个方法接受一个数组Stripe API 支持的客户更新选项:

$stripeCustomer = $user->updateStripeCustomer($options);

Balances

Stripe 允许您贷记或借记客户的“余额”。稍后,此余额将在新发票上贷记或借记。要检查客户的总余额,您可以使用balance 您的计费模型上可用的方法。这balance 方法将返回以客户货币表示的余额的格式化字符串表示:

$balance = $user->balance();

要记入客户的余额,您可以为creditBalance 方法。如果您愿意,您还可以提供描述:

$user->creditBalance(500, 'Premium customer top-up.');

提供价值给debitBalance 方法将借记客户的余额:

$user->debitBalance(300, 'Bad usage penalty.');

applyBalance 方法将为客户创建新的客户余额交易。您可以使用以下方式检索这些交易记录balanceTransactions 方法,这可能有助于提供贷方和借方日志供客户查看:

// Retrieve all transactions...
$transactions = $user->balanceTransactions();

foreach ($transactions as $transaction) {
    // Transaction amount...
    $amount = $transaction->amount(); // $2.31

    // Retrieve the related invoice when available...
    $invoice = $transaction->invoice();
}

税号

Cashier 提供了一种管理客户税号的简便方法。例如,taxIds 方法可用于检索所有的税号 作为集合分配给客户的:

$taxIds = $user->taxIds();

您还可以通过标识符检索客户的特定税号:

$taxId = $user->findTaxId('txi_belgium');

您可以通过提供有效的type 和价值createTaxId 方法:

$taxId = $user->createTaxId('eu_vat', 'BE0123456789');

createTaxId 方法会立即将增值税号添加到客户的帐户中。增值税 ID 的验证也由 Stripe 完成;然而,这是一个异步过程。您可以通过订阅customer.tax_id.updated Webhook 事件和检查增值税号verification 范围.有关处理 webhook 的更多信息,请参阅有关定义 webhook 处理程序的文档.

您可以使用删除税号deleteTaxId 方法:

$user->deleteTaxId('txi_belgium');

将客户数据与 Stripe 同步

通常,当您的应用程序的用户更新他们的姓名、电子邮件地址或其他同样由 Stripe 存储的信息时,您应该将更新通知 Stripe。通过这样做,Stripe 的信息副本将与您的应用程序同步。

要自动执行此操作,您可以在计费模型上定义一个事件侦听器,以响应模型的updated 事件。然后,在您的事件侦听器中,您可以调用syncStripeCustomerDetails 模型上的方法:

use App\Models\User;
use function Illuminate\Events\queueable;

/**
 * The "booted" method of the model.
 */
protected static function booted(): void
{
    static::updated(queueable(function (User $customer) {
        if ($customer->hasStripeId()) {
            $customer->syncStripeCustomerDetails();
        }
    }));
}

现在,每次您的客户模型更新时,其信息都会与 Stripe 同步。为方便起见,Cashier 会在客户初始创建时自动将客户信息与 Stripe 同步。

您可以通过覆盖 Cashier 提供的各种方法来自定义用于将客户信息同步到 Stripe 的列。例如,您可以覆盖stripeName 方法来自定义当 Cashier 将客户信息同步到 Stripe 时应被视为客户“姓名”的属性:

/**
 * Get the customer name that should be synced to Stripe.
 */
public function stripeName(): string|null
{
    return $this->company_name;
}

同样,您可以覆盖stripeEmail,stripePhone,stripeAddress, 和stripePreferredLocales 方法。这些方法将信息同步到他们相应的客户参数时更新 Stripe 客户对象.如果您希望完全控制客户信息同步过程,您可以覆盖syncStripeCustomerDetails 方法。

计费门户

条纹优惠设置计费门户的简单方法 以便您的客户可以管理他们的订阅、付款方式并查看他们的账单历史记录。您可以通过调用redirectToBillingPortal 来自控制器或路由的计费模型上的方法:

use Illuminate\Http\Request;

Route::get('/billing-portal', function (Request $request) {
    return $request->user()->redirectToBillingPortal();
});

默认情况下,当用户完成订阅管理后,他们将能够返回到home 通过 Stripe 计费门户中的链接访问您的应用程序。您可以通过将 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 来安全地收集客户的支付方式详细信息。 “设置意图”向 Stripe 指示向客户的付款方式收费的意图。收银台Billable 特征包括createSetupIntent 轻松创建新设置意图的方法。您应该从将呈现收集客户付款方式详细信息的表单的路由或控制器调用此方法:

return view('update-payment-method', [
    'intent' => $user->createSetupIntent()
]);

创建设置意图并将其传递给视图后,您应该将其秘密附加到将收集付款方式的元素。例如,考虑这个“更新付款方式”表单:

<input id="card-holder-name" type="text">

<!-- Stripe Elements Placeholder -->
<div id="card-element"></div>

<button id="card-button" data-secret="{{ $intent->client_secret }}">
    Update Payment Method
</button>

接下来,可以使用 Stripe.js 库附加一个条纹元素 填写表格并安全地收集客户的付款详细信息:

<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 方法:

const cardHolderName = document.getElementById('card-holder-name');
const cardButton = document.getElementById('card-button');
const clientSecret = cardButton.dataset.secret;

cardButton.addEventListener('click', async (e) => {
    const { setupIntent, error } = await stripe.confirmCardSetup(
        clientSecret, {
            payment_method: {
                card: cardElement,
                billing_details: { name: cardHolderName.value }
            }
        }
    );

    if (error) {
        // Display "error.message" to the user...
    } else {
        // The card has been verified successfully...
    }
});

卡片经过 Stripe 验证后,您可以通过结果setupIntent.payment_method 你的 Laravel 应用程序的标识符,它可以附加到客户。付款方式可以是添加为新的付款方式 或者用于更新默认支付方式.您还可以立即使用付款方式标识符创建新订阅.

Note
如果您想了解有关设置意图和收集客户付款详细信息的更多信息,请查看 Stripe 提供的此概述.

单笔费用的支付方式

当然,在针对客户的支付方式进行单笔收费时,我们只需要使用一次支付方式标识符。由于 Stripe 的限制,您不能使用客户存储的默认付款方式进行单笔收费。您必须允许客户使用 Stripe.js 库输入他们的付款方式详细信息。例如,考虑以下形式:

<input id="card-holder-name" type="text">

<!-- Stripe Elements Placeholder -->
<div id="card-element"></div>

<button id="card-button">
    Process Payment
</button>

在定义了这样一个表单之后,可以使用 Stripe.js 库来附加一个条纹元素 填写表格并安全地收集客户的付款详细信息:

<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 方法:

const cardHolderName = document.getElementById('card-holder-name');
const cardButton = document.getElementById('card-button');

cardButton.addEventListener('click', async (e) => {
    const { paymentMethod, error } = await stripe.createPaymentMethod(
        'card', cardElement, {
            billing_details: { name: cardHolderName.value }
        }
    );

    if (error) {
        // Display "error.message" to the user...
    } else {
        // The card has been verified successfully...
    }
});

如果卡验证成功,您可以通过paymentMethod.id 到你的 Laravel 应用程序并处理一个一次充电.

检索付款方式

paymentMethods 计费模型实例上的方法返回一个集合Laravel\Cashier\PaymentMethod 实例:

$paymentMethods = $user->paymentMethods();

默认情况下,此方法将返回付款方式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();

Warning
客户的默认付款方式只能用于开具发票和创建新订阅。由于 Stripe 施加的限制,它可能无法用于单次收费。

添加付款方式

要添加新的付款方式,您可以致电addPaymentMethod 计费模型上的方法,传递支付方式标识符:

$user->addPaymentMethod($paymentMethod);

Note
要了解如何检索付款方式标识符,请查看付款方式存储文件.

删除付款方式

要删除付款方式,您可以致电delete 上的方法Laravel\Cashier\PaymentMethod 您要删除的实例:

$paymentMethod->delete();

deletePaymentMethod 方法将从计费模型中删除特定的支付方式:

$user->deletePaymentMethod('pm_visa');

deletePaymentMethods 方法将删除计费模型的所有付款方式信息:

$user->deletePaymentMethods();

默认情况下,此方法将删除付款方式card 类型。要删除不同类型的付款方式,您可以通过type 作为方法的参数:

$user->deletePaymentMethods('sepa_debit');

Warning
如果用户有一个有效的订阅,你的应用程序不应该允许他们删除他们的默认支付方式。

Subscriptions

订阅提供了一种为您的客户设置定期付款的方法。 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 method 应该是订阅的内部名称。如果您的应用程序只提供一个订阅,您可以调用它default 或者primary.此订阅名称仅供内部应用程序使用,无意向用户显示。此外,它不应包含空格,并且在创建订阅后绝不能更改。第二个参数是用户订阅的具体价格。该值应对应于 Stripe 中的价格标识符。

create 方法,它接受条纹支付方式标识符 或条纹PaymentMethod 对象,将开始订阅并使用计费模型的 Stripe 客户 ID 和其他相关计费信息更新您的数据库。

Warning
将支付方式标识符直接传递给create 订阅方式也会自动添加到用户存储的支付方式中。

通过发票电子邮件收取定期付款

您可以指示 Stripe 在每次定期付款到期时通过电子邮件向客户发送发票,而不是自动收取客户的经常性付款。然后,客户可以在收到发票后手动支付。通过发票收取经常性付款时,客户无需预先提供付款方式:

$user->newSubscription('default', 'price_monthly')->createAndSendInvoice();

客户在取消订阅之前必须支付发票的时间由days_until_due 选项。默认情况下,这是 30 天;但是,如果您愿意,可以为此选项提供特定值:

$user->newSubscription('default', 'price_monthly')->createAndSendInvoice([], [
    'days_until_due' => 30
]);

Quantities

如果你想设置一个特定的quantity 对于创建订阅时的价格,您应该调用quantity 创建订阅之前订阅生成器上的方法:

$user->newSubscription('default', 'price_monthly')
     ->quantity(5)
     ->create($paymentMethod);

额外细节

如果您想指定额外的customer 或者subscription 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);

或者,如果您想申请条纹促销代码, 你可以使用withPromotionCode 方法:

$user->newSubscription('default', 'price_monthly')
     ->withPromotionCode('promo_code_id')
     ->create($paymentMethod);

给定的促销代码 ID 应该是分配给促销代码的 Stripe API ID,而不是面向客户的促销代码。如果您需要根据给定的面向客户的促销代码查找促销代码 ID,您可以使用findPromotionCode 方法:

// Find a promotion code ID by its customer facing code...
$promotionCode = $user->findPromotionCode('SUMMERSALE');

// Find an active promotion code ID by its customer facing code...
$promotionCode = $user->findActivePromotionCode('SUMMERSALE');

在上面的例子中,返回的$promotionCode 对象是一个实例Laravel\Cashier\PromotionCode.这个类装饰了一个底层Stripe\PromotionCode目的。您可以通过调用coupon 方法:

$coupon = $user->findPromotionCode('SUMMERSALE')->coupon();

优惠券实例允许您确定折扣金额以及优惠券是代表固定折扣还是基于百分比的折扣:

if ($coupon->isPercentage()) {
    return $coupon->percentOff().'%'; // 21.5%
} else {
    return $coupon->amountOff(); // $5.99
}

您还可以检索当前应用于客户或订阅的折扣:

$discount = $billable->discount();

$discount = $subscription->discount();

返回的Laravel\Cashier\Discount 实例装饰底层Stripe\Discount 对象实例。您可以通过调用coupon 方法:

$coupon = $subscription->discount()->coupon();

如果您想将新的优惠券或促销代码应用于客户或订阅,您可以通过applyCoupon 或者applyPromotionCode 方法:

$billable->applyCoupon('coupon_id');
$billable->applyPromotionCode('promotion_code_id');

$subscription->applyCoupon('coupon_id');
$subscription->applyPromotionCode('promotion_code_id');

请记住,您应该使用分配给促销代码的 Stripe API ID,而不是面向客户的促销代码。在给定时间只能将一个优惠券或促销代码应用于客户或订阅。

有关此主题的更多信息,请参阅有关 Stripe 的文档coupons促销代码.

添加订阅

如果您想向已有默认付款方式的客户添加订阅,您可以调用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;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class EnsureUserIsSubscribed
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        if ($request->user() && ! $request->user()->subscribed('default')) {
            // This user is not a paying customer...
            return redirect('billing');
        }

        return $next($request);
    }
}

如果您想确定用户是否仍在试用期内,您可以使用onTrial 方法。此方法可用于确定是否应向用户显示他们仍在试用期的警告:

if ($user->subscription('default')->onTrial()) {
    // ...
}

subscribedToProduct 方法可用于根据给定的 Stripe 产品标识符确定用户是否订阅了给定的产品。在 Stripe 中,产品是价格的集合。在此示例中,我们将确定用户的default 订阅是主动订阅应用程序的“高级”产品。给定的 Stripe 产品标识符应与您在 Stripe 仪表板中的产品标识符之一相对应:

if ($user->subscribedToProduct('prod_premium', 'default')) {
    // ...
}

通过将数组传递给subscribedToProduct 方法,您可以确定用户的default subscription 主动订阅应用程序的“基本”或“高级”产品:

if ($user->subscribedToProduct(['prod_basic', 'prod_premium'], 'default')) {
    // ...
}

subscribedToPrice 方法可用于确定客户的订阅是否对应于给定的价格 ID:

if ($user->subscribedToPrice('price_basic_monthly', 'default')) {
    // ...
}

recurring 方法可用于确定用户当前是否已订阅并且不再处于试用期内:

if ($user->subscription('default')->recurring()) {
    // ...
}

Warning
如果一个用户有两个同名的订阅,最近的订阅将始终由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.订阅状态存储在stripe_status 出纳栏subscriptions 数据库表。

同样,如果在交换价格时需要二次支付操作,订阅将被标记为past_due.当您的订阅处于这些状态中的任何一种时,在客户确认付款之前,它不会激活。确定订阅是否有未完成的付款可以使用hasIncompletePayment 计费模型或订阅实例上的方法:

if ($user->hasIncompletePayment('default')) {
    // ...
}

if ($user->subscription('default')->hasIncompletePayment()) {
    // ...
}

当订阅有未完成的付款时,您应该将用户引导至 Cashier 的付款确认页面,传递latestPayment 标识符。您可以使用latestPayment 订阅实例上可用的方法来检索此标识符:

<a href="{{ route('cashier.payment', $subscription->latestPayment()->id) }}">
    Please confirm your payment.
</a>

如果您希望订阅在处于past_due 或者incomplete 状态,你可以使用keepPastDueSubscriptionsActivekeepIncompleteSubscriptionsActive Cashier 提供的方法。通常,这些方法应该在register 你的方法App\Providers\AppServiceProvider:

use Laravel\Cashier\Cashier;

/**
 * Register any application services.
 */
public function register(): void
{
    Cashier::keepPastDueSubscriptionsActive();
    Cashier::keepIncompleteSubscriptionsActive();
}

Warning
当订阅处于incomplete 声明在确认付款之前不能更改。因此,swapupdateQuantity 当订阅处于incomplete 状态。

订阅范围

大多数订阅状态也可用作查询范围,以便您可以轻松查询数据库以查找处于给定状态的订阅:

// Get all active subscriptions...
$subscriptions = Subscription::query()->active()->get();

// Get all of the canceled subscriptions for a user...
$subscriptions = $user->subscriptions()->canceled()->get();

可用范围的完整列表如下:

Subscription::query()->active();
Subscription::query()->canceled();
Subscription::query()->ended();
Subscription::query()->incomplete();
Subscription::query()->notCanceled();
Subscription::query()->notOnGracePeriod();
Subscription::query()->notOnTrial();
Subscription::query()->onGracePeriod();
Subscription::query()->onTrial();
Subscription::query()->pastDue();
Subscription::query()->recurring();

更改价格

客户订阅您的应用程序后,他们可能偶尔想要更改为新的订阅价格。要将客户换成新价​​格,请将 Stripe 价格的标识符传递给swap 方法。交换价格时,假设用户想要重新激活他们的订阅(如果之前已取消订阅)。给定的价格标识符应对应于 Stripe 仪表板中可用的 Stripe 价格标识符:

use App\Models\User;

$user = App\Models\User::find(1);

$user->subscription('default')->swap('price_yearly');

如果客户处于试用期,则将保持试用期。此外,如果订阅存在“数量”,则也将保留该数量。

如果您想调换价格并取消客户当前处于的任何试用期,您可以调用skipTrial 方法:

$user->subscription('default')
        ->skipTrial()
        ->swap('price_yearly');

如果您想交换价格并立即向客户开具发票而不是等待他们的下一个结算周期,您可以使用swapAndInvoice 方法:

$user = User::find(1);

$user->subscription('default')->swapAndInvoice('price_yearly');

Prorations

默认情况下,Stripe 在价格之间交换时按比例分配费用。这noProrate 方法可用于在不按比例分配费用的情况下更新订阅价格:

$user->subscription('default')->noProrate()->swap('price_yearly');

有关订阅按比例分配的更多信息,请参阅条纹文档.

Warning
执行noProrate 之前的方法swapAndInvoice 方法对按比例分配没有影响。将始终开具发票。

认购数量

有时订阅会受到“数量”的影响。例如,项目管理应用程序可能对每个项目收取每月 10 美元的费用。您可以使用incrementQuantitydecrementQuantity 轻松增加或减少订阅数量的方法:

use App\Models\User;

$user = User::find(1);

$user->subscription('default')->incrementQuantity();

// Add five to the subscription's current quantity...
$user->subscription('default')->incrementQuantity(5);

$user->subscription('default')->decrementQuantity();

// Subtract five from the subscription's current quantity...
$user->subscription('default')->decrementQuantity(5);

或者,您可以使用updateQuantity 方法:

$user->subscription('default')->updateQuantity(10);

noProrate 方法可用于在不按比例分配费用的情况下更新订阅的数量:

$user->subscription('default')->noProrate()->updateQuantity(10);

有关订阅数量的更多信息,请参阅条纹文档.

多个产品的订阅数量

如果您的订阅是订阅多种产品,您应该将您希望增加或减少其数量的价格的 ID 作为第二个参数传递给增量/减量方法:

$user->subscription('default')->incrementQuantity(1, 'price_chat');

订阅多个产品

订阅多个产品 允许您将多个计费产品分配给单个订阅。例如,假设您正在构建一个客户服务“帮助台”应用程序,其基本订阅价格为每月 10 美元,但提供实时聊天附加产品,每月额外收费 15 美元。多个产品的订阅信息存储在 Cashier'ssubscription_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');

Warning
您不得删除订阅的最后价格。相反,您应该简单地取消订阅。

掉期价格

您还可以更改附加到具有多个产品的订阅的价格。例如,假设客户有一个price_basic 订阅一个price_chat 附加产品,并且您希望从price_basicprice_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

如果您想更新单个订阅价格的数量,您可以使用现有的数量方法 通过将价格名称作为附加参数传递给该方法:

$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');

Warning
当订阅有多个价格时stripe_pricequantity 上的属性Subscription 模型将是null.要访问各个价格属性,您应该使用items 上可用的关系Subscription 模型。

订阅项目

当订阅有多个价格时,它将有多个订阅“项目”存储在您的数据库中subscription_items 桌子。您可以通过访问这些items 订阅关系:

use App\Models\User;

$user = User::find(1);

$subscriptionItem = $user->subscription('default')->items->first();

// Retrieve the Stripe price and quantity for a specific item...
$stripePrice = $subscriptionItem->stripe_price;
$quantity = $subscriptionItem->quantity;

您还可以使用findItemOrFail 方法:

$user = User::find(1);

$subscriptionItem = $user->subscription('default')->findItemOrFail('price_chat');

多个订阅

Stripe 允许您的客户同时拥有多个订阅。例如,您可能经营一家提供游泳订阅和举重订阅的健身房,并且每个订阅可能有不同的定价。当然,客户应该能够订阅其中一个或两个计划。

当您的应用程序创建订阅时,您可以将订阅的名称提供给newSubscription 方法。该名称可以是表示用户正在启动的订阅类型的任何字符串:

use Illuminate\Http\Request;

Route::post('/swimming/subscribe', function (Request $request) {
    $request->user()->newSubscription('swimming')
        ->price('price_swimming_monthly')
        ->create($request->paymentMethodId);

    // ...
});

在此示例中,我们为客户发起了每月一次的游泳订阅。但是,他们以后可能想换成按年订阅。在调整客户的订阅时,我们可以简单地交换价格swimming 订阅:

$user->subscription('swimming')->swap('price_swimming_yearly');

当然,您也可以完全取消订阅:

$user->subscription('swimming')->cancel();

计量计费

计量计费 允许您在计费周期内根据客户的产品使用情况向客户收费。例如,您可以根据客户每月发送的短信或电子邮件的数量向客户收费。

要开始使用计量计费,您首先需要在 Stripe 控制面板中创建一个具有计量价格的新产品。然后,使用meteredPrice 将计量价格 ID 添加到客户订阅:

use Illuminate\Http\Request;

Route::post('/user/subscribe', function (Request $request) {
    $request->user()->newSubscription('default')
        ->meteredPrice('price_metered')
        ->create($request->paymentMethodId);

    // ...
});

您也可以通过以下方式开始计量订阅条纹结帐:

$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');

usageRecordsusageRecordsFor 方法返回一个包含使用记录关联数组的 Collection 实例。您可以遍历此数组以显示客户的总使用量:

@foreach ($usageRecords as $usageRecord)
    - Period Starting: {{ $usageRecord['period']['start'] }}
    - Period Ending: {{ $usageRecord['period']['end'] }}
    - Total Usage: {{ $usageRecord['total_usage'] }}
@endforeach

有关返回的所有使用数据的完整参考以及如何使用 Stripe 基于游标的分页,请参阅官方 Stripe API 文档.

订阅税

Warning
不用手动计算税率,您可以使用 Stripe Tax 自动计算税金

要指定用户为订阅支付的税率,您应该实现taxRates 计费模型上的方法并返回一个包含 Stripe 税率 ID 的数组。您可以在中定义这些税率你的条纹仪表板:

/**
 * The tax rates that should apply to the customer's subscriptions.
 *
 * @return array<int, string>
 */
public function taxRates(): array
{
    return ['txr_id'];
}

taxRates 方法使您能够在逐个客户的基础上应用税率,这可能有助于跨越多个国家和税率的用户群。

如果您提供包含多种产品的订阅,您可以通过实施priceTaxRates 计费模型上的方法:

/**
 * The tax rates that should apply to the customer's subscriptions.
 *
 * @return array<string, array<int, string>>
 */
public function priceTaxRates(): array
{
    return [
        'price_monthly' => ['txr_id'],
    ];
}

Warning
taxRates 方法仅适用于订阅费。如果您使用 Cashier 进行“一次性”收费,您将需要手动指定当时的税率。

同步税率

更改由返回的硬编码税率 ID 时taxRates 方法,用户任何现有订阅的税收设置将保持不变。如果您希望使用新的更新现有订阅的税值taxRates 值,你应该调用syncTaxRates 用户订阅实例上的方法:

$user->subscription('default')->syncTaxRates();

这还将同步具有多个产品的订阅的任何项目税率。如果您的应用程序提供多种产品的订阅,您应该确保您的计费模型实现priceTaxRates 方法上面讨论过.

免税

收银员还提供isNotTaxExempt,isTaxExempt, 和reverseChargeApplies 确定客户是否免税的方法。这些方法将调用 Stripe API 来确定客户的免税状态:

use App\Models\User;

$user = User::find(1);

$user->isTaxExempt();
$user->isNotTaxExempt();
$user->reverseChargeApplies();

Warning
这些方法也可用于任何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 会自动设置ends_at 你的专栏subscriptions 数据库表。此列用于了解何时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 中的价格配置的任何默认试用期。

Warning
如果客户的订阅在试用结束日期之前没有取消,他们将在试用期满后立即收费,因此您应该确保通知您的用户他们的试用结束日期。

trialUntil 方法允许您提供一个DateTime 指定试用期何时结束的实例:

use Carbon\Carbon;

$user->newSubscription('default', 'price_monthly')
            ->trialUntil(Carbon::now()->addDays(10))
            ->create($paymentMethod);

您可以使用以下任一方法确定用户是否在试用期内onTrial 用户实例的方法或onTrial 订阅实例的方法。下面的两个例子是等价的:

if ($user->onTrial('default')) {
    // ...
}

if ($user->subscription('default')->onTrial()) {
    // ...
}

您可以使用endTrial 立即结束订阅试用的方法:

$user->subscription('default')->endTrial();

要确定现有试用版是否已过期,您可以使用hasExpiredTrial 方法:

if ($user->hasExpiredTrial('default')) {
    // ...
}

if ($user->subscription('default')->hasExpiredTrial()) {
    // ...
}

在 Stripe / Cashier 中定义试用日

您可以选择在 Stripe 仪表板中定义您的价格接收的试用天数,或者始终使用 Cashier 明确传递它们。如果您选择在 Stripe 中定义您的价格的试用天数,您应该知道新订阅,包括过去有订阅的客户的新订阅,将始终获得试用期,除非您明确调用skipTrial() 方法。

没有预先付款方式

如果您想在不预先收集用户付款方式信息的情况下提供试用期,您可以设置trial_ends_at 用户记录上的列到您想要的试用结束日期。这通常在用户注册期间完成:

use App\Models\User;

$user = User::create([
    // ...
    'trial_ends_at' => now()->addDays(10),
]);

Warning
请务必添加一个约会演员 为了trial_ends_at 计费模型的类定义中的属性。

Cashier 将这种类型的试用称为“通用试用”,因为它不附加到任何现有订阅。这onTrial 计费模型实例上的方法将返回true 如果当前日期没有超过trial_ends_at:

if ($user->onTrial()) {
    // User is within their trial period...
}

准备好为用户创建实际订阅后,您可以使用newSubscription 像往常一样的方法:

$user = User::find(1);

$user->newSubscription('default', 'price_monthly')->create($paymentMethod);

要检索用户的试用结束日期,您可以使用trialEndsAt方法。如果用户正在试用或null 如果他们不是。如果您想获取特定订阅而非默认订阅的试用结束日期,您还可以传递一个可选的订阅名称参数:

if ($user->onTrial()) {
    $trialEndsAt = $user->trialEndsAt('main');
}

您也可以使用onGenericTrial 方法,如果您想具体了解用户是否在他们的“通用”试用期内并且尚未创建实际订阅:

if ($user->onGenericTrial()) {
    // User is within their "generic" trial period...
}

延长试验

extendTrial 方法允许您在创建订阅后延长订阅的试用期。如果试用期已经过期并且已经向客户收取订阅费用,您仍然可以为他们提供延长试用期。在试用期内花费的时间将从客户的下一张发票中扣除:

use App\Models\User;

$subscription = User::find(1)->subscription('default');

// End the trial 7 days from now...
$subscription->extendTrial(
    now()->addDays(7)
);

// Add an additional 5 days to the trial...
$subscription->extendTrial(
    $subscription->trial_ends_at->addDays(5)
);

处理 Stripe Webhook

Note
你可以使用条纹 CLI 帮助在本地开发期间测试 webhook。

Stripe 可以通过 webhook 通知您的应用程序各种事件。默认情况下,指向 Cashier 的 webhook 控制器的路由由 Cashier 服务提供商自动注册。该控制器将处理所有传入的 webhook 请求。

默认情况下,Cashier webhook 控制器将自动处理取消订阅失败次数过多(由您的 Stripe 设置定义)、客户更新、客户删除、订阅更新和付款方式更改;然而,我们很快就会发现,您可以扩展这个控制器来处理您喜欢的任何 Stripe webhook 事件。

为确保您的应用程序可以处理 Stripe webhook,请务必在 Stripe 控制面板中配置 webhook URL。默认情况下,Cashier 的 webhook 控制器响应/stripe/webhook 网址路径。您应该在 Stripe 控制面板中启用的所有 webhooks 的完整列表是:

为方便起见,Cashier 包含一个cashier:webhook 工匠命令。此命令将在 Stripe 中创建一个 webhook,用于侦听 Cashier 所需的所有事件:

php artisan cashier:webhook

默认情况下,创建的 webhook 将指向由APP_URL 环境变量和cashier.webhook 包含在 Cashier 中的路由。你可以提供--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

Warning
确保你使用 Cashier 来保护传入的 Stripe webhook 请求webhook 签名验证 中间件。

Webhook 和 CSRF 保护

由于 Stripe webhooks 需要绕过 Laravel 的CSRF保护, 请务必在您的应用程序的App\Http\Middleware\VerifyCsrfToken 中间件或列出路由之外的web 中间件组:

protected $except = [
    'stripe/*',
];

定义 Webhook 事件处理程序

Cashier 自动处理失败收费和其他常见 Stripe webhook 事件的订阅取消。但是,如果您有其他想要处理的 webhook 事件,您可以通过收听以下由 Cashier 调度的事件来实现:

这两个事件都包含 Stripe webhook 的完整负载。例如,如果您希望处理invoice.payment_succeeded webhook,你可以注册一个listener 将处理事件:

<?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...
        }
    }
}

一旦你的监听器被定义,你可以在你的应用程序中注册它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 验证,请确保STRIPE_WEBHOOK_SECRET 环境变量在您的应用程序中设置.env 文件。网络钩子secret 可以从您的 Stripe 帐户仪表板中检索。

单次收费

简易充电

如果您想对客户进行一次性收费,您可以使用charge 计费模型实例上的方法。你将需要提供付款方式标识符 作为第二个参数charge 方法:

use Illuminate\Http\Request;

Route::post('/purchase', function (Request $request) {
    $stripeCharge = $request->user()->charge(
        100, $request->paymentMethodId
    );

    // ...
});

charge 方法接受一个数组作为它的第三个参数,允许您将您希望的任何选项传递给基础 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) {
    // ...
}

Warning
charge 方法接受以您的应用程序使用的货币的最低分母表示的付款金额。例如,如果客户以美元付款,则应以美分指定金额。

凭发票收费

有时您可能需要一次性收费并向您的客户提供 PDF 收据。这invoicePrice 方法可以让你做到这一点。例如,让我们为五件新衬衫向客户开具发票:

$user->invoicePrice('price_tshirt', 5);

发票将立即根据用户的默认付款方式收取。这invoicePrice 方法也接受一个数组作为它的第三个参数。该数组包含发票项目的计费选项。该方法接受的第四个参数也是一个数组,其中应包含发票本身的计费选项:

$user->invoicePrice('price_tshirt', 5, [
    'discounts' => [
        ['coupon' => 'SUMMER21SALE']
    ],
], [
    'default_tax_rates' => ['txr_id'],
]);

类似于invoicePrice, 你可以使用tabPrice 通过将多个项目添加到客户的“选项卡”然后向客户开具发票来为多个项目(每张发票最多 250 个项目)创建一次性费用的方法。例如,我们可能会为客户开具五件衬衫和两个杯子的发票:

$user->tabPrice('price_tshirt', 5);
$user->tabPrice('price_mug', 2);
$user->invoice();

或者,您可以使用invoiceFor 对客户的默认付款方式进行“一次性”收费的方法:

$user->invoiceFor('One Time Fee', 500);

虽然invoiceFor 方法可供您使用,建议您使用invoicePricetabPrice 具有预定义价格的方法。通过这样做,您将可以在您的 Stripe 仪表板中访问关于每个产品销售情况的更好的分析和数据。

Warning
invoice,invoicePrice, 和invoiceFor 方法将创建一个 Stripe 发票,它将重试失败的计费尝试。如果您不希望发票重试失败的收费,您将需要在第一次收费失败后使用 Stripe API 关闭它们。

创建付款意向

您可以通过调用pay 计费模型实例上的方法。调用此方法将创建一个包装在Laravel\Cashier\Payment 实例:

use Illuminate\Http\Request;

Route::post('/pay', function (Request $request) {
    $payment = $request->user()->pay(
        $request->get('amount')
    );

    return $payment->client_secret;
});

创建支付意图后,您可以将客户端密码返回到应用程序的前端,以便用户可以在其浏览器中完成支付。要阅读有关使用 Stripe 支付意图构建整个支付流程的更多信息,请参阅条纹文档.

当使用pay 方法,您的 Stripe 仪表板中启用的默认付款方式将可供客户使用。或者,如果您只想允许使用某些特定的付款方式,您可以使用payWith 方法:

use Illuminate\Http\Request;

Route::post('/pay', function (Request $request) {
    $payment = $request->user()->payWith(
        $request->get('amount'), ['card', 'bancontact']
    );

    return $payment->client_secret;
});

Warning
paypayWith 方法接受以您的应用程序使用的货币的最低分母表示的付款金额。例如,如果客户以美元付款,则应以美分指定金额。

退款费用

如果您需要退还 Stripe 费用,您可以使用refund 方法。此方法接受条纹付款意向 ID 作为它的第一个参数:

$payment = $user->charge(100, $paymentMethodId);

$user->refund($payment->id);

Invoices

检索发票

您可以使用invoices 方法。这invoices 方法返回一个集合Laravel\Cashier\Invoice 实例:

$invoices = $user->invoices();

如果您想在结果中包含待处理的发票,您可以使用invoicesIncludingPending 方法:

$invoices = $user->invoicesIncludingPending();

您可以使用findInvoice 通过 ID 检索特定发票的方法:

$invoice = $user->findInvoice($invoiceId);

显示发票信息

在列出客户的发票时,您可以使用发票的方法来显示相关的发票信息。例如,您可能希望在表格中列出每张发票,以便用户轻松下载其中的任何一张:

<table>
    @foreach ($invoices as $invoice)
        <tr>
            <td>{{ $invoice->date()->toFormattedDateString() }}</td>
            <td>{{ $invoice->total() }}</td>
            <td><a href="/user/invoice/{{ $invoice->id }}">Download</a></td>
        </tr>
    @endforeach
</table>

即将开具的发票

要为客户检索即将到来的发票,您可以使用upcomingInvoice 方法:

$invoice = $user->upcomingInvoice();

同样,如果客户有多个订阅,您还可以检索特定订阅的即将到来的发票:

$invoice = $user->subscription('default')->upcomingInvoice();

预览订阅发票

使用previewInvoice 方法,您可以在更改价格之前预览发票。这将允许您确定在进行给定价格更改时客户的发票将是什么样子:

$invoice = $user->subscription('default')->previewInvoice('price_yearly');

您可以将一系列价格传递给previewInvoice 预览具有多个新价格的发票的方法:

$invoice = $user->subscription('default')->previewInvoice(['price_yearly', 'price_metered']);

生成发票 PDF

在生成发票 PDF 之前,您应该使用 Composer 安装 Dompdf 库,这是 Cashier 的默认发票渲染器:

composer require dompdf/dompdf

在路由或控制器中,您可以使用downloadInvoice 生成给定发票的 PDF 下载的方法。此方法将自动生成下载发票所需的正确 HTTP 响应:

use Illuminate\Http\Request;

Route::get('/user/invoice/{invoice}', function (Request $request, string $invoiceId) {
    return $request->user()->downloadInvoice($invoiceId);
});

默认情况下,发票上的所有数据都来自存储在 Stripe 中的客户和发票数据。文件名是根据你的app.name 配置值。但是,您可以通过提供一个数组作为第二个参数来自定义其中的一些数据downloadInvoice 方法。该数组允许您自定义信息,例如您的公司和产品详细信息:

return $request->user()->downloadInvoice($invoiceId, [
    'vendor' => 'Your Company',
    'product' => 'Your Product',
    'street' => 'Main Str. 1',
    'location' => '2000 Antwerp, Belgium',
    'phone' => '+32 499 00 00 00',
    'email' => 'info@example.com',
    'url' => 'https://example.com',
    'vendorVat' => 'BE123456789',
]);

downloadInvoice 方法还允许通过其第三个参数自定义文件名。此文件名将自动带有后缀.pdf:

return $request->user()->downloadInvoice($invoiceId, [], 'my-invoice');

自定义发票渲染器

Cashier 还可以使用自定义发票渲染器。默认情况下,Cashier 使用DompdfInvoiceRenderer 实现,它利用了dompdf 用于生成收银员发票的 PHP 库。但是,您可以通过实现Laravel\Cashier\Contracts\InvoiceRenderer 界面。例如,您可能希望使用对第三方 PDF 渲染服务的 API 调用来渲染发票 PDF:

use Illuminate\Support\Facades\Http;
use Laravel\Cashier\Contracts\InvoiceRenderer;
use Laravel\Cashier\Invoice;

class ApiInvoiceRenderer implements InvoiceRenderer
{
    /**
     * Render the given invoice and return the raw PDF bytes.
     */
    public function render(Invoice $invoice, array $data = [], array $options = []): string
    {
        $html = $invoice->view($data)->render();

        return Http::get('https://example.com/html-to-pdf', ['html' => $html])->get()->body();
    }
}

实施发票呈现器合同后,您应该更新cashier.invoices.renderer 应用程序中的配置值config/cashier.php 配置文件。此配置值应设置为自定义渲染器实现的类名。

Checkout

Cashier Stripe 还支持条纹结帐. Stripe Checkout 通过提供预构建的托管支付页面,消除了实现自定义页面以接受付款的痛苦。

以下文档包含有关如何开始使用 Stripe Checkout with Cashier 的信息。要了解有关 Stripe Checkout 的更多信息,您还应该考虑查看Stripe 自己的 Checkout 文档.

产品结帐

您可以使用checkout 计费模型上的方法。这checkout 方法将启动一个新的 Stripe Checkout 会话。默认情况下,您需要传递 Stripe 价格 ID:

use Illuminate\Http\Request;

Route::get('/product-checkout', function (Request $request) {
    return $request->user()->checkout('price_tshirt');
});

如果需要,您还可以指定产品数量:

use Illuminate\Http\Request;

Route::get('/product-checkout', function (Request $request) {
    return $request->user()->checkout(['price_tshirt' => 15]);
});

当客户访问这条路线时,他们将被重定向到 Stripe 的结账页面。默认情况下,当用户成功完成或取消购买时,他们将被重定向到您的home 路由位置,但您可以使用success_urlcancel_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);
});

Warning
当使用checkoutCharge 方法,Stripe 将始终在您的 Stripe 仪表板中创建新产品和价格。因此,我们建议您在 Stripe 控制面板中预先创建产品并使用checkout方法代替。

订阅结帐

Warning
使用 Stripe Checkout 进行订阅需要您启用customer.subscription.created 您的 Stripe 仪表板中的 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();
});

Warning
不幸的是,Stripe Checkout 在开始订阅时不支持所有订阅计费选项。使用anchorBillingCycleOn 订阅生成器上的方法、设置按比例分配行为或设置支付行为在 Stripe Checkout 会话期间不会有任何影响。请咨询Stripe Checkout Session API 文档 查看哪些参数可用。

条纹结帐和试用期

当然,您可以在构建将使用 Stripe Checkout 完成的订阅时定义试用期:

$checkout = Auth::user()->newSubscription('default', 'price_monthly')
    ->trialDays(3)
    ->checkout();

但是,试用期必须至少为 48 小时,这是 Stripe Checkout 支持的最短试用时间。

订阅和 Webhooks

请记住,Stripe 和 Cashier 通过 webhook 更新订阅状态,因此当客户在输入付款信息后返回到应用程序时,订阅可能尚未激活。要处理这种情况,您可能希望显示一条消息,通知用户他们的付款或订阅正在等待处理。

收集税号

Checkout 还支持收集客户的税号。要在结帐会话中启用此功能,请调用collectTaxIds 创建会话时的方法:

$checkout = $user->collectTaxIds()->checkout('price_tshirt');

调用此方法时,客户将可以使用一个新的复选框,允许他们表明他们是否作为公司进行采购。如果是这样,他们将有机会提供他们的税号。

Warning
如果您已经配置自动征税 在您的应用程序的服务提供商中,则此功能将自动启用,无需调用collectTaxIds 方法。

客人结帐

使用Checkout::guest 方法,您可以为没有“帐户”的应用程序的访客启动结帐会话:

use Illuminate\Http\Request;
use Laravel\Cashier\Checkout;

Route::get('/product-checkout', function (Request $request) {
    return Checkout::guest()->create('price_tshirt', [
        'success_url' => route('your-success-route'),
        'cancel_url' => route('your-cancel-route'),
    ]);
});

与为现有用户创建结账会话时类似,您可以使用网站上提供的其他方法Laravel\Cashier\CheckoutBuilder 自定义访客结帐会话的实例:

use Illuminate\Http\Request;
use Laravel\Cashier\Checkout;

Route::get('/product-checkout', function (Request $request) {
    return Checkout::guest()
        ->withPromotionCode('promo-code')
        ->create('price_tshirt', [
            'success_url' => route('your-success-route'),
            'cancel_url' => route('your-cancel-route'),
        ]);
});

客人结账完成后,Stripe 可以发送一个checkout.session.completed webhook 事件,因此请确保配置你的 Stripe webhook 实际将此事件发送到您的应用程序。在 Stripe 仪表板中启用 webhook 后,您可以使用 Cashier 处理 webhook. Webhook 负载中包含的对象将是checkout 目的 您可以检查以便完成客户的订单。

处理失败的付款

有时,订阅付款或单笔收费可能会失败。发生这种情况时,Cashier 会抛出一个Laravel\Cashier\Exceptions\IncompletePayment 通知您发生了这种情况的异常。捕获此异常后,您有两种选择可以继续。

首先,您可以将客户重定向到 Cashier 附带的专用付款确认页面。该页面已经有一个关联的命名路由,该路由是通过 Cashier 的服务提供商注册的。所以,你可能会赶上IncompletePayment 异常并将用户重定向到付款确认页面:

use Laravel\Cashier\Exceptions\IncompletePayment;

try {
    $subscription = $user->newSubscription('default', 'price_monthly')
                            ->create($paymentMethod);
} catch (IncompletePayment $exception) {
    return redirect()->route(
        'cashier.payment',
        [$exception->payment->id, 'redirect' => route('home')]
    );
}

在付款确认页面上,将提示客户再次输入他们的信用卡信息并执行 Stripe 要求的任何其他操作,例如“3D Secure”确认。确认付款后,用户将被重定向到由redirect 上面指定的参数。重定向后,message (字符串)和success (整数)查询字符串变量将添加到 URL。支付页面目前支持以下支付方式类型:

  • 信用卡
  • Alipay
  • Bancontact
  • BECS 直接借记
  • EPS
  • Giropay
  • iDEAL
  • SEPA 直接借记

或者,您可以让 Stripe 为您处理付款确认。在这种情况下,您可以不重定向到付款确认页面,而是设置 Stripe 的自动计费邮件 在您的 Stripe 仪表板中。然而,如果一个IncompletePayment 捕获到异常,您仍应通知用户他们将收到一封包含进一步付款确认说明的电子邮件。

以下方法可能会抛出支付异常:charge,invoiceFor, 和invoice 在模型上使用Billable 特征。与订阅交互时,create 上的方法SubscriptionBuilder, 和incrementAndInvoiceswapAndInvoice 上的方法SubscriptionSubscriptionItem 模型可能会抛出不完整的支付异常。

确定现有订阅是否有未完成的付款可以使用hasIncompletePayment 计费模型或订阅实例上的方法:

if ($user->hasIncompletePayment('default')) {
    // ...
}

if ($user->subscription('default')->hasIncompletePayment()) {
    // ...
}

您可以通过检查payment 异常实例的属性:

use Laravel\Cashier\Exceptions\IncompletePayment;

try {
    $user->charge(1000, 'pm_card_threeDSecure2Required');
} catch (IncompletePayment $exception) {
    // Get the payment intent status...
    $exception->payment->status;

    // Check specific conditions...
    if ($exception->payment->requiresPaymentMethod()) {
        // ...
    } elseif ($exception->payment->requiresConfirmation()) {
        // ...
    }
}

强大的客户认证

如果您的企业或您的客户之一位于欧洲,您将需要遵守欧盟的强客户认证 (SCA) 法规。欧盟于 2019 年 9 月实施了这些规定,以防止支付欺诈。幸运的是,Stripe 和 Cashier 已准备好构建符合 SCA 的应用程序。

Warning
在开始之前,回顾一下Stripe 关于 PSD2 和 SCA 的指南 以及他们的有关新 SCA API 的文档.

需要额外确认的付款

SCA 法规通常需要额外验证以确认和处理付款。发生这种情况时,Cashier 会抛出一个Laravel\Cashier\Exceptions\IncompletePayment 通知您需要额外验证的异常。有关如何处理这些异常的更多信息,请参阅文档处理失败的付款.

Stripe 或 Cashier 显示的支付确认屏幕可能会根据特定银行或发卡机构的支付流程进行定制,并且可能包括额外的卡确认、临时小额收费、单独的设备身份验证或其他形式的验证。

未完成和逾期状态

当付款需要额外确认时,订阅将保留在incomplete 或者past_due 状态如其所示stripe_status 数据库列。付款确认完成后,Cashier 将自动激活客户的订阅,并且 Stripe 通过 webhook 通知您的应用程序已完成。

有关更多信息incompletepast_due 状态,请参考我们关于这些状态的附加文档.

场外付款通知

由于 SCA 法规要求客户偶尔验证他们的付款细节,即使他们的订阅处于活动状态,Cashier 也可以在需要非会话付款确认时向客户发送通知。例如,这可能在订阅续订时发生。可以通过设置启用收银员的付款通知CASHIER_PAYMENT_NOTIFICATION 通知类的环境变量。默认情况下,此通知处于禁用状态。当然,Cashier 包含一个您可以用于此目的的通知类,但如果需要,您可以自由提供自己的通知类:

CASHIER_PAYMENT_NOTIFICATION=Laravel\Cashier\Notifications\ConfirmPayment

为确保发送会话外付款确认通知,请验证Stripe webhooks 已配置 为您的应用程序和invoice.payment_action_required webhook 已在您的 Stripe 仪表板中启用。此外,您的Billable 模型也应该使用 Laravel 的Illuminate\Notifications\Notifiable 特征。

Warning
即使客户手动进行需要额外确认的付款,也会发送通知。不幸的是,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 上的方法Cashier 类,如果你想使用Stripe\StripeClient 客户端直接。例如,您可以使用此方法访问StripeClient 实例并从您的 Stripe 帐户中检索价格列表:

use Laravel\Cashier\Cashier;

$prices = Cashier::stripe()->prices->all();

Testing

在测试使用 Cashier 的应用程序时,您可以模拟对 Stripe API 的实际 HTTP 请求;但是,这需要您部分地重新实现 Cashier 自己的行为。因此,我们建议让您的测试命中实际的 Stripe API。虽然速度较慢,但​​它可以让您更加确信您的应用程序正在按预期工作,并且任何缓慢的测试都可以放在他们自己的 PHPUnit 测试组中。

测试时,请记住 Cashier 本身已经有一个很好的测试套件,因此您应该只专注于测试您自己的应用程序的订阅和支付流程,而不是每一个底层 Cashier 行为。

首先,添加testing 您的 Stripe 秘密版本phpunit.xml 文件:

<env name="STRIPE_SECRET" value="sk_test_<your-key>"/>

现在,无论何时您在测试时与 Cashier 交互,它都会向您的 Stripe 测试环境发送实际的 API 请求。为方便起见,您应该使用您在测试期间可能使用的订阅/价格预先填写您的 Stripe 测试帐户。

Note
为了测试各种计费场景,例如信用卡拒绝和失败,您可以使用范围广泛的测试卡号和令牌 条纹提供。

豫ICP备18041297号-2