Stripe Checkoutによる 単発決済とエラーハンドリングの 事例紹介

2.2K Views

November 12, 24

スライド概要

歌に特化にたライブ配信アプリ「ColorSing」でウェブから単発決済する実装をしました。
実装方法の決定した経緯、不整合をどう防ぐか、Webhookのハンドリングに失敗した場合にどうするのか、実装時に困ったことなどの事例紹介です。

profile-image

go言語、kubernetes、GCPが好きなバックエンドのフリーランスエンジニア👨🏻‍💻

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

関連スライド

各ページのテキスト
1.

Stripe Checkoutによる 単発決済とエラーハンドリングの 実装事例 2024/11/10 ねこした

2.

ColorSingとは ColorSingは カラオケ機能つき ライブ配信アプリです

3.

Stripeを利用して実装した機能 コインを使うと配信者に ギフトを送れます コインをウェブから 購入できる機能を作りました

4.

Stripeで単発決済を実装する方法 【単発決済するのに利用する機能】 ・Payments 【実装方法】 ・Stripe Checkout ・Payment Intents API ・Charges API

5.

単発決済を実装方法の比較① 【Checkout】 決済フォームをStripeが用意してくれる実装方法 Stripeがホストする決済ページにリダイレクト、自社サイトにフォームを埋め込み メリット ・実装コストを抑えられる デメリット ・Stripeの決済フォームはある程度しかカスタマイズできない その他 ・カード決済、コンビニ決済、銀行振り込み、その他決済手段にも幅広く対応 ・前回の決済情報を保存しておくことは可能

6.

単発決済を実装方法の比較② 【Payment Intents API と Charges API】 決済フォームを自社で用意する実装方法 この2つのどちらかを利用する場合は、基本的にはPayment Intents APIを利用すればOK メリット ・決済フォームや支払いフローを自由にカスタマイズできる デメリット ・実装コストがCheckoutと比べて高い ユースケース例 ・ユーザーごとに複数種類のカードを保存しておきたい ・決済フォームなしで決済したい

7.

単発決済を実装方法の比較③ ColorSingでは、Checkout を採用しました • Checkoutの機能で最低限実現したいことは実現可能 • 実装コストを抑え、なるべく早くユーザーにウェブ課金の機能を提供したい • 支払いフローと決済フォームを自由にカスタマイズできないのは許容

8.

実装方針 【不整合が発生するパターン】 1. ColorSing内でコインを付与したのにStripeで決済できていない 2. Stripeで決済をしたのにColorSing内でコインを付与をしていない 3. 1回の決済に対してColorSingで重複してコインを付与してしまう 【不整合を絶対に避ける】 • ColorSing内でコインを付与するのは、Stripeで決済が完了してから • Stripeに対して何か操作を行う場合は、ColorSing内のDBにデータを作成してから行い、 ステータス管理もする

9.

テーブル設計 【Paymentsテーブル】 Name Type PK PaymentID STRING o Description ColorSingが発行したID PENDING: Status ENUM COMPLETED: コイン付与完了 EXPIRED: CheckoutSessionID STRING 決済待ち 有効期限切れ Stripeが発行したID 1. PaymentIDを発行し、[PENDING] で保存する 2. StripeのAPIを叩いてCheckoutSessionを作成し、CheckoutSessionIDを更新する 3. コイン付与と同時にStatusを [COMPLETED] に更新する

10.

処理の流れ① ~決済フォームを表示するまで~

11.

処理の流れ① ~決済フォームを表示するまで~

12.

処理の流れ① ~決済フォームを表示するまで~ ClientReferenceIDに PaymentIDを保存する 失敗する可能性あり

13.

処理の流れ② ~決済が完了するまで~

14.

処理の流れ③ ~決済完了処理~

15.

処理の流れ③ ~決済完了処理~ 同一トランザクション で行う

16.

Webhookエンドポイントとは 【Webhook エンドポイント】 •Stripe上でイベントが発生したら、自社サービス側のWebhookを叩いてくれるStripeの仕組み •ColorSingでコインを付与する処理はWebhookの受信をトリガーとしている •最大3日間リトライしてくれる Webhookの失敗に備える必要はあるのか?

17.

Webhookが失敗するケース リトライ期限を過ぎる • ColorSingサーバーが3日間以上、正常に動作しなかった場合、コインを付与する処理 をトリガーできなくなってしまう イベントが欠損する • StripeのWebhookエンドポイントの再作成などにより、イベントを欠損させてしまった 場合、コインを付与する処理をトリガーできなくなってしまう • StriepのWebhookエンドポイントのAPIバージョンを上げるためには再作成が必要 • (実際にColorSingでやらかしました)

18.

Webhookの失敗した時のためのバッチ処理 Webhookの失敗に備えて、バッチ処理で決済状態を定期的に監視しています 【バッチ処理の流れ】 1. ColorSingのPaymentsテーブルから [PENDING] ステータスのレコードを取得する 2. Stripeの Get CheckoutSession API を叩き、CheckoutSessionを取得する 3. CheckoutSessionの状態に合わせてハンドリング • 決済未完了 → 何もしない • 決済完了 → コインを付与し、[COMPLETED] ステータスに更新する • 有効期限切れ → [EXPIRED] ステータスに更新する

19.

1年間運用した振り返り 【データ不整合は発生したか?】 Stripeの運用を始めてから10万件以上の決済が行われましたが、Webhookとバッチ処理に よってデータ不整合は0件でした! 【Webhookが失敗した時のバッチ処理は役に立ったか?】 役に立ちました! StripeのWebhookエンドポイントのバージョンをあげる手順を間違えたため、Webhookイ ベントを欠損させてしまいましたが、復旧するまでの間、バッチ処理を手動で実行し続けた ため、遅延をほぼ発生させることなく、コインの付与を実施できました。

20.

実装時に困ったこと① 【 どのWebhookイベントを受け取ればいいのかわからない】 Webhookイベントの種類がかなり多いので、どのイベントを受け取ればいいのか調査が大変でした。ColorSingで は以下のWebhookイベントを受け取っています。 • 決済まわりの処理を行うためのイベント • checkout.session.completed • checkout.session.expired • checkout.session.async̲payment̲succeeded • checkout.session.async̲payment̲failed • 不正利用を検知するためのイベント • charge.dispute.created • radar.early̲fraud̲warning.created • radar.early̲fraud̲warning.updated

21.

実装時に困ったこと② 【 CheckoutSessionをClientReferenceIDで検索できない】 ClientReferenceIDを元に特定のCheckoutSessionのみを取得することはできない →ユーザーに紐づくCheckoutSessionをすべて取得し、ClientReferenceIDの一致により特定

22.

実装時に困ったこと③ 【 コンビニ決済の有効期限切れの確認】 コンビニ決済の有効期限の仕組み • コンビニ決済の有効期限、日付を指定し、その日付の 23:59(JST)まで • 1~60日後を指定可能 コンビニ決済の有効期限が切れたかどうかを知りたかった • 有効期限切れになるまでバッチ処理で監視するため • 指定した日数を元にした「有効期限が切れているはず」前提のロジックは不安 • コンビニ決済確定後に有効期限を保存する方法もあるが、データ不整合のリスクをなるべく下 げたい

23.

実装時に困ったこと③ 【 コンビニ決済の有効期限切れの確認】 コンビニ決済の有効期限が切れたかどうかを知る方法 • CheckutSessionからコンビニ支払いの完了は分かるが、有効期限切れかどうかは分からない • PaymentIntent オブジェクトは、コンビニ決済の有効期限が切れたかエラーを保持 • CheckoutSessionオブジェクト は PaymentIntentオブジェクトを保持していないので、API 経由でPaymentIntentを取得する必要がある PaymentIntent.status == “requires̲payment̲method” PaymentIntent.lastPaymentErrorCode == “payment̲intent̲payment̲attempt̲expired”

24.

ご清聴ありがとうございました ColorSingはエンジニア採用中です! https://corp.colorsing.com/recruit