alexanderpoellmann/cashier-braintree

Laravel Cashier 提供了一个表达性和流畅的接口,用于 Braintree 的订阅计费服务。

v4.0.0 2021-01-30 14:07 UTC

README

介绍

Laravel Cashier Braintree 提供了一个表达性和流畅的接口,用于访问 Braintree 的订阅计费服务。它处理了你可能不想编写的几乎所有样板订阅计费代码。除了基本的订阅管理外,Cashier Braintree 还可以处理优惠券、订阅交换、宽限期取消以及甚至生成发票 PDF。

Based on the original Laravel/Cashier-Braintree package.

测试

为了运行库的测试,您需要在本地和您的 Braintree 账户中设置以下详细信息。

本地

环境变量

BRAINTREE_MERCHANT_ID=
BRAINTREE_PUBLIC_KEY=
BRAINTREE_PRIVATE_KEY=
BRAINTREE_MODEL=Laravel\Cashier\Tests\User

您可以在 phpunit.xml.dist 文件中设置这些变量。

Braintree

计划

* Plan ID: monthly-10-1, Price: $10, Billing cycle of every month
* Plan ID: monthly-10-2, Price: $10, Billing cycle of every month
* Plan ID: yearly-100-1, Price: $100, Billing cycle of every 12 months

折扣

* Discount ID: coupon-1, Price: $5
* Discount ID: plan-credit, Price $1

设置

  • 禁用重复检查:在 设置 > 处理 > 交易:取消选择 重复交易检查

原始文档

Cashier Braintree 的文档可以在 Laravel 网站 上找到。

更新后的文档

Laravel Cashier 提供了一个表达性和流畅的接口,用于 Stripe 和 Braintree 的订阅计费服务。它处理了你可能不想编写的几乎所有样板订阅计费代码。除了基本的订阅管理外,Cashier 还可以处理优惠券、交换订阅、订阅 "数量"、宽限期取消,甚至生成发票 PDF。

如果你只进行 "一次性" 收费并且不提供订阅,则不应使用 Cashier。相反,直接使用 Stripe 和 Braintree SDK。升级 Cashier

当你升级到 Cashier 的新主要版本时,仔细查看升级指南非常重要。

配置

Braintree

Braintree 注意事项

对于许多操作,Cashier 的 Stripe 和 Braintree 实现功能相同。两者都提供信用卡订阅计费服务,但 Braintree 还支持通过 PayPal 进行支付。然而,Braintree 也缺少一些 Stripe 支持的功能。在决定使用 Stripe 或 Braintree 时,你应该注意以下事项

Braintree 支持PayPal,而 Stripe 不支持。Braintree 不支持订阅的递增和递减方法。这是 Braintree 的限制,而不是 Cashier 的限制。Braintree 不支持基于百分比的折扣。这是 Braintree 的限制,而不是 Cashier 的限制。

Composer

首先,将 Braintree 的 Cashier 包添加到您的依赖项

composer require "laravel/cashier-braintree":"^4.0"

计划信用优惠券

在开始使用 Cashier 与 Braintree 之前,您需要在 Braintree 控制面板中定义一个计划信用折扣。此折扣将用于正确地按比例计算从年度计费更改为月度计费或从月度计费更改为年度计费的订阅。

在 Braintree 控制面板中配置的折扣金额可以是任何你希望的价值,因为 Cashier 将在每次应用优惠券时用我们自己的自定义金额覆盖定义的金额。由于 Braintree 本身不支持在订阅频率之间按比例计算订阅,因此需要此优惠券。

数据库迁移

在开始使用 Cashier 之前,我们需要准备数据库。我们需要向您的用户表添加几个列并创建一个新的订阅表来存储我们所有客户的订阅

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();
});

创建迁移后,运行 migrate Artisan 命令。

可计费模型

接下来,将 Billable 特性添加到您的模型定义中

use Laravel\Cashier\Billable;

class User extends Authenticatable
{
    use Billable;
}

API 密钥

接下来,您应该在 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', '');

订阅

创建订阅

要创建订阅,首先获取您的可计费模型的实例,这通常是 App\User 的实例。获取模型实例后,您可以使用 newSubscription 方法创建模型的订阅

$user = User::find(1);

$user->newSubscription('main', 'premium')->create($stripeToken);

传递给 newSubscription 方法的第一个参数应该是订阅的名称。如果您的应用程序只提供单个订阅,您可能会将其称为主要或主要。第二个参数是用户订阅的特定 Stripe / Braintree 计划。此值应与 Stripe 或 Braintree 中计划的标识符相对应

接受 Stripe 信用卡/源令牌的 create 方法将开始订阅,并将客户 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()) {
    //
}

subscribedToPlan 方法可以用来确定用户是否基于给定的 Stripe / Braintree 计划 ID 订阅了给定的计划。在这个例子中,我们将确定用户的主要订阅是否正在订阅月度计划

if ($user->subscribedToPlan('monthly', 'main')) {
    //
}

已取消订阅状态

要确定用户是否曾经是活跃的订阅者但已取消订阅,可以使用 cancelled 方法

if ($user->subscription('main')->cancelled()) {
    //
}

您还可以确定用户已取消订阅,但仍处于“宽限期”直到订阅完全到期。例如,如果用户在3月5日取消了一个原定于3月10日到期的订阅,用户将在3月10日之前处于“宽限期”。注意,在此期间 subscribed 方法仍然返回 true

if ($user->subscription('main')->onGracePeriod()) {
    //
}

更改计划

用户订阅了您的应用程序后,他们可能会偶尔想要更改到新的订阅计划。要将用户切换到新的订阅计划,请将计划的标识符传递给 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();

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

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

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

或者,您可以使用updateQuantity方法设置特定的数量。

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

可以使用noProrate方法更新订阅的数量,而无需按比例计费。

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

有关订阅数量的更多信息,请参阅Stripe文档。

订阅税项

要指定用户在订阅上支付的税率,请在您的可计费模型上实现taxPercentage方法,并返回一个介于0和100之间的数值,最多保留两位小数。

public function taxPercentage() {
    return 20;
}

taxPercentage方法允许您根据模型应用税率,这对于跨越多个国家和税率的用户基础可能很有帮助。

taxPercentage方法仅适用于订阅费用。如果您使用Cashier进行一次性计费,您需要在此时刻手动指定税率。

同步税率

当更改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列设置为。此列用于知道何时开始返回false的订阅方法。例如,如果客户在3月1日取消了订阅,但订阅原计划在3月5日结束,则订阅方法将继续在3月5日之前返回true。

您可以使用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);

此方法将在数据库中的订阅记录上设置试用期结束日期,并指示Stripe / 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()) {
    //
}

无需 upfront 信用卡

如果您想提供无需预先收集用户支付方式信息的试用期限,您可以设置用户记录中的trial_ends_at列为您希望的试用结束日期。这通常在用户注册期间完成。

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

请确保在模型定义中为trial_ends_at添加日期转换器。Cashier将此类试用称为“通用试用”,因为它不与任何现有订阅相关联。如果当前日期不是在trial_ends_at的值之后,则User实例上的onTrial方法将返回true。

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

如果您想具体知道用户是否处于“通用”试用期内并且尚未创建实际订阅,您也可以使用onGenericTrial方法。

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

一旦您准备好为用户创建实际订阅,您可以使用newSubscription方法。

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

如果用户有一个活跃的订阅,您应考虑阻止他们删除最后一个剩余的支付来源。

处理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的预期约定相符合,具体来说,方法应以前缀handle和您要处理的Braintree webhook的“camel case”名称开头。例如,如果您要处理dispute_opened webhook,您应在控制器中添加一个handleDisputeOpened方法。

<?php

namespace App\Http\Controllers;

use Braintree\WebhookNotification;
use Laravel\Cashier\Http\Controllers\WebhookController as CashierController;

class WebhookController extends CashierController
{
    /**
     * Handle a new dispute.
     *
     * @param  \Braintree\WebhookNotification  $webhook
     * @return \Symfony\Component\HttpFoundation\Responses
     */
    public function handleDisputeOpened(WebhookNotification $webhook)
    {
        // Handle The Webhook...
    }
}

失败的订阅

如果客户的信用卡过期了怎么办?别担心 - 收银员包含一个Webhook控制器,可以轻松为您取消客户的订阅。只需将路由指向该控制器

Route::post(
    'braintree/webhook',
    '\Laravel\Cashier\Http\Controllers\WebhookController@handleWebhook'
);

就这样!失败的支付将被捕获并由控制器处理。当Braintree确定订阅失败时(通常在三次失败的支付尝试之后),控制器将取消客户的订阅。别忘了:您需要在Braintree控制面板设置中配置webhook URI。

单次收费

简单收费

当使用Stripe时,收费方法接受您希望以您应用程序使用的货币的最小单位计价的金额。然而,当使用Braintree时,您应该将完整的美元金额传递给收费方法:如果您想对已订阅客户的信用卡进行一次性收费,您可以在可收费模型实例上使用收费方法。

// Stripe Accepts Charges In Cents...
$stripeCharge = $user->charge(100);

// Braintree Accepts Charges In Dollars...
$user->charge(1);

收费方法接受一个数组作为其第二个参数,允许您将任何选项传递给底层Stripe / Braintree收费创建。有关创建收费时可用选项的详细信息,请参阅Stripe或Braintree文档

$user->charge(100, [
    'custom_option' => $value,
]);

如果收费失败,收费方法将抛出异常。如果收费成功,方法将返回完整的Stripe / Braintree响应

try {
    $response = $user->charge(100);
} catch (Exception $e) {
    //
}

带发票的收费

有时您可能需要一次性收费,同时也要为收费生成发票,以便您可以向客户提供PDF收据。invoiceFor方法允许您这样做。例如,让我们为“一次性费用”向客户开张5.00美元的发票

// Stripe Accepts Charges In Cents...
$user->invoiceFor('One Time Fee', 500);

// Braintree Accepts Charges In Dollars...
$user->invoiceFor('One Time Fee', 5);

发票将立即对用户的信用卡进行收费。invoiceFor方法也接受一个数组作为其第三个参数。该数组包含发票项目的账单选项。该方法接受的第四个参数也是一个数组。该最终参数接受发票本身的账单选项

$user->invoiceFor('Stickers', 500, [
    'quantity' => 50,
], [
    'tax_percent' => 21,
]);

如果您使用Braintree作为您的账单提供商,在调用invoiceFor方法时必须包含描述选项

$user->invoiceFor('One Time Fee', 500, [
    'description' => 'your invoice description here',
]);

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

退款收费

如果您需要退款Stripe收费,可以使用退款方法。该方法将Stripe收费ID作为其唯一参数

$stripeCharge = $user->charge(100);

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

发票

您可以使用invoices方法轻松检索可收费模型的发票数组

$invoices = $user->invoices();

// Include pending invoices in the results...
$invoices = $user->invoicesIncludingPending();

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

<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>

生成发票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',
    ]);
});

许可证

Laravel Cashier Braintree是开源软件,许可协议为MIT许可证