[ Laravel 5.8 文件 ] 官方擴充套件包 —— 訂閱支付解決方案:Laravel Cashier(Braintree)
簡介
Laravel Cashier Braintree 為通過Braintree 實現訂閱支付服務提供了一個優雅的流式介面。它封裝了幾乎所有你恐懼編寫的樣板化的訂閱支付程式碼。除了基本的訂閱管理外,Cashier 還支援處理優惠券、訂閱升級/替換、訂閱「數量」、取消寬限期,甚至生成 PDF 發票。
注:該文件適用於 Cashier 與 Braintree 的整合,如果你在使用 Stripe,請參考Stripe 整合文件。
注:如果你只需要一次性支付,並不提供訂閱,就不應該使用 Cashier,而是直接使用 Braintree SDK。
注意事項
對於絕大多數操作而言,Cashier 的 Stripe 和 Braintree 實現功能都是一樣的,兩個服務都提供了通過信用卡進行訂閱支付的功能,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
配置
計劃信用優惠券
在使用基於 Braintree 的 Cashier 之前,需要在 Braintree 的控制面板中定義一個plan-credit
折扣,這個折扣將會用於從年到月或者從月到年支付的優惠額度比例分配。
配置在 Braintree 控制面板中的這個折扣值可以是你希望的任何值,Cashier 將會在每次使用優惠券的時候通過自定義的值來重寫這個預設定義的值。該優惠券之所以是必須的是因為 Braintree 不支援本地根據訂閱時長進行優惠額度的按比例分配。
資料庫遷移
使用 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->integer('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 金鑰
接下來,需要在配置檔案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('rmb', '¥');
訂閱
建立訂閱
要建立一個訂閱,首先要獲取一個賬單模型的例項,通常是App\User
的例項。獲取到該模型例項之後,你可以使用newSubscription
方法來建立該模型的訂閱:
$user = User::find(1); $user->newSubscription('main', 'premium')->create($token);
第一個傳遞給newSubscription
方法的引數是該訂閱的名字,如果應用只有一個訂閱,可以將其稱作main
或primary
,第二個引數用於指定使用者訂閱的 Braintree 計劃,該值對應 Braintree 中相應計劃的 id。
create
方法接收信用卡/源令牌,會自動建立這個訂閱,同時更新資料庫中 對應訂閱的客戶 ID 和其它相關的賬單資訊。
額外的使用者資訊
如果你想要指定額外的客戶資訊,你可以將其作為第二個引數傳遞給create
方法:
$user->newSubscription('main', 'monthly')->create($stripeToken, [ 'email' => $email, ]);
要了解更多 Braintree 支援的欄位,可以檢視相應的Braintree文件 。
優惠券
如果你想要在建立訂閱的時候使用優惠券,可以使用withCoupon
方法:
$user->newSubscription('main', 'monthly') ->withCoupon('code') ->create($token);
檢查訂閱狀態
使用者訂閱你的應用後,你可以使用各種便利的方法來簡單檢查訂閱狀態。首先,如果使用者有一個有效的訂閱,則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 訂閱了給定的計劃,在本例中,我們會判斷使用者的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');
訂閱稅金
要指定使用者支付訂閱的稅率,實現賬單模型的taxPercentage
方法,並返回一個在0到100之間的數值,不要超過兩位小數:
public function taxPercentage() { return 20; }
這將使你可以在模型基礎上使用稅率,對跨越不同國家不同稅率的使用者很有用。
注:taxPercentage
方法只能用於訂閱支付,如果你使用 Cashier 生成一次性賬單,需要手動指定稅率。
取消訂閱
要取消訂閱,可以呼叫使用者訂閱上的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($token);
該方法會在資料庫訂閱記錄上設定試用期結束日期,以便告知 Braintree 在此之前不要計算使用者的賬單資訊。
注:如果使用者的訂閱沒有在試用期結束之前取消,則會在試用期結束時立即支付,所以要確保通知使用者試用期結束時間。
可以使用使用者例項或訂閱例項上的onTrial
方法判斷使用者是否處於試用期,下面兩個例子作用是等價的:
if ($user->onTrial('main')) { // } if ($user->subscription('main')->onTrial()) { // }
不帶信用卡資訊
如果你不想在提供試用期的時候收集使用者支付方式資訊,只需設定使用者記錄的trial_ends_at
欄位為期望的試用期結束日期即可,這通常在使用者註冊期間完成:
$user = User::create([ // Populate other user properties... 'trial_ends_at' => Carbon::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($token);
顧客
建立顧客
少數情況下,你可能希望建立一個沒有開始訂閱的 Braintree 顧客,這可以通過createAsStripeCustomer
方法來實現:
$user->createAsStripeCustomer();
一旦在 Braintree 中建立這樣的顧客後,需要在稍晚時候讓他開始訂閱。
信用卡
更新信用卡
updateCard
方法可用於更新使用者的信用卡資訊,該方法接收一個 Braintree 令牌並設定新的信用卡作為支付源:
$user->updateCard($token);
處理 Webhooks
Braintree 可以通過 webhook 通知應用各種事件,要處理 Braintree webhook,需要定義一個指向 Cashier webhook 控制器的路由,這個控制器將會處理所有輸入 webhook 請求並將它們分發到合適的控制器方法:
Route::post( 'braintree/webhook', '\Laravel\Cashier\Http\Controllers\WebhookController@handleWebhook' );
注:註冊好控制器後,還要在 Braintree 控制面板中配置 webhook URL。
預設情況下,這個控制器將會自動對支付失敗次數(這個次數可以在 Braintree 設定中定義)過多的訂閱進行取消;不過,我們很快會發現,你可以擴充套件這個控制器來處理任何你想要處理的 webhook 事件。
Webhooks & CSRF 防護
由於 webhook 需要繞開 Laravel 的CSRF 保護,所以需要將其羅列到VerifyCsrfToken
中介軟體的排除列表或者將其置於web
中介軟體組之外:
protected $except = [ 'braintree/*', ];
定義 Webhook 事件處理器
Cashier 會基於支付失敗次數自動取消訂閱,但是如果你想要處理額外的 webhook 事件,擴充套件 Webhook 控制器即可。定義的方法名需要與 Cashier 約定的格式保持一致,特別是方法名需要以handle
開頭並且是想要處理的 webhook 的駝峰格式。例如,如果你想要處理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... } }
失敗的訂閱
如果使用者的信用卡過期怎麼辦?不用擔心 —— Cashier webhook 控制器可以輕鬆為你取消該使用者的訂閱,正如上面所提到的,你所需要做的只是將路由指向該控制器:
Route::post( 'braintree/webhook', '\Laravel\Cashier\Http\Controllers\WebhookController@handleWebhook' );
就是這樣,失敗的支付將會被控制器捕獲和處理,該控制器將會在 Braintree 判斷訂閱支付失敗次數(通常是3次)達到上限時取消該使用者的訂閱。不要忘了,你需要在 Braintree控制面板設定中配置 webhook URI。
一次性支付
簡單支付
注:必須傳遞完整的美元金額到charge
方法。
如果你想要使用訂閱客戶的信用卡一次性結清賬單,可以使用賬單模型例項上的charge
方法:
$user->charge(1);
charge
方法接收一個數組作為第二個引數,允許你傳遞任何你想要傳遞的底層 Braintree 賬單建立引數,建立賬單時我們可以參考 Braintree 文件提供的可用選項:
$user->charge(1, [ 'custom_option' => $value, ]);
如果支付失敗charge
方法將丟擲異常,如果支付成功,該方法會返回完整的 Braintree 響應:
try { $response = $user->charge(1); } catch (Exception $e) { // }
帶發票的支付
有時候你需要建立一個一次性支付並且同時生成對應發票以便為使用者提供一個PDF單據,invoiceFor
方法可以幫助我們實現這個需求。例如,讓我們為使用者的「一次性費用」生成一張$5.00
的發票:
$user->invoiceFor('One Time Fee', 5);
該單據會通過使用者信用卡立即支付,invoiceFor
方法還可以接收一個數組作為第三個引數,該陣列包含發票專案的計費選項。在呼叫invoiceFor
方法時必須包含description
選項:
$user->invoiceFor('One Time Fee', 5, [ 'description' => 'your invoice description here', ]);
發票
你可以使用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', ]); });