Cashier 交易工具包
Cashier 交易工具包
Laravel Cashier
简介
Laravel Cashier 提供了直观、流畅的接口来接入Stripe's和Braintree's的付费订阅服务。它可以处理几乎让您头疼的付费订阅代码。除了提供基本的订阅管理之外,Cashier 还可以帮您处理优惠券、交换订阅、订阅 “数量”、取消宽限期,甚至还可以生成 PDF 发票。
{注意} 如果您只是需要 “一次性” 的收费并且不提供订阅,就不应该使用 Cashier。建议您使用 Stripe 和 Braintree 的 SDK。
升级 Cashier
当您从旧版本升级到最新版本的 Cashier 前,建议您优先阅读Cashier 升级指南.
配置
Stripe
Composer
首先,将 Stripe 的 Cashier 包添加到您的项目依赖项中:
composer require laravel/cashier
数据库迁移
在使用 Cashier 之前,需要 准备数据库。 Cashier 需要向您的users
表中添加几个列,并创建一个新的subscriptions
表来保存所有客户的订阅:
Schema::table('users', function ($table) { $table->string('stripe_id')->nullable()->collation('utf8mb4_bin'); $table->string('card_brand')->nullable(); $table->string('card_last_four', 4)->nullable(); $table->timestamp('trial_ends_at')->nullable(); }); Schema::create('subscriptions', function ($table) { $table->increments('id'); $table->unsignedInteger('user_id'); $table->string('name'); $table->string('stripe_id')->collation('utf8mb4_bin'); $table->string('stripe_plan'); $table->integer('quantity'); $table->timestamp('trial_ends_at')->nullable(); $table->timestamp('ends_at')->nullable(); $table->timestamps(); });
一旦迁移文件建立好后,运行 Artisan 的migrate
命令。
Billable 模型
接下来,添加Billable
Trait 到您的模型定义。这个 Trait 提供了多个方法以便执行常用支付任务,例如创建订阅、使用优惠券以及更新信用卡信息:
use Laravel\Cashier\Billable;class User extends Authenticatable{ use Billable; }
API Keys
最后,在配置文件services.php
中配置 Stripe 的 Key,您可以在 Stripe 官网个人控制面板中获取这些 Stripe API Key 信息:
'stripe' => [ 'model' => App\User::class, 'key' => env('STRIPE_KEY'), 'secret' => env('STRIPE_SECRET'), ],
Braintree
Braintree 注意事项
在很多情况下,Stripe 和 Braintree 实现 Cashier 的功能都是一样的,两者都提供了通过信用卡进行订阅支付的功能, Braintree 还额外支持通过 PayPal 支付。但 Braintree 也缺失一些 Stripe 支持的功能,在决定使用 Stripe 或 Braintree 之前,您需要考虑以下几点:
- Braintree 支持 PayPal 而 Stripe 不支持。
- Braintree 不支持
increment
和decrement
方法,这是 Braintree 的限制,而不是 Cashier 限制。 - Braintree 不支持基于百分比的折扣。这是 Braintree 的限制,而不是 Cashier 限制。
Composer
首先,将 Braintree 的 Cashier 包添加到您项目的依赖项中:
composer require "laravel/cashier-braintree":"~2.0"
信用卡优惠计划
在使用 Cashier 之前,你需要首先在 Braintree 控制面板中定义一个plan-credit
折扣。这个折扣会根据用户选择的支付选项匹配合适的折扣比例,比如选择年付或者月付。
在 Braintree 控制面板中配置的折扣总额可以随意填写,Cashier 会在每次使用优惠券的时候根据您的配置来覆盖该默认值。由于 Braintree 不支持使用订阅频率来匹配折扣比例,所以这个优惠券是必需的。
数据库迁移
开始使用 Cashier 之前,需要 准备数据库。Cashier 需要在您的数据库users
表中新增几个列,以及创建一个新的subscriptions
表来存储客户的订阅信息:
Schema::table('users', function ($table) { $table->string('braintree_id')->nullable(); $table->string('paypal_email')->nullable(); $table->string('card_brand')->nullable(); $table->string('card_last_four')->nullable(); $table->timestamp('trial_ends_at')->nullable(); }); Schema::create('subscriptions', function ($table) { $table->increments('id'); $table->unsignedInteger('user_id'); $table->string('name'); $table->string('braintree_id'); $table->string('braintree_plan'); $table->integer('quantity'); $table->timestamp('trial_ends_at')->nullable(); $table->timestamp('ends_at')->nullable(); $table->timestamps(); });
一旦迁移文件建立好后,运行 Artisan 的migrate
命令。
Billable 模型
然后,添加Billable
Trait 到你的模型定义中:
use Laravel\Cashier\Billable; class User extends Authenticatable{ use Billable; }
API Keys
紧接着,您应该在services.php
文件中配置以下选项:
'braintree' => [ 'model' => App\User::class, 'environment' => env('BRAINTREE_ENV'), 'merchant_id' => env('BRAINTREE_MERCHANT_ID'), 'public_key' => env('BRAINTREE_PUBLIC_KEY'), 'private_key' => env('BRAINTREE_PRIVATE_KEY'), ],
最后,您必须向AppServiceProvider
服务提供者的boot
方法中,添加以下的 Braintree SDK 调用:
\Braintree_Configuration::environment(config('services.braintree.environment')); \Braintree_Configuration::merchantId(config('services.braintree.merchant_id')); \Braintree_Configuration::publicKey(config('services.braintree.public_key')); \Braintree_Configuration::privateKey(config('services.braintree.private_key'));
货币配置
Cashier 使用美元(USD)作为默认货币。您可以通过在服务提供者的boot
方法中调用Cashier::useCurrency
方法来更改默认的货币。这个useCurrency
方法接受两个字符串参数:货币和货币符号:
use Laravel\Cashier\Cashier; Cashier::useCurrency('eur', '€');
订阅
创建订阅
创建订阅,首先需要获取到一个 Billable 模型实例,这通常是App\User
的一个实例。一旦您获取了模型实例,您可以使用newSubscription
方法创建模型的订阅:
$user = User::find(1); $user->newSubscription('main', 'premium')->create($stripeToken);
newSubscription
方法的第一个参数应该是订阅的名称。如果您的应用程序只提供一个订阅,那么您可以将其设置为main
orprimary
。第二个参数是用户订阅的 Stripe / Braintree 计划。这个值应该与 Stripe 或 Braintree 中的标识符对应。
create
方法接受一个 Stripe 信用卡 / 源令牌,它将开始订阅,并使用客户 ID 和其他相关的账单信息更新数据库。
用户其他的详细信息
如果您想要指定用户其他的详细信息,您可以通过将它们作为第二个参数传递给create
方法:
$user->newSubscription('main', 'monthly')->create($stripeToken, [ 'email' => $email, ]);
要了解更多关于 Stripe 或 Braintree 支持的额外字段,请查看 Stripe 的内容创建客户文档或对应的Braintree 文档。
优惠券
如果您想在创建订阅时使用优惠券,您可以使用withCoupon
方法:
$user->newSubscription('main', 'monthly') ->withCoupon('code') ->create($stripeToken);
检查订阅状态
一旦用户在您的应用程序订阅了,您可以使用各种方便的方法轻松地检查他们的订阅状态。首先,如果用户有一个激活的订阅,那么subscribed
的方法将返回true
,即使订阅当前处于试用阶段:
if ($user->subscribed('main')) { // }
这个subscribed
方法还可以在 路由中间件 使用,允许您根据用户的订阅状态对路由和控制器进行访问:
public function handle($request, Closure $next){ if ($request->user() && ! $request->user()->subscribed('main')) { // This user is not a paying customer... return redirect('billing'); } return $next($request); }
如果您想要确定用户是否仍然处于试用阶段,您可以使用onTrial
方法。这个方法对于向用户显示他们仍然处于试用期的警告是很有用的:
if ($user->subscription('main')->onTrial()) { // }
基于给定的 Stripe / Braintree 计划 ID,可以使用subscribedToPlan
方法来确定用户是否订阅了该计划。在本例中,我们将确定用户的main
订阅是否激活了monthly
计划:
if ($user->subscribedToPlan('monthly', 'main')) { // }
recurring
方法可用于确定用户当前是否已订阅,并且不再处于试用阶段:
if ($user->subscription('main')->recurring()) { // }
取消订阅状态
为了确定用户是否曾经订阅,但是已经取消了他们的订阅,您可以使用cancelled
方法:
if ($user->subscription('main')->cancelled()) { // }
您还可以确定用户是否已经取消了订阅,但是仍然处于订阅的「宽限期」,直到订阅完全过期为止。例如,如果用户在 3 月 5 日取消了原定于 3 月 10 日到期的订阅,那么用户将在 3 月 10 日之前进行「宽限期」。请注意,在此期间subscribed
方法仍然返回true
:
if ($user->subscription('main')->onGracePeriod()) { // }
如果要确定用户取消订阅的时间是否已不在其 “宽限期” 内,可以使用ended
方法:
if ($user->subscription('main')->ended()) { // }
修改订阅计划
用户在您的应用程序中订阅了之后,他们可能会偶尔想要更改一个新的订阅计划。要将一个用户切换到一个新的订阅,需将订阅计划的标识符传递给swap
方法:
$user = App\User::find(1); $user->subscription('main')->swap('provider-plan-id');
如果用户在试用期,试用期的期限会被保留。另外,如果订阅的数量存在「份额」,那么该份额也将保持。
如果你想在更改用户订阅计划的时候取消用户当前订阅的试用期,可以使用skipTrial
方法:
$user->subscription('main') ->skipTrial() ->swap('provider-plan-id');
订阅量
{注意} 订阅量仅由 Cashier 的 Stripe 支持。Braintree 没有一个对应于 Stripe 的「数量」的特性。
有些时候订阅是会受「数量」影响的。举个例子,你的应用程序的付费方式可能是每个账户$10 / 月。你可以使用incrementQuantity
和decrementQuantity
方法轻松地增加或减少你的订阅量:
$user = User::find(1); $user->subscription('main')->incrementQuantity(); // 对当前的订阅量加5... $user->subscription('main')->incrementQuantity(5); $user->subscription('main')->decrementQuantity(); // 对当前的订阅量减5... $user->subscription('main')->decrementQuantity(5);
或者,你可以使用updateQuantity
方法设定一个特定的数量:
$user->subscription('main')->updateQuantity(10);
noProrate
方法可用于更新订阅的数量,而不会对收费进行定价:
$user->subscription('main')->noProrate()->updateQuantity(10);
要获得更多关于订阅量的信息,请参考Stripe 文档.
订阅税额
在计费模式上实现taxPercentage
方法,并且返回一个 0 到 100 不超过 2 位小数的数字,用来指定用户在订阅中支付的税率百分比。
public function taxPercentage() { return 20; }
taxPercentage
方法使你能够在模型的基础上应用税率,这对于一个跨越多个国家和税率的用户群可能有帮助。
{注意}
taxPercentage
方法只适用于付费订阅模式。如果你用 charges 来做「一次性」收费,你需要同时手工指定税率。
同步税率百分比
当更改taxPercentage
方法返回的硬编码值时,用户的任何现有订阅的税率设置将保持不变。如果要用返回的taxPercentage
值更新现有订阅的税率,应在用户的订阅实例上调用syncTaxPercentage
方法:
$user->subscription('main')->syncTaxPercentage();
订阅锚定日期
{注意} Cashier 中只有 Stripe 支持修改订阅锚定日期。
默认情况下,计费周期锚定是创建订阅的日期,如果使用试用期,则是试用结束的日期。如果要修改账单锚定日期,可以使用anchorBillingCycleOn
方法:
use App\User;use Carbon\Carbon; $user = User::find(1); $anchor = Carbon::parse('first day of next month'); $user->newSubscription('main', 'premium') ->anchorBillingCycleOn($anchor->startOfDay()) ->create($stripeToken);
有关管理订阅计费周期的详细信息,请参阅Stripe 计费周期文档
取消订阅
在用户订阅上调用cancel
方法用来取消订阅:
$user->subscription('main')->cancel();
当一个订阅被取消时,Cashier 将会自动在你的数据库中设置ends_at
列。这个列经常被用来获悉subscribed
字段何时应该开始返回false
。例如,如果客户在 3 月 1 日取消订阅,但是订阅计划直到 3 月 5 日才结束,subscribed
方法将会继续返回true
一直到 3 月 5 日。
你可以使用onGracePeriod
方法确定用户是否确定订阅,但是仍然存在一个「宽限期」:
if ($user->subscription('main')->onGracePeriod()) { // }
如果你想马上取消订阅,请在用户的订阅中调用cancelNow
方法:
$user->subscription('main')->cancelNow();
恢复订阅
如果一个用已经取消订阅,你可以在你希望恢复它的时候使用resume
方法。用户必须仍然在他们的宽限期内才可以恢复订阅:
$user->subscription('main')->resume();
如果用户已取消订阅,然后在订阅宽限期前恢复该订阅,他们将不会被立即计费。相反,他们的订阅将会被重新激活,需要按照原来的支付流程再次进行支付。
试用订阅
以信用卡订阅
如果你想给你的顾客提供试用期,同时收集支付方法信息,那么你应该在创建订阅使用trialDays
方法:
$user = User::find(1);$user->newSubscription('main', 'monthly') ->trialDays(10) ->create($stripeToken);
该方法会在数据库订阅记录上设置订阅期结束时间,以便告知 Sripe / Braintree 在此之前不要计算用户的账单信息。
{注意} 如果顾客没有在试用期结束前取消订阅,订阅会被自动结算,所以你应该确保告知你的用户他们的试用结束期。
trialUntil
方法允许提供DateTime
实例指定试用结束期:
use Carbon\Carbon;$user->newSubscription('main', 'monthly') ->trialUntil(Carbon::now()->addDays(10)) ->create($stripeToken);
你可以使用用户实例的onTrial
方法或者订阅实例的onTrial
方法判断用户是否处于试用期。下面两个示例等价:
if ($user->onTrial('main')) { // } if ($user->subscription('main')->onTrial()) { // }
非信用卡订阅
如果你不想在提供试用期的时候收集用户支付方式信息,只需设置用户记录的trial_ends_at
列为期望的试用期结束日期即可,这通常在用户注册期间完成:
$user = User::create([ // Populate other user properties... 'trial_ends_at' => now()->addDays(10), ]);
{注意} 确保已添加
trial_ends_at
日期修改器 到模型定义。
Cashier 把这种类型的引用称为「一般体验」,因为它没有关联任何已存在的订阅。如果当前的日期没有超过trail_ends_at
值,User
实例的onTrial
方法将会返回true
:
if ($user->onTrial()) { // 用户在他们的试用期内... }
如果你希望明确的知道用户处于「一般」试用期,并且还未创建实际的订阅,那么你可以使用onGenericTrial
方法:
if ($user->onGenericTrial()) { // 用户在他们「一般」试用期... }
如果你准备给用户创建实际的订阅,通常你可以使用newSubsription
方法:
$user = User::find(1); $user->newSubscription('main', 'monthly')->create($stripeToken);
客户
创建客户
有时,您可能希望在未订阅的情况下创建 Stripe 客户。您可以使用createAsStripeCustomer
方法完成此操作:
$user->createAsStripeCustomer();
一旦在 Stripe 中创建了客户,您可以稍后开始订阅。
{提示} 在 Braintree 中创建客户使用的是
createAsBraintreeCustomer
方法。
银行卡
接收信用卡
可计费模型实例上的cards
方法返回Laravel\Cashier\Card
实例的集合:
$cards = $user->cards();
要检索默认卡,可以使用defaultCard
方法;
$card = $user->defaultCard();
确定卡号存档
您可以使用hasCardOnFile
方法检查客户是否在其帐户上储存了信用卡:
if ($user->hasCardOnFile()) { // }
更新信用卡
updateCard
方法可用于更新用户的信用卡信息,该方法接受一个 Stripe 令牌并设置一个新的信用卡作为默认支付源:
$user->updateCard($stripeToken);
将您的卡信息与客户的默认卡信息进行 Stripe 同步,可以使用updateCardFromStripe
方法:
$user->updateCardFromStripe();
删除信用卡
要删除卡,应首先使用cards
方法检索客户的卡。然后,可以调用要删除的卡实例上的delete
方法:
foreach ($user->cards() as $card) { $card->delete(); }
{注意} 如果要删除默认卡,请确保使用
updateCardFromStripe
方法将新的默认卡与数据库同步。
deleteCards
方法将删除应用程序存储的用户的所有卡信息:
$user->deleteCards();
{注意} 如果用户已有订阅,则应考虑阻止他们删除最后剩余的付款方式。
处理 Stripe Webhooks
Stripe 和 Braintree 都可以通过 webhook 通知应用各种各样的事件。要处理 Stripe webhook,需要定义一个 Cashier 的 webhook 控制器的路由。这个控制器可以处理所有进入 webhook 的请求并将他们分发到合适的控制器方法中:
Route::post( 'stripe/webhook', '\Laravel\Cashier\Http\Controllers\WebhookController@handleWebhook' );
{注意} 一旦注册了路由,确保在您的 Stripe 控制面板配置了 webhook URL。
默认情况下,这个控制器将会自动对支付失败次数过多(这个次数可以在 Stripe 设置中定义)的订阅进行取消;此外,我们很快会发现,你可以扩展这个控制器去处理任何你想要处理的 webhook 事件。
{注意} 请确保使用 Cashier 的 webhook 验签 中间件来保护传入请求。
Webhooks & CSRF 保护
因为 Stripe webhooks 需要绕过 Laraval 的 CSRF 保护,请确保在你的VerifyCsrfToken
中间件含有 URI ,或者将其置于web
中间件组之外:
protected $except = [ 'stripe/*', ];
定义 Webhook 事件处理程序
Cashier 对于失败支付自动进行取消订阅,但是如果你有其他的 Stripe webhook 事件希望去处理,可以扩展 Webhook 控制器。你的方法名应该与 Cashier 期望的约定相符,更具体的说,你希望处理 Stripe webhook 的方法应该以handle
和 「驼峰」 名为前缀。举例来说,如果你希望处理invoice.payment_succeeded
的 webhook,你应该在控制器添加handleInvoicePaymentSucceeded
方法:
接下来,在
routes/web.php
文件中定义 Cashier 控制器的路由:Route::post( 'stripe/webhook', '\App\Http\Controllers\WebhookController@handleWebhook' );订阅失败
如果用户的信用卡过期怎么办?不用担心 - Cashier 包含了一个 Webhook 控制器可以轻松为你取消用户的订阅。正如上文所述,你需要做的只是将路由指向控制器:
Route::post( 'stripe/webhook', '\Laravel\Cashier\Http\Controllers\WebhookController@handleWebhook' );就是这样!失败的支付将会被控制器捕获并处理,该控制器会在 Stripe 判断订阅失败后(通常尝试支付失败 3 次及以上)取消用户的订阅。
Webhook 验签
为了保护 Webhook,你需要使用Stripe 的 webhook 签名。为了方便起见,Cashier 包含一个中间件,用于验证传入 Stripe webhook 的请求是否有效。
如果要启用 Webhook 验证,请确保在
services
配置文件中设置了stripe.webhook.secret
的值。 Webhook 的secret
可以从 Stripe 用户控制面板中找到。处理 Braintree Webhooks
Stripe 和 Braintree 都可以通过 webhooks 通知应用各种各样的事件。要处理 Braintree webhooks,需要定义一个 Cashier webhook 控制器的路由。这个控制器可以处理所有传入 webhook 的请求并将它们分发到合适的路由器方法中:
Route::post( 'braintree/webhook', '\Laravel\Cashier\Http\Controllers\WebhookController@handleWebhook' );{注意} 一旦注册了路由,确保在 Braintree 控制器面板配置了 webhook URL。
默认情况下,这个控制器将会自动对支付失败次数过多(这个次数可以在 Braintree 设置中定义)的订阅进行取消;此外,我们很快会发现,你可以扩展这控制器去处理任何你想要处理的 webhook 事件。
Webhooks & CSRF 保护
因为 Braintree webhooks 需要绕过 Laravel 的 CSRF 保护,请确保在你的
VerifyCsrfToken
中间件列表中含有 URI ,或者将其置于web
中间件组之外:protected $except = [ 'braintree/*', ];定义 Webhook 事件处理程序
Cashier 对于失败支付自动进行取消订阅,但是如果你有其他的 Braintree webhook 事件希望去处理,可以扩展 Webhook 控制器。你的方法名应该与 Cashier 期望的约定相符,更具体的说,你希望处理 Braintree webhook 的方法应该以
handle
和「驼峰」名为前缀。举例来说,如果你希望处理dispute_opened
这个 webhook,你应该在控制器添加handleDisputeOpened
方法:订阅失败
如果用户的信用卡过期怎么办?不用担心 - Cashier 包含了一个 Webhook 控制器可以轻松为你取消用户的订阅。只需要将路由指向控制器中:
Route::post( 'braintree/webhook', '\Laravel\Cashier\Http\Controllers\WebhookController@handleWebhook' );就是这样!失败的支付将会被控制器捕获和处理,该控制器会在 Braintree 判断订阅失败后(通常尝试支付失败 3 次及以上)取消用户的订阅。 不要忘记:在你的 Braintree 控制器面板中配置 webhook URI。
一次性支付
简单支付
{注意} 当使用 Stripe 时,
charge
方法接收你想支付于应用程序使用的货币的最小单位的金额。然而,当使用 Braintree 时,你应该将全部的美元金额传入charge
方法:如果你想对订阅客户的信用卡收取「一次性」费用,可以在可计费模型实例上使用
charge
方法:// Stripe 接收分为单位的费用... $stripeCharge = $user->charge(100); // Braintree 接收美元为单位的费用... $user->charge(1);
charge
方法接受一个数组作为它的第二个参数,允许你创建支付时将任何你想要的选项传递给底层的 Stripe / Braintree 。 有关在创建支付时可用的选项,请参阅 Stripe 或 Braintree 文档:$user->charge(100, [ 'custom_option' => $value, ]);如果支付失败,
charge
方法将会抛出异常。如果支付成功,该方法将会返回完整的 Stripe / Braintree 响应:try { $response = $user->charge(100); } catch (Exception $e) { // }费用与发票
有时你可能需要支付一次性费用同时也需要生成费用发票,以便可以向客户提供 PDF 文件格式的收据。
invoiceFor
方法可以让你做到这一点。 例如,向客户开具 5.00 美元的「一次性费用」发票:// Stripe 接收分为单位的费用... $user->invoiceFor('One Time Fee', 500); // Braintree 接收美元为单位的费用... $user->invoiceFor('One Time Fee', 5);该发票会立即通过用户信用卡支付。
invoiceFor
方法接收一个数组作为第三个参数,允许你在创建支付时将任何你想要的选项传递给底层的 Stripe / Braintree :$user->invoiceFor('Stickers', 500, [ 'quantity' => 50, ], [ 'tax_percent' => 21, ]);如果你使用 Braintree 作为你的账单提供者,你在调用
invoiceFor
方法时必须包含description
选项:$user->invoiceFor('One Time Fee', 500, [ 'description' => 'your invoice description here', ]);{注意}
invoiceFor
方法将会创建 Stripe 发票,该发票将会在支付失败后重试。如果你不想失败后重试,你需要在第一次支付失败后调用 Stripe API 关闭它。关于退款
如果您需要处理退款,您可以使用
refund
方法。此方法接受 Stripe charge ID 作为其唯一参数:$stripeCharge = $user->charge(100); $user->refund($stripeCharge->id);发票
您可以使用
invoices
方法轻松获取账单模型的发票数组:$invoices = $user->invoices(); // 结果包含处理中的发票... $invoices = $user->invoicesIncludingPending();当列出客户发票清单时,可以使用发票辅助函数来显示相关的发票信息。例如,您可能希望在表格中列出每张发票,从而方便客户下载它们:
{{ $invoice->date()->toFormattedDateString() }} | {{ $invoice->total() }} | Download |
生成 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', ]); });