Stripe Checkoutの決済URLを、任意の時間またはタイミングで期限切れにする

Stripe Checkoutで発行する決済URLは、顧客のセッションごとに作成されます。

そしてデフォルトでは、この決済URLは、「決済が完了する」か「発行から24時間経過する」ことで利用できなくなります。

しかし以下のようなケースでは、任意のタイミングで決済URLを無効化する必要があります。

- タイムセールなど、特定の時間内に注文を完了させる必要のあるケース
- 在庫システムと連携し、「カゴ落ち」が発生した場合に確保した在庫を解放したいケース
- 料金改訂などで、旧料金での決済を任意の時間で受付終了したい場合

これらのケースに利用できるAPIとAPIパラメータについて、紹介します。

https://stripe.com/docs/payments/checkout/managing-limited-inventory

# Checkoutセッション（決済URL）開始時に、有効期限を設定する場合

もっともシンプルな方法は、Checkoutセッションを作成する際のAPIパラメータに有効期限を設定する方法です。
以下のサンプルでは、day.jsを利用し、90分後に無効化されるセッションを作成しています。

```js
import dayjs from 'dayjs'

...

await stripe.checkout.sessions.create({
  line_items: [{
    price: 'price_xxxxx',
    quantity: 1
  }],
  success_url: 'https://stripe.com',
  cancel_url: 'https://stripe.com',
  expires_at: dayjs().add(90, 'minutes').unix(),
  mode: 'payment'
})
```

## Tips: 30分未満の有効期限は、セッション開始時の設定がNG

なお、セッション開始時のAPIリクエストでは、「`expires_at`に30分以上後の時間のみ設定できる」仕様であることに注意が必要です。

そのため、在庫の一時確保など、数分で無効化したいユースケースでは、後に紹介する「セッション終了APIの呼び出し」で対応しましょう。

## Tips: 決まった時間でセッションを無効化する

以下のような実装にすることで、「２０時をすぎると注文できなくなる商品」を提供することができます。

```js
try {
  const endingTime = dayjs().hour(20).minute(0).second(0).millisecond(0)
  
  const session = await stripe.checkout.sessions.create({
    line_items: [{
      price: 'price_xxxxx',
      quantity: 1
    }],
    success_url: 'https://stripe.com',
    cancel_url: 'https://stripe.com',
    expires_at: endingTime.unix(),
    mode: 'payment'
  })
  return session

} catch (e) {
  const { type, statusCode, param } = e
  // タイムセール終了時間を過ぎてからのリクエストを処理する
  if (
    type === 'StripeInvalidRequestError' &&
    statusCode == 400 &&
    param === 'expires_at'
  ) {
    throw new Error('タイムセールは終了しました')
  }
  // それ以外のエラーはそのまま投げる
  throw e
}
```

ただし、このケースでは、以下のようなケースを考慮する必要があります。

- 終了時間経過後にAPIが呼び出されると、Stripe APIがエラーを返すため、エラーをハンドルして「セール時間が終了していること」をユーザーに知らせる必要がある
- 日付が変わると注文が可能になるため、営業時間開始前の注文を拒否する実装を追加するか否かを判断する
- AWS Lambdaなどのプラットフォームを利用する場合、タイムゾーンが現地時間に設定されているかを確認する
- 「終了１時間前」からのセッションもAPIエラーになるため、内部的には終了時間 + 1時間で設定する必要がある

## Tips: セッションの終了時間をユーザーに見せたい場合

「X時までにご注文ください」のように、そのセッションが終了するまでの時間をユーザーに表示させたい場合、APIレスポンスの値を利用することができます。

```js

  const session = await stripe.checkout.sessions.create({
    line_items: [{
      price: 'price_xxxxx',
      quantity: 1
    }],
    success_url: 'https://stripe.com',
    cancel_url: 'https://stripe.com',
    expires_at: endingTime.unix(),
    mode: 'payment'
  })
  return {
    session_id: session.id,
    // UNIXタイムスタンプをそのまま返す場合
    expires_at: session.expires_at,
    // サーバー側でフォーマットしてから返したい場合
    expires_at_string: dayjs.unix(session.expires_at).format('YYYY/MM/DD HH:MM'),
    url: session.url,
  }
```

# 手動で任意のタイミングにセッションを終了させる場合

「１時間以内にセッションを期限切れにさせたい場合」や「時間以外の要因でセッションを期限切れにしたい場合」などでは、別途セッションを期限切れにするAPIを呼び出します。

```js
await stripe.checkout.sessions.expire(session.id)
```

Checkoutセッション作成時にcronやAWS StepFunctionsなどにセッションIDを含めたジョブを、期限切れにさせたいタイミングを実行時間に設定して登録します。

そして、実行時間に上記のExpire APIを実行し、セッションを終了させることができます。

「Checkoutのセッションを作成したタイミング」のWebhookイベントが現時点で提供されておりませんので、すこし組み込みに工夫が必要な点に注意しましょう。

# セッションを期限切れにさせた後の処理を実行させる方法

「セッションが有効な間は在庫を仮押さえするタイプの通販サイト」などでは、セッションを有効期限切れにさせた後に在庫を戻す必要があります。

この場合に利用できるのが「`checkout.session.expired`のWebhookイベント」です。

Webhookを利用することで、ネットワークエラー時のリトライや自動化されたワークフローの一本化などが期待できます。

```js
  if (['checkout.session.expired'].includes(event.type)) {
    const session = event.data.object
    console.log(session)
  }
```

## Tips: `checkout.session.expired`イベントに、カートの中身の情報を渡す

現在、Checkoutのセッション情報には`line_items`のデータが含まれていません。

[checkout.sessions.listLineItems API](https://stripe.com/docs/api/checkout/sessions/line_items)を利用して、カートの中身を取得しましょう。

```diff_javascript
  if (['checkout.session.expired'].includes(event.type)) {
    const session = event.data.object
+　　　　　　　　const items = await stripe.checkout.sessions.listLineItems(session.id); // cs_xxxx
  }
```

**詳細**

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

# おわりに

Stripe Checkoutを利用することで、表示内容だけでなく、今回のケースのような在庫やタイムセールなどのユースケースにも少ないコード・実装で対応することができます。

また、カゴ落ち時に顧客にリマインドメールを送付するための実装・設定方法についても機能・ドキュメントを用意しています。

https://stripe.com/docs/payments/checkout/abandoned-carts

ぜひCheckoutの機能をフル活用して、新しいセールス・プロモーションの企画・開発にお役立てください。

# 参考・関連ドキュメント

https://stripe.com/docs/payments/checkout/managing-limited-inventory


# [PR] Stripe開発者向け情報をQiitaにて配信中！
2021年12月よりQiitaにて、Stripe開発者のためのブログ記事更新を開始しました。

- [Stripe Updates]：開発者向けStripeアップデート紹介・解説
- ユースケース別のStripe製品や実装サンプルの紹介
- Stripeと外部サービス・OSSとの連携方法やTipsの紹介
- 初心者向けのチュートリアル(予定)

など、Stripeを利用してオンラインビジネスを始める方法について随時更新してまいります。

-> [Stripe Organizationsをフォローして最新情報をQiitaで受け取る](https://qiita.com/organizations/stripe)