【Stripe】Invoice作成時に頑張ってオーソリだけ通す

概要

StripeのInvoiceでオーソリをかけたい場面に出くわしたのでサポートに問い合わせた。

オーソリ/キャプチャは請求書から自動的に生成されたPaymentIntentでは機能しません。

Stripeサポート

つまり、Invoiceでオーソリだけ通す方法は公式には存在しない(2021年2月18日時点)。
仕事上、どうしてもオーソリだけ通す必要があったのでStripeサポートに方法を提案してみた。

提案した方法で対応可能と返答もらったのでメモしておく。やり方はざっくり以下の流れ。

  1. Invoiceを下書きで作成する
  2. Invoiceを確定する
  3. 確定後、Invoiceの金額に応じたPaymentIntentが自動紐付けされる(※1)
  4. PaymentIntentを自前作成してオーソリしつつ、Invoiceのmetadataに紐付ける
  5. Invoiceの請求(Pay an Invoice)ではなく、metadataに紐付けたPaymentIntentを
    キャプチャする(Capture a PaymentIntent)やり方で決済する
  6. Pay an Invoicepaid_out_of_band = true を渡してInvoiceを支払済と見なす

(※1)この自動紐付けされる PaymentIntent がオーソリできない

ただし、この方法は請求書ごとに2つのPaymentIntentが作成される(3で自動紐付けされるPaymentIntentはキャンセルされる前提になる)。自前作成したPaymentIntentのイベントはInvoiceに影響を与えないため、管理が複雑になることからStripeサポートとしてはお勧めしないらしい。

サンプルコード

PaymentIntentの自前作成〜Invoiceの確定まで

StripeServivce.php

<?php

use Stripe\StripeClient;

class StripeService
{
    public $client;

    public function __construct()
    {
        $this->client = new StripeClient( 'YOUR_STRIPE_SECRET_KEY' );
    }

    public function createInvoice( \Stripe\Customer $customer, $amount )
    {
        // InvoiceItem の作成
        $this->client->invoiceItems->create( [
            'customer' => $customer->id,
            'amount' => $amount,
            'currency' => 'jpy',
            'description' => 'サービス利用料',
            'tax_rates' => 'txr_xxx',
        ] );

        $params = [
            'customer' => $customer->id,
            'collection_method' => 'charge_automatically',
            'auto_advance' => false,
        ];

        // Invoice の作成
        return $this->client->invoices->create( $params );
    }

    public function authorize( \Stripe\Customer $customer, \Stripe\Invoice $invoice )
    {
        try {
            // オーソリのために PaymentIntent を自前作成
            $payment_intent = $this->client->paymentIntents->create( [
                'amount' => $invoice->total,
                'currency' => 'jpy',
                'customer' => $customer->id,
                'description' => 'サービス利用料オーソリ',
                'payment_method' => $customer->invoice_settings->default_payment_method,
                'payment_method_types' => [ 'card' ],
                'capture_method' => 'manual',
                'confirm' => true,
            ] );

            // 自前作成の PaymentIntent と Invoice を紐付け
            $this->client->invoices->update( $stripe_invoice->id, [
                'metadata' => [ 'authorized_payment_intent_id' => $payment_intent->id ],
            ] );

            // Invoice の確定
            $this->client->invoices->finalizeInvoice( $invoice->id );

            return true;
        }
        catch ( \Throwable $t ) {
            // Invoice が確定できなかった場合は削除
            // 自前作成した PaymentIntent 側でカード利用枠不足や有効期限切れなどがあった場合
            $this->client->invoices->delete( $invoice->id );

             return false;
        }
    }
}
<?php

$stripe = new StripeService();

// Customer の取得 (expandでデフォルト設定のクレカも取る)
$customer = $stripe->client->customers->retrieve( 'cus_xxx', [ 'expand' => [
    'invoice_settings.default_payment_method',
] );

$invoice = $stripe->createInvoice( $customer, 1000 );
$result = $stripe->authorize( $customer, $invoice );

if ( $result ) {
    // 成功時の処理
}
else {
    // 失敗時の処理
}

キャプチャ〜Invoiceを支払済と見なすまで

<?php

$stripe = new StripeService();

// Invoice の取得
$invoice = $stripe->client->invoices->retrieve( 'in_xxx' );

// キャプチャしつつ Invoice を支払済と見なす
$stripe->client->paymentIntents->capture( $invoice->metadata[ 'authorized_payment_intent_id' ] );
$stripe->client->invoices->pay( $invoice->id, [ 'paid_out_of_band' => true ] );

paid_out_of_band = true とするので、Stripeダッシュボード上は「外部で支払い済み」として表記される。
(実際には自前生成したPaymentIntentで決済されている)

Invoiceのメタデータには自前作成したPaymentIntentのIDが入る。
(メタデータにStripeオブジェクトが入ると自動でリンク化してくれるので偉い)

また、Invoiceで自動紐付けされるPaymentIntentは「キャンセル済み」になっていることが分かる。

おわりに

冒頭でも書いたとおり、この方法は管理が複雑になることからStripeサポートとしてはお勧めしないらしい。

実際には、業務ロジックがそこまで複雑でない限りは影響無さそうな感じ。
ただし公式がお勧めしないと言っているので採用する際はよく検討したい…

どちらかと言えばシステム側の実装というよりは、運営側 = Stripeダッシュボード利用者との認識合わせが必要になりそう。Invoiceに紐づく決済情報が直感的ではなくなるため。