LaravelDocs(中文)

Laravel Cashier (Stripe)

Laravel Cashier Stripe 提供了訂閱計費服務的流暢介面

簡介 (Introduction)

Laravel Cashier StripeStripe 的訂閱計費服務提供了一個語意化且流暢的介面。它幾乎處理了所有你害怕編寫的訂閱計費樣板程式碼。除了基本的訂閱管理之外,Cashier 還可以處理折價券、交換訂閱、訂閱「數量」、取消寬限期,甚至產生發票 PDF。

升級 Cashier (Upgrading Cashier)

當升級到新版本的 Cashier 時,仔細閱讀 升級指南 非常重要。

[!WARNING] 為了防止破壞性變更,Cashier 使用固定的 Stripe API 版本。Cashier 16 使用 Stripe API 版本 2025-06-30.basil。Stripe API 版本將在次要版本發布時更新,以利用新的 Stripe 功能和改進。

安裝 (Installation)

首先,使用 Composer 套件管理器安裝 Stripe 的 Cashier 套件:

composer require laravel/cashier

安裝套件後,使用 vendor:publish Artisan 指令發布 Cashier 的 Migration:

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

然後,遷移你的資料庫:

php artisan migrate

Cashier 的 Migration 將向你的 users 資料表新增幾個欄位。它們還將建立一個新的 subscriptions 資料表來保存你所有客戶的訂閱,以及一個用於具有多個價格的訂閱的 subscription_items 資料表。

如果你願意,也可以使用 vendor:publish Artisan 指令發布 Cashier 的設定檔:

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

最後,為了確保 Cashier 正確處理所有 Stripe 事件,請記得 設定 Cashier 的 Webhook 處理

[!WARNING] Stripe 建議任何用於儲存 Stripe 識別碼的欄位都應區分大小寫。因此,當使用 MySQL 時,你應該確保 stripe_id 欄位的校對設定 (Collation) 為 utf8_bin。有關此內容的更多資訊,請參閱 Stripe 文件

設定 (Configuration)

可計費模型 (Billable Model)

在使用 Cashier 之前,請將 Billable Trait 新增到你的可計費模型定義中。通常,這將是 App\Models\User 模型。此 Trait 提供了各種方法,允許你執行常見的計費任務,例如建立訂閱、套用折價券和更新付款方式資訊:

use Laravel\Cashier\Billable;

class User extends Authenticatable
{
    use Billable;
}

Cashier 假設你的可計費模型將是 Laravel 隨附的 App\Models\User 類別。如果你想更改此設定,可以透過 useCustomerModel 方法指定不同的模型。此方法通常應在 AppServiceProvider 類別的 boot 方法中呼叫:

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

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

[!WARNING] 如果你使用的是 Laravel 提供的 App\Models\User 模型以外的模型,你需要發布並修改提供的 Cashier Migration,以符合你的替代模型的資料表名稱。

API 金鑰 (API Keys)

接下來,你應該在應用程式的 .env 檔案中設定你的 Stripe API 金鑰。你可以從 Stripe 控制面板檢索你的 Stripe API 金鑰:

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

[!WARNING] 你應該確保應用程式的 .env 檔案中定義了 STRIPE_WEBHOOK_SECRET 環境變數,因為此變數用於確保傳入的 Webhook 實際上來自 Stripe。

貨幣設定 (Currency Configuration)

預設的 Cashier 貨幣是美元 (USD)。你可以透過在應用程式的 .env 檔案中設定 CASHIER_CURRENCY 環境變數來更改預設貨幣:

CASHIER_CURRENCY=eur

除了設定 Cashier 的貨幣之外,你還可以指定在發票上顯示貨幣值時使用的區域設定。在內部,Cashier 利用 PHP 的 NumberFormatter 類別 來設定貨幣區域設定:

CASHIER_CURRENCY_LOCALE=nl_BE

[!WARNING] 為了使用 en 以外的區域設定,請確保你的伺服器上安裝並設定了 ext-intl PHP 擴充功能。

稅務設定 (Tax Configuration)

感謝 Stripe Tax,可以自動計算 Stripe 產生的所有發票的稅款。你可以透過在應用程式的 App\Providers\AppServiceProvider 類別的 boot 方法中呼叫 calculateTaxes 方法來啟用自動稅款計算:

use Laravel\Cashier\Cashier;

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

啟用稅款計算後,任何新訂閱和產生的任何一次性發票都將自動計算稅款。

為了使此功能正常運作,你的客戶的帳單詳細資訊(例如客戶的姓名、地址和稅務 ID)需要同步到 Stripe。你可以使用 Cashier 提供的 客戶資料同步稅務 ID 方法來完成此操作。

日誌 (Logging)

Cashier 允許你指定在記錄致命 Stripe 錯誤時使用的日誌通道。你可以透過在應用程式的 .env 檔案中定義 CASHIER_LOGGER 環境變數來指定日誌通道:

CASHIER_LOGGER=stack

由 Stripe API 呼叫產生的異常將透過應用程式的預設日誌通道進行記錄。

使用自訂模型 (Using Custom Models)

你可以透過定義自己的模型並繼承相應的 Cashier 模型來自由擴充 Cashier 內部使用的模型:

use Laravel\Cashier\Subscription as CashierSubscription;

class Subscription extends CashierSubscription
{
    // ...
}

定義模型後,你可以透過 Laravel\Cashier\Cashier 類別指示 Cashier 使用你的自訂模型。通常,你應該在應用程式的 App\Providers\AppServiceProvider 類別的 boot 方法中通知 Cashier 你的自訂模型:

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

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

快速入門 (Quickstart)

銷售產品 (Selling Products)

[!NOTE] 在使用 Stripe Checkout 之前,你應該在 Stripe 儀表板中定義具有固定價格的產品。此外,你應該 設定 Cashier 的 Webhook 處理

透過應用程式提供產品和訂閱計費可能會讓人感到畏懼。然而,感謝 Cashier 和 Stripe Checkout,你可以輕鬆建立現代、強大的支付整合。

為了向客戶收取非經常性、單次收費產品的費用,我們將利用 Cashier 引導客戶前往 Stripe Checkout,他們將在那裡提供付款詳細資訊並確認購買。透過 Checkout 完成付款後,客戶將被重新導向到你在應用程式中選擇的成功 URL:

use Illuminate\Http\Request;

Route::get('/checkout', function (Request $request) {
    $stripePriceId = 'price_deluxe_album';

    $quantity = 1;

    return $request->user()->checkout([$stripePriceId => $quantity], [
        'success_url' => route('checkout-success'),
        'cancel_url' => route('checkout-cancel'),
    ]);
})->name('checkout');

Route::view('/checkout/success', 'checkout.success')->name('checkout-success');
Route::view('/checkout/cancel', 'checkout.cancel')->name('checkout-cancel');

如你在上面的範例中所見,我們將利用 Cashier 提供的 checkout 方法將客戶重新導向到 Stripe Checkout 以進行給定的「價格識別碼」交易。當使用 Stripe 時,「價格」指的是 特定產品的定義價格

如果有必要,checkout 方法將自動在 Stripe 中建立客戶,並將該 Stripe 客戶記錄連接到應用程式資料庫中的相應使用者。完成結帳 Session 後,客戶將被重新導向到專用的成功或取消頁面,你可以在其中向客戶顯示資訊性訊息。

提供中繼資料給 Stripe Checkout (Providing Meta Data To Stripe Checkout)

銷售產品時,通常會透過應用程式定義的 CartOrder 模型來追蹤已完成的訂單和購買的產品。當將客戶重新導向到 Stripe Checkout 以完成購買時,你可能需要提供現有的訂單識別碼,以便在客戶被重新導向回你的應用程式時,將已完成的購買與相應的訂單關聯起來。

為此,你可以向 checkout 方法提供一個 metadata 陣列。讓我們想像一下,當使用者開始結帳流程時,我們的應用程式中會建立一個待處理的 Order。請記住,此範例中的 CartOrder 模型僅供說明,並非由 Cashier 提供。你可以根據自己應用程式的需求自由實作這些概念:

use App\Models\Cart;
use App\Models\Order;
use Illuminate\Http\Request;

Route::get('/cart/{cart}/checkout', function (Request $request, Cart $cart) {
    $order = Order::create([
        'cart_id' => $cart->id,
        'price_ids' => $cart->price_ids,
        'status' => 'incomplete',
    ]);

    return $request->user()->checkout($order->price_ids, [
        'success_url' => route('checkout-success').'?session_id={CHECKOUT_SESSION_ID}',
        'cancel_url' => route('checkout-cancel'),
        'metadata' => ['order_id' => $order->id],
    ]);
})->name('checkout');

如你在上面的範例中所見,當使用者開始結帳流程時,我們將提供所有購物車 / 訂單相關的 Stripe 價格識別碼給 checkout 方法。當然,當客戶新增這些項目時,你的應用程式負責將這些項目與「購物車」或訂單關聯起來。我們還透過 metadata 陣列將訂單 ID 提供給 Stripe Checkout Session。最後,我們將 CHECKOUT_SESSION_ID 樣板變數新增到 Checkout 成功路由中。當 Stripe 將客戶重新導向回你的應用程式時,此樣板變數將自動填入 Checkout Session ID。

接下來,讓我們建立 Checkout 成功路由。這是使用者在透過 Stripe Checkout 完成購買後將被重新導向到的路由。在此路由中,我們可以檢索 Stripe Checkout Session ID 和相關的 Stripe Checkout 實例,以便存取我們提供的中繼資料並相應地更新客戶的訂單:

use App\Models\Order;
use Illuminate\Http\Request;
use Laravel\Cashier\Cashier;

Route::get('/checkout/success', function (Request $request) {
    $sessionId = $request->get('session_id');

    if ($sessionId === null) {
        return;
    }

    $session = Cashier::stripe()->checkout->sessions->retrieve($sessionId);

    if ($session->payment_status !== 'paid') {
        return;
    }

    $orderId = $session['metadata']['order_id'] ?? null;

    $order = Order::findOrFail($orderId);

    $order->update(['status' => 'completed']);

    return view('checkout-success', ['order' => $order]);
})->name('checkout-success');

請參閱 Stripe 的文件以獲取有關 Checkout Session 物件包含的資料 的更多資訊。

銷售訂閱 (Selling Subscriptions)

[!NOTE] 在使用 Stripe Checkout 之前,你應該在 Stripe 儀表板中定義具有固定價格的產品。此外,你應該 設定 Cashier 的 Webhook 處理

透過應用程式提供產品和訂閱計費可能會讓人感到畏懼。然而,感謝 Cashier 和 Stripe Checkout,你可以輕鬆建立現代、強大的支付整合。

要學習如何使用 Cashier 和 Stripe Checkout 銷售訂閱,讓我們考慮一個簡單的訂閱服務場景,其中包含基本月繳 (price_basic_monthly) 和年繳 (price_basic_yearly) 方案。這兩個價格可以在我們的 Stripe 儀表板中歸類為「Basic」產品 (pro_basic)。此外,我們的訂閱服務可能會提供一個 Expert 方案作為 pro_expert

首先,讓我們了解客戶如何訂閱我們的服務。當然,你可以想像客戶可能會點擊我們應用程式定價頁面上 Basic 方案的「訂閱」按鈕。此按鈕或連結應將使用者引導至 Laravel 路由,該路由將為他們選擇的方案建立 Stripe Checkout Session:

use Illuminate\Http\Request;

Route::get('/subscription-checkout', function (Request $request) {
    return $request->user()
        ->newSubscription('default', 'price_basic_monthly')
        ->trialDays(5)
        ->allowPromotionCodes()
        ->checkout([
            'success_url' => route('your-success-route'),
            'cancel_url' => route('your-cancel-route'),
        ]);
});

如你在上面的範例中所見,我們將客戶重新導向到 Stripe Checkout Session,這將允許他們訂閱我們的 Basic 方案。成功結帳或取消後,客戶將被重新導向回我們提供給 checkout 方法的 URL。要知道他們的訂閱何時實際開始(因為某些付款方式需要幾秒鐘來處理),我們還需要 設定 Cashier 的 Webhook 處理

現在客戶可以開始訂閱了,我們需要限制應用程式的某些部分,以便只有已訂閱的使用者才能存取它們。當然,我們始終可以透過 Cashier 的 Billable Trait 提供的 subscribed 方法來確定使用者的當前訂閱狀態:

@if ($user->subscribed())
    <p>你已訂閱。</p>
@endif

我們甚至可以輕鬆確定使用者是否訂閱了特定產品或價格:

@if ($user->subscribedToProduct('pro_basic'))
    <p>你已訂閱我們的 Basic 產品。</p>
@endif

@if ($user->subscribedToPrice('price_basic_monthly'))
    <p>你已訂閱我們的月繳 Basic 方案。</p>
@endif

建立已訂閱 Middleware (Quickstart Building A Subscribed Middleware)

為了方便起見,你可能希望建立一個 Middleware 來確定傳入的請求是否來自已訂閱的使用者。定義此 Middleware 後,你可以輕鬆地將其分配給路由,以防止未訂閱的使用者存取該路由:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class Subscribed
{
    /**
     * Handle an incoming request.
     */
    public function handle(Request $request, Closure $next): Response
    {
        if (! $request->user()?->subscribed()) {
            // Redirect user to billing page and ask them to subscribe...
            return redirect('/billing');
        }

        return $next($request);
    }
}

定義 Middleware 後,你可以將其分配給路由:

use App\Http\Middleware\Subscribed;

Route::get('/dashboard', function () {
    // ...
})->middleware([Subscribed::class]);

允許客戶管理他們的計費方案 (Quickstart Allowing Customers To Manage Their Billing Plan)

當然,客戶可能希望將其訂閱方案更改為其他產品或「層級」。允許這樣做的最簡單方法是將客戶引導至 Stripe 的 客戶計費入口網站,該入口網站提供了一個託管的使用者介面,允許客戶下載發票、更新付款方式和更改訂閱方案。

首先,在你的應用程式中定義一個連結或按鈕,將使用者引導至 Laravel 路由,我們將利用該路由來啟動計費入口網站 Session:

<a href="{{ route('billing') }}">
    Billing
</a>

接下來,讓我們定義啟動 Stripe 客戶計費入口網站 Session 並將使用者重新導向到入口網站的路由。redirectToBillingPortal 方法接受使用者在退出入口網站時應返回的 URL:

use Illuminate\Http\Request;

Route::get('/billing', function (Request $request) {
    return $request->user()->redirectToBillingPortal(route('dashboard'));
})->middleware(['auth'])->name('billing');

[!NOTE] 只要你已設定 Cashier 的 Webhook 處理,Cashier 將透過檢查來自 Stripe 的傳入 Webhook 自動保持應用程式的 Cashier 相關資料庫資料表同步。因此,例如,當使用者透過 Stripe 的客戶計費入口網站取消訂閱時,Cashier 將收到相應的 Webhook 並在應用程式資料庫中將訂閱標記為「已取消」。

客戶 (Customers)

檢索客戶 (Retrieving Customers)

你可以使用 Cashier::findBillable 方法透過 Stripe ID 檢索客戶。此方法將回傳可計費模型的實例:

use Laravel\Cashier\Cashier;

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

建立客戶 (Creating Customers)

有時,你可能希望在不開始訂閱的情況下建立 Stripe 客戶。你可以使用 createAsStripeCustomer 方法來完成此操作:

$stripeCustomer = $user->createAsStripeCustomer();

一旦在 Stripe 中建立了客戶,你就可以在以後的日期開始訂閱。你可以提供一個可選的 $options 陣列來傳入任何額外的 Stripe API 支援的客戶建立參數

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

如果你想回傳可計費模型的 Stripe 客戶物件,可以使用 asStripeCustomer 方法:

$stripeCustomer = $user->asStripeCustomer();

如果你想檢索給定可計費模型的 Stripe 客戶物件,但不確定可計費模型是否已經是 Stripe 中的客戶,可以使用 createOrGetStripeCustomer 方法。如果 Stripe 中尚不存在客戶,此方法將建立一個新客戶:

$stripeCustomer = $user->createOrGetStripeCustomer();

更新客戶 (Updating Customers)

有時,你可能希望直接使用其他資訊更新 Stripe 客戶。你可以使用 updateStripeCustomer 方法來完成此操作。此方法接受 Stripe API 支援的客戶更新選項 陣列:

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

餘額 (Balances)

Stripe 允許你貸記或借記客戶的「餘額」。稍後,此餘額將在新的發票上貸記或借記。要檢查客戶的總餘額,你可以使用可計費模型上可用的 balance 方法。balance 方法將回傳客戶貨幣餘額的格式化字串表示形式:

$balance = $user->balance();

要貸記客戶的餘額,你可以向 creditBalance 方法提供一個值。如果你願意,你也可以提供一個描述:

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

debitBalance 方法提供一個值將借記客戶的餘額:

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

applyBalance 方法將為客戶建立新的客戶餘額交易。你可以使用 balanceTransactions 方法檢索這些交易記錄,這對於提供貸記和借記日誌供客戶查看可能很有用:

// 檢索所有交易...
$transactions = $user->balanceTransactions();

foreach ($transactions as $transaction) {
    // 交易金額...
    $amount = $transaction->amount(); // $2.31

    // 可用時檢索相關發票...
    $invoice = $transaction->invoice();
}

稅務 ID (Tax IDs)

Cashier 提供了一種簡單的方法來管理客戶的稅務 ID。例如,taxIds 方法可用於檢索分配給客戶的所有 稅務 ID 集合:

$taxIds = $user->taxIds();

你也可以透過其識別碼檢索客戶的特定稅務 ID:

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

你可以透過向 createTaxId 方法提供有效的 類型 和值來建立新的稅務 ID:

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

createTaxId 方法將立即將增值稅號新增到客戶的帳戶中。增值稅號的驗證也由 Stripe 完成;然而,這是一個非同步過程。你可以透過訂閱 customer.tax_id.updated Webhook 事件並檢查 增值稅號 verification 參數 來獲得驗證更新通知。有關處理 Webhook 的更多資訊,請參閱 定義 Webhook 處理器的文件

你可以使用 deleteTaxId 方法刪除稅務 ID:

$user->deleteTaxId('txi_belgium');

與 Stripe 同步客戶資料 (Syncing Customer Data With Stripe)

通常,當你的應用程式使用者更新他們的姓名、電子郵件地址或其他也儲存在 Stripe 中的資訊時,你應該通知 Stripe 這些更新。這樣做可以使 Stripe 的資訊副本與你的應用程式保持同步。

為了自動執行此操作,你可以在可計費模型上定義一個事件監聽器,以回應模型的 updated 事件。然後,在你的事件監聽器中,你可以呼叫模型上的 syncStripeCustomerDetails 方法:

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

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

現在,每當你的客戶模型更新時,其資訊都會與 Stripe 同步。為了方便起見,Cashier 會在最初建立客戶時自動將你的客戶資訊與 Stripe 同步。

你可以透過覆寫 Cashier 提供的各種方法來自訂用於將客戶資訊同步到 Stripe 的欄位。例如,你可以覆寫 stripeName 方法來自訂 Cashier 將客戶資訊同步到 Stripe 時應被視為客戶「姓名」的屬性:

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

同樣地,你可以覆寫 stripeEmailstripePhone(最多 20 個字元)、stripeAddressstripePreferredLocales 方法。當 更新 Stripe 客戶物件 時,這些方法將把資訊同步到其相應的客戶參數。如果你希望完全控制客戶資訊同步過程,你可以覆寫 syncStripeCustomerDetails 方法。

計費入口網站 (Billing Portal)

Stripe 提供了 一種設定計費入口網站的簡單方法,以便你的客戶可以管理他們的訂閱、付款方式並查看他們的計費記錄。你可以透過從控制器或路由呼叫可計費模型上的 redirectToBillingPortal 方法,將使用者重新導向到計費入口網站:

use Illuminate\Http\Request;

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

預設情況下,當使用者完成管理他們的訂閱時,他們將能夠透過 Stripe 計費入口網站內的連結返回應用程式的 home 路由。你可以透過將 URL 作為參數傳遞給 redirectToBillingPortal 方法來提供使用者應返回的自訂 URL:

use Illuminate\Http\Request;

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

如果你想產生計費入口網站的 URL 而不產生 HTTP 重新導向回應,你可以呼叫 billingPortalUrl 方法:

$url = $request->user()->billingPortalUrl(route('billing'));

付款方式 (Payment Methods)

儲存付款方式 (Storing Payment Methods)

為了使用 Stripe 建立訂閱或執行「一次性」收費,你需要儲存付款方式並從 Stripe 檢索其識別碼。用於完成此操作的方法取決於你打算將付款方式用於訂閱還是單次收費,因此我們將在下面檢查這兩種方法。

訂閱的付款方式 (Payment Methods for Subscriptions)

當儲存客戶的信用卡資訊以供訂閱將來使用時,必須使用 Stripe "Setup Intents" API 來安全地收集客戶的付款方式詳細資訊。"Setup Intent" 向 Stripe 表明向客戶付款方式收費的意圖。Cashier 的 Billable Trait 包含 createSetupIntent 方法,可輕鬆建立新的 Setup Intent。你應該從呈現收集客戶付款方式詳細資訊的表單的路由或控制器呼叫此方法:

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

建立 Setup Intent 並將其傳遞給視圖後,你應該將其 secret 附加到將收集付款方式的元素上。例如,考慮這個「更新付款方式」表單:

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

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

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

接下來,可以使用 Stripe.js 函式庫將 Stripe Element 附加到表單,並安全地收集客戶的付款詳細資訊:

<script src="https://js.stripe.com/v3/"></script>

<script>
  const stripe = Stripe("stripe-public-key");

  const elements = stripe.elements();
  const cardElement = elements.create("card");

  cardElement.mount("#card-element");
</script>

接下來,可以使用 Stripe 的 confirmCardSetup 方法 驗證卡片並從 Stripe 檢索安全的「付款方式識別碼」:

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

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

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

卡片經 Stripe 驗證後,你可以將產生的 setupIntent.payment_method 識別碼傳遞給你的 Laravel 應用程式,在那裡它可以附加到客戶。付款方式可以 新增為新付款方式用於更新預設付款方式。你也可以立即使用付款方式識別碼 建立新訂閱

[!NOTE] 如果你想了解有關 Setup Intents 和收集客戶付款詳細資訊的更多資訊,請 查看 Stripe 提供的此概述

單次收費的付款方式 (Payment Methods for Single Charges)

當然,當對客戶的付款方式進行單次收費時,我們只需要使用一次付款方式識別碼。由於 Stripe 的限制,你不能使用客戶儲存的預設付款方式進行單次收費。你必須允許客戶使用 Stripe.js 函式庫輸入他們的付款方式詳細資訊。例如,考慮以下表單:

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

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

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

定義此類表單後,可以使用 Stripe.js 函式庫將 Stripe Element 附加到表單,並安全地收集客戶的付款詳細資訊:

<script src="https://js.stripe.com/v3/"></script>

<script>
  const stripe = Stripe("stripe-public-key");

  const elements = stripe.elements();
  const cardElement = elements.create("card");

  cardElement.mount("#card-element");
</script>

接下來,可以使用 Stripe 的 createPaymentMethod 方法 驗證卡片並從 Stripe 檢索安全的「付款方式識別碼」:

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

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

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

如果卡片驗證成功,你可以將 paymentMethod.id 傳遞給你的 Laravel 應用程式並處理 單次收費

檢索付款方式 (Retrieving Payment Methods)

可計費模型實例上的 paymentMethods 方法回傳 Laravel\Cashier\PaymentMethod 實例的集合:

$paymentMethods = $user->paymentMethods();

預設情況下,此方法將回傳所有類型的付款方式。要檢索特定類型的付款方式,你可以將 type 作為參數傳遞給該方法:

$paymentMethods = $user->paymentMethods('sepa_debit');

要檢索客戶的預設付款方式,可以使用 defaultPaymentMethod 方法:

$paymentMethod = $user->defaultPaymentMethod();

你可以使用 findPaymentMethod 方法檢索附加到可計費模型的特定付款方式:

$paymentMethod = $user->findPaymentMethod($paymentMethodId);

付款方式存在性 (Payment Method Presence)

要確定可計費模型的帳戶是否附加了預設付款方式,請呼叫 hasDefaultPaymentMethod 方法:

if ($user->hasDefaultPaymentMethod()) {
    // ...
}

你可以使用 hasPaymentMethod 方法來確定可計費模型的帳戶是否有至少一個付款方式:

if ($user->hasPaymentMethod()) {
    // ...
}

此方法將確定可計費模型是否有任何付款方式。要確定模型是否存在特定類型的付款方式,你可以將 type 作為參數傳遞給該方法:

if ($user->hasPaymentMethod('sepa_debit')) {
    // ...
}

更新預設付款方式 (Updating the Default Payment Method)

updateDefaultPaymentMethod 方法可用於更新客戶的預設付款方式資訊。此方法接受 Stripe 付款方式識別碼,並將新的付款方式分配為預設計費付款方式:

$user->updateDefaultPaymentMethod($paymentMethod);

要將你的預設付款方式資訊與 Stripe 中客戶的預設付款方式資訊同步,你可以使用 updateDefaultPaymentMethodFromStripe 方法:

$user->updateDefaultPaymentMethodFromStripe();

[!WARNING] 客戶的預設付款方式只能用於開立發票和建立新訂閱。由於 Stripe 施加的限制,它可能無法用於單次收費。

新增付款方式 (Adding Payment Methods)

要新增付款方式,你可以在可計費模型上呼叫 addPaymentMethod 方法,並傳遞付款方式識別碼:

$user->addPaymentMethod($paymentMethod);

[!NOTE] 要了解如何檢索付款方式識別碼,請查看 付款方式儲存文件

刪除付款方式 (Deleting Payment Methods)

要刪除付款方式,你可以在希望刪除的 Laravel\Cashier\PaymentMethod 實例上呼叫 delete 方法:

$paymentMethod->delete();

deletePaymentMethod 方法將從可計費模型中刪除特定的付款方式:

$user->deletePaymentMethod('pm_visa');

deletePaymentMethods 方法將刪除可計費模型的所有付款方式資訊:

$user->deletePaymentMethods();

預設情況下,此方法將刪除所有類型的付款方式。要刪除特定類型的付款方式,你可以將 type 作為參數傳遞給該方法:

$user->deletePaymentMethods('sepa_debit');

[!WARNING] 如果使用者有有效的訂閱,你的應用程式不應允許他們刪除預設付款方式。

訂閱 (Subscriptions)

訂閱提供了一種為客戶設定定期付款的方法。由 Cashier 管理的 Stripe 訂閱支援多種訂閱價格、訂閱數量、試用等。

建立訂閱 (Creating Subscriptions)

要建立訂閱,首先檢索可計費模型的實例,通常是 App\Models\User 的實例。檢索到模型實例後,你可以使用 newSubscription 方法建立模型的訂閱:

use Illuminate\Http\Request;

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

    // ...
});

傳遞給 newSubscription 方法的第一個參數應該是訂閱的內部類型。如果你的應用程式只提供單個訂閱,你可以將其稱為 defaultprimary。此訂閱類型僅供內部應用程式使用,不旨在向使用者顯示。此外,它不應包含空格,並且在建立訂閱後絕不應更改。第二個參數是使用者訂閱的特定價格。此值應對應於 Stripe 中的價格識別碼。

create 方法接受 Stripe 付款方式識別碼 或 Stripe PaymentMethod 物件,將開始訂閱並使用可計費模型的 Stripe 客戶 ID 和其他相關計費資訊更新你的資料庫。

[!WARNING] 將付款方式識別碼直接傳遞給 create 訂閱方法也會自動將其新增到使用者儲存的付款方式中。

透過發票電子郵件收取定期付款 (Collecting Recurring Payments via Invoice Emails)

你可以指示 Stripe 在每次定期付款到期時向客戶發送發票電子郵件,而不是自動收取客戶的定期付款。然後,客戶在收到發票後可以手動支付發票。在透過發票收取定期付款時,客戶不需要預先提供付款方式:

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

客戶在訂閱被取消之前支付發票的時間由 days_until_due 選項決定。預設情況下,這是 30 天;但是,如果你願意,你可以為此選項提供特定值:

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

數量 (Quantities)

如果你想在建立訂閱時設定價格的特定 數量,你應該在建立訂閱之前呼叫訂閱建構器上的 quantity 方法:

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

額外詳細資訊 (Additional Details)

如果你想指定 Stripe 支援的額外 客戶訂閱 選項,你可以透過將它們作為第二個和第三個參數傳遞給 create 方法來實現:

$user->newSubscription('default', 'price_monthly')->create($paymentMethod, [
    'email' => $email,
], [
    'metadata' => ['note' => 'Some extra information.'],
]);

折價券 (Coupons)

如果你想在建立訂閱時套用折價券,可以使用 withCoupon 方法:

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

或者,如果你想套用 Stripe 促銷代碼,可以使用 withPromotionCode 方法:

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

給定的促銷代碼 ID 應該是分配給促銷代碼的 Stripe API ID,而不是面向客戶的促銷代碼。如果你需要根據給定的面向客戶的促銷代碼找到促銷代碼 ID,可以使用 findPromotionCode 方法:

// 透過面向客戶的代碼尋找促銷代碼 ID...
$promotionCode = $user->findPromotionCode('SUMMERSALE');

// 透過面向客戶的代碼尋找有效的促銷代碼 ID...
$promotionCode = $user->findActivePromotionCode('SUMMERSALE');

在上面的範例中,回傳的 $promotionCode 物件是 Laravel\Cashier\PromotionCode 的實例。此類別裝飾了底層的 Stripe\PromotionCode 物件。你可以透過呼叫 coupon 方法來檢索與促銷代碼相關的折價券:

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

折價券實例允許你確定折扣金額以及折價券是代表固定折扣還是基於百分比的折扣:

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

你也可以檢索目前套用於客戶或訂閱的折扣:

$discount = $billable->discount();

$discount = $subscription->discount();

回傳的 Laravel\Cashier\Discount 實例裝飾了底層的 Stripe\Discount 物件實例。你可以透過呼叫 coupon 方法來檢索與此折扣相關的折價券:

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

如果你想將新的折價券或促銷代碼套用於客戶或訂閱,可以透過 applyCouponapplyPromotionCode 方法來實現:

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

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

請記住,你應該使用分配給促銷代碼的 Stripe API ID,而不是面向客戶的促銷代碼。在給定時間內,只能將一個折價券或促銷代碼套用於客戶或訂閱。

有關此主題的更多資訊,請參閱關於 折價券促銷代碼 的 Stripe 文件。

新增訂閱 (Adding Subscriptions)

如果你想為已經有預設付款方式的客戶新增訂閱,你可以在訂閱建構器上呼叫 add 方法:

use App\Models\User;

$user = User::find(1);

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

從 Stripe 儀表板建立訂閱 (Creating Subscriptions From the Stripe Dashboard)

你也可以從 Stripe 儀表板本身建立訂閱。這樣做時,Cashier 將同步新新增的訂閱並為其分配 default 類型。要自訂分配給儀表板建立的訂閱的訂閱類型,請 定義 Webhook 事件處理器

此外,你只能透過 Stripe 儀表板建立一種類型的訂閱。如果你的應用程式提供使用不同類型的多個訂閱,則只能透過 Stripe 儀表板新增一種類型的訂閱。

最後,你應該始終確保應用程式提供的每種訂閱類型只新增一個有效訂閱。如果客戶有兩個 default 訂閱,即使兩者都將與你的應用程式資料庫同步,Cashier 也只會使用最近新增的訂閱。

檢查訂閱狀態 (Checking Subscription Status)

一旦客戶訂閱了你的應用程式,你可以使用各種方便的方法輕鬆檢查他們的訂閱狀態。首先,如果客戶有有效的訂閱,即使訂閱目前處於試用期,subscribed 方法也會回傳 truesubscribed 方法接受訂閱類型作為其第一個參數:

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

subscribed 方法也是 路由 Middleware 的絕佳候選者,允許你根據使用者的訂閱狀態過濾對路由和控制器的存取:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class EnsureUserIsSubscribed
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        if ($request->user() && ! $request->user()->subscribed('default')) {
            // 這位使用者不是付費客戶...
            return redirect('/billing');
        }

        return $next($request);
    }
}

如果你想確定使用者是否仍處於試用期內,可以使用 onTrial 方法。此方法對於確定是否應向使用者顯示他們仍處於試用期內的警告很有用:

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

subscribedToProduct 方法可用於根據給定的 Stripe 產品識別碼確定使用者是否訂閱了給定產品。在 Stripe 中,產品是價格的集合。在此範例中,我們將確定使用者的 default 訂閱是否有效訂閱了應用程式的「premium」產品。給定的 Stripe 產品識別碼應對應於 Stripe 儀表板中的產品識別碼之一:

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

透過將陣列傳遞給 subscribedToProduct 方法,你可以確定使用者的 default 訂閱是否有效訂閱了應用程式的「basic」或「premium」產品:

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

subscribedToPrice 方法可用於確定客戶的訂閱是否對應於給定的價格 ID:

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

recurring 方法可用於確定使用者目前是否已訂閱且不再處於試用期內:

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

[!WARNING] 如果使用者有兩個相同類型的訂閱,subscription 方法將始終回傳最近的訂閱。例如,使用者可能有兩個類型為 default 的訂閱記錄;然而,其中一個訂閱可能是一個舊的、已過期的訂閱,而另一個是當前的、有效的訂閱。最近的訂閱將始終被回傳,而較舊的訂閱保留在資料庫中以供歷史審查。

已取消的訂閱狀態 (Canceled Subscription Status)

要確定使用者是否曾是有效訂閱者但已取消訂閱,可以使用 canceled 方法:

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

你也可以確定使用者是否已取消訂閱,但仍處於「寬限期」,直到訂閱完全過期。例如,如果使用者在 3 月 5 日取消了原定於 3 月 10 日到期的訂閱,則使用者在 3 月 10 日之前處於「寬限期」。請注意,在此期間 subscribed 方法仍回傳 true

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

要確定使用者是否已取消訂閱且不再處於「寬限期」內,可以使用 ended 方法:

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

未完成和逾期狀態 (Incomplete and Past Due Status)

如果訂閱在建立後需要二次付款操作,訂閱將被標記為 incomplete。訂閱狀態儲存在 Cashier 的 subscriptions 資料庫資料表的 stripe_status 欄位中。

同樣地,如果在交換價格時需要二次付款操作,訂閱將被標記為 past_due。當你的訂閱處於這兩種狀態之一時,直到客戶確認付款之前,它將不會啟用。確定訂閱是否有未完成的付款可以使用可計費模型或訂閱實例上的 hasIncompletePayment 方法來完成:

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

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

當訂閱有未完成的付款時,你應該將使用者引導至 Cashier 的付款確認頁面,並傳遞 latestPayment 識別碼。你可以使用訂閱實例上可用的 latestPayment 方法來檢索此識別碼:

<a href="{{ route('cashier.payment', $subscription->latestPayment()->id) }}">
  請確認你的付款。
</a>

如果你希望訂閱在 past_dueincomplete 狀態下仍被視為有效,你可以使用 Cashier 提供的 keepPastDueSubscriptionsActivekeepIncompleteSubscriptionsActive 方法。通常,這些方法應在 App\Providers\AppServiceProviderregister 方法中呼叫:

use Laravel\Cashier\Cashier;

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

[!WARNING] 當訂閱處於 incomplete 狀態時,直到確認付款之前都無法更改。因此,當訂閱處於 incomplete 狀態時,swapupdateQuantity 方法將拋出例外。

訂閱範圍 (Subscription Scopes)

大多數訂閱狀態也可用作查詢範圍,以便你可以輕鬆查詢資料庫中處於給定狀態的訂閱:

// 獲取所有有效訂閱...
$subscriptions = Subscription::query()->active()->get();

// 獲取使用者的所有已取消訂閱...
$subscriptions = $user->subscriptions()->canceled()->get();

可用範圍的完整列表如下:

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

更改價格 (Changing Prices)

客戶訂閱你的應用程式後,他們可能偶爾會想更改為新的訂閱價格。要將客戶交換到新價格,請將 Stripe 價格的識別碼傳遞給 swap 方法。交換價格時,假設使用者希望重新啟用他們的訂閱(如果之前已取消)。給定的價格識別碼應對應於 Stripe 儀表板中可用的 Stripe 價格識別碼:

use App\Models\User;

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

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

如果客戶處於試用期,試用期將被保留。此外,如果訂閱存在「數量」,該數量也將被保留。

如果你想交換價格並取消客戶目前所在的任何試用期,可以使用 skipTrial 方法:

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

如果你想交換價格並立即向客戶開立發票,而不是等待下一個計費週期,可以使用 swapAndInvoice 方法:

$user = User::find(1);

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

依比例計算 (Prorations)

預設情況下,Stripe 在交換價格時會依比例計算費用。noProrate 方法可用於在不依比例計算費用的情況下更新訂閱價格:

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

有關訂閱依比例計算的更多資訊,請參閱 Stripe 文件

[!WARNING] 在 swapAndInvoice 方法之前執行 noProrate 方法對依比例計算沒有影響。將始終開立發票。

訂閱數量 (Subscription Quantity)

有時訂閱會受到「數量」的影響。例如,專案管理應用程式可能每月向每個專案收取 10 美元。你可以使用 incrementQuantitydecrementQuantity 方法輕鬆增加或減少訂閱數量:

use App\Models\User;

$user = User::find(1);

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

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

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

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

Alternatively, you may set a specific quantity using the updateQuantity method:

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

noProrate 方法可用於在不依比例計算費用的情況下更新訂閱數量:

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

有關訂閱數量的更多資訊,請參閱 Stripe 文件

具有多個產品的訂閱的數量 (Quantities for Subscriptions With Multiple Products)

如果你的訂閱是 具有多個產品的訂閱,你應該將希望增加或減少數量的價格的 ID 作為第二個參數傳遞給增加 / 減少方法:

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

具有多個產品的訂閱 (Subscriptions With Multiple Products)

具有多個產品的訂閱 允許你將多個計費產品分配給單個訂閱。例如,想像一下你正在建立一個客戶服務「幫助台」應用程式,其基本訂閱價格為每月 10 美元,但提供每月額外 15 美元的即時聊天附加產品。具有多個產品的訂閱資訊儲存在 Cashier 的 subscription_items 資料表中。

你可以透過將價格陣列作為第二個參數傳遞給 newSubscription 方法來為給定訂閱指定多個產品:

use Illuminate\Http\Request;

Route::post('/user/subscribe', function (Request $request) {
    $request->user()->newSubscription('default', [
        'price_monthly',
        'price_chat',
    ])->create($request->paymentMethodId);

    // ...
});

在上面的範例中,客戶的 default 訂閱將附加兩個價格。這兩個價格都將按其各自的計費週期收費。如果有必要,你可以使用 quantity 方法來為每個價格指定特定數量:

$user = User::find(1);

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

如果你想向現有訂閱新增另一個價格,可以呼叫訂閱的 addPrice 方法:

$user = User::find(1);

$user->subscription('default')->addPrice('price_chat');

上面的範例將新增新價格,客戶將在下一個計費週期收到帳單。如果你想立即向客戶收費,可以使用 addPriceAndInvoice 方法:

$user->subscription('default')->addPriceAndInvoice('price_chat');

如果你想新增具有特定數量的價格,可以將數量作為 addPriceaddPriceAndInvoice 方法的第二個參數傳遞:

$user = User::find(1);

$user->subscription('default')->addPrice('price_chat', 5);

你可以使用 removePrice 方法從訂閱中移除價格:

$user->subscription('default')->removePrice('price_chat');

[!WARNING] 你不能移除訂閱的最後一個價格。相反,你應該簡單地取消訂閱。

交換價格 (Swapping Prices)

你也可以更改附加到具有多個產品的訂閱的價格。例如,想像一下客戶擁有帶有 price_chat 附加產品的 price_basic 訂閱,你想將客戶從 price_basic 升級到 price_pro 價格:

use App\Models\User;

$user = User::find(1);

$user->subscription('default')->swap(['price_pro', 'price_chat']);

執行上面的範例時,帶有 price_basic 的底層訂閱項目將被刪除,而帶有 price_chat 的項目將被保留。此外,將為 price_pro 建立一個新的訂閱項目。

你也可以透過將鍵 / 值對陣列傳遞給 swap 方法來指定訂閱項目選項。例如,你可能需要指定訂閱價格數量:

$user = User::find(1);

$user->subscription('default')->swap([
    'price_pro' => ['quantity' => 5],
    'price_chat'
]);

如果你想交換訂閱上的單個價格,可以在訂閱項目本身上使用 swap 方法。如果你想保留訂閱其他價格的所有現有中繼資料,此方法特別有用:

$user = User::find(1);

$user->subscription('default')
    ->findItemOrFail('price_basic')
    ->swap('price_pro');

依比例計算 (Proration)

預設情況下,當從具有多個產品的訂閱中新增或移除價格時,Stripe 將依比例計算費用。如果你想在不依比例計算的情況下進行價格調整,你應該將 noProrate 方法串連到你的價格操作上:

$user->subscription('default')->noProrate()->removePrice('price_chat');

數量 (Quantities)

如果你想更新各個訂閱價格的數量,可以使用 現有的數量方法,並將價格 ID 作為該方法的額外參數傳遞:

$user = User::find(1);

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

$user->subscription('default')->decrementQuantity(3, 'price_chat');

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

[!WARNING] 當訂閱有多個價格時,Subscription 模型上的 stripe_pricequantity 屬性將為 null。要存取各個價格屬性,你應該使用 Subscription 模型上可用的 items 關聯。

訂閱項目 (Subscription Items)

當訂閱有多個價格時,它將在資料庫的 subscription_items 資料表中擁有多個訂閱「項目」。你可以透過訂閱上的 items 關聯來存取這些項目:

use App\Models\User;

$user = User::find(1);

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

// 檢索特定項目的 Stripe 價格和數量...
$stripePrice = $subscriptionItem->stripe_price;
$quantity = $subscriptionItem->quantity;

你也可以使用 findItemOrFail 方法檢索特定價格:

$user = User::find(1);

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

多個訂閱 (Multiple Subscriptions)

Stripe 允許你的客戶同時擁有多個訂閱。例如,你可能經營一家健身房,提供游泳訂閱和舉重訂閱,並且每個訂閱可能有不同的定價。當然,客戶應該能夠訂閱其中一個或兩個方案。

當你的應用程式建立訂閱時,你可以向 newSubscription 方法提供訂閱類型。類型可以是代表使用者正在啟動的訂閱類型的任何字串:

use Illuminate\Http\Request;

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

    // ...
});

在此範例中,我們為客戶啟動了每月游泳訂閱。然而,如果不滿意,他們可能會想在以後交換到年度訂閱。當調整客戶的訂閱時,我們可以簡單地交換 swimming 訂閱上的價格:

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

當然,你也可以完全取消訂閱:

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

基於使用量的計費 (Usage Based Billing)

基於使用量的計費 允許你根據客戶在計費週期內的產品使用量向他們收費。例如,你可以根據客戶每月發送的簡訊或電子郵件數量向他們收費。

要開始使用用量計費,你首先需要在 Stripe 儀表板中建立一個具有 基於使用量的計費模型計量器 的新產品。建立計量器後,儲存相關的事件名稱和計量器 ID,你需要它們來報告和檢索用量。然後,使用 meteredPrice 方法將計量價格 ID 新增到客戶訂閱:

use Illuminate\Http\Request;

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

    // ...
});

你也可以透過 Stripe Checkout 啟動計量訂閱:

$checkout = Auth::user()
    ->newSubscription('default', [])
    ->meteredPrice('price_metered')
    ->checkout();

return view('your-checkout-view', [
    'checkout' => $checkout,
]);

報告用量 (Reporting Usage)

當你的客戶使用你的應用程式時,你將向 Stripe 報告他們的用量,以便準確地向他們收費。要報告計量事件的用量,你可以使用 Billable 模型上的 reportMeterEvent 方法:

$user = User::find(1);

$user->reportMeterEvent('emails-sent');

預設情況下,計費週期會新增 1 的「用量數量」。或者,你可以傳遞特定數量的「用量」以新增到客戶在計費週期的用量中:

$user = User::find(1);

$user->reportMeterEvent('emails-sent', quantity: 15);

要檢索客戶計量器的事件摘要,你可以使用 Billable 實例的 meterEventSummaries 方法:

$user = User::find(1);

$meterUsage = $user->meterEventSummaries($meterId);

$meterUsage->first()->aggregated_value // 10

有關計量器事件摘要的更多資訊,請參閱 Stripe 的 計量器事件摘要物件文件

列出所有計量器,你可以使用 Billable 實例的 meters 方法:

$user = User::find(1);

$user->meters();

訂閱稅務 (Subscription Taxes)

[!WARNING] 你可以使用 Stripe Tax 自動計算稅款,而不是手動計算稅率。

要指定使用者為訂閱支付的稅率,你應該在可計費模型上實作 taxRates 方法,並回傳包含 Stripe 稅率 ID 的陣列。你可以在 Stripe 儀表板 中定義這些稅率:

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

taxRates 方法使你能夠在逐個客戶的基礎上套用稅率,這對於跨越並擁有多個國家和稅率的使用者群可能很有幫助。

如果你提供具有多個產品的訂閱,你可以透過在可計費模型上實作 priceTaxRates 方法為每個價格定義不同的稅率:

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

[!WARNING] taxRates 方法僅適用於訂閱費用。如果你使用 Cashier 進行「一次性」收費,你需要在當時手動指定稅率。

同步稅率 (Syncing Tax Rates)

當更改 taxRates 方法回傳的硬編碼稅率 ID 時,使用者任何現有訂閱的稅務設定將保持不變。如果你希望使用新的 taxRates 值更新現有訂閱的稅務值,你應該在使用者訂閱實例上呼叫 syncTaxRates 方法:

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

這還將同步具有多個產品的訂閱的任何項目稅率。如果你的應用程式提供具有多個產品的訂閱,你應該確保你的可計費模型實作了 上面討論的 priceTaxRates 方法。

免稅 (Tax Exemption)

Cashier 還提供 isNotTaxExemptisTaxExemptreverseChargeApplies 方法來確定客戶是否免稅。這些方法將呼叫 Stripe API 來確定客戶的免稅狀態:

use App\Models\User;

$user = User::find(1);

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

[!WARNING] 這些方法也可以在任何 Laravel\Cashier\Invoice 物件上使用。然而,當在 Invoice 物件上呼叫時,這些方法將確定發票建立時的免稅狀態。

訂閱錨定日期 (Subscription Anchor Date)

預設情況下,計費週期錨點是建立訂閱的日期,或者如果使用試用期,則是試用結束的日期。如果你想修改計費錨定日期,可以使用 anchorBillingCycleOn 方法:

use Illuminate\Http\Request;

Route::post('/user/subscribe', function (Request $request) {
    $anchor = Carbon::parse('first day of next month');

    $request->user()->newSubscription('default', 'price_monthly')
        ->anchorBillingCycleOn($anchor->startOfDay())
        ->create($request->paymentMethodId);

    // ...
});

有關管理訂閱計費週期的更多資訊,請諮詢 Stripe 計費週期文件

取消訂閱 (Canceling Subscriptions)

要取消訂閱,請在使用者訂閱上呼叫 cancel 方法:

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

取消訂閱後,Cashier 將自動設定 subscriptions 資料庫資料表中的 ends_at 欄位。此欄位用於了解 subscribed 方法何時應開始回傳 false

例如,如果客戶在 3 月 1 日取消訂閱,但訂閱原定於 3 月 5 日才結束,則 subscribed 方法將繼續回傳 true 直到 3 月 5 日。這樣做是因為通常允許使用者繼續使用應用程式直到他們的計費週期結束。

你可以使用 onGracePeriod 方法確定使用者是否已取消訂閱但仍處於「寬限期」:

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

如果你希望立即取消訂閱,請呼叫使用者訂閱上的 cancelNow 方法:

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

如果你希望立即取消訂閱並為任何剩餘的未開發票的計量用量或新的 / 待處理的依比例計算發票項目開立發票,請呼叫使用者訂閱上的 cancelNowAndInvoice 方法:

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

你也可以選擇在特定時間取消訂閱:

$user->subscription('default')->cancelAt(
    now()->addDays(10)
);

最後,你應該始終在刪除相關使用者模型之前取消使用者訂閱:

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

$user->delete();

恢復訂閱 (Resuming Subscriptions)

如果客戶取消了訂閱,而你希望恢復它,你可以在訂閱上呼叫 resume 方法。客戶必須仍處於「寬限期」內才能恢復訂閱:

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

如果客戶取消訂閱,然後在訂閱完全過期之前恢復該訂閱,則不會立即向客戶收費。相反,他們的訂閱將被重新啟用,並將在原來的計費週期向他們收費。

訂閱試用 (Subscription Trials)

預先提供付款方式 (With Payment Method Up Front)

如果你想向客戶提供試用期,同時仍預先收集付款方式資訊,你應該在建立訂閱時使用 trialDays 方法:

use Illuminate\Http\Request;

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

    // ...
});

此方法將在資料庫中的訂閱記錄上設定試用期結束日期,並指示 Stripe 在此日期之後才開始向客戶收費。當使用 trialDays 方法時,Cashier 將覆寫 Stripe 中為價格設定的任何預設試用期。

[!WARNING] 如果客戶的訂閱未在試用結束日期之前取消,他們將在試用期滿後立即被收費,因此你應該務必通知使用者他們的試用結束日期。

trialUntil 方法允許你提供一個 DateTime 實例,用於指定試用期何時結束:

use Illuminate\Support\Carbon;

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

你可以使用使用者實例的 onTrial 方法或訂閱實例的 onTrial 方法來確定使用者是否處於試用期內。以下兩個範例是等效的:

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

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

你可以使用 endTrial 方法立即結束訂閱試用:

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

要確定現有試用是否已過期,你可以使用 hasExpiredTrial 方法:

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

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

在 Stripe / Cashier 中定義試用天數 (Defining Trial Days in Stripe / Cashier)

你可以選擇在 Stripe 儀表板中定義你的價格獲得多少試用天數,或者始終使用 Cashier 明確傳遞它們。如果你選擇在 Stripe 中定義價格的試用天數,你應該知道,除非你明確呼叫 skipTrial() 方法,否則新訂閱(包括過去擁有訂閱的客戶的新訂閱)將始終獲得試用期。

無需預先提供付款方式 (Without Payment Method Up Front)

如果你想在不預先收集使用者付款方式資訊的情況下提供試用期,你可以將使用者記錄上的 trial_ends_at 欄位設定為你想要的試用結束日期。這通常在使用者註冊期間完成:

use App\Models\User;

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

[!WARNING] 請務必在可計費模型的類別定義中為 trial_ends_at 屬性新增 日期轉換 (Date Cast)

Cashier 將這種類型的試用稱為「通用試用」,因為它未附加到任何現有訂閱。如果當前日期未超過 trial_ends_at 的值,則可計費模型實例上的 onTrial 方法將回傳 true

if ($user->onTrial()) {
    // 使用者處於試用期...
}

一旦你準備好為使用者建立實際訂閱,你可以像往常一樣使用 newSubscription 方法:

$user = User::find(1);

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

要檢索使用者的試用結束日期,可以使用 trialEndsAt 方法。如果使用者處於試用期,此方法將回傳 Carbon 日期實例,如果不是,則回傳 null。如果你想獲取預設訂閱以外的特定訂閱的試用結束日期,你也可以傳遞可選的訂閱類型參數:

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

如果你想特別知道使用者是否處於其「通用」試用期內且尚未建立實際訂閱,也可以使用 onGenericTrial 方法:

if ($user->onGenericTrial()) {
    // 使用者處於「通用」試用期...
}

延長試用 (Extending Trials)

extendTrial 方法允許你在建立訂閱後延長訂閱的試用期。如果試用期已過期,並且客戶已開始為訂閱付費,你仍然可以為他們提供延長試用。試用期內花費的時間將從客戶的下一張發票中扣除:

use App\Models\User;

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

// 在 7 天後結束試用...
$subscription->extendTrial(
    now()->addDays(7)
);

// 試用期再增加 5 天...
$subscription->extendTrial(
    $subscription->trial_ends_at->addDays(5)
);

處理 Stripe Webhook (Handling Stripe Webhooks)

[!NOTE] 你可以使用 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.created
  • customer.subscription.updated
  • customer.subscription.deleted
  • customer.updated
  • customer.deleted
  • payment_method.automatically_updated
  • invoice.payment_action_required
  • invoice.payment_succeeded

為了方便起見,Cashier 包含一個 cashier:webhook Artisan 指令。此指令將在 Stripe 中建立一個 Webhook,監聽 Cashier 所需的所有事件:

php artisan cashier:webhook

預設情況下,建立的 Webhook 將指向 APP_URL 環境變數定義的 URL 和 Cashier 包含的 cashier.webhook 路由。如果你想使用不同的 URL,可以在呼叫指令時提供 --url 選項:

php artisan cashier:webhook --url "https://example.com/stripe/webhook"

建立的 Webhook 將使用與你的 Cashier 版本相容的 Stripe API 版本。如果你想使用不同的 Stripe 版本,可以提供 --api-version 選項:

php artisan cashier:webhook --api-version="2019-12-03"

建立後,Webhook 將立即啟用。如果你希望建立 Webhook 但暫時將其停用,直到你準備好為止,可以在呼叫指令時提供 --disabled 選項:

php artisan cashier:webhook --disabled

[!WARNING] 確保使用 Cashier 包含的 Webhook 簽章驗證 Middleware 保護傳入的 Stripe Webhook 請求。

Webhook 與 CSRF 保護 (Webhooks and CSRF Protection)

由於 Stripe Webhook 需要繞過 Laravel 的 CSRF 保護,你應該確保 Laravel 不會嘗試驗證傳入 Stripe Webhook 的 CSRF Token。為此,你應該在應用程式的 bootstrap/app.php 檔案中將 stripe/* 排除在 CSRF 保護之外:

->withMiddleware(function (Middleware $middleware): void {
    $middleware->validateCsrfTokens(except: [
        'stripe/*',
    ]);
})

定義 Webhook 事件處理器 (Defining Webhook Event Handlers)

Cashier 會自動處理失敗收費的訂閱取消和其他常見的 Stripe Webhook 事件。但是,如果你有其他想要處理的 Webhook 事件,你可以透過監聽 Cashier 派發的以下事件來完成:

  • Laravel\Cashier\Events\WebhookReceived
  • Laravel\Cashier\Events\WebhookHandled

這兩個事件都包含 Stripe Webhook 的完整 Payload。例如,如果你希望處理 invoice.payment_succeeded Webhook,可以註冊一個將處理該事件的 監聽器

<?php

namespace App\Listeners;

use Laravel\Cashier\Events\WebhookReceived;

class StripeEventListener
{
    /**
     * Handle received Stripe webhooks.
     */
    public function handle(WebhookReceived $event): void
    {
        if ($event->payload['type'] === 'invoice.payment_succeeded') {
            // Handle the incoming event...
        }
    }
}

驗證 Webhook 簽章 (Verifying Webhook Signatures)

為了保護你的 Webhook,你可以使用 Stripe 的 Webhook 簽章。為了方便起見,Cashier 自動包含一個 Middleware,用於驗證傳入的 Stripe Webhook 請求是否有效。

要啟用 Webhook 驗證,請確保在應用程式的 .env 檔案中設定了 STRIPE_WEBHOOK_SECRET 環境變數。可以從你的 Stripe 帳戶儀表板中檢索 Webhook secret

單次收費 (Single Charges)

簡單收費 (Simple Charge)

如果你想對客戶進行一次性收費,可以在可計費模型實例上使用 charge 方法。你需要 提供付款方式識別碼 作為 charge 方法的第二個參數:

use Illuminate\Http\Request;

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

    // ...
});

charge 方法接受一個陣列作為其第三個參數,允許你將任何選項傳遞給底層 Stripe 收費建立。有關建立收費時可用的選項的更多資訊,可以在 Stripe 文件 中找到:

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

你也可以在沒有底層客戶或使用者的情況下使用 charge 方法。為此,請在應用程式的可計費模型的新實例上呼叫 charge 方法:

use App\Models\User;

$stripeCharge = (new User)->charge(100, $paymentMethod);

如果收費失敗,charge 方法將拋出例外。如果收費成功,該方法將回傳 Laravel\Cashier\Payment 的實例:

try {
    $payment = $user->charge(100, $paymentMethod);
} catch (Exception $e) {
    // ...
}

[!WARNING] charge 方法接受以應用程式使用的貨幣的最小單位表示的付款金額。例如,如果客戶以美元付款,則金額應以美分指定。

發票收費 (Charge With Invoice)

有時你可能需要進行一次性收費並向客戶提供 PDF 發票。invoicePrice 方法可以讓你做到這一點。例如,讓我們向客戶開具五件新襯衫的發票:

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

發票將立即從使用者的預設付款方式中扣款。invoicePrice 方法也接受一個陣列作為其第三個參數。此陣列包含發票項目的計費選項。該方法接受的第四個參數也是一個陣列,其中應包含發票本身的計費選項:

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

invoicePrice 類似,你可以使用 tabPrice 方法透過將多個項目(每張發票最多 250 個項目)新增到客戶的「帳單」然後向客戶開具發票來建立一次性收費。例如,我們可以向客戶開具五件襯衫和兩個馬克杯的發票:

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

或者,你可以使用 invoiceFor 方法對客戶的預設付款方式進行「一次性」收費:

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

雖然 invoiceFor 方法可供你使用,但建議你將 invoicePricetabPrice 方法與預定義價格一起使用。這樣做,你將能夠在 Stripe 儀表板中存取有關每種產品銷售的更好分析和數據。

[!WARNING] invoiceinvoicePriceinvoiceFor 方法將建立一個 Stripe 發票,該發票將重試失敗的計費嘗試。如果你不希望發票重試失敗的收費,你需要在第一次失敗的收費後使用 Stripe API 關閉它們。

建立 Payment Intent (Creating Payment Intents)

你可以透過在可計費模型實例上呼叫 pay 方法來建立新的 Stripe Payment Intent。呼叫此方法將建立一個包裝在 Laravel\Cashier\Payment 實例中的 Payment Intent:

use Illuminate\Http\Request;

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

    return $payment->client_secret;
});

建立 Payment Intent 後,你可以將 Client Secret 回傳給應用程式的前端,以便使用者可以在瀏覽器中完成付款。要閱讀有關使用 Stripe Payment Intent 建構完整付款流程的更多資訊,請參閱 Stripe 文件

使用 pay 方法時,Stripe 儀表板中啟用的預設付款方式將可供客戶使用。或者,如果你只想允許使用某些特定付款方式,可以使用 payWith 方法:

use Illuminate\Http\Request;

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

    return $payment->client_secret;
});

[!WARNING] paypayWith 方法接受以應用程式使用的貨幣的最小單位表示的付款金額。例如,如果客戶以美元付款,則金額應以美分指定。

退款 (Refunding Charges)

如果你需要退款 Stripe 收費,可以使用 refund 方法。此方法接受 Stripe Payment Intent ID 作為其第一個參數:

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

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

發票 (Invoices)

檢索發票 (Retrieving Invoices)

你可以使用 invoices 方法輕鬆檢索可計費模型的發票陣列。invoices 方法回傳 Laravel\Cashier\Invoice 實例的集合:

$invoices = $user->invoices();

如果你想在結果中包含待處理的發票,可以使用 invoicesIncludingPending 方法:

$invoices = $user->invoicesIncludingPending();

你可以使用 findInvoice 方法透過其 ID 檢索特定發票:

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

顯示發票資訊 (Displaying Invoice Information)

列出客戶的發票時,你可以使用發票的方法來顯示相關的發票資訊。例如,你可能希望在表格中列出每個發票,允許使用者輕鬆下載其中任何一個:

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

即將到來的發票 (Upcoming Invoices)

要檢索客戶即將到來的發票,可以使用 upcomingInvoice 方法:

$invoice = $user->upcomingInvoice();

同樣地,如果客戶有多個訂閱,你也可以檢索特定訂閱的即將到來的發票:

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

預覽訂閱發票 (Previewing Subscription Invoices)

使用 previewInvoice 方法,你可以在更改價格之前預覽發票。這將允許你確定在進行給定價格更改時客戶的發票會是什麼樣子:

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

你可以將價格陣列傳遞給 previewInvoice 方法,以便預覽具有多個新價格的發票:

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

產生發票 PDF (Generating Invoice PDFs)

在產生發票 PDF 之前,你應該使用 Composer 安裝 Dompdf 函式庫,這是 Cashier 的預設發票渲染器:

composer require dompdf/dompdf

在路由或控制器中,你可以使用 downloadInvoice 方法產生給定發票的 PDF 下載。此方法將自動產生下載發票所需的適當 HTTP 回應:

use Illuminate\Http\Request;

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

預設情況下,發票上的所有資料都來自 Stripe 中儲存的客戶和發票資料。檔名基於你的 app.name 設定值。但是,你可以透過向 downloadInvoice 方法提供一個陣列作為第二個參數來自訂其中一些資料。此陣列允許你自訂資訊,例如你的公司和產品詳細資訊:

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

downloadInvoice 方法也允許透過其第三個參數自訂檔名。此檔名將自動加上 .pdf 後綴:

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

自訂發票渲染器 (Custom Invoice Renderer)

Cashier 也使得使用自訂發票渲染器成為可能。預設情況下,Cashier 使用 DompdfInvoiceRenderer 實作,該實作利用 dompdf PHP 函式庫來產生 Cashier 的發票。但是,你可以透過實作 Laravel\Cashier\Contracts\InvoiceRenderer 介面來使用任何你想要的渲染器。例如,你可能希望使用對第三方 PDF 渲染服務的 API 呼叫來渲染發票 PDF:

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

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

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

實作發票渲染器合約後,你應該更新應用程式 config/cashier.php 設定檔中的 cashier.invoices.renderer 設定值。此設定值應設定為你的自訂渲染器實作的類別名稱。

Checkout

Cashier Stripe 也提供對 Stripe Checkout 的支援。Stripe Checkout 透過提供預先建立的託管付款頁面,消除了實作自訂頁面以接受付款的痛苦。

以下文件包含有關如何開始使用 Stripe Checkout 與 Cashier 的資訊。要了解有關 Stripe Checkout 的更多資訊,你還應該考慮查看 Stripe 自己的 Checkout 文件

產品 Checkout (Product Checkouts)

你可以使用可計費模型上的 checkout 方法對 Stripe 儀表板中已建立的現有產品執行 Checkout。checkout 方法將啟動一個新的 Stripe Checkout 會話。預設情況下,你需要傳遞 Stripe Price ID:

use Illuminate\Http\Request;

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

如果需要,你還可以指定產品數量:

use Illuminate\Http\Request;

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

當客戶造訪此路由時,他們將被重新導向到 Stripe 的 Checkout 頁面。預設情況下,當使用者成功完成或取消購買時,他們將被重新導向到你的 home 路由位置,但你可以使用 success_urlcancel_url 選項指定自訂回呼 URL:

use Illuminate\Http\Request;

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

定義 success_url Checkout 選項時,你可以指示 Stripe 在呼叫你的 URL 時將 Checkout Session ID 新增為查詢字串參數。為此,請將文字字串 {CHECKOUT_SESSION_ID} 新增到你的 success_url 查詢字串中。Stripe 將用實際的 Checkout Session ID 替換此佔位符:

use Illuminate\Http\Request;
use Stripe\Checkout\Session;
use Stripe\Customer;

Route::get('/product-checkout', function (Request $request) {
    return $request->user()->checkout(['price_tshirt' => 1], [
        'success_url' => route('checkout-success').'?session_id={CHECKOUT_SESSION_ID}',
        'cancel_url' => route('checkout-cancel'),
    ]);
});

Route::get('/checkout-success', function (Request $request) {
    $checkoutSession = $request->user()->stripe()->checkout->sessions->retrieve($request->get('session_id'));

    return view('checkout.success', ['checkoutSession' => $checkoutSession]);
})->name('checkout-success');

促銷代碼 (Promotion Codes)

預設情況下,Stripe Checkout 不允許 使用者可兌換的促銷代碼。幸運的是,有一種簡單的方法可以為你的 Checkout 頁面啟用這些功能。為此,你可以呼叫 allowPromotionCodes 方法:

use Illuminate\Http\Request;

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

單次收費 Checkout (Single Charge Checkouts)

你也可以對未在 Stripe 儀表板中建立的臨時產品執行簡單收費。為此,你可以在可計費模型上使用 checkoutCharge 方法,並傳遞可收費金額、產品名稱和可選數量。當客戶造訪此路由時,他們將被重新導向到 Stripe 的 Checkout 頁面:

use Illuminate\Http\Request;

Route::get('/charge-checkout', function (Request $request) {
    return $request->user()->checkoutCharge(1200, 'T-Shirt', 5);
});

[!WARNING] 使用 checkoutCharge 方法時,Stripe 將始終在 Stripe 儀表板中建立新產品和價格。因此,我們建議你在 Stripe 儀表板中預先建立產品,並改用 checkout 方法。

訂閱 Checkout (Subscription Checkouts)

[!WARNING] 使用 Stripe Checkout 進行訂閱需要在 Stripe 儀表板中啟用 customer.subscription.created Webhook。此 Webhook 將在你的資料庫中建立訂閱記錄並儲存所有相關的訂閱項目。

你也可以使用 Stripe Checkout 來啟動訂閱。使用 Cashier 的訂閱建構器方法定義訂閱後,你可以呼叫 checkout 方法。當客戶造訪此路由時,他們將被重新導向到 Stripe 的 Checkout 頁面:

use Illuminate\Http\Request;

Route::get('/subscription-checkout', function (Request $request) {
    return $request->user()
        ->newSubscription('default', 'price_monthly')
        ->checkout();
});

就像產品 Checkout 一樣,你可以自訂成功和取消 URL:

use Illuminate\Http\Request;

Route::get('/subscription-checkout', function (Request $request) {
    return $request->user()
        ->newSubscription('default', 'price_monthly')
        ->checkout([
            'success_url' => route('your-success-route'),
            'cancel_url' => route('your-cancel-route'),
        ]);
});

當然,你也可以為訂閱 Checkout 啟用促銷代碼:

use Illuminate\Http\Request;

Route::get('/subscription-checkout', function (Request $request) {
    return $request->user()
        ->newSubscription('default', 'price_monthly')
        ->allowPromotionCodes()
        ->checkout();
});

[!WARNING] 遺憾的是,Stripe Checkout 在啟動訂閱時並不支援所有訂閱計費選項。在訂閱建構器上使用 anchorBillingCycleOn 方法、設定依比例計算行為或設定付款行為在 Stripe Checkout 會話期間不會產生任何影響。請參閱 Stripe Checkout Session API 文件 以查看哪些參數可用。

Stripe Checkout 與試用期 (Stripe Checkout and Trial Periods)

當然,你可以在建立將使用 Stripe Checkout 完成的訂閱時定義試用期:

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

但是,試用期必須至少為 48 小時,這是 Stripe Checkout 支援的最短試用時間。

訂閱與 Webhook (Subscriptions and Webhooks)

請記住,Stripe 和 Cashier 透過 Webhook 更新訂閱狀態,因此當客戶在輸入付款資訊後返回應用程式時,訂閱可能尚未生效。為了處理這種情況,你可能希望顯示一條訊息,通知使用者他們的付款或訂閱正在處理中。

收集稅務 ID (Collecting Tax IDs)

Checkout 也支援收集客戶的稅務 ID。要在 Checkout 會話上啟用此功能,請在建立會話時呼叫 collectTaxIds 方法:

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

當呼叫此方法時,客戶將看到一個新的核取方塊,允許他們表明他們是作為公司購買的。如果是這樣,他們將有機會提供他們的稅務 ID 號碼。

[!WARNING] 如果你已經在應用程式的服務提供者中設定了 自動稅務收集,則此功能將自動啟用,無需呼叫 collectTaxIds 方法。

訪客 Checkout (Guest Checkouts)

使用 Checkout::guest 方法,你可以為沒有「帳戶」的應用程式訪客啟動 Checkout 會話:

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

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

與為現有使用者建立 Checkout 會話類似,你可以利用 Laravel\Cashier\CheckoutBuilder 實例上可用的其他方法來自訂訪客 Checkout 會話:

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

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

訪客 Checkout 完成後,Stripe 可以派發 checkout.session.completed Webhook 事件,因此請務必 設定 Stripe Webhook 以將此事件實際傳送到你的應用程式。一旦在 Stripe 儀表板中啟用了 Webhook,你可以 使用 Cashier 處理 Webhook。Webhook Payload 中包含的物件將是一個 Checkout 物件,你可以檢查該物件以完成客戶的訂單。

處理失敗的付款 (Handling Failed Payments)

有時,訂閱或單次收費的付款可能會失敗。發生這種情況時,Cashier 會拋出 Laravel\Cashier\Exceptions\IncompletePayment 異常,通知你發生了這種情況。捕獲此異常後,你有兩個選擇如何繼續。

首先,你可以將客戶重新導向到 Cashier 包含的專用付款確認頁面。此頁面已有一個透過 Cashier 的服務提供者註冊的關聯命名路由。因此,你可以捕獲 IncompletePayment 異常並將使用者重新導向到付款確認頁面:

use Laravel\Cashier\Exceptions\IncompletePayment;

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

在付款確認頁面上,客戶將被提示再次輸入信用卡資訊並執行 Stripe 要求的任何其他操作,例如「3D Secure」確認。確認付款後,使用者將被重新導向到上面指定的 redirect 參數提供的 URL。重新導向後,message (字串) 和 success (整數) 查詢字串變數將被新增到 URL。付款頁面目前支援以下付款方式類型:

  • Credit Cards
  • Alipay
  • Bancontact
  • BECS Direct Debit
  • EPS
  • Giropay
  • iDEAL
  • SEPA Direct Debit

或者,你可以允許 Stripe 為你處理付款確認。在這種情況下,你可以 在 Stripe 儀表板中設定 Stripe 的自動計費電子郵件,而不是重新導向到付款確認頁面。但是,如果捕獲到 IncompletePayment 例外,你仍應通知使用者他們將收到一封包含進一步付款確認說明的電子郵件。

以下方法可能會拋出付款例外:使用 Billable Trait 的模型上的 chargeinvoiceForinvoice。與訂閱互動時,SubscriptionBuilder 上的 create 方法以及 SubscriptionSubscriptionItem 模型上的 incrementAndInvoiceswapAndInvoice 方法可能會拋出未完成付款例外。

可以使用可計費模型或訂閱實例上的 hasIncompletePayment 方法來確定現有訂閱是否有未完成的付款:

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

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

你可以透過檢查例外實例上的 payment 屬性來獲取未完成付款的特定狀態:

use Laravel\Cashier\Exceptions\IncompletePayment;

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

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

確認付款 (Confirming Payments)

某些付款方式需要額外資料才能確認付款。例如,SEPA 付款方式在付款過程中需要額外的「授權」資料。你可以使用 withPaymentConfirmationOptions 方法將此資料提供給 Cashier:

$subscription->withPaymentConfirmationOptions([
    'mandate_data' => '...',
])->swap('price_xxx');

你可以諮詢 Stripe API 文件 以查看確認付款時接受的所有選項。

強大客戶驗證 (Strong Customer Authentication)

如果你的企業或你的客戶之一位於歐洲,你需要遵守歐盟的強大客戶驗證 (SCA) 法規。這些法規由歐盟於 2019 年 9 月實施,旨在防止支付詐欺。幸運的是,Stripe 和 Cashier 已準備好建立符合 SCA 的應用程式。

[!WARNING] 在開始之前,請查看 Stripe 關於 PSD2 和 SCA 的指南 以及他們 關於新 SCA API 的文件

需要額外確認的付款 (Payments Requiring Additional Confirmation)

SCA 法規通常需要額外驗證才能確認和處理付款。發生這種情況時,Cashier 會拋出 Laravel\Cashier\Exceptions\IncompletePayment 異常,通知你需要額外驗證。有關如何處理這些異常的更多資訊,請參閱 處理失敗的付款 文件。

Stripe 或 Cashier 顯示的付款確認畫面可能會針對特定銀行或發卡機構的付款流程進行客製化,並可能包含額外的卡片確認、臨時小額收費、單獨的裝置驗證或其他形式的驗證。

未完成和逾期狀態 (Incomplete and Past Due State)

當付款需要額外確認時,訂閱將保持在 incompletepast_due 狀態,如其 stripe_status 資料庫欄位所示。一旦付款確認完成並且你的應用程式透過 Webhook 收到 Stripe 的完成通知,Cashier 將自動啟用客戶的訂閱。

有關 incompletepast_due 狀態的更多資訊,請參閱 我們關於這些狀態的其他文件

非會話付款通知 (Off-Session Payment Notifications)

由於 SCA 法規要求客戶即使在訂閱有效的情況下也偶爾驗證其付款詳細資訊,因此當需要非會話付款確認時,Cashier 可以向客戶發送通知。例如,這可能發生在訂閱續訂時。可以透過將 CASHIER_PAYMENT_NOTIFICATION 環境變數設定為通知類別來啟用 Cashier 的付款通知。預設情況下,此通知已停用。當然,Cashier 包含一個你可以用於此目的的通知類別,但如果需要,你可以自由提供自己的通知類別:

CASHIER_PAYMENT_NOTIFICATION=Laravel\Cashier\Notifications\ConfirmPayment

為確保發送非會話付款確認通知,請確認已為你的應用程式 設定 Stripe Webhook 並且在 Stripe 儀表板中啟用了 invoice.payment_action_required Webhook。此外,你的 Billable 模型也應使用 Laravel 的 Illuminate\Notifications\Notifiable Trait。

[!WARNING] 即使客戶手動進行需要額外確認的付款,也會發送通知。遺憾的是,Stripe 無法知道付款是手動完成的還是「非會話」完成的。但是,如果客戶在確認付款後造訪付款頁面,他們只會看到「付款成功」訊息。不允許客戶意外地兩次確認同一筆付款並產生意外的第二次費用。

Stripe SDK

許多 Cashier 的物件都是 Stripe SDK 物件的包裝器。如果你想直接與 Stripe 物件互動,可以使用 asStripe 方法方便地檢索它們:

$stripeSubscription = $subscription->asStripeSubscription();

$stripeSubscription->application_fee_percent = 5;

$stripeSubscription->save();

你也可以使用 updateStripeSubscription 方法直接更新 Stripe 訂閱:

$subscription->updateStripeSubscription(['application_fee_percent' => 5]);

如果你想直接使用 Stripe\StripeClient 客戶端,可以在 Cashier 類別上呼叫 stripe 方法。例如,你可以使用此方法存取 StripeClient 實例並從你的 Stripe 帳戶檢索價格列表:

use Laravel\Cashier\Cashier;

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

測試 (Testing)

在測試使用 Cashier 的應用程式時,你可以模擬對 Stripe API 的實際 HTTP 請求;但是,這需要你部分重新實作 Cashier 自己的行為。因此,我們建議允許你的測試觸及實際的 Stripe API。雖然這比較慢,但它提供了更多的信心,確信你的應用程式按預期工作,並且任何緩慢的測試都可以放在它們自己的 Pest / PHPUnit 測試群組中。

測試時,請記住 Cashier 本身已經有一個很棒的測試套件,因此你應該只專注於測試你自己的應用程式的訂閱和付款流程,而不是每個底層 Cashier 行為。

首先,將你的 Stripe Secret 的 測試 版本新增到你的 phpunit.xml 檔案中:

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

現在,每當你在測試時與 Cashier 互動,它都會向你的 Stripe 測試環境發送實際的 API 請求。為了方便起見,你應該在 Stripe 測試帳戶中預先填入可在測試期間使用的訂閱 / 價格。

[!NOTE] 為了測試各種計費場景,例如信用卡拒絕和失敗,你可以使用 Stripe 提供的各種 測試卡號和 Token