Stripe Elements / Checkoutを利用したコンビニ決済で、Webhookを利用した在庫管理・発送業務との連携を実装する

Stripeでは、カード決済の他にコンビニ決済についても顧客に提供できます。
ただし、カード決済とは**決済のタイミングが異なる**ため、導入前にシステムの対応状況を確認する必要があります。

Dashboardで都度状況を確認することももちろん可能です。
ですが、StripeのWebhookを利用することで、**手動のオペレーションを最小化できるシステムを最小化**することができます。

また、Webhookを利用したアプリケーションを実装する以外にも、**Zapierなどのタスク自動化ツールを活用することで、ノーコードで実現**させることも可能です。

今回は、在庫管理や発送業務といった、決済が完了した後に執り行う業務への連携について、Stripe Webhookを利用する方法の例を紹介します。

なお、コンビニ決済の導入方法については、以下の記事をご覧ください。

https://qiita.com/hideokamoto/items/c59c4c4ee24b98ac997b


# コンビニ決済の導入前に確認が必要なこと

まず、確認が必要な項目をおさらいしましょう。
重要な点は以下の３つです。

- 1: ElementsやCheckoutでの操作完了時に、まだ決済は完了していない
- 2: 数日後、未決済を理由に注文が自動キャンセルされることがある
- 3: 決済が完了したことは、`payment_intent.succeeded`イベントで知ることができる

このことから、以下の要件を満たしているかを確認する必要があることがわかります。

- PaymentIntentまたはCheckout セッション作成時に、在庫を確保する
- 「Elements / Checkoutの操作完了時」ではなく、`payment_intent.succeeded`イベント受信時に発送業務を行う
- 未決済を理由に注文がキャンセルされた場合、確保した在庫を解放する

上記の３点について、簡単なJavaScriptアプリケーションを例に実装のアイディアを深めましょう。

# コンビニ決済に対応したWebhookイベント処理を実装する

ここからは前述の３要件を満たす実装の例を紹介します。
## PaymentIntent / Checkout セッションを作成し、在庫を確保する
まずは注文処理に入るため、PaymentIntentまたはCheckout セッションを作成しましょう。
### 例1: Payment Intentを作成するサンプルコード

```js
    /**
    　　*　在庫の確認と確保の処理を追加する。
     **/
    
    try {
      /**
       * PaymentIntentを作成する。
       * payment_method_optionsを使って、支払い期限や明細表示などをカスタムする。
       */
      const paymentIntent = await stripe.paymentIntents.create({
        amount: 2000,
        currency: 'jpy',
        payment_method_types: ['konbini', 'card'],
        payment_method_options: {
            konbini: {
                product_description: "Tシャツ",
                expires_after_days: 1
            }
        },
      });
    
      /**
       * 注文管理のDBがある場合、
       * PaymentIntentのIDと在庫を確保した商品のデータを保存する。
       */
    
      return paymentIntent;
    } catch (e) {
      /**
       * PaymentIntentの作成などに失敗した場合、
       * 確保した在庫を解放する
       **/
    }
```

### 例2: Checkoutセッションを作成するサンプルコード

Checkoutセッションの場合も、PaymentIntent作成処理部分を以下のように変更すればOKです。

PaymentIntentの場合と異なり、Checkoutセッションの場合は、Product / PriceのデータをStripeに登録する必要があります。
その代わりに合計金額の計算を行う必要がなくなります。
また、`shipping_options`を利用して、配送方法・料金の選択画面を表示できるようにしましょう。

https://stripe.com/docs/payments/checkout/shipping

```js
    /**
    　　*　在庫の確認と確保の処理を追加する。
     **/
    
    try {
        const session = await stripe.checkout.sessions.create({
          line_items: [{
            price: 'price_xxxx',
            quantity: 1
          }],
          /**
           * The URL to which Stripe should send customers when payment or setup is complete.
           **/
          success_url: 'http://localhost:3001/success',
          /**
           * The URL the customer will be directed to if they decide to cancel payment and return to your website.
           **/
          cancel_url:'http://localhost:3001/cancel',
          mode: 'payment',
          payment_method_types: ['card', 'konbini'],
          customer_creation: 'if_required',
          shipping_address_collection:{
              allowed_countries: ["JP"]
          },
          shipping_options: [{
              shipping_rate: 'shr_xxxxx'
          }]
        });
    
       /**
        * 注文管理のDBがある場合、
        * CheckoutセッションのIDと在庫を確保した商品のデータを保存する。
        */
    
        return session;
    } catch (e) {
      /**
       * Checkoutセッションの作成などに失敗した場合、
       * 確保した在庫を解放する
       **/
    }
```

Checkoutセッションを利用する場合、カートの中身のデータは`checkout.sessions.listLineItems`APIから取得できます。

https://qiita.com/hideokamoto/items/565e3fbba33885be726e

こちらを利用する方法も可能ですので、興味がある方はぜひお試しください。

## クレジットカードやコンビニ決済の注文を処理するWebhookを作成する
いずれの決済方法でも、処理が完了した時に`payment_intent.succeeded`イベントが発火します。
ただし発火のタイミングが決済方法によって変わることになるため、顧客のために在庫を確保している期間が変動することにご注意ください。

```js
      switch (event.type) {
        case 'payment_intent.succeeded': {
            /**
             * オブジェクトの中身は、APIドキュメントをご確認ください。
             * @see https://stripe.com/docs/api/payment_intents/object
             **/
            const paymentIntent = event.data.object;
    
            /**
             *　　注文した商品のデータをDBなどに保存している場合、
             * PaymentIntentのIDを利用してデータを取得する処理を入れる
             */
           const orderItems = await getOrderedItemByPaymentIntentId(paymentIntent.id)
    
            /**
             * Checkoutなどで顧客が入力した配送先住所を取得する
             */
            const shippingData = paymentIntent.shipping;
    
            /**
             * 配送先情報や注文した商品・顧客データを利用し、
             * 発送業務などを実行するAPIまたは処理を呼び出す。
             */
    
            break;
        }
        default:
            break;
      }
```

受信したPaymentIntentのデータには、以下のデータなどが含まれています。

- 顧客とPaymentIntentのID
- 注文金額・決済金額
- PaymentIntent作成時に登録したmetadata
- 顧客が選択した配送先情報
- 領収書送信のためのメールアドレス

より詳細なデータを確認したい場合は、APIドキュメントをご覧ください。

https://stripe.com/docs/api/payment_intents/object

また、発送処理が失敗した時に備えて、Amazon CloudWatchやSentryなどを設定することをお勧めします。
これにより、Slackまたはメールで問題発生に気づきやすくなります。

## 決済が未完了だったケースを処理するWebhookを作成する
有効期限までに決済が完了されなかった場合、`payment_intent.payment_failed`イベントが発火します。また、Checkoutでカゴ落ちが発生し、セッションが有効期限切れになった場合には`payment_intent.canceled`イベントが発火します。
そのため、確保した在庫を解放するには、この２イベントを受け付ける必要があります。

```js
      switch (event.type) {
        // 決済成功時の処理
        case 'payment_intent.succeeded': {
            const paymentIntent = event.data.object;
            ...
            break;
        }
    
        // 注文未完了（キャンセル）時の処理
        case 'payment_intent.canceled':
        case 'payment_intent.payment_failed': {
            const paymentIntent = event.data.object;
            /**
             * PaymentIntentのIDを利用して、在庫の数などを元に戻す処理を実行する
             */
            break;
        }
        default:
            break;
      }
```
また、もし在庫を確保したままで問題がないケースであれば、PaymentLinksとクーポンを利用して、ユーザーに特別オファーを提供するメールを送信するなども考えることができます。

```js
    const promotionCode = await stripe.promotionCodes.create({
      customer: dataObject.customer,
      coupon: 'COUPON_ID'
    })
    const link = await stripe.paymentLinks.create({
        line_items: [...],
        allow_promotion_codes: true,
    })
    /**
     * Payment LinksのURLとPromotionCodeを送信するメールを送付する
     **/
```

# おわりに

今回紹介した手法は、現在ベータ版として提供中の銀行振り込みのように、今後Stripeが新しい日本固有の決済方法をサポートした場合などでも応用できます。
また、３Dセキュアなどの本人確認に対応する場合にも対応が必要となるケースがありますので、この機会にシステムの確認をしてみてはいかがでしょうか？


# [関連記事]コンビニ決済入門シリーズ

## Checkout / Elementsへの導入方法

https://qiita.com/hideokamoto/items/c59c4c4ee24b98ac997b

## Billing（サブスクリプション）への導入方法

https://qiita.com/hideokamoto/items/605096153bdf473895f0

## Connectを利用したマーケットプレイスへの導入方法

https://qiita.com/hideokamoto/items/9a4258ad5a41440274a2