Laravel Cashier (Stripe)
- 簡介
- 升級 Cashier
- 安裝
- 配置
- 快速入門
- 客戶
- 支付方式
- 訂閱
- 訂閱試用
- 處理 Stripe Webhooks
- 單次扣款
- 賬單 (Invoices)
- Checkout (結賬)
- 處理支付失敗
- 強客戶認證 (SCA)
- Stripe SDK
- 測試
簡介
Laravel Cashier Stripe 為 Stripe 的訂閱計費服務提供了一個富有表現力的流式介面。它處理了絕大多數你不想編寫的樣板式訂閱計費程式碼。除了基礎的訂閱管理外,Cashier 還可以處理優惠券、切換訂閱、訂閱“數量”、取消寬限期,甚至生成 PDF 賬單。
升級 Cashier
升級到 Cashier 的新版本時,請務必仔細閱讀 升級指南。
為了防止破壞性變更,Cashier 使用固定的 Stripe API 版本。Cashier 16 使用 Stripe API 版本 2025-06-30.basil。Stripe API 版本會在次要版本更新時同步更新,以便利用新的 Stripe 功能和改進。
安裝
首先,使用 Composer 包管理器安裝 Stripe 版的 Cashier 包
1composer require laravel/cashier
安裝包後,使用 vendor:publish Artisan 命令釋出 Cashier 的遷移檔案
1php artisan vendor:publish --tag="cashier-migrations"
然後,執行資料庫遷移
1php artisan migrate
Cashier 的遷移將在你的 users 表中新增幾列。它們還將建立一個新的 subscriptions 表來存放所有客戶的訂閱,以及一個 subscription_items 表,用於儲存包含多種價格的訂閱。
如果你願意,也可以使用 vendor:publish Artisan 命令釋出 Cashier 的配置檔案
1php artisan vendor:publish --tag="cashier-config"
最後,為了確保 Cashier 能正確處理所有 Stripe 事件,請記得 配置 Cashier 的 Webhook 處理。
Stripe 建議任何用於儲存 Stripe 識別符號的列都應區分大小寫。因此,如果你使用的是 MySQL,應確保 stripe_id 列的排序規則設定為 utf8_bin。更多相關資訊可在 Stripe 文件 中找到。
配置
可計費模型 (Billable Model)
在使用 Cashier 之前,請在你的可計費模型定義中新增 Billable Trait。通常,這就是 App\Models\User 模型。此 Trait 提供了各種方法,允許你執行常見的計費任務,例如建立訂閱、應用優惠券以及更新支付方式資訊。
1use Laravel\Cashier\Billable;2 3class User extends Authenticatable4{5 use Billable;6}
Cashier 預設你的可計費模型是 Laravel 自帶的 App\Models\User 類。如果你想更改此設定,可以透過 useCustomerModel 方法指定一個不同的模型。此方法通常應在 AppServiceProvider 類的 boot 方法中呼叫。
1use App\Models\Cashier\User; 2use Laravel\Cashier\Cashier; 3 4/** 5 * Bootstrap any application services. 6 */ 7public function boot(): void 8{ 9 Cashier::useCustomerModel(User::class);10}
如果你使用的模型不是 Laravel 自帶的 App\Models\User,則需要釋出並修改 Cashier 提供的遷移檔案,以匹配你自定義模型對應的表名。
API 金鑰
接下來,你應該在應用程式的 .env 檔案中配置 Stripe API 金鑰。你可以從 Stripe 控制面板檢索你的 Stripe API 金鑰。
1STRIPE_KEY=your-stripe-key2STRIPE_SECRET=your-stripe-secret3STRIPE_WEBHOOK_SECRET=your-stripe-webhook-secret
你應該確保在應用程式的 .env 檔案中定義了 STRIPE_WEBHOOK_SECRET 環境變數,該變數用於確保收到的 Webhook 請求確實來自 Stripe。
貨幣配置
Cashier 的預設貨幣是美元 (USD)。你可以透過在 .env 檔案中設定 CASHIER_CURRENCY 環境變數來更改預設貨幣。
1CASHIER_CURRENCY=eur
除了配置貨幣外,你還可以指定一個區域設定,用於在賬單顯示金額時格式化數值。在內部,Cashier 使用 PHP 的 NumberFormatter 類 來設定貨幣區域。
1CASHIER_CURRENCY_LOCALE=nl_BE
為了使用除 en 之外的區域設定,請確保伺服器上安裝並配置了 ext-intl PHP 擴充套件。
稅務配置
得益於 Stripe Tax,可以自動計算由 Stripe 生成的所有賬單的稅費。你可以在 App\Providers\AppServiceProvider 類的 boot 方法中呼叫 calculateTaxes 方法來啟用自動稅費計算。
1use Laravel\Cashier\Cashier;2 3/**4 * Bootstrap any application services.5 */6public function boot(): void7{8 Cashier::calculateTaxes();9}
一旦啟用了稅費計算,所有新的訂閱和一次性生成的賬單都將自動進行稅務計算。
為了使此功能正常工作,需要將客戶的賬單詳細資訊(如姓名、地址和稅務 ID)同步到 Stripe。你可以使用 Cashier 提供的 客戶資料同步 和 稅務 ID 方法來實現這一點。
日誌
Cashier 允許你在記錄 Stripe 致命錯誤時指定日誌通道。你可以在 .env 檔案中定義 CASHIER_LOGGER 環境變數來指定日誌通道。
1CASHIER_LOGGER=stack
Stripe API 呼叫生成的異常將透過你的應用程式預設日誌通道進行記錄。
使用自定義模型
你可以自由擴充套件 Cashier 內部使用的模型,只需定義你自己的模型並繼承相應的 Cashier 模型即可。
1use Laravel\Cashier\Subscription as CashierSubscription;2 3class Subscription extends CashierSubscription4{5 // ...6}
定義模型後,你可以透過 Laravel\Cashier\Cashier 類指示 Cashier 使用你的自定義模型。通常,你應該在 App\Providers\AppServiceProvider 類的 boot 方法中向 Cashier 註冊你的自定義模型。
1use App\Models\Cashier\Subscription; 2use App\Models\Cashier\SubscriptionItem; 3 4/** 5 * Bootstrap any application services. 6 */ 7public function boot(): void 8{ 9 Cashier::useSubscriptionModel(Subscription::class);10 Cashier::useSubscriptionItemModel(SubscriptionItem::class);11}
快速入門
銷售產品
在使用 Stripe Checkout 之前,你應該在 Stripe 面板中定義具有固定價格的產品。此外,你還應該 配置 Cashier 的 Webhook 處理。
透過你的應用程式提供產品和訂閱計費可能會讓人望而生畏。然而,得益於 Cashier 和 Stripe Checkout,你可以輕鬆構建現代且健壯的支付整合。
要對非週期性的單次購買產品進行收費,我們將使用 Cashier 將客戶引導至 Stripe Checkout,在那裡他們將提供支付詳細資訊並確認購買。一旦透過 Checkout 完成支付,客戶將被重定向到你在應用程式中選擇的成功 URL。
1use Illuminate\Http\Request; 2 3Route::get('/checkout', function (Request $request) { 4 $stripePriceId = 'price_deluxe_album'; 5 6 $quantity = 1; 7 8 return $request->user()->checkout([$stripePriceId => $quantity], [ 9 'success_url' => route('checkout-success'),10 'cancel_url' => route('checkout-cancel'),11 ]);12})->name('checkout');13 14Route::view('/checkout/success', 'checkout.success')->name('checkout-success');15Route::view('/checkout/cancel', 'checkout.cancel')->name('checkout-cancel');
如上例所示,我們將利用 Cashier 提供的 checkout 方法將客戶重定向到 Stripe Checkout 以處理指定的“價格標識符”。使用 Stripe 時,“價格”是指 針對特定產品定義的價格。
如果有必要,checkout 方法會自動在 Stripe 中建立客戶,並將該 Stripe 客戶記錄與你應用程式資料庫中相應的使用者關聯起來。結賬會話完成後,客戶將被重定向到專門的成功或取消頁面,你可以在該頁面向客戶顯示資訊提示。
向 Stripe Checkout 提供元資料 (Meta Data)
銷售產品時,通常需要透過應用程式定義的 Cart 和 Order 模型來追蹤已完成的訂單和已購買的產品。當將客戶重定向到 Stripe Checkout 以完成購買時,你可能需要提供現有的訂單識別符號,以便在客戶被重定向回應用程式時,能將完成的購買與相應的訂單關聯起來。
要實現這一點,你可以向 checkout 方法提供一個 metadata 陣列。假設當用戶開始結賬流程時,在我們的應用程式中會建立一個處於待處理狀態的 Order。請記住,此示例中的 Cart 和 Order 模型僅供說明,並非由 Cashier 提供。你可以根據應用程式的需求自由實現這些概念。
1use App\Models\Cart; 2use App\Models\Order; 3use Illuminate\Http\Request; 4 5Route::get('/cart/{cart}/checkout', function (Request $request, Cart $cart) { 6 $order = Order::create([ 7 'cart_id' => $cart->id, 8 'price_ids' => $cart->price_ids, 9 'status' => 'incomplete',10 ]);11 12 return $request->user()->checkout($order->price_ids, [13 'success_url' => route('checkout-success').'?session_id={CHECKOUT_SESSION_ID}',14 'cancel_url' => route('checkout-cancel'),15 'metadata' => ['order_id' => $order->id],16 ]);17})->name('checkout');
如上例所示,當用戶開始結賬流程時,我們將把購物車/訂單關聯的所有 Stripe 價格標識符提供給 checkout 方法。當然,你的應用程式負責在客戶新增商品時將這些商品與“購物車”或訂單關聯起來。我們還透過 metadata 陣列向 Stripe Checkout 會話提供了訂單 ID。最後,我們在結賬成功路由中添加了 CHECKOUT_SESSION_ID 模板變數。當 Stripe 將客戶重定向回你的應用程式時,此模板變數將自動填充為結賬會話 ID。
接下來,讓我們構建結賬成功路由。這是使用者透過 Stripe Checkout 完成購買後將被重定向到的路由。在此路由中,我們可以檢索 Stripe 結賬會話 ID 和關聯的 Stripe Checkout 例項,以訪問我們提供的元資料並相應地更新客戶的訂單。
1use App\Models\Order; 2use Illuminate\Http\Request; 3use Laravel\Cashier\Cashier; 4 5Route::get('/checkout/success', function (Request $request) { 6 $sessionId = $request->get('session_id'); 7 8 if ($sessionId === null) { 9 return;10 }11 12 $session = Cashier::stripe()->checkout->sessions->retrieve($sessionId);13 14 if ($session->payment_status !== 'paid') {15 return;16 }17 18 $orderId = $session['metadata']['order_id'] ?? null;19 20 $order = Order::findOrFail($orderId);21 22 $order->update(['status' => 'completed']);23 24 return view('checkout-success', ['order' => $order]);25})->name('checkout-success');
有關 結賬會話物件所包含資料 的更多資訊,請參考 Stripe 的文件。
銷售訂閱
在使用 Stripe Checkout 之前,你應該在 Stripe 面板中定義具有固定價格的產品。此外,你還應該 配置 Cashier 的 Webhook 處理。
透過你的應用程式提供產品和訂閱計費可能會讓人望而生畏。然而,得益於 Cashier 和 Stripe Checkout,你可以輕鬆構建現代且健壯的支付整合。
要了解如何使用 Cashier 和 Stripe Checkout 銷售訂閱,讓我們考慮一個簡單的場景:一個提供基礎月付 (price_basic_monthly) 和年付 (price_basic_yearly) 計劃的訂閱服務。這兩個價格可以在 Stripe 面板中歸類於一個“Basic”產品 (pro_basic)。此外,我們的訂閱服務可能還會提供一個 Expert 計劃,標識為 pro_expert。
首先,讓我們瞭解客戶如何訂閱我們的服務。當然,你可以想象客戶會在我們應用程式的定價頁面上點選“訂閱”按鈕。此按鈕或連結應將使用者重定向到一個 Laravel 路由,該路由負責為他們選擇的計劃建立 Stripe Checkout 會話。
1use Illuminate\Http\Request; 2 3Route::get('/subscription-checkout', function (Request $request) { 4 return $request->user() 5 ->newSubscription('default', 'price_basic_monthly') 6 ->trialDays(5) 7 ->allowPromotionCodes() 8 ->checkout([ 9 'success_url' => route('your-success-route'),10 'cancel_url' => route('your-cancel-route'),11 ]);12});
如上例所示,我們將把客戶重定向到一個 Stripe Checkout 會話,這將允許他們訂閱我們的 Basic 計劃。在結賬成功或取消後,客戶將被重定向回我們提供給 checkout 方法的 URL。為了知道他們的訂閱何時真正開始(因為某些支付方式需要幾秒鐘來處理),我們還需要 配置 Cashier 的 Webhook 處理。
現在客戶可以開始訂閱了,我們需要限制應用程式的某些部分,以便只有已訂閱的使用者才能訪問。當然,我們可以隨時透過 Cashier 的 Billable Trait 提供的 subscribed 方法來確定使用者的當前訂閱狀態。
1@if ($user->subscribed())2 <p>You are subscribed.</p>3@endif
我們甚至可以輕鬆確定使用者是否訂閱了特定的產品或價格。
1@if ($user->subscribedToProduct('pro_basic'))2 <p>You are subscribed to our Basic product.</p>3@endif4 5@if ($user->subscribedToPrice('price_basic_monthly'))6 <p>You are subscribed to our monthly Basic plan.</p>7@endif
構建已訂閱中介軟體
為了方便起見,你可能希望建立一個 中介軟體,用於確定傳入的請求是否來自已訂閱的使用者。一旦定義了此中介軟體,你就可以輕鬆地將其分配給路由,以防止未訂閱的使用者訪問該路由。
1<?php 2 3namespace App\Http\Middleware; 4 5use Closure; 6use Illuminate\Http\Request; 7use Symfony\Component\HttpFoundation\Response; 8 9class Subscribed10{11 /**12 * Handle an incoming request.13 */14 public function handle(Request $request, Closure $next): Response15 {16 if (! $request->user()?->subscribed()) {17 // Redirect user to billing page and ask them to subscribe...18 return redirect('/billing');19 }20 21 return $next($request);22 }23}
定義中介軟體後,你可以將其分配給路由。
1use App\Http\Middleware\Subscribed;2 3Route::get('/dashboard', function () {4 // ...5})->middleware([Subscribed::class]);
允許客戶管理他們的計費計劃
當然,客戶可能希望將他們的訂閱計劃更改為其他產品或“級別”。實現這一點的最簡單方法是將客戶引導至 Stripe 的 客戶結算門戶 (Customer Billing Portal),它提供了一個託管的使用者介面,允許客戶下載賬單、更新支付方式並更改訂閱計劃。
首先,在應用程式內定義一個連結或按鈕,將使用者引導至我們將用於啟動結算門戶會話的 Laravel 路由。
1<a href="{{ route('billing') }}">2 Billing3</a>
接下來,讓我們定義初始化 Stripe 客戶結算門戶會話並重定向使用者的路由。redirectToBillingPortal 方法接受一個 URL,作為使用者退出門戶時應返回的地址。
1use Illuminate\Http\Request;2 3Route::get('/billing', function (Request $request) {4 return $request->user()->redirectToBillingPortal(route('dashboard'));5})->middleware(['auth'])->name('billing');
只要你配置了 Cashier 的 Webhook 處理,Cashier 就會透過檢查來自 Stripe 的傳入 Webhook,自動保持應用程式中與 Cashier 相關的資料庫表同步。例如,當用戶透過 Stripe 的客戶結算門戶取消訂閱時,Cashier 將收到相應的 Webhook 並在你應用程式的資料庫中將該訂閱標記為“已取消”。
客戶
獲取客戶
你可以使用 Cashier::findBillable 方法透過 Stripe ID 檢索客戶。此方法將返回可計費模型的一個例項。
1use Laravel\Cashier\Cashier;2 3$user = Cashier::findBillable($stripeId);
建立客戶
有時,你可能希望在不開啟訂閱的情況下建立一個 Stripe 客戶。你可以使用 createAsStripeCustomer 方法來實現這一點。
1$stripeCustomer = $user->createAsStripeCustomer();
一旦客戶在 Stripe 中建立完成,你可以在以後的日期開啟訂閱。你可以提供一個可選的 $options 陣列,以傳遞任何 Stripe API 支援的額外客戶建立引數。
1$stripeCustomer = $user->createAsStripeCustomer($options);
如果你想返回可計費模型的 Stripe 客戶物件,可以使用 asStripeCustomer 方法。
1$stripeCustomer = $user->asStripeCustomer();
如果你想檢索給定可計費模型的 Stripe 客戶物件,但不確定該模型是否已經是 Stripe 中的客戶,可以使用 createOrGetStripeCustomer 方法。如果客戶尚不存在,此方法將在 Stripe 中建立一個新客戶。
1$stripeCustomer = $user->createOrGetStripeCustomer();
更新客戶
有時,你可能希望直接用額外資訊更新 Stripe 客戶。你可以使用 updateStripeCustomer 方法來實現。此方法接受一個包含 Stripe API 支援的客戶更新選項 的陣列。
1$stripeCustomer = $user->updateStripeCustomer($options);
餘額
Stripe 允許你貸記或借記客戶的“餘額”。稍後,此餘額將在新賬單中進行抵扣或結算。要檢視客戶的總餘額,你可以使用可計費模型上可用的 balance 方法。balance 方法將返回以客戶貨幣單位格式化後的餘額字串。
1$balance = $user->balance();
要增加客戶餘額(貸記),你可以為 creditBalance 方法提供一個值。如果需要,還可以提供描述。
1$user->creditBalance(500, 'Premium customer top-up.');
為 debitBalance 方法提供一個值將扣除客戶餘額(借記)。
1$user->debitBalance(300, 'Bad usage penalty.');
applyBalance 方法將為客戶建立新的餘額交易。你可以使用 balanceTransactions 方法檢索這些交易記錄,這對於提供供客戶檢視的借貸日誌非常有用。
1// Retrieve all transactions... 2$transactions = $user->balanceTransactions(); 3 4foreach ($transactions as $transaction) { 5 // Transaction amount... 6 $amount = $transaction->amount(); // $2.31 7 8 // Retrieve the related invoice when available... 9 $invoice = $transaction->invoice();10}
稅務 ID
Cashier 提供了一種簡單的方法來管理客戶的稅務 ID。例如,taxIds 方法可用於檢索分配給客戶的所有 稅務 ID,並以集合形式返回。
1$taxIds = $user->taxIds();
你還可以透過識別符號檢索客戶的特定稅務 ID。
1$taxId = $user->findTaxId('txi_belgium');
你可以透過向 createTaxId 方法提供有效的 型別 和值來建立新的稅務 ID。
1$taxId = $user->createTaxId('eu_vat', 'BE0123456789');
createTaxId 方法將立即把該 VAT ID 新增到客戶賬戶中。VAT ID 的驗證也是由 Stripe 完成的;然而,這是一個非同步過程。你可以透過訂閱 customer.tax_id.updated Webhook 事件並檢查 VAT ID 的 verification 引數 來獲取驗證更新的通知。有關處理 Webhook 的更多資訊,請查閱 定義 Webhook 處理器的文件。
你可以使用 deleteTaxId 方法刪除稅務 ID。
1$user->deleteTaxId('txi_belgium');
與 Stripe 同步客戶資料
通常,當應用程式使用者更新其姓名、電子郵件地址或其他 Stripe 也儲存的資訊時,你應該通知 Stripe 這些更新。透過這樣做,Stripe 上的資訊副本將與你的應用程式保持同步。
為了自動化此過程,你可以在可計費模型上定義一個事件監聽器,用於響應模型的 updated 事件。然後,在事件監聽器內部,你可以呼叫模型上的 syncStripeCustomerDetails 方法。
1use App\Models\User; 2use function Illuminate\Events\queueable; 3 4/** 5 * The "booted" method of the model. 6 */ 7protected static function booted(): void 8{ 9 static::updated(queueable(function (User $customer) {10 if ($customer->hasStripeId()) {11 $customer->syncStripeCustomerDetails();12 }13 }));14}
現在,每次客戶模型更新時,其資訊都會與 Stripe 同步。為方便起見,Cashier 會在最初建立客戶時自動將客戶資訊與 Stripe 同步。
你可以透過覆蓋 Cashier 提供的多種方法來自定義用於同步客戶資訊的列。例如,你可以覆蓋 stripeName 方法來自定義當 Cashier 向 Stripe 同步資訊時,應被視為客戶“姓名”的屬性。
1/**2 * Get the customer name that should be synced to Stripe.3 */4public function stripeName(): string|null5{6 return $this->company_name;7}
同樣,你可以覆蓋 stripeEmail、stripePhone(最多 20 個字元)、stripeAddress 和 stripePreferredLocales 方法。當 更新 Stripe 客戶物件 時,這些方法會將資訊同步到對應的客戶引數。如果你想完全控制客戶資訊同步過程,可以覆蓋 syncStripeCustomerDetails 方法。
結算門戶 (Billing Portal)
Stripe 提供了一種 設定結算門戶的簡便方法,以便客戶可以管理其訂閱、支付方式並檢視賬單歷史記錄。你可以透過控制器或路由呼叫可計費模型上的 redirectToBillingPortal 方法,將使用者重定向到結算門戶。
1use Illuminate\Http\Request;2 3Route::get('/billing-portal', function (Request $request) {4 return $request->user()->redirectToBillingPortal();5});
預設情況下,當用戶完成訂閱管理後,他們可以透過 Stripe 結算門戶內的連結返回到你應用程式的 home 路由。你可以透過將 URL 作為引數傳遞給 redirectToBillingPortal 方法來提供使用者應返回的自定義 URL。
1use Illuminate\Http\Request;2 3Route::get('/billing-portal', function (Request $request) {4 return $request->user()->redirectToBillingPortal(route('billing'));5});
如果你想生成指向結算門戶的 URL 而不生成 HTTP 重定向響應,可以呼叫 billingPortalUrl 方法。
1$url = $request->user()->billingPortalUrl(route('billing'));
支付方式
儲存支付方式
為了在 Stripe 中建立訂閱或執行“一次性”扣款,你需要儲存支付方式並從 Stripe 檢索其識別符號。實現此目的的方法取決於你打算將支付方式用於訂閱還是單次扣款,因此我們將在下面探討這兩種情況。
用於訂閱的支付方式
當儲存客戶的信用卡資訊以供訂閱將來使用時,必須使用 Stripe 的“Setup Intents” API 來安全地收集客戶的支付方式詳細資訊。“Setup Intent”向 Stripe 表明了收費客戶支付方式的意圖。Cashier 的 Billable Trait 包含了 createSetupIntent 方法,可以輕鬆建立新的 Setup Intent。你應該從渲染用於收集客戶支付方式詳情表單的路由或控制器中呼叫此方法。
1return view('update-payment-method', [2 'intent' => $user->createSetupIntent()3]);
建立 Setup Intent 並將其傳遞給檢視後,你應該將其 secret 附加到收集支付方式的元素上。例如,考慮這個“更新支付方式”表單:
1<input id="card-holder-name" type="text">2 3<!-- Stripe Elements Placeholder -->4<div id="card-element"></div>5 6<button id="card-button" data-secret="{{ $intent->client_secret }}">7 Update Payment Method8</button>
接下來,可以使用 Stripe.js 庫將 Stripe Element 附加到表單,並安全地收集客戶的支付詳情。
1<script src="https://js.stripe.com/v3/"></script> 2 3<script> 4 const stripe = Stripe('stripe-public-key'); 5 6 const elements = stripe.elements(); 7 const cardElement = elements.create('card'); 8 9 cardElement.mount('#card-element');10</script>
接下來,可以使用 Stripe 的 confirmCardSetup 方法驗證卡片並從 Stripe 檢索安全的“支付方式識別符號”。
1const cardHolderName = document.getElementById('card-holder-name'); 2const cardButton = document.getElementById('card-button'); 3const clientSecret = cardButton.dataset.secret; 4 5cardButton.addEventListener('click', async (e) => { 6 const { setupIntent, error } = await stripe.confirmCardSetup( 7 clientSecret, { 8 payment_method: { 9 card: cardElement,10 billing_details: { name: cardHolderName.value }11 }12 }13 );14 15 if (error) {16 // Display "error.message" to the user...17 } else {18 // The card has been verified successfully...19 }20});
在卡片透過 Stripe 驗證後,你可以將生成的 setupIntent.payment_method 識別符號傳遞給你的 Laravel 應用程式,在那裡它會被附加到客戶身上。支付方式可以 新增為新的支付方式 或 用於更新預設支付方式。你還可以立即使用該支付方式識別符號來 建立新訂閱。
如果你想了解更多關於 Setup Intents 和收集客戶支付詳情的資訊,請 檢視 Stripe 提供的此概述。
用於單次扣款的支付方式
當然,在對客戶的支付方式進行單次扣款時,我們只需要使用一次支付方式識別符號。由於 Stripe 的限制,你不能將客戶儲存的預設支付方式用於單次扣款。你必須允許客戶使用 Stripe.js 庫輸入他們的支付方式詳情。例如,考慮以下表單:
1<input id="card-holder-name" type="text">2 3<!-- Stripe Elements Placeholder -->4<div id="card-element"></div>5 6<button id="card-button">7 Process Payment8</button>
定義此類表單後,可以使用 Stripe.js 庫將 Stripe Element 附加到表單,並安全地收集客戶的支付詳情。
1<script src="https://js.stripe.com/v3/"></script> 2 3<script> 4 const stripe = Stripe('stripe-public-key'); 5 6 const elements = stripe.elements(); 7 const cardElement = elements.create('card'); 8 9 cardElement.mount('#card-element');10</script>
接下來,可以使用 Stripe 的 createPaymentMethod 方法驗證卡片並從 Stripe 檢索安全的“支付方式識別符號”。
1const cardHolderName = document.getElementById('card-holder-name'); 2const cardButton = document.getElementById('card-button'); 3 4cardButton.addEventListener('click', async (e) => { 5 const { paymentMethod, error } = await stripe.createPaymentMethod( 6 'card', cardElement, { 7 billing_details: { name: cardHolderName.value } 8 } 9 );10 11 if (error) {12 // Display "error.message" to the user...13 } else {14 // The card has been verified successfully...15 }16});
如果卡片驗證成功,你可以將 paymentMethod.id 傳遞給你的 Laravel 應用程式並處理 單次扣款。
檢索支付方式
可計費模型例項上的 paymentMethods 方法返回一個 Laravel\Cashier\PaymentMethod 例項集合。
1$paymentMethods = $user->paymentMethods();
預設情況下,此方法將返回所有型別的支付方式。要檢索特定型別的支付方式,你可以將 type 作為引數傳遞給該方法。
1$paymentMethods = $user->paymentMethods('sepa_debit');
要檢索客戶的預設支付方式,可以使用 defaultPaymentMethod 方法。
1$paymentMethod = $user->defaultPaymentMethod();
你可以使用 findPaymentMethod 方法檢索附加到可計費模型的特定支付方式。
1$paymentMethod = $user->findPaymentMethod($paymentMethodId);
支付方式存在性檢查
要確定可計費模型是否已將預設支付方式附加到其賬戶,請呼叫 hasDefaultPaymentMethod 方法。
1if ($user->hasDefaultPaymentMethod()) {2 // ...3}
你可以使用 hasPaymentMethod 方法來確定可計費模型是否至少有一個支付方式附加到其賬戶。
1if ($user->hasPaymentMethod()) {2 // ...3}
此方法將確定可計費模型是否擁有任何支付方式。要確定模型是否存在特定型別的支付方式,你可以將 type 作為引數傳遞給該方法。
1if ($user->hasPaymentMethod('sepa_debit')) {2 // ...3}
更新預設支付方式
updateDefaultPaymentMethod 方法可用於更新客戶的預設支付方式資訊。此方法接受 Stripe 支付方式識別符號,並將新支付方式指定為預設計費支付方式。
1$user->updateDefaultPaymentMethod($paymentMethod);
要將你的預設支付方式資訊與 Stripe 中客戶的預設支付方式資訊同步,可以使用 updateDefaultPaymentMethodFromStripe 方法。
1$user->updateDefaultPaymentMethodFromStripe();
客戶的預設支付方式只能用於開發票和建立新訂閱。由於 Stripe 的限制,它不能用於單次扣款。
新增支付方式
要新增新的支付方式,你可以呼叫可計費模型上的 addPaymentMethod 方法,並傳入支付方式識別符號。
1$user->addPaymentMethod($paymentMethod);
要了解如何檢索支付方式識別符號,請查閱 支付方式儲存文件。
刪除支付方式
要刪除支付方式,你可以對希望刪除的 Laravel\Cashier\PaymentMethod 例項呼叫 delete 方法。
1$paymentMethod->delete();
deletePaymentMethod 方法將從可計費模型中刪除特定的支付方式。
1$user->deletePaymentMethod('pm_visa');
deletePaymentMethods 方法將刪除可計費模型的所有支付方式資訊。
1$user->deletePaymentMethods();
預設情況下,此方法將刪除所有型別的支付方式。要刪除特定型別的支付方式,你可以將 type 作為引數傳遞給該方法。
1$user->deletePaymentMethods('sepa_debit');
如果使用者有活動訂閱,你的應用程式不應允許他們刪除其預設支付方式。
訂閱
訂閱為你的客戶提供了設定週期性支付的方式。由 Cashier 管理的 Stripe 訂閱支援多訂閱價格、訂閱數量、試用等功能。
建立訂閱
要建立訂閱,首先檢索你的可計費模型例項,這通常是 App\Models\User 的例項。檢索模型例項後,你可以使用 newSubscription 方法來建立模型的訂閱。
1use Illuminate\Http\Request;2 3Route::post('/user/subscribe', function (Request $request) {4 $request->user()->newSubscription(5 'default', 'price_monthly'6 )->create($request->paymentMethodId);7 8 // ...9});
傳遞給 newSubscription 方法的第一個引數應是訂閱的內部型別。如果你的應用程式只提供單個訂閱,你可以稱之為 default 或 primary。此訂閱型別僅用於內部應用程式使用,不應向用戶顯示。此外,它不應包含空格,並且在建立訂閱後絕對不應更改。第二個引數是使用者正在訂閱的具體價格。該值應對應於 Stripe 中價格的識別符號。
create 方法接受 一個 Stripe 支付方式識別符號 或 Stripe PaymentMethod 物件,它將開始訂閱並使用可計費模型的 Stripe 客戶 ID 和其他相關計費資訊更新你的資料庫。
直接將支付方式識別符號傳遞給 create 訂閱方法,也會自動將其新增到使用者的已儲存支付方式中。
透過郵件賬單收集週期性支付
你可以不自動收集客戶的週期性支付,而是指示 Stripe 在每次週期性支付到期時向客戶傳送賬單郵件。然後,客戶在收到賬單後可以手動支付。透過賬單收集週期性支付時,客戶無需預先提供支付方式。
1$user->newSubscription('default', 'price_monthly')->createAndSendInvoice();
客戶在訂閱被取消前支付賬單的時間由 days_until_due 選項確定。預設情況下為 30 天;但是,如果你願意,可以為此選項提供特定值。
1$user->newSubscription('default', 'price_monthly')->createAndSendInvoice([], [2 'days_until_due' => 303]);
數量
如果你希望在建立訂閱時為價格設定特定 數量,你應該在建立訂閱之前在訂閱構建器上呼叫 quantity 方法。
1$user->newSubscription('default', 'price_monthly')2 ->quantity(5)3 ->create($paymentMethod);
詳細資訊
如果你想指定 Stripe 支援的額外 客戶 或 訂閱 選項,可以透過將它們作為第二個和第三個引數傳遞給 create 方法來實現。
1$user->newSubscription('default', 'price_monthly')->create($paymentMethod, [2 'email' => $email,3], [4 'metadata' => ['note' => 'Some extra information.'],5]);
優惠券
如果你想在建立訂閱時應用優惠券,可以使用 withCoupon 方法。
1$user->newSubscription('default', 'price_monthly')2 ->withCoupon('code')3 ->create($paymentMethod);
或者,如果你想應用 Stripe 促銷程式碼,可以使用 withPromotionCode 方法。
1$user->newSubscription('default', 'price_monthly')2 ->withPromotionCode('promo_code_id')3 ->create($paymentMethod);
給定的促銷程式碼 ID 應是分配給促銷程式碼的 Stripe API ID,而不是面向客戶的促銷程式碼。如果你需要根據面向客戶的促銷程式碼查詢促銷程式碼 ID,可以使用 findPromotionCode 方法。
1// Find a promotion code ID by its customer facing code...2$promotionCode = $user->findPromotionCode('SUMMERSALE');3 4// Find an active promotion code ID by its customer facing code...5$promotionCode = $user->findActivePromotionCode('SUMMERSALE');
在上面的示例中,返回的 $promotionCode 物件是 Laravel\Cashier\PromotionCode 的例項。該類修飾了底層的 Stripe\PromotionCode 物件。你可以透過呼叫 coupon 方法檢索與促銷程式碼相關的優惠券。
1$coupon = $user->findPromotionCode('SUMMERSALE')->coupon();
優惠券例項允許你確定折扣金額以及優惠券代表的是固定折扣還是百分比折扣。
1if ($coupon->isPercentage()) {2 return $coupon->percentOff().'%'; // 21.5%3} else {4 return $coupon->amountOff(); // $5.995}
你還可以檢索當前應用於客戶或訂閱的折扣。
1$discount = $billable->discount();2 3$discount = $subscription->discount();
返回的 Laravel\Cashier\Discount 例項修飾了底層的 Stripe\Discount 物件例項。你可以透過呼叫 coupon 方法檢索與此折扣相關的優惠券。
1$coupon = $subscription->discount()->coupon();
如果你想向客戶或訂閱應用新的優惠券或促銷程式碼,可以透過 applyCoupon 或 applyPromotionCode 方法進行。
1$billable->applyCoupon('coupon_id');2$billable->applyPromotionCode('promotion_code_id');3 4$subscription->applyCoupon('coupon_id');5$subscription->applyPromotionCode('promotion_code_id');
請記住,你應該使用分配給促銷程式碼的 Stripe API ID,而不是面向客戶的促銷程式碼。一次只能向客戶或訂閱應用一張優惠券或一個促銷程式碼。
有關此主題的更多資訊,請參閱 Stripe 關於 優惠券 和 促銷程式碼 的文件。
新增訂閱
如果你想向已經有預設支付方式的客戶新增訂閱,你可以呼叫訂閱構建器上的 add 方法。
1use App\Models\User;2 3$user = User::find(1);4 5$user->newSubscription('default', 'price_monthly')->add();
從 Stripe 面板建立訂閱
你也可以從 Stripe 面板本身建立訂閱。執行此操作時,Cashier 將同步新新增的訂閱併為其分配 default 型別。要自定義分配給儀表板建立的訂閱的訂閱型別,請 定義 Webhook 事件處理器。
此外,你只能透過 Stripe 面板建立一種型別的訂閱。如果你的應用程式提供使用不同型別的多種訂閱,則只能透過 Stripe 面板新增一種型別的訂閱。
最後,你應該始終確保每個訂閱型別僅新增一個有效訂閱。如果客戶有兩個 default 訂閱,即使兩者都會與你的應用程式資料庫同步,Cashier 也只會使用最近新增的訂閱。
檢查訂閱狀態
一旦客戶訂閱了你的應用程式,你可以使用多種方便的方法輕鬆檢查其訂閱狀態。首先,如果客戶有活動訂閱,即使訂閱目前處於試用期,subscribed 方法也會返回 true。subscribed 方法接受訂閱型別作為其第一個引數。
1if ($user->subscribed('default')) {2 // ...3}
subscribed 方法也是 路由中介軟體 的絕佳選擇,允許你根據使用者的訂閱狀態過濾對路由和控制器的訪問。
1<?php 2 3namespace App\Http\Middleware; 4 5use Closure; 6use Illuminate\Http\Request; 7use Symfony\Component\HttpFoundation\Response; 8 9class EnsureUserIsSubscribed10{11 /**12 * Handle an incoming request.13 *14 * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next15 */16 public function handle(Request $request, Closure $next): Response17 {18 if ($request->user() && ! $request->user()->subscribed('default')) {19 // This user is not a paying customer...20 return redirect('/billing');21 }22 23 return $next($request);24 }25}
如果你想確定使用者是否仍處於試用期,可以使用 onTrial 方法。此方法對於確定是否應向用戶顯示他們仍處於試用期的警告很有用。
1if ($user->subscription('default')->onTrial()) {2 // ...3}
subscribedToProduct 方法可用於根據給定的 Stripe 產品識別符號確定使用者是否訂閱了特定產品。在 Stripe 中,產品是價格的集合。在此示例中,我們將確定使用者的 default 訂閱是否處於應用程式的“premium”產品的活躍訂閱狀態。給定的 Stripe 產品識別符號應對應於 Stripe 面板中你的產品識別符號之一。
1if ($user->subscribedToProduct('prod_premium', 'default')) {2 // ...3}
透過將陣列傳遞給 subscribedToProduct 方法,你可以確定使用者的 default 訂閱是否處於應用程式的“basic”或“premium”產品的活躍訂閱狀態。
1if ($user->subscribedToProduct(['prod_basic', 'prod_premium'], 'default')) {2 // ...3}
subscribedToPrice 方法可用於確定客戶的訂閱是否對應於給定的價格 ID。
1if ($user->subscribedToPrice('price_basic_monthly', 'default')) {2 // ...3}
recurring 方法可用於確定使用者是否已訂閱且不再處於試用期。
1if ($user->subscription('default')->recurring()) {2 // ...3}
如果使用者有兩個相同型別的訂閱,subscription 方法將始終返回最新的訂閱。例如,使用者可能有兩條型別為 default 的訂閱記錄;但是,其中一個訂閱可能是已過期的舊訂閱,而另一個是當前的活動訂閱。最新的訂閱將始終被返回,而舊的訂閱則儲存在資料庫中以供歷史審查。
已取消的訂閱狀態
要確定使用者是否曾經是活躍訂閱者但已取消訂閱,可以使用 canceled 方法。
1if ($user->subscription('default')->canceled()) {2 // ...3}
你還可以確定使用者是否已取消訂閱但仍處於“寬限期”內,直到訂閱完全到期。例如,如果使用者在 3 月 5 日取消了一個原定於 3 月 10 日到期的訂閱,則該使用者處於 3 月 10 日之前的“寬限期”內。請注意,在此期間 subscribed 方法仍返回 true。
1if ($user->subscription('default')->onGracePeriod()) {2 // ...3}
要確定使用者是否已取消訂閱且不再處於“寬限期”內,可以使用 ended 方法。
1if ($user->subscription('default')->ended()) {2 // ...3}
未完成和逾期狀態
如果訂閱在建立後需要二次支付操作,則訂閱將被標記為 incomplete。訂閱狀態儲存在 Cashier 的 subscriptions 資料庫表的 stripe_status 列中。
同樣,如果在切換價格時需要二次支付操作,訂閱將被標記為 past_due。當訂閱處於這些狀態中的任何一個時,除非客戶確認了付款,否則它將不會處於活動狀態。確定訂閱是否存在未完成付款可以使用可計費模型或訂閱例項上的 hasIncompletePayment 方法。
1if ($user->hasIncompletePayment('default')) {2 // ...3}4 5if ($user->subscription('default')->hasIncompletePayment()) {6 // ...7}
當訂閱有未完成付款時,你應該將使用者引導至 Cashier 的支付確認頁面,並傳入 latestPayment 識別符號。你可以使用訂閱例項上提供的 latestPayment 方法來檢索此識別符號。
1<a href="{{ route('cashier.payment', $subscription->latestPayment()->id) }}">2 Please confirm your payment.3</a>
如果你希望訂閱在 past_due 或 incomplete 狀態下仍被視為活動狀態,可以使用 Cashier 提供的 keepPastDueSubscriptionsActive 和 keepIncompleteSubscriptionsActive 方法。通常,這些方法應在 App\Providers\AppServiceProvider 的 register 方法中呼叫。
1use Laravel\Cashier\Cashier; 2 3/** 4 * Register any application services. 5 */ 6public function register(): void 7{ 8 Cashier::keepPastDueSubscriptionsActive(); 9 Cashier::keepIncompleteSubscriptionsActive();10}
當訂閱處於 incomplete 狀態時,在確認付款之前無法進行更改。因此,當訂閱處於 incomplete 狀態時,swap 和 updateQuantity 方法將丟擲異常。
訂閱作用域 (Subscription Scopes)
大多數訂閱狀態也可用作查詢作用域,以便你可以輕鬆地查詢資料庫中處於給定狀態的訂閱。
1// Get all active subscriptions...2$subscriptions = Subscription::query()->active()->get();3 4// Get all of the canceled subscriptions for a user...5$subscriptions = $user->subscriptions()->canceled()->get();
可用作用域的完整列表如下:
1Subscription::query()->active(); 2Subscription::query()->canceled(); 3Subscription::query()->ended(); 4Subscription::query()->incomplete(); 5Subscription::query()->notCanceled(); 6Subscription::query()->notOnGracePeriod(); 7Subscription::query()->notOnTrial(); 8Subscription::query()->onGracePeriod(); 9Subscription::query()->onTrial();10Subscription::query()->pastDue();11Subscription::query()->recurring();
更改價格
一旦客戶訂閱了你的應用程式,他們有時可能想要更改為新的訂閱價格。要將客戶切換到新價格,請將 Stripe 價格的識別符號傳遞給 swap 方法。切換價格時,假設使用者希望在之前取消的情況下重新啟用其訂閱。給定的價格標識符應對應於 Stripe 面板中可用的 Stripe 價格標識符。
1use App\Models\User;2 3$user = App\Models\User::find(1);4 5$user->subscription('default')->swap('price_yearly');
如果客戶處於試用期,試用期將被保留。此外,如果訂閱存在“數量”,該數量也將被保留。
如果你想切換價格並取消客戶目前正在進行的任何試用期,可以呼叫 skipTrial 方法。
1$user->subscription('default')2 ->skipTrial()3 ->swap('price_yearly');
如果你想切換價格並立即向客戶開具賬單,而不是等待下一個計費週期,可以使用 swapAndInvoice 方法。
1$user = User::find(1);2 3$user->subscription('default')->swapAndInvoice('price_yearly');
按比例計算 (Prorations)
預設情況下,在價格之間切換時,Stripe 會按比例計算費用。noProrate 方法可用於更新訂閱價格而不進行按比例計算。
1$user->subscription('default')->noProrate()->swap('price_yearly');
有關訂閱按比例計算的更多資訊,請查閱 Stripe 文件。
在 swapAndInvoice 方法之前執行 noProrate 方法對按比例計算無效。賬單將始終被開具。
訂閱數量
有時訂閱會受到“數量”的影響。例如,專案管理應用程式可能每月每個專案收取 10 美元。你可以使用 incrementQuantity 和 decrementQuantity 方法輕鬆增加或減少訂閱數量。
1use App\Models\User; 2 3$user = User::find(1); 4 5$user->subscription('default')->incrementQuantity(); 6 7// Add five to the subscription's current quantity... 8$user->subscription('default')->incrementQuantity(5); 9 10$user->subscription('default')->decrementQuantity();11 12// Subtract five from the subscription's current quantity...13$user->subscription('default')->decrementQuantity(5);
或者,你可以使用 updateQuantity 方法設定特定數量。
1$user->subscription('default')->updateQuantity(10);
noProrate 方法可用於更新訂閱數量而不進行按比例計算。
1$user->subscription('default')->noProrate()->updateQuantity(10);
有關訂閱數量的更多資訊,請查閱 Stripe 文件。
包含多個產品的訂閱數量
如果你的訂閱是 包含多個產品的訂閱,你應該將你想要增加或減少數量的價格 ID 作為第二個引數傳遞給增加/減少方法。
1$user->subscription('default')->incrementQuantity(1, 'price_chat');
包含多個產品的訂閱
包含多個產品的訂閱 允許你將多個計費產品分配給單個訂閱。例如,假設你正在構建一個客戶服務“幫助臺”應用程式,該應用程式的基本訂閱價格為每月 10 美元,但提供每月額外 15 美元的即時聊天附加產品。包含多個產品的訂閱資訊儲存在 Cashier 的 subscription_items 資料庫表中。
你可以透過將價格陣列作為第二個引數傳遞給 newSubscription 方法來為給定訂閱指定多個產品。
1use Illuminate\Http\Request; 2 3Route::post('/user/subscribe', function (Request $request) { 4 $request->user()->newSubscription('default', [ 5 'price_monthly', 6 'price_chat', 7 ])->create($request->paymentMethodId); 8 9 // ...10});
在上面的示例中,客戶的 default 訂閱中將附加兩個價格。這兩個價格將在各自的計費週期內收費。如果有必要,你可以使用 quantity 方法為每個價格指定特定數量。
1$user = User::find(1);2 3$user->newSubscription('default', ['price_monthly', 'price_chat'])4 ->quantity(5, 'price_chat')5 ->create($paymentMethod);
如果你想向現有訂閱新增另一個價格,可以呼叫訂閱的 addPrice 方法。
1$user = User::find(1);2 3$user->subscription('default')->addPrice('price_chat');
上面的示例將新增新價格,客戶將在下一個計費週期內為其付費。如果你想立即向客戶收費,可以使用 addPriceAndInvoice 方法。
1$user->subscription('default')->addPriceAndInvoice('price_chat');
如果你想新增具有特定數量的價格,可以將數量作為 addPrice 或 addPriceAndInvoice 方法的第二個引數傳遞。
1$user = User::find(1);2 3$user->subscription('default')->addPrice('price_chat', 5);
你可以使用 removePrice 方法從訂閱中刪除價格。
1$user->subscription('default')->removePrice('price_chat');
你不能刪除訂閱上的最後一個價格。相反,你應該直接取消該訂閱。
切換價格
你還可以更改附加到包含多個產品訂閱的價格。例如,假設客戶有 price_basic 訂閱並附加了 price_chat 產品,你想將客戶從 price_basic 升級到 price_pro 價格:
1use App\Models\User;2 3$user = User::find(1);4 5$user->subscription('default')->swap(['price_pro', 'price_chat']);
當執行上面的示例時,帶有 price_basic 的底層訂閱項會被刪除,而帶有 price_chat 的項會被保留。此外,會建立一個用於 price_pro 的新訂閱項。
你還可以透過將鍵值對陣列傳遞給 swap 方法來指定訂閱項選項。例如,你可能需要指定訂閱價格數量。
1$user = User::find(1);2 3$user->subscription('default')->swap([4 'price_pro' => ['quantity' => 5],5 'price_chat'6]);
如果你想切換訂閱上的單個價格,可以透過訂閱項本身使用 swap 方法。如果你想保留訂閱其他價格上的所有現有元資料,這種方法特別有用。
1$user = User::find(1);2 3$user->subscription('default')4 ->findItemOrFail('price_basic')5 ->swap('price_pro');
按比例計算
預設情況下,當向包含多個產品的訂閱新增或刪除價格時,Stripe 會按比例計算費用。如果你想進行不帶按比例計算的價格調整,你應該在你的價格操作鏈中呼叫 noProrate 方法。
1$user->subscription('default')->noProrate()->removePrice('price_chat');
數量
如果你想更新單個訂閱價格的數量,可以使用 現有的數量方法,並將價格 ID 作為額外引數傳遞給該方法。
1$user = User::find(1);2 3$user->subscription('default')->incrementQuantity(5, 'price_chat');4 5$user->subscription('default')->decrementQuantity(3, 'price_chat');6 7$user->subscription('default')->updateQuantity(10, 'price_chat');
當訂閱有多個價格時,Subscription 模型上的 stripe_price 和 quantity 屬性將為 null。要訪問單個價格屬性,你應該使用 Subscription 模型上可用的 items 關係。
訂閱項 (Subscription Items)
當訂閱有多個價格時,它將在資料庫的 subscription_items 表中儲存多個訂閱“項”。你可以透過訂閱上的 items 關係訪問它們。
1use App\Models\User;2 3$user = User::find(1);4 5$subscriptionItem = $user->subscription('default')->items->first();6 7// Retrieve the Stripe price and quantity for a specific item...8$stripePrice = $subscriptionItem->stripe_price;9$quantity = $subscriptionItem->quantity;
你還可以使用 findItemOrFail 方法檢索特定價格。
1$user = User::find(1);2 3$subscriptionItem = $user->subscription('default')->findItemOrFail('price_chat');
多個訂閱
Stripe 允許你的客戶同時擁有多個訂閱。例如,你可能經營一家健身房,提供游泳訂閱和舉重訂閱,並且每個訂閱可能有不同的定價。當然,客戶應該能夠訂閱其中一個或兩個計劃。
當你的應用程式建立訂閱時,你可以將訂閱型別提供給 newSubscription 方法。該型別可以是代表使用者正在啟動的訂閱型別的任何字串。
1use Illuminate\Http\Request;2 3Route::post('/swimming/subscribe', function (Request $request) {4 $request->user()->newSubscription('swimming')5 ->price('price_swimming_monthly')6 ->create($request->paymentMethodId);7 8 // ...9});
在此示例中,我們為客戶啟動了每月游泳訂閱。但是,他們可能以後想切換到年度訂閱。在調整客戶訂閱時,我們可以簡單地交換 swimming 訂閱上的價格。
1$user->subscription('swimming')->swap('price_swimming_yearly');
當然,你也可以完全取消訂閱。
1$user->subscription('swimming')->cancel();
基於用量的計費
基於用量的計費 允許你根據產品在計費週期內的使用情況向客戶收費。例如,你可以根據客戶每月傳送的簡訊或電子郵件數量收費。
要開始使用基於用量的計費,你需要首先在 Stripe 面板中建立一個具有 基於用量的計費模型 和 計量器 (meter) 的新產品。建立計量器後,儲存關聯的事件名稱和計量器 ID,你需要它們來上報和檢索使用情況。然後,使用 meteredPrice 方法將計量的價格 ID 新增到客戶訂閱中。
1use Illuminate\Http\Request;2 3Route::post('/user/subscribe', function (Request $request) {4 $request->user()->newSubscription('default')5 ->meteredPrice('price_metered')6 ->create($request->paymentMethodId);7 8 // ...9});
你還可以透過 Stripe Checkout 啟動計量訂閱。
1$checkout = Auth::user()2 ->newSubscription('default', [])3 ->meteredPrice('price_metered')4 ->checkout();5 6return view('your-checkout-view', [7 'checkout' => $checkout,8]);
上報用量
當客戶使用你的應用程式時,你將向 Stripe 上報他們的用量,以便可以準確計費。要上報計量事件的用量,你可以在 Billable 模型上使用 reportMeterEvent 方法。
1$user = User::find(1);2 3$user->reportMeterEvent('emails-sent');
預設情況下,計費週期會增加 1 的“用量數量”。或者,你可以傳遞特定的“用量”數值來增加客戶在計費週期的用量。
1$user = User::find(1);2 3$user->reportMeterEvent('emails-sent', quantity: 15);
要檢索客戶的計量器事件彙總,可以使用 Billable 例項的 meterEventSummaries 方法。
1$user = User::find(1);2 3$meterUsage = $user->meterEventSummaries($meterId);4 5$meterUsage->first()->aggregated_value // 10
有關計量器事件彙總的更多資訊,請參考 Stripe 的 計量器事件彙總物件文件。
要 列出所有計量器,可以使用 Billable 例項的 meters 方法。
1$user = User::find(1);2 3$user->meters();
訂閱稅費
與其手動計算稅率,你可以 透過 Stripe Tax 自動計算稅費。
要指定使用者在訂閱中支付的稅率,你應該在可計費模型上實現 taxRates 方法,並返回包含 Stripe 稅率 ID 的陣列。你可以在 你的 Stripe 面板 中定義這些稅率。
1/**2 * The tax rates that should apply to the customer's subscriptions.3 *4 * @return array<int, string>5 */6public function taxRates(): array7{8 return ['txr_id'];9}
taxRates 方法使你能夠按客戶為基礎應用稅率,這對於跨多個國家和稅率的使用者群體很有幫助。
如果你提供包含多個產品的訂閱,可以透過在可計費模型上實現 priceTaxRates 方法來為每個價格定義不同的稅率。
1/** 2 * The tax rates that should apply to the customer's subscriptions. 3 * 4 * @return array<string, array<int, string>> 5 */ 6public function priceTaxRates(): array 7{ 8 return [ 9 'price_monthly' => ['txr_id'],10 ];11}
taxRates 方法僅適用於訂閱扣款。如果你使用 Cashier 進行“一次性”扣款,則需要在當時手動指定稅率。
同步稅率
當更改 taxRates 方法返回的硬編碼稅率 ID 時,使用者的任何現有訂閱的稅務設定將保持不變。如果你想使用新的 taxRates 值更新現有訂閱的稅務值,你應該在使用者的訂閱例項上呼叫 syncTaxRates 方法。
1$user->subscription('default')->syncTaxRates();
這也將同步包含多個產品的訂閱的所有專案稅率。如果你的應用程式提供包含多個產品的訂閱,則應確保你的可計費模型實現了 上面討論的 priceTaxRates 方法。
稅務豁免
Cashier 還提供 isNotTaxExempt、isTaxExempt 和 reverseChargeApplies 方法來確定客戶是否免稅。這些方法將呼叫 Stripe API 來確定客戶的稅務豁免狀態。
1use App\Models\User;2 3$user = User::find(1);4 5$user->isTaxExempt();6$user->isNotTaxExempt();7$user->reverseChargeApplies();
這些方法在任何 Laravel\Cashier\Invoice 物件上也可用。但是,當在 Invoice 物件上呼叫時,這些方法將確定賬單建立時的豁免狀態。
訂閱錨點日期
預設情況下,計費週期錨點是訂閱建立的日期,或者如果使用了試用期,則是試用結束的日期。如果你想修改計費錨點日期,可以使用 anchorBillingCycleOn 方法。
1use Illuminate\Http\Request; 2 3Route::post('/user/subscribe', function (Request $request) { 4 $anchor = Carbon::parse('first day of next month'); 5 6 $request->user()->newSubscription('default', 'price_monthly') 7 ->anchorBillingCycleOn($anchor->startOfDay()) 8 ->create($request->paymentMethodId); 9 10 // ...11});
有關管理訂閱計費週期的更多資訊,請查閱 Stripe 計費週期文件。
取消訂閱
要取消訂閱,請呼叫使用者訂閱上的 cancel 方法。
1$user->subscription('default')->cancel();
當訂閱被取消時,Cashier 將自動設定你的 subscriptions 資料庫表中的 ends_at 列。此列用於判斷 subscribed 方法何時應該開始返回 false。
例如,如果客戶在 3 月 1 日取消了訂閱,但訂閱原定於 3 月 5 日到期,則 subscribed 方法將持續返回 true 直到 3 月 5 日。這是因為使用者通常被允許繼續使用應用程式直到其計費週期結束。
你可以使用 onGracePeriod 方法確定使用者是否已取消訂閱但仍處於“寬限期”內。
1if ($user->subscription('default')->onGracePeriod()) {2 // ...3}
如果你想立即取消訂閱,請呼叫使用者訂閱上的 cancelNow 方法。
1$user->subscription('default')->cancelNow();
如果你想立即取消訂閱並對任何剩餘未入賬的計量使用量或新的/待處理的比例賬單項開具賬單,請呼叫使用者訂閱上的 cancelNowAndInvoice 方法。
1$user->subscription('default')->cancelNowAndInvoice();
你還可以選擇在特定的時刻取消訂閱。
1$user->subscription('default')->cancelAt(2 now()->plus(days: 10)3);
最後,你應該始終在刪除關聯的使用者模型之前取消使用者的訂閱。
1$user->subscription('default')->cancelNow();2 3$user->delete();
恢復訂閱
如果客戶已取消其訂閱並且你希望恢復它,你可以呼叫訂閱上的 resume 方法。客戶必須仍處於其“寬限期”內才能恢復訂閱。
1$user->subscription('default')->resume();
如果客戶取消了訂閱,然後在訂閱完全到期前恢復了該訂閱,客戶將不會立即被收費。相反,他們的訂閱將被重新啟用,並將在原始計費週期內被收費。
訂閱試用
預先提供支付方式
如果你想在預先收集支付方式資訊的同時為客戶提供試用期,你應該在建立訂閱時使用 trialDays 方法。
1use Illuminate\Http\Request;2 3Route::post('/user/subscribe', function (Request $request) {4 $request->user()->newSubscription('default', 'price_monthly')5 ->trialDays(10)6 ->create($request->paymentMethodId);7 8 // ...9});
此方法將設定資料庫中訂閱記錄的試用期結束日期,並指示 Stripe 在此日期之前不開始向客戶收費。使用 trialDays 方法時,Cashier 將覆蓋 Stripe 中為該價格配置的任何預設試用期。
如果客戶的訂閱在試用結束日期之前沒有被取消,他們將在試用期結束後立即被收費,因此請務必通知你的使用者他們的試用結束日期。
trialUntil 方法允許你提供一個指定試用期應何時結束的 DateTime 例項。
1use Illuminate\Support\Carbon;2 3$user->newSubscription('default', 'price_monthly')4 ->trialUntil(Carbon::now()->plus(days: 10))5 ->create($paymentMethod);
你可以使用使用者例項上的 onTrial 方法或訂閱例項上的 onTrial 方法來確定使用者是否處於試用期。以下兩個示例是等效的。
1if ($user->onTrial('default')) {2 // ...3}4 5if ($user->subscription('default')->onTrial()) {6 // ...7}
你可以使用 endTrial 方法立即結束訂閱試用。
1$user->subscription('default')->endTrial();
要確定現有的試用期是否已過期,你可以使用 hasExpiredTrial 方法。
1if ($user->hasExpiredTrial('default')) {2 // ...3}4 5if ($user->subscription('default')->hasExpiredTrial()) {6 // ...7}
在 Stripe / Cashier 中定義試用天數
你可以選擇在 Stripe 面板中定義價格接收多少試用天數,或者始終使用 Cashier 顯式傳遞它們。如果你選擇在 Stripe 中定義價格的試用天數,你應該意識到新的訂閱(包括過去有過訂閱的客戶的新訂閱)將始終收到試用期,除非你顯式呼叫 skipTrial() 方法。
無需預先提供支付方式
如果你想在不預先收集使用者支付方式資訊的情況下提供試用期,你可以將使用者記錄上的 trial_ends_at 列設定為你想要的試用結束日期。這通常在使用者註冊期間完成。
1use App\Models\User;2 3$user = User::create([4 // ...5 'trial_ends_at' => now()->plus(days: 10),6]);
請務必在可計費模型的類定義中為 trial_ends_at 屬性新增 日期轉換 (date cast)。
Cashier 將這種型別的試用稱為“通用試用 (generic trial)”,因為它未附加到任何現有訂閱。如果當前日期未超過 trial_ends_at 的值,可計費模型例項上的 onTrial 方法將返回 true。
1if ($user->onTrial()) {2 // User is within their trial period...3}
一旦你準備好為使用者建立實際訂閱,你可以像往常一樣使用 newSubscription 方法。
1$user = User::find(1);2 3$user->newSubscription('default', 'price_monthly')->create($paymentMethod);
要檢索使用者的試用結束日期,可以使用 trialEndsAt 方法。如果使用者處於試用期,此方法將返回 Carbon 日期例項,如果不是,則返回 null。如果你想獲取除預設訂閱之外的特定訂閱的試用結束日期,也可以傳遞可選的訂閱型別引數。
1if ($user->onTrial()) {2 $trialEndsAt = $user->trialEndsAt('main');3}
如果你想明確知道使用者處於“通用”試用期內且尚未建立實際訂閱,也可以使用 onGenericTrial 方法。
1if ($user->onGenericTrial()) {2 // User is within their "generic" trial period...3}
延長試用
extendTrial 方法允許你在訂閱建立後延長訂閱的試用期。如果試用期已經過期且客戶已經被收取訂閱費用,你仍然可以為他們提供延長的試用期。試用期內花費的時間將從客戶的下一個賬單中扣除。
1use App\Models\User; 2 3$subscription = User::find(1)->subscription('default'); 4 5// End the trial 7 days from now... 6$subscription->extendTrial( 7 now()->plus(days: 7) 8); 9 10// Add an additional 5 days to the trial...11$subscription->extendTrial(12 $subscription->trial_ends_at->plus(days: 5)13);
處理 Stripe Webhooks
你可以使用 Stripe CLI 來幫助在本地開發期間測試 Webhook。
Stripe 可以透過 Webhook 通知你的應用程式各種事件。預設情況下,指向 Cashier Webhook 控制器的路由由 Cashier 服務提供者自動註冊。此控制器將處理所有傳入的 Webhook 請求。
預設情況下,Cashier Webhook 控制器將自動處理取消具有過多失敗扣款的訂閱(由你的 Stripe 設定定義)、客戶更新、客戶刪除、訂閱更新和支付方式變更;然而,正如我們將很快發現的,你可以擴充套件此控制器以處理任何你喜歡的 Stripe Webhook 事件。
為確保你的應用程式能夠處理 Stripe Webhook,請務必在 Stripe 控制面板中配置 Webhook URL。預設情況下,Cashier 的 Webhook 控制器響應 /stripe/webhook URL 路徑。你應該在 Stripe 控制面板中啟用的所有 Webhook 的完整列表是:
customer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedcustomer.updatedcustomer.deletedpayment_method.automatically_updatedinvoice.payment_action_requiredinvoice.payment_succeeded
為方便起見,Cashier 包含了一個 cashier:webhook Artisan 命令。此命令將在 Stripe 中建立一個監聽 Cashier 所需所有事件的 Webhook。
1php artisan cashier:webhook
預設情況下,建立的 Webhook 將指向由 APP_URL 環境變數定義的 URL 和 Cashier 包含的 cashier.webhook 路由。如果你想使用不同的 URL,可以在呼叫該命令時提供 --url 選項。
1php artisan cashier:webhook --url "https://example.com/stripe/webhook"
建立的 Webhook 將使用與你的 Cashier 版本相容的 Stripe API 版本。如果你想使用不同的 Stripe 版本,可以提供 --api-version 選項。
1php artisan cashier:webhook --api-version="2019-12-03"
建立後,Webhook 將立即生效。如果你希望在準備好之前建立 Webhook 但使其保持停用狀態,可以在呼叫該命令時提供 --disabled 選項。
1php artisan cashier:webhook --disabled
確保使用 Cashier 包含的 Webhook 簽名驗證 中介軟體來保護傳入的 Stripe Webhook 請求。
Webhook 與 CSRF 保護
由於 Stripe Webhook 需要繞過 Laravel 的 CSRF 保護,因此你應該確保 Laravel 不會嘗試驗證傳入 Stripe Webhook 的 CSRF 令牌。為此,你應該在應用程式的 bootstrap/app.php 檔案中將 stripe/* 從 CSRF 保護中排除。
1->withMiddleware(function (Middleware $middleware): void {2 $middleware->preventRequestForgery(except: [3 'stripe/*',4 ]);5})
定義 Webhook 事件處理器
Cashier 會自動處理訂閱扣款失敗的取消以及其他常見的 Stripe Webhook 事件。但是,如果你有其他想要處理的 Webhook 事件,可以透過監聽 Cashier 分發的以下事件來實現:
Laravel\Cashier\Events\WebhookReceivedLaravel\Cashier\Events\WebhookHandled
這兩個事件都包含 Stripe Webhook 的完整有效負載。例如,如果你想處理 invoice.payment_succeeded Webhook,可以註冊一個 監聽器 來處理該事件。
1<?php 2 3namespace App\Listeners; 4 5use Laravel\Cashier\Events\WebhookReceived; 6 7class StripeEventListener 8{ 9 /**10 * Handle received Stripe webhooks.11 */12 public function handle(WebhookReceived $event): void13 {14 if ($event->payload['type'] === 'invoice.payment_succeeded') {15 // Handle the incoming event...16 }17 }18}
驗證 Webhook 簽名
為了保護你的 Webhook,你可以使用 Stripe 的 Webhook 簽名。為方便起見,Cashier 自動包含了一個驗證傳入 Stripe Webhook 請求是否有效的中介軟體。
要啟用 Webhook 驗證,請確保在應用程式的 .env 檔案中設定了 STRIPE_WEBHOOK_SECRET 環境變數。Webhook secret 可以從你的 Stripe 賬戶控制面板中檢索。
單次扣款
簡單扣款
如果你想對客戶進行一次性扣款,可以使用可計費模型例項上的 charge 方法。你需要 提供一個支付方式識別符號 作為 charge 方法的第二個引數。
1use Illuminate\Http\Request;2 3Route::post('/purchase', function (Request $request) {4 $stripeCharge = $request->user()->charge(5 100, $request->paymentMethodId6 );7 8 // ...9});
charge 方法接受一個數組作為其第三個引數,允許你將任何你想要的選項傳遞給底層的 Stripe 扣款建立過程。有關建立扣款時可用的選項的更多資訊,請查閱 Stripe 文件。
1$user->charge(100, $paymentMethod, [2 'custom_option' => $value,3]);
你也可以在沒有底層客戶或使用者的情況下使用 charge 方法。為此,請在你應用程式的可計費模型的新例項上呼叫 charge 方法。
1use App\Models\User;2 3$stripeCharge = (new User)->charge(100, $paymentMethod);
如果扣款失敗,charge 方法將丟擲異常。如果扣款成功,該方法將返回一個 Laravel\Cashier\Payment 例項。
1try {2 $payment = $user->charge(100, $paymentMethod);3} catch (Exception $e) {4 // ...5}
charge 方法接受以應用程式所用貨幣的最小面值單位表示的支付金額。例如,如果客戶以美元支付,金額應以美分表示。
透過賬單扣款
有時你可能需要進行一次性扣款併為你的客戶提供 PDF 賬單。invoicePrice 方法正好可以實現這一點。例如,讓我們為客戶購買的五件新襯衫開具賬單:
1$user->invoicePrice('price_tshirt', 5);
該賬單將立即從使用者的預設支付方式中扣除。invoicePrice 方法也接受一個數組作為其第三個引數。此陣列包含賬單專案的計費選項。該方法接受的第四個引數也是一個數組,應包含賬單本身的計費選項。
1$user->invoicePrice('price_tshirt', 5, [2 'discounts' => [3 ['coupon' => 'SUMMER21SALE']4 ],5], [6 'default_tax_rates' => ['txr_id'],7]);
與 invoicePrice 類似,你可以使用 tabPrice 方法透過將多個專案(每個賬單最多 250 個專案)新增到客戶的“標籤頁”並隨後向客戶開具賬單,來為多個專案建立一次性扣款。例如,我們可以為客戶的五件襯衫和兩個馬克杯開具賬單:
1$user->tabPrice('price_tshirt', 5);2$user->tabPrice('price_mug', 2);3$user->invoice();
或者,你可以使用 invoiceFor 方法對客戶的預設支付方式進行“一次性”扣款。
1$user->invoiceFor('One Time Fee', 500);
雖然可以使用 invoiceFor 方法,但建議使用帶有預定義價格的 invoicePrice 和 tabPrice 方法。透過這樣做,你將在 Stripe 面板中獲得有關按產品銷售額的更好分析和資料。
invoice、invoicePrice 和 invoiceFor 方法將建立一條 Stripe 賬單,該賬單將在計費失敗時重試。如果你不希望賬單在扣款失敗後重試,則需要在第一次扣款失敗後使用 Stripe API 關閉它們。
建立支付意圖 (Payment Intents)
你可以透過在可計費模型例項上呼叫 pay 方法來建立新的 Stripe 支付意圖。呼叫此方法將建立一個封裝在 Laravel\Cashier\Payment 例項中的支付意圖。
1use Illuminate\Http\Request;2 3Route::post('/pay', function (Request $request) {4 $payment = $request->user()->pay(5 $request->get('amount')6 );7 8 return $payment->client_secret;9});
建立支付意圖後,你可以將 client secret 返回到應用程式的前端,以便使用者可以在瀏覽器中完成支付。要閱讀有關使用 Stripe 支付意圖構建完整支付流程的更多資訊,請查閱 Stripe 文件。
使用 pay 方法時,你的 Stripe 面板中啟用的預設支付方式將可供客戶使用。或者,如果你只想允許使用某些特定的支付方式,可以使用 payWith 方法。
1use Illuminate\Http\Request;2 3Route::post('/pay', function (Request $request) {4 $payment = $request->user()->payWith(5 $request->get('amount'), ['card', 'bancontact']6 );7 8 return $payment->client_secret;9});
pay 和 payWith 方法接受以應用程式所用貨幣的最小面值單位表示的支付金額。例如,如果客戶以美元支付,金額應以美分表示。
退款
如果你需要退還 Stripe 扣款,可以使用 refund 方法。此方法接受 Stripe 支付意圖 ID 作為其第一個引數。
1$payment = $user->charge(100, $paymentMethodId);2 3$user->refund($payment->id);
賬單 (Invoices)
檢索賬單
你可以使用 invoices 方法輕鬆檢索可計費模型賬單的陣列。invoices 方法返回 Laravel\Cashier\Invoice 例項的集合。
1$invoices = $user->invoices();
如果你想在結果中包含待處理的賬單,可以使用 invoicesIncludingPending 方法。
1$invoices = $user->invoicesIncludingPending();
你可以使用 findInvoice 方法透過其 ID 檢索特定賬單。
1$invoice = $user->findInvoice($invoiceId);
顯示賬單資訊
列出客戶賬單時,可以使用賬單的方法來顯示相關的賬單資訊。例如,你可能希望在表格中列出每個賬單,允許使用者輕鬆下載其中任何一個。
1<table>2 @foreach ($invoices as $invoice)3 <tr>4 <td>{{ $invoice->date()->toFormattedDateString() }}</td>5 <td>{{ $invoice->total() }}</td>6 <td><a href="/user/invoice/{{ $invoice->id }}">Download</a></td>7 </tr>8 @endforeach9</table>
即將到來的賬單
要檢索客戶即將到來的賬單,可以使用 upcomingInvoice 方法。
1$invoice = $user->upcomingInvoice();
同樣,如果客戶有多個訂閱,你也可以檢索特定訂閱即將到來的賬單。
1$invoice = $user->subscription('default')->upcomingInvoice();
預覽訂閱賬單
使用 previewInvoice 方法,你可以在進行價格更改之前預覽賬單。這將允許你確定在進行給定價格更改後客戶的賬單是什麼樣子。
1$invoice = $user->subscription('default')->previewInvoice('price_yearly');
你可以將價格陣列傳遞給 previewInvoice 方法,以便預覽包含多個新價格的賬單。
1$invoice = $user->subscription('default')->previewInvoice(['price_yearly', 'price_metered']);
生成 PDF 賬單
在生成 PDF 賬單之前,你應該使用 Composer 安裝 Dompdf 庫,它是 Cashier 的預設賬單渲染器。
1composer require dompdf/dompdf
在路由或控制器中,你可以使用 downloadInvoice 方法來生成給定賬單的 PDF 下載。此方法將自動生成下載賬單所需的正確 HTTP 響應。
1use Illuminate\Http\Request;2 3Route::get('/user/invoice/{invoice}', function (Request $request, string $invoiceId) {4 return $request->user()->downloadInvoice($invoiceId);5});
預設情況下,賬單上的所有資料均來自儲存在 Stripe 中的客戶和賬單資料。檔名基於你的 app.name 配置值。但是,你可以透過將陣列作為第二個引數傳遞給 downloadInvoice 方法來自定義其中一些資料。此陣列允許你自定義諸如公司和產品詳細資訊等資訊。
1return $request->user()->downloadInvoice($invoiceId, [ 2 'vendor' => 'Your Company', 3 'product' => 'Your Product', 4 'street' => 'Main Str. 1', 5 'location' => '2000 Antwerp, Belgium', 6 'phone' => '+32 499 00 00 00', 8 'url' => 'https://example.com', 9 'vendorVat' => 'BE123456789',10]);
downloadInvoice 方法還允許透過其第三個引數使用自定義檔名。此檔名將自動新增 .pdf 字尾。
1return $request->user()->downloadInvoice($invoiceId, [], 'my-invoice');
自定義賬單渲染器
Cashier 也可以使用自定義賬單渲染器。預設情況下,Cashier 使用 DompdfInvoiceRenderer 實現,它利用 dompdf PHP 庫來生成賬單。但是,你可以透過實現 Laravel\Cashier\Contracts\InvoiceRenderer 介面來使用任何你想要的渲染器。例如,你可能希望透過對第三方 PDF 渲染服務的 API 呼叫來渲染 PDF 賬單。
1use Illuminate\Support\Facades\Http; 2use Laravel\Cashier\Contracts\InvoiceRenderer; 3use Laravel\Cashier\Invoice; 4 5class ApiInvoiceRenderer implements InvoiceRenderer 6{ 7 /** 8 * Render the given invoice and return the raw PDF bytes. 9 */10 public function render(Invoice $invoice, array $data = [], array $options = []): string11 {12 $html = $invoice->view($data)->render();13 14 return Http::get('https://example.com/html-to-pdf', ['html' => $html])->get()->body();15 }16}
實現賬單渲染器契約後,你應該在應用程式的 config/cashier.php 配置檔案中更新 cashier.invoices.renderer 配置值。此配置值應設定為你的自定義渲染器實現的類名。
Checkout (結賬)
Cashier Stripe 還提供對 Stripe Checkout 的支援。Stripe Checkout 透過提供預構建的託管支付頁面,省去了實施自定義支付頁面所帶來的麻煩。
以下文件包含有關如何開始使用 Cashier 和 Stripe Checkout 的資訊。要了解有關 Stripe Checkout 的更多資訊,你還應該考慮查閱 Stripe 關於 Checkout 的文件。
產品結賬
你可以使用可計費模型上的 checkout 方法,為已在 Stripe 面板中建立的現有產品執行結賬。checkout 方法將啟動一個新的 Stripe Checkout 會話。預設情況下,你需要傳遞 Stripe 價格 ID。
1use Illuminate\Http\Request;2 3Route::get('/product-checkout', function (Request $request) {4 return $request->user()->checkout('price_tshirt');5});
如果需要,還可以指定產品數量。
1use Illuminate\Http\Request;2 3Route::get('/product-checkout', function (Request $request) {4 return $request->user()->checkout(['price_tshirt' => 15]);5});
當客戶訪問此路由時,他們將被重定向到 Stripe 的結賬頁面。預設情況下,當用戶成功完成或取消購買時,他們將被重定向到你的 home 路由位置,但你可以使用 success_url 和 cancel_url 選項指定自定義回撥 URL。
1use Illuminate\Http\Request;2 3Route::get('/product-checkout', function (Request $request) {4 return $request->user()->checkout(['price_tshirt' => 1], [5 'success_url' => route('your-success-route'),6 'cancel_url' => route('your-cancel-route'),7 ]);8});
定義 success_url 結賬選項時,你可以指示 Stripe 在呼叫你的 URL 時將結賬會話 ID 作為查詢字串引數新增。為此,請將字面字串 {CHECKOUT_SESSION_ID} 新增到你的 success_url 查詢字串中。Stripe 會將此佔位符替換為實際的結賬會話 ID。
1use Illuminate\Http\Request; 2use Stripe\Checkout\Session; 3use Stripe\Customer; 4 5Route::get('/product-checkout', function (Request $request) { 6 return $request->user()->checkout(['price_tshirt' => 1], [ 7 'success_url' => route('checkout-success').'?session_id={CHECKOUT_SESSION_ID}', 8 'cancel_url' => route('checkout-cancel'), 9 ]);10});11 12Route::get('/checkout-success', function (Request $request) {13 $checkoutSession = $request->user()->stripe()->checkout->sessions->retrieve($request->get('session_id'));14 15 return view('checkout.success', ['checkoutSession' => $checkoutSession]);16})->name('checkout-success');
促銷程式碼
預設情況下,Stripe Checkout 不允許 使用者兌換促銷程式碼。幸運的是,有一種簡單的方法可以在你的結賬頁面啟用它們。為此,可以呼叫 allowPromotionCodes 方法。
1use Illuminate\Http\Request;2 3Route::get('/product-checkout', function (Request $request) {4 return $request->user()5 ->allowPromotionCodes()6 ->checkout('price_tshirt');7});
單次扣款結賬
你還可以為尚未在 Stripe 面板中建立的臨時產品進行簡單扣款。為此,你可以在可計費模型上使用 checkoutCharge 方法,並向其傳遞應付金額、產品名稱和可選的數量。當客戶訪問此路由時,他們將被重定向到 Stripe 的結賬頁面。
1use Illuminate\Http\Request;2 3Route::get('/charge-checkout', function (Request $request) {4 return $request->user()->checkoutCharge(1200, 'T-Shirt', 5);5});
使用 checkoutCharge 方法時,Stripe 總是會在你的 Stripe 面板中建立一個新產品和價格。因此,我們建議你預先在 Stripe 面板中建立產品,並改用 checkout 方法。
訂閱結賬
使用 Stripe Checkout 進行訂閱需要你在 Stripe 面板中啟用 customer.subscription.created Webhook。此 Webhook 將在你的資料庫中建立訂閱記錄並存儲所有相關的訂閱項。
你也可以使用 Stripe Checkout 來啟動訂閱。在使用 Cashier 的訂閱構建器方法定義你的訂閱後,可以呼叫 checkout 方法。當客戶訪問此路由時,他們將被重定向到 Stripe 的結賬頁面。
1use Illuminate\Http\Request;2 3Route::get('/subscription-checkout', function (Request $request) {4 return $request->user()5 ->newSubscription('default', 'price_monthly')6 ->checkout();7});
就像產品結賬一樣,你可以自定義成功和取消 URL。
1use Illuminate\Http\Request; 2 3Route::get('/subscription-checkout', function (Request $request) { 4 return $request->user() 5 ->newSubscription('default', 'price_monthly') 6 ->checkout([ 7 'success_url' => route('your-success-route'), 8 'cancel_url' => route('your-cancel-route'), 9 ]);10});
當然,你也可以為訂閱結賬啟用促銷程式碼。
1use Illuminate\Http\Request;2 3Route::get('/subscription-checkout', function (Request $request) {4 return $request->user()5 ->newSubscription('default', 'price_monthly')6 ->allowPromotionCodes()7 ->checkout();8});
遺憾的是,Stripe Checkout 在啟動訂閱時不支援所有訂閱計費選項。在訂閱構建器上使用 anchorBillingCycleOn 方法、設定按比例計算行為或設定支付行為在 Stripe Checkout 會話期間不會有任何效果。請查閱 Stripe Checkout Session API 文件 以瞭解哪些引數可用。
Stripe Checkout 和試用期
當然,你可以在構建將使用 Stripe Checkout 完成的訂閱時定義試用期。
1$checkout = Auth::user()->newSubscription('default', 'price_monthly')2 ->trialDays(3)3 ->checkout();
但是,試用期必須至少為 48 小時,這是 Stripe Checkout 支援的最短試用時間。
訂閱和 Webhook
請記住,Stripe 和 Cashier 透過 Webhook 更新訂閱狀態,因此客戶輸入支付資訊返回應用程式後,訂閱可能尚未處於活動狀態。要處理此場景,你可能希望顯示一條訊息,告知使用者他們的付款或訂閱正在處理中。
收集稅務 ID
Checkout 還支援收集客戶的稅務 ID。要在結賬會話中啟用此功能,請在建立會話時呼叫 collectTaxIds 方法。
1$checkout = $user->collectTaxIds()->checkout('price_tshirt');
呼叫此方法後,客戶將看到一個新的複選框,允許他們指示是否以公司身份購買。如果是,他們將有機會提供他們的稅務 ID 號碼。
如果你已經在應用程式的服務提供者中配置了 自動稅務收集,那麼此功能將自動啟用,無需呼叫 collectTaxIds 方法。
訪客結賬
使用 Checkout::guest 方法,你可以為應用程式中沒有“賬戶”的訪客啟動結賬會話。
1use Illuminate\Http\Request;2use Laravel\Cashier\Checkout;3 4Route::get('/product-checkout', function (Request $request) {5 return Checkout::guest()->create('price_tshirt', [6 'success_url' => route('your-success-route'),7 'cancel_url' => route('your-cancel-route'),8 ]);9});
與為現有使用者建立結賬會話類似,你可以利用 Laravel\Cashier\CheckoutBuilder 例項上可用的額外方法來自定義訪客結賬會話。
1use Illuminate\Http\Request; 2use Laravel\Cashier\Checkout; 3 4Route::get('/product-checkout', function (Request $request) { 5 return Checkout::guest() 6 ->withPromotionCode('promo-code') 7 ->create('price_tshirt', [ 8 'success_url' => route('your-success-route'), 9 'cancel_url' => route('your-cancel-route'),10 ]);11});
訪客結賬完成後,Stripe 可以分發一個 checkout.session.completed Webhook 事件,因此請務必 配置你的 Stripe Webhook 以實際將此事件傳送到你的應用程式。在 Stripe 面板中啟用 Webhook 後,你就可以 使用 Cashier 處理 Webhook。Webhook 有效負載中包含的物件將是一個 結賬物件,你可以對其進行檢查以履行客戶的訂單。
處理支付失敗
有時,訂閱或單次扣款的支付可能會失敗。當發生這種情況時,Cashier 將會丟擲一個 Laravel\Cashier\Exceptions\IncompletePayment 異常來通知你。捕獲此異常後,你有兩種處理方式。
首先,你可以將客戶重定向到 Cashier 自帶的專用支付確認頁面。此頁面已經擁有透過 Cashier 服務提供者註冊的相關路由。因此,你可以捕獲 IncompletePayment 異常並將使用者重定向到該支付確認頁面。
1use Laravel\Cashier\Exceptions\IncompletePayment; 2 3try { 4 $subscription = $user->newSubscription('default', 'price_monthly') 5 ->create($paymentMethod); 6} catch (IncompletePayment $exception) { 7 return redirect()->route( 8 'cashier.payment', 9 [$exception->payment->id, 'redirect' => route('home')]10 );11}
在支付確認頁面上,客戶將被提示重新輸入信用卡資訊,並執行 Stripe 所需的任何額外操作,例如“3D Secure”驗證。確認支付後,使用者將被重定向到上述 redirect 引數指定的 URL。重定向時,URL 將會新增 message(字串)和 success(整數)查詢字串變數。目前支付頁面支援以下支付方式型別:
- 信用卡 (Credit Cards)
- 支付寶 (Alipay)
- Bancontact
- BECS 直接借記 (BECS Direct Debit)
- EPS
- Giropay
- iDEAL
- SEPA 直接借記 (SEPA Direct Debit)
另外,你也可以讓 Stripe 為你處理支付確認。在這種情況下,無需重定向到支付確認頁面,你可以在 Stripe 儀表板中 設定 Stripe 的自動賬單郵件。不過,如果捕獲到 IncompletePayment 異常,你仍然應該告知使用者,他們將收到一封包含進一步支付確認說明的電子郵件。
在使用 Billable trait 的模型上,以下方法可能會丟擲支付異常:charge、invoiceFor 和 invoice。在處理訂閱時,SubscriptionBuilder 上的 create 方法,以及 Subscription 和 SubscriptionItem 模型上的 incrementAndInvoice 和 swapAndInvoice 方法也可能會丟擲支付不完整異常。
你可以使用可計費模型或訂閱例項上的 hasIncompletePayment 方法來確定現有訂閱是否存在支付不完整的情況。
1if ($user->hasIncompletePayment('default')) {2 // ...3}4 5if ($user->subscription('default')->hasIncompletePayment()) {6 // ...7}
你可以透過檢查異常例項上的 payment 屬性來獲取支付不完整的具體狀態。
1use Laravel\Cashier\Exceptions\IncompletePayment; 2 3try { 4 $user->charge(1000, 'pm_card_threeDSecure2Required'); 5} catch (IncompletePayment $exception) { 6 // Get the payment intent status... 7 $exception->payment->status; 8 9 // Check specific conditions...10 if ($exception->payment->requiresPaymentMethod()) {11 // ...12 } elseif ($exception->payment->requiresConfirmation()) {13 // ...14 }15}
確認支付
某些支付方式需要額外資料才能確認支付。例如,SEPA 支付方式在支付過程中需要額外的“授權 (mandate)”資料。你可以使用 withPaymentConfirmationOptions 方法將這些資料提供給 Cashier。
1$subscription->withPaymentConfirmationOptions([2 'mandate_data' => '...',3])->swap('price_xxx');
你可以查閱 Stripe API 文件 以檢視確認支付時接受的所有選項。
強客戶身份驗證 (Strong Customer Authentication)
如果你的企業或你的客戶位於歐洲,則需要遵守歐盟的強客戶身份驗證 (SCA) 法規。這些法規由歐盟於 2019 年 9 月實施,旨在防止支付欺詐。幸運的是,Stripe 和 Cashier 已為構建符合 SCA 標準的應用程式做好了準備。
在開始之前,請查閱 Stripe 關於 PSD2 和 SCA 的指南 以及他們 關於新 SCA API 的文件。
需要額外確認的支付
SCA 法規通常需要額外的驗證來確認和處理支付。當發生這種情況時,Cashier 將丟擲一個 Laravel\Cashier\Exceptions\IncompletePayment 異常,通知你需要額外的驗證。關於如何處理這些異常的更多資訊,可以在 處理支付失敗 的文件中找到。
由 Stripe 或 Cashier 提供的支付確認介面可能會根據特定銀行或髮卡機構的支付流程進行調整,並可能包含額外的卡片驗證、臨時小額扣款、獨立的裝置認證或其他形式的驗證。
不完整與逾期狀態
當支付需要額外確認時,訂閱將保持在 incomplete 或 past_due 狀態,具體顯示在其 stripe_status 資料庫列中。一旦支付確認完成,且你的應用程式透過 Webhook 從 Stripe 收到完成通知,Cashier 將自動啟用客戶的訂閱。
有關 incomplete 和 past_due 狀態的更多資訊,請參閱 我們關於這些狀態的補充文件。
非會話支付通知 (Off-Session Payment Notifications)
由於 SCA 法規要求客戶即使在訂閱處於活躍狀態時,也偶爾需要驗證其支付詳情,因此當需要進行非會話支付確認時,Cashier 可以向客戶傳送通知。例如,當訂閱續訂時可能會發生這種情況。你可以透過將 CASHIER_PAYMENT_NOTIFICATION 環境變數設定為通知類來啟用 Cashier 的支付通知。預設情況下,此通知是停用的。當然,Cashier 包含了一個可用於此目的的通知類,但如果需要,你也可以自由提供自己的通知類。
1CASHIER_PAYMENT_NOTIFICATION=Laravel\Cashier\Notifications\ConfirmPayment
為確保非會話支付確認通知能夠送達,請驗證你的應用程式是否已 配置了 Stripe Webhook,並且在你的 Stripe 儀表板中啟用了 invoice.payment_action_required Webhook。此外,你的 Billable 模型還應使用 Laravel 的 Illuminate\Notifications\Notifiable trait。
即使客戶正在手動進行需要額外確認的支付時,通知也會被髮送。遺憾的是,Stripe 無法知道支付是手動完成的還是“非會話”完成的。但是,如果客戶在確認支付後訪問支付頁面,他們只會看到“支付成功”的訊息。客戶不會被允許意外地確認同一筆支付兩次,從而避免產生意外的二次扣款。
Stripe SDK
Cashier 的許多物件都是 Stripe SDK 物件的包裝器。如果你想直接與 Stripe 物件互動,可以使用 asStripe 方法方便地獲取它們。
1$stripeSubscription = $subscription->asStripeSubscription();2 3$stripeSubscription->application_fee_percent = 5;4 5$stripeSubscription->save();
你也可以使用 updateStripeSubscription 方法直接更新 Stripe 訂閱。
1$subscription->updateStripeSubscription(['application_fee_percent' => 5]);
如果你想直接使用 Stripe\StripeClient 客戶端,可以呼叫 Cashier 類上的 stripe 方法。例如,你可以使用此方法訪問 StripeClient 例項,並從你的 Stripe 賬戶中獲取價格列表。
1use Laravel\Cashier\Cashier;2 3$prices = Cashier::stripe()->prices->all();
測試
在測試使用 Cashier 的應用程式時,你可以模擬對 Stripe API 的實際 HTTP 請求;但這要求你部分重現 Cashier 本身的行為。因此,我們建議讓你的測試訪問真實的 Stripe API。雖然這樣速度較慢,但它能讓你更有把握確保應用程式按預期執行,且任何緩慢的測試都可以放置在它們自己的 Pest / PHPUnit 測試組中。
測試時請記住,Cashier 本身已經擁有完善的測試套件,因此你應該只專注於測試你自己應用程式的訂閱和支付流程,而不是測試每一個底層的 Cashier 行為。
要開始測試,請將你的 Stripe 金鑰的 testing 版本新增到 phpunit.xml 檔案中:
1<env name="STRIPE_SECRET" value="sk_test_<your-key>"/>
現在,每當你在測試中與 Cashier 互動時,它都會向你的 Stripe 測試環境傳送真實的 API 請求。為方便起見,你應該預先在你的 Stripe 測試賬戶中填充可在測試期間使用的訂閱/價格。
為了測試各種計費場景,例如信用卡拒絕和失敗,你可以使用 Stripe 提供的各種 測試卡號和令牌。