Stripeの「冪等キー」を利用して、多重決済や顧客の重複作成を予防するシステムを実装する方法

ECサイトなど、多くの決済を取り扱うシステムでは重複処理・多重決済をいかに防止するかが重要です。

Stripeの場合、同一IDのPayment IntentやInvoiceであれば、2回目以降の決済リクエストについてはデフォルトでエラーで返します。

例えば、以下のコードでは、1回目の`confirm`以降に`confirm`や`capture`を実行すると、`This PaymentIntent could not be captured because it has already been captured.`エラーが発生します。

```js
    const stripe = new Stripe('sk_test_xxxx', {
      apiVersion: '2022-08-01',
    })

    // Payment Intentを作成
    const paymentIntent = await stripe.paymentIntents.create({
      customer: 'cus_xxxx',
      amount: 199,
      currency: 'jpy',
      payment_method: 'card_xxx'
    })

    // サーバー側でconfirmして決済
    await stripe.paymentIntents.confirm(paymentIntent.id)

    // 決済済みのPayment Intent IDを再び指定すると、エラーになる
    await stripe.paymentIntents.confirm(paymentIntent.id)
    await stripe.paymentIntents.capture(paymentIntent.id)
```

Payment IntentやInvoiceのIDを事前に作成するシステムでは、これらのIDを顧客の購入フローで共有することで、二重決済を防止できます。

## 都度Payment IntentやInvoiceを作成する場合は、「冪等キー」を利用する

ライドシェアや施設予約などの「注文と決済のタイミングが異なるケース」では、請求処理時に都度Payment Intentなどを作成することがあります。

この場合は、都度Payment Intentなどのリソースを作成するため、ネットワークエラーによるリトライ処理などで多重決済が発生する可能性がでてきます。

以下のコードのように、新しくリソースIDを作成する種類のAPI呼び出しを行なった場合、リクエストごとにオーソリや決済が発生します。

```js
  await stripe.paymentIntents.create({
      customer: 'cus_xxxx',
      amount: 199,
      currency: 'jpy',
      confirm: true,
      off_session: true,
      payment_method: 'card_xxx'
  })
  await stripe.paymentIntents.create({
      customer: 'cus_xxxx',
      amount: 199,
      currency: 'jpy',
      confirm: true,
      off_session: true,
      payment_method: 'card_xxx'
  })
```

Stripeの場合、「冪等キー」をAPIリクエストに設定することで、重複してStripe APIを呼び出した場合にも、結果の冪等性を担保できます。


```js
  const requestId = 'UUIDなどを設定する'

  // 1回目のPayment Intent作成処理
  await stripe.paymentIntents.create({
      customer: 'cus_xxxx',
      amount: 199,
      currency: 'jpy',
      confirm: true,
      off_session: true,
      payment_method: 'card_xxx'
  }, {
    idempotencyKey: requestId
  })

  // 重複したPayment Intent作成処理
  await stripe.paymentIntents.create({
      customer: 'cus_xxxx',
      amount: 199,
      currency: 'jpy',
      confirm: true,
      off_session: true,
      payment_method: 'card_xxx'
  }, {
    idempotencyKey: requestId
  })
}
```

冪等キーを渡しているため、このサンプルコードでは「2回目の決済」が実行されません。

リクエストログを確認すると、2回目のAPIリクエストに、「オリジナルのリクエストID」が表示されていることがわかります。

![スクリーンショット 2022-11-08 15.09.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/daccce62-0c19-93a8-0f0b-6dfd97ac7ea4.png)

冪等キーが同一のリクエストがすでに送られていた場合、Stripe APIは「オリジナルのリクエスト結果だけ」を返します。

そのため、システム側で冪等キー管理機能を用意することで、Stripeへの重複呼び出しを制御できます。

## Stripe SDKのネットワークエラー対策にも

Stripe APIを呼び出す際、稀にネットワークエラーが発生することがあります。

SDKを利用していると、SDKがネットワークエラーを検知した際に、自動でAPI呼び出しのリトライを行うことができます。

```js
    const stripe = new Stripe('sk_test_xxxx', {
      apiVersion: '2022-08-01',
      maxNetworkRetries: 3,
    })
```

リクエスト個別に設定することも可能です。

```js
await stripe.customers.create(
  {
    email: 'customer@example.com',
  },
  {
    maxNetworkRetries: 2, // Retry this specific request twice before giving up
  }
);
```

https://github.com/stripe/stripe-node#network-retries

SDKの内部的で冪等キーを作成することで、このリトライ処理も実装されています。

## 冪等キーの発行・管理方法について

「冪等キーが発行されていれば」Stripe APIの重複呼び出しを制御することができます。

ただし、冪等キーの発行や管理方法については、システム側で開発・実装する必要があります。

AWS Lambdaの例では、AWSがリリースしている`lambda-power-tools`を利用してキーを作成することが可能です。

https://aws.amazon.com/jp/blogs/compute/handling-lambda-functions-idempotency-with-aws-lambda-powertools/

また、AWSの学習資料でも「冪等性について」やAWSを利用した実装方法について紹介されています。

https://aws.amazon.com/jp/builders-flash/202104/serverless-idempotency/?awsf.filter-name=*all

## 冪等キーを使いこなして、より堅牢な決済システムを構築しよう

マイクロサービスや分散アーキテクチャでは、FaaS側のリトライ処理などによる重複処理への対応が求められます。

キーの発行方法や管理についてはサービスの構成によって異なりますが、Stripe API呼び出し部分については「冪等性を持つキー」が用意できれば、あとはStripe側に任せることが可能です。

https://stripe.com/docs/error-low-level#idempotency

英語ではありますが、動画でデモやコードの紹介も行っておりますので、こちらもあわせてご覧ください。

https://stripe.com/docs/videos/developer-foundations?video=idempotency-and-retries&lang=php