プロンプトと適用されるコンテキスト
Eコマース決済処理システムを設計してください。購入者が注文を送信した後、システムは外部の決済サービスプロバイダーを使用して都度カード決済を処理します。チェックアウト時にオーソリ(与信枠確保)を行い、在庫が確保された後にキャプチャ(売上確定)を実行し、注文がキャンセルされた場合はオーソリを解放します。キャプチャ後は複数回の一部返金をサポートしますが、その累計額がキャプチャ額を超えてはなりません。決済手段によっては3DSなどの追加認証が必要になる場合があり、最終結果はブラウザが復帰した後に届くことがあります。
1日あたり500万件の決済試行、平均で約58 TPS、ピーク時で500 TPSを想定します。決済作成APIのp99は300ミリ秒未満、ステータス読み取りのp99は200ミリ秒未満、ローカルAPIの月間可用性目標は99.99%とします。この可用性はプロバイダー側の完了を保証するものではありません。金銭は通貨の最小単位の整数として扱い、1回の決済は厳密に1つの通貨を持ちます。プロバイダー、ネットワーク、およびローカルプロセスはすべて失敗する可能性があります。これらの数値や期限は面接上の前提条件であり、決済製品による保証ではありません。
スコープには、決済作成、追加認証、オーソリ、キャプチャ、オーソリキャンセル、一部返金および全額返金、金銭台帳、プロバイダーWebhook、および照合が含まれます。チャージバック、不正検知モデル、外国為替、加盟店への入金(ペイアウト)、税金、および完全なPCIコンプライアンスプログラムはスコープ外ですが、回答ではそれらの境界を特定する必要があります。プロバイダーのホスト型ページまたはトークン化コンポーネントがカード詳細を収集します。本システムは決済手段トークンを保存し、生カード番号やセキュリティコードを受信またはログ記録してはなりません。
面接官が評価するポイント
第1のシグナルは、候補者がビジネスインテント、プロバイダーへの試行、および金銭の確定事実を分離して捉えているかどうかです。1つのカートは1つの内部payment_idに対応しますが、複数の認証試行やプロバイダー試行を持つことがあります。また、リクエストのタイムアウトは決済の失敗を意味しません。優れた回答は、ライフサイクル全体を1つのpaid=trueに圧縮しません。決済アグリゲート、すべての操作と不明な結果、プロバイダー参照、および台帳エントリを個別に保存します。
第2のシグナルは、分散境界を越えた正確な論理的推論です。データベーストランザクションに外部決済プロバイダーをアトミックに含めることはできません。プロバイダー側でキャプチャが成功したにもかかわらずレスポンスが失われることや、同期レスポンスよりも先にWebhookが到着すること、あるいはローカルコミット後にプロセスがクラッシュすることがあります。呼び出し元の冪等性キー、内部操作ID、プロバイダー冪等性キー、トランザクショナルアウトボックス、Webhookの重複排除、およびステータス照会によって、再実行が収束するようにします。これらはエンドツーエンドの厳密に1回(exactly-once)のトランザクションを作成するものではありません。
第3のシグナルは、単調増加で監査可能なステートマシンです。オーソリとキャプチャは異なる金銭ステージです。REQUIRES_ACTIONは失敗ではなく、プロバイダーのタイムアウトによってUNKNOWN操作が発生しても、即座に決済を失敗させることはできません。返金はキャプチャ済みの決済を参照し、正確な金銭単位を使用し、並行処理下でも確定済み返金額と処理中(in-flight)の返金額の合計がキャプチャ額を超えないという不変条件を維持する必要があります。
第4のシグナルは、ワークフローステータスと会計の分離です。決済テーブルは、ユーザーが次に何を行えるかに答えます。追記専用(append-only)の台帳は、なぜ残高が現在の値になっているのかに答えます。転記された事実は決してその場で編集されません。返金や修正は反対仕訳や相殺エントリを追加します。各仕訳帳の借方、貸方、通貨、およびビジネス参照はトランザクション内で検証され、その後プロバイダーレポートや銀行入金と照合されます。
最後のシグナルは、セキュリティと反証可能性です。回答では、カードデータの露出低減、Webhook署名の検証、プロバイダーシークレットの保護、権限とログの制限を行うとともに、レスポンスの喪失、重複または順不同のWebhook、並行するキャプチャと返金、不均衡な仕訳帳、および照合不一致を注入(テスト)する必要があります。不変条件、復旧パス、および検証が示されていないサービス図では、正しさを証明できません。
回答前に明確にすべき質問
- 誰がカードデータを収集するのか? このプロンプトでは、プロバイダーのホスト型ページまたはトークン化コンポーネントを使用します。ビジネスバックエンドは決済手段トークンのみを受信し、生のカード番号、トラックデータ、PIN、またはセキュリティコードを保存しません。実際のPCIスコープはコンプライアンスチームが確認する必要があります。
- オーソリとキャプチャはいつ実行されるのか? 注文送信時にオーソリを行い、在庫確保後にキャプチャを実行します。在庫確保に失敗した場合は有効期限前にキャンセルします。決済手段が遅延キャプチャをサポートしているかどうかは機能テーブルによって決定され、設計においてすべての手段がサポートしていると仮定することはできません。
- 何をもって成功とするのか? ブラウザのリダイレクトは単なるユーザー体験上のシグナルであり、注文履行(フルフィルメント)を許可することはできません。認証されたプロバイダーレスポンス、Webhook、またはアクティブな問い合わせによって信頼できる証拠が提供され、ビジネスアクションが発生する前にローカルステートマシンがこれを受け入れる必要があります。
- 返金の規約はどうなっているか? 複数回の一部返金と全額返金をサポートし、キャプチャ額を超えてはなりません。元の決済が存在しないスタンドアロンの返金はありません。返金は非同期で完了するか、プロバイダーによって拒否されることがあります。
- 複数のプロバイダーが必要か? バージョン1では1つのプロバイダーを使用しますが、インターフェースには内部操作IDとプロバイダー参照を保存します。結果が不明な状態では、両方のプロバイダーから二重請求されるリスクがあるため、自動フェイルオーバーを行ってはなりません。
- 台帳の対象範囲は何か? このプロンプトでは、プロバイダー売掛金と加盟店買掛金を記録します。プロバイダー手数料、加盟店への入金、税金はスコープ外です。すべての通貨は個別にバランス(借方貸方一致)し、台帳内で浮動小数点数による金銭表現や暗黙の為替レートは許可されません。
- 保持と監査の要件は何か? 機密性の高いペイロードへのアクセスを最小限に抑え、暗号化し、監査しながら、規制および企業ポリシーに従って決済、操作、Webhook受信記録、および台帳参照を保持します。PCI SSCは、暗号化されている場合であっても、オーソリ後に機密認証データを保存することを禁止しています。
- 可用性と整合性のトレードオフはどうするか? プロバイダーが利用できない場合、システムはリクエストを受け付けて「処理中」と表示できますが、偽の成功を表示することはできません。即座に最終結果を返すことよりも、金銭の正確性と追跡可能性が優先されます。
30秒の回答フレームワーク
「1つの決済インテントに対して安定したpayment_idを使用し、決済状態をオーソリ、キャプチャ、返金の各操作から分離します。呼び出し元の冪等性キーによって、リクエストダイジェスト、操作、およびアウトボックスをアトミックに保存し、ワーカーはプロバイダー側で操作IDを再利用します。タイムアウトはUNKNOWNとなり、新たな請求を行うのではなく、検証済みWebhook、問い合わせ、および照合を通じて収束させます。信頼できる結果によって単調増加のステートマシンを進め、バランスの取れた仕訳帳とビジネスイベントをアトミックに追記します。返金の作成時は、残りの返金可能枠を条件付きで確保します。その後、プロバイダーレポートと銀行入金によって内部台帳を照合します。プロバイダーのトークン化によりカードデータをバックエンドから排除し、ブラウザの成功ページによって注文履行がトリガーされないようにします。」
ステップごとの詳細解説
ステップ1:キャパシティ、不変条件、所有権から始める
500万件を86,400秒で割ると約58 TPSになります。500 TPSのピークは、最初からすべてのテーブルをシャーディングする必要がある規模ではありません。この設計では、外部境界を越えた正しさ、監査可能性、および復旧が支配的な要素となります。決済APIは水平方向にスケールし、リレーショナルデータベースが信頼できる状態を保持し、payment_idによってルーティングされる永続キューがピークを吸収してプロバイダーのレイテンシを隔離します。まず不変条件を定義します。
amount > 0
currency is immutable after the first provider attempt
captured_amount <= authorized_amount
refunded_amount + pending_refund_amount <= captured_amount
for every journal: sum(debits) == sum(credits), per currency
one merchant + one idempotency_key describes one immutable request intent
one successful business operation produces at most one journal referenceこの規模では、1つの書き込みリージョンとフェイルオーバーを備えたリレーショナルデータベースを使用することで、アクティブ/アクティブのマルチリージョン書き込みよりも、冪等性キー、状態バージョン、および台帳の順序性を維持しやすくなります。アクティブ/アクティブはリージョンフェイルオーバー時間を短縮できますが、同一キーの同時使用や競合する金銭操作を解決する必要があり、明示的なリージョン可用性要件がある場合にのみ正当化されます。また、完全なイベントソーシングも履歴を保存できますが、リプレイ、スキーマ移行、およびクエリの複雑さが決済ワークフロー全体に広がります。本設計では金銭仕訳帳のみを追記専用とし、ワークフローステータスにはバージョン管理された現在のアグリゲートを使用することで、監査のメリットと運用コストのバランスを取ります。
注文サービスは在庫と履行を所有します。決済サービスは決済状態、操作、および金銭の事実への参照を所有します。プロバイダーはカードネットワークの状態を所有します。台帳は内部の金銭事実を所有します。注文サービスが決済行を直接書き込むことはできず、決済Webhookが注文を直接発送済みにすることもできません。決済サービスは安定したビジネスイベントを発行し、注文サービスはpayment_idによってそれを冪等に消費します。
ステップ2:API、冪等性セッション、データモデルを定義する
コアインターフェースは以下のようになります。
POST /payments create one payment for an order
POST /payments/{id}/capture capture an authorized amount
POST /payments/{id}/cancel cancel an uncaptured authorization
POST /payments/{id}/refunds request a partial or full refund
GET /payments/{id} return current status and allowed next actions
POST /provider/webhooks persist a verified provider event金銭を変更するすべての呼び出しには、呼び出し元から提供される冪等性キーが必要です。(merchant_id, operation_type, idempotency_key)に対するユニーク制約により、正規化されたリクエストダイジェスト、進行中状態、および再生可能なレスポンスが保護されます。最初のリクエストでは、payments、payment_operations、およびアウトボックスレコードが1つのデータベーストランザクション内で作成されます。同じキーとダイジェストの場合は、既知のレスポンスが再送されます。同じキーで異なる金額、通貨、決済、または操作タイプが指定された場合は、コンフリクト(競合エラー)を返します。Amazonの社内エンジニアリングガイダンスでも同様に、呼び出し元が指定するリクエストIDと、IDおよび変更処理の両方を含むACID境界を推奨しており、異なる意図で再利用されたIDは拒否されます。
責務を個別に保存します。
payments:注文参照、金額、通貨、アグリゲート状態、オーソリ/キャプチャ/返金の合計額、およびバージョン。payment_operations:タイプ、操作ID、要求金額、状態、プロバイダー、プロバイダー参照、試行回数、および不明理由。provider_events:プロバイダーイベントID、署名検証結果、受信時刻、暗号化ペイロード参照、および処理状態。journal_entries:仕訳帳ID、勘定科目、借方または貸方、金額、通貨、および操作参照。outbox_events:ローカルでコミットされ、発行を待機しているビジネスイベント。
内部操作IDはプロバイダーの冪等性キーとして使用できます。Adyenのドキュメントには、決済タイムアウト後に同じキーを使用して安全にリトライする方法や、キーのスコープ、保持期間、リージョンを跨ぐ制限が記載されています。したがって、システムはUUIDを永続的かつグローバルな保証として扱うのではなく、実際のプロバイダー規約をモデル化します。
ステップ3:決済と操作を2つのステートマシンとしてモデル化する
決済アグリゲートは、注文およびユーザー向けライフサイクルです。
CREATED -> REQUIRES_ACTION -> AUTHORIZED -> CAPTURED
\-> FAILED \-> CANCELED
CAPTURED -> PARTIALLY_REFUNDED -> REFUNDED各オーソリ、キャプチャ、キャンセル、返金には、独立した操作ライフサイクルがあります。
PENDING -> SUCCEEDED | FAILED | UNKNOWN
UNKNOWN -> SUCCEEDED | FAILED (after query, webhook, or reconciliation)FAILEDは、この操作が後から成功することはないという信頼できる証拠があることを意味します。ネットワークタイムアウト、5xxエラー、またはレスポンスの喪失はUNKNOWNとなります。アグリゲートは有効な単調遷移のみを受け入れます。古い状態を報告するWebhookは監査のために保持されますが、アグリゲートを後戻りさせることはできません。Webhookとアクティブな問い合わせが競合した場合、行バージョンまたは条件付き更新によって遷移が一度だけコミットされます。Stripeのドキュメントでは、PaymentIntentを作成からチェックアウト(追加認証を含む)にまたがるリソースとして定義し、手動キャプチャ決済はキャプチャ前にキャプチャ可能(capturable)状態になると説明されています。これは、決済を1つのHTTPレスポンスと同一視するのではなく、長期間存在する決済リソースをサポートする設計を裏付けています。
注文オーソリは在庫確認から分離されています。在庫確保の成功によりキャプチャがトリガーされ、失敗によりキャンセルがトリガーされます。これらの操作は競合する可能性があるため、条件付きデータベース更新により、1つの操作のみが現行のAUTHORIZEDバージョンを獲得できるようにします。プロバイダー呼び出しが復帰する前にWebhookが到着することもあります。同期レスポンスとWebhookは、2つの遷移パスを実装するのではなく、同じ状態適用関数に入力されなければなりません。
ステップ4:外部の非アトミック性を受け入れ、ワークフローを収束させる
ローカルのトランザクションは、操作状態をアウトボックスとともにコミットします。リレーは少なくとも1回(at-least-once)発行し、ワーカーは操作IDによって処理を要求(クレーム)します。ワーカーはそのIDをプロバイダーの冪等性キーとして再利用し、接続、リクエスト、および全体の期限に上限を設定します。確定した結果が得られた場合は、プロバイダー参照とレスポンスダイジェストを保存します。レスポンスが失われた場合はUNKNOWNとマークし、ステータス照会をスケジュールします。新しい操作を作成したり、盲目的にプロバイダーを切り替えたりすることは決してありません。
重要なクラッシュウィンドウが3つ存在します。
- ローカルのトランザクションがコミットされる前のクラッシュでは、可視となる操作が残らないため、呼び出し元は同じキーでリトライします。
- アウトボックスコミット後に発行確認(Ack)が失われた場合、リレーは再発行し、ワーカーは同じ操作をクレームします。
- プロバイダー側で成功したもののレスポンスが失われた場合、同じプロバイダーキー、プロバイダー参照による照会、またはWebhookを使用して収束させます。
これにより、企業間をまたぐトランザクションではなく、1つのインテントの再試行可能で監査可能な表現が提供されます。プロバイダーの冪等性保持期間を過ぎても結果が不明な場合は、自動リトライを停止します。プロバイダーレポートと運用担当者が結果を確定させる必要があり、経過時間だけで失敗と判断してはなりません。
ステップ5:非同期Webhookを安全に受信し、順序の乱れを許容する
エンドポイントは生のリクエストボディを保持し、現在およびローテーション期間中の古いシークレットを使用して署名とタイムスタンプウィンドウを検証します。無効な署名は拒否します。ユニークな(provider, provider_event_id)受信記録が永続化された後、エンドポイントは速やかに2xxを返し、非同期処理をキューに追加します。ログには、機密性の高いペイロード全体やシークレットではなく、イベントID、プロバイダー参照、および理由コードを含めます。
Webhookは重複、遅延、順序の乱れが発生する可能性があります。Stripeのドキュメントには、本番モードでの自動リトライとイベントの順序保証がないことが明記されています。ハンドラーは、オーソリが常にキャプチャより先に到着すると仮定することはできません。イベントのオブジェクト参照を通じてプロバイダーの現在の状態を取得するか、外部の事実を許可されたローカル遷移にマッピングします。すべてのイベントは、同一の操作IDまたはプロバイダー参照を通じて重複排除されます。すでに転記されたキャプチャが2回転記されることはなく、古いオーソリによってCAPTUREDがAUTHORIZEDにダウングレードされることもありません。
ブラウザはローカルの決済状態をポーリングまたは購読するだけです。リターンURL上の「success」パラメータによって注文を履行することはできず、クライアントがCAPTUREDを送信することもできません。確定的なキャプチャ成功と台帳コミットの後、決済サービスはアウトボックスを介してPaymentCapturedを発行します。注文サービスはイベントIDに基づいて冪等に注文を履行します。
ステップ6:追記専用台帳で金銭の事実を表現する
決済状態は運用上のビューであり、台帳は金銭の監査記録です。このプロンプトでは、会計をプロバイダー売掛金(資産)と加盟店買掛金(負債)の2つの勘定科目に簡略化します。最小単位が「分(fen)」であるCNY 100.00のキャプチャは、次のように転記されます。
journal capture-<operation_id>, CNY
debit processor_receivable 10000
credit merchant_payable 10000CNY 30.00の返金は、反対仕訳を追加します。
journal refund-<operation_id>, CNY
debit merchant_payable 3000
credit processor_receivable 3000各仕訳帳には少なくとも2つのエントリがあり、通貨ごとの借方と貸方の合計が等しくなります。そのjournal_idとビジネス操作参照は一意です。信頼できる状態をCAPTUREDまたは返金成功に進めるトランザクションは、仕訳帳とアウトボックスイベントも同時に挿入します。手数料、チャージバック、または加盟店への入金がスコープに含まれる場合は、明示的な勘定科目と新しいエントリを追加し、古いエントリを決して書き換えてはなりません。残高はエントリから導出されるか、再構築可能なプロジェクションによって高速化されます。プロジェクションが金銭の信頼できる情報源(Single Source of Truth)になってはなりません。
並行する返金処理は、まず決済行に対して条件付きで返金可能枠を確保します。新しい操作の作成と処理中金額の増加は、captured_amount - refunded_amount - pending_refund_amountが十分な場合にのみ許可されます。成功すると金額が保留中から返金済みに移動し、確定的な失敗で解放され、不明な場合は枠の確保を維持します。これにより、別の返金によって上限を超えてしまうことを防ぎます。プロバイダー側で返金上限が適用される場合もありますが、ローカルの不変条件がその外部の仕組みに依存してはなりません。
ステップ7:サイレントな不一致を照合し、安全に修復する
リアルタイムの処理パスだけでは、見落としが一切なかったことを証明できません。第1の照合レイヤーは、プロバイダー参照または冪等性キーによってUNKNOWNの操作を解決し、金額、通貨、操作タイプ、および最終状態を比較します。第2のレイヤーは、不変のプロバイダー取引または決済レポートを日次でロードし、決済、操作、仕訳帳、および注文の参照を照合します。第3のレイヤーは、プロバイダーの決済バッチを実際の銀行入金と照合し、未決済額、手数料、返金、チャージバックを区別します。
不一致は、外部のみに存在、内部成功/外部不在、金額や通貨の誤り、古い状態、重複参照、台帳の不均衡、または決済項目の欠落として分類します。高リスクの不一致が発生した場合は、影響を受ける加盟店残高の解放をブロックし、アラートを発報します。修復処理は冪等です。すでに確定しているプロバイダー操作をインポートする場合は監査理由を付与して新しい仕訳帳を追加し、会計上の修正では履歴に対するUPDATEではなく相殺仕訳を使用します。Stripeのレポートドキュメントでは、残高取引は不変であり、新しい返金取引が元の事実を相殺すると説明されており、決済、送金バッチ、および銀行受領書が個別に照合されます。
メトリクスは、APIの成功率とp99、決済状態の分布、UNKNOWN操作の数と経過時間、プロバイダーエラーとスロットリング、Webhook署名失敗/重複/遅延、オーソリからキャプチャまでの時間、確保された返金可能枠、アウトボックスとキューのバックログ、拒否された不均衡仕訳帳、不一致件数、および最も古い未解決の不一致を個別に計測します。ビジネスレポートでは、オーソリ率、キャプチャ率、返金率、決済率を明確に区別します。「リクエスト受付完了」を「入金完了」としてカウントしてはなりません。
ステップ8:機密領域を最小化し、障害を注入する
プロバイダーのトークン化コンポーネントがカードデータを直接収集します。バックエンドは、不可逆なプロバイダートークンと、ブランドや下4桁などの承認された表示用フィールドのみを保存します。クライアントシークレットがURLやログに入ることはありません。プロバイダーキーは、管理されたシークレットシステムで保持およびローテーションされます。Webhookシークレットは個別に管理され、サンドボックスと本番環境は分離されます。決済の閲覧、返金の開始、台帳の読み取り、手動修復の実行には別々の権限が使用されます。すべての高リスクなアクションは、操作者と理由を記録します。
PCI SSCは、暗号化されている場合であっても、オーソリ後にカード検証コード、PIN、およびPINブロックを保持することを明示的に禁止しています。ホスト型収集によりこのプロンプトの露出範囲は狭まりますが、コンプライアンスの判断には正式な評価が必要です。面接の回答で「トークンを使用しているためPCIは対象外になる」と主張してはなりません。
受け入れテストでは、クライアントが同じキーでリトライすること、そのキーで金額を変更すること、プロバイダー成功後にレスポンスが失われること、APIレスポンスより前にWebhookが到着すること、重複・順不同・遅延したWebhook、重複したアウトボックス発行、オーソリとキャンセルの競合、同一の返金可能枠を奪い合う2つの返金、一部返金後の全額返金、プロバイダー冪等性の有効期限切れ、Webhookシークレットのローテーション、片側のみの仕訳帳の拒否、照合によって発見された外部のみに存在するトランザクション、修復処理の反復実行、長時間のキューまたはプロバイダー停止後のキャッチアップなどをカバーします。すべてのシナリオで、決済状態、操作状態、仕訳数、注文の副作用、およびアラートをアサート(検証)します。
質の高い模範解答
「私は決済を1回のHTTP呼び出しではなく、長期にわたるビジネスリソースとしてモデル化します。1つの注文は安定したpayment_idを持ちます。アグリゲートは金額、通貨、およびオーソリ/キャプチャ/返金状態を保持します。各オーソリ、キャプチャ、キャンセル、返金は個別の操作IDを使用し、PENDING、SUCCEEDED、FAILED、またはUNKNOWNを持ちます。外部のタイムアウトはUNKNOWNとなり、即座に失敗したりプロバイダーを切り替えたりすることはできません。
決済の作成およびすべての金銭操作には、呼び出し元の冪等性キーが必要です。加盟店、操作タイプ、キーの組み合わせは一意です。最初のリクエストでは、そのダイジェスト、決済または操作、およびアウトボックスをアトミックに保存します。同じリクエストに対しては結果を再送し、キーに対して異なる金額や通貨が指定された場合は競合を返します。ワーカーは内部操作IDをプロバイダーキーとして使用します。アウトボックス、キュー、ワーカーはすべて少なくとも1回(at-least-once)の実行であり、同じ操作が1つの信頼できる遷移を適用します。
プロバイダーの同期レスポンス、署名検証済みWebhook、およびアクティブな照会は、同じ状態適用関数に入力されます。Webhookは生のペイロードを検証し、イベントIDを重複排除し、2xxを返す前に永続化し、重複や順序の乱れを許容します。古いイベントによって決済状態が後戻りすることはありません。ブラウザはローカル状態を表示するだけであり、注文履行をトリガーすることはできません。バランスの取れた仕訳帳およびビジネスアウトボックスとともにコミットされた確定的キャプチャ成功のみが、注文サービスによる冪等な履行を許可します。
キャプチャ仕訳帳はプロバイダー売掛金を借方に、加盟店買掛金を貸方に計上します。返金は反対仕訳を追加し、履歴は不変です。返金作成時は利用可能な返金可能枠を条件付きで確保するため、確定済み返金と処理中返金の合計がキャプチャ額を超えることはありません。金銭には整数の最小単位を使用し、最初の試行以降は通貨を変更できず、各通貨は個別にバランスします。
復旧には3つのレイヤーがあります。不明な操作はプロバイダーキーを再利用するかステータスを照会し、日次のプロバイダーレポートで決済、操作、仕訳帳を照合し、決済バッチを銀行入金と突き合わせます。不一致は隔離してアラートを通知し、修復は冪等なインポートまたは相殺仕訳の追記のみを行います。ホスト型のプロバイダーコンポーネントがカード情報を収集するため、バックエンドにはカード番号もセキュリティコードも保存されません。レスポンスの喪失、重複や順序の乱れたWebhook、並行するキャプチャと返金、プロバイダー停止、不均衡な仕訳帳、および照合で見つかる外部のみの項目などのテストにより、各外部結果が1つの監査可能な金銭事実へと収束することを証明します。」
よくある落とし穴
- ブラウザの成功ページを決済成功として扱う → リダイレクトは偽造可能であり、決済はまだ認証待ちや非同期確認待ちである可能性があります → ローカルステートマシンによって受け入れられた信頼できるプロバイダーの証拠のみが注文履行をトリガーします。
- ライフサイクル全体に1つの
paidフィールドを使用する → 追加認証、オーソリ、キャプチャ、一部返金、または不明な結果を表現できません → 決済アグリゲートを個別の操作試行から分離します。 - タイムアウト後に新しいIDや別のプロバイダーでリトライする → 最初の試行ですでに請求されている可能性があり、二重請求が発生します → 操作キーおよびプロバイダーキーを再利用し、不明な結果を照会または照合します。
- パラメータを検証せず冪等性キーのみを比較する → 金額を変更してキーを再利用した呼び出し元が誤ったビジネス結果を受け取ることになります → 正規化されたリクエストダイジェストを永続化し、意図の変更に対しては競合を返します。
- Webhookの順序に依存する → プロバイダーはイベントを再送、遅延、順不同で送信する可能性があります → 重複排除を行い、現在の状態を取得するか、正当な単調遷移のみを適用します。
- 決済テーブルの残高を台帳として扱う → その場での更新は金銭変更の理由を失わせ、個別に照合できなくなります → バランスの取れた仕訳帳を追記し、残高は再構築可能なプロジェクションとして保持します。
- 返金可能残高を読み取ってから並行して書き込む → 2つの呼び出し元が同時に空き枠を確認し、キャプチャ額を超過する可能性があります → 操作に一意に紐づくトランザクショナルな条件付き更新を使用して枠を確保します。
- APIの200レスポンスのみを監視する → 受付済み、オーソリ済み、キャプチャ済み、決済完了はそれぞれ意味が異なります → 各状態、不明状態の継続期間、および照合不一致を測定します。
- トークン化によりPCIの責任が自動的に免除されると主張する → ページ、ログ、スクリプト、および運用プロセスが依然としてスコープ内に残る可能性があります → カードデータを最小限に抑え、コンプライアンスチームに実際の境界を確認してもらいます。
- 不一致を修復するために過去のエントリを編集する → 監査チェーンが破壊され、過去のレポートを再生成できなくなります → 理由を明記したインポートまたは相殺仕訳を追記します。
フォローアップの質問と回答
フォローアップ1:プロバイダー側でキャプチャされたが、同期レスポンスとWebhookの両方が失われました。どうなりますか?
操作はUNKNOWNのままとなり、関連するオーソリまたは返金枠の確保を維持し、同じインテントに対する別の操作の実行を防ぎます。まず、サポートされている照会を行うか、元のプロバイダー冪等性キーで呼び出しを再試行し、次にプロバイダー参照によって能動的に問い合わせます。それでも確定しない場合は、取引レポートの照合を待ちます。成功が確認された場合は同じ状態適用関数に入力され、仕訳帳とアウトボックスを転記します。確定的な失敗の場合にのみ枠を解放します。証拠がないままプロバイダーキーの保持期間が切れた場合は、経過時間から失敗を推測するのではなく、運用担当者にケースをエスカレーションします。
フォローアップ2:CNY 100のキャプチャに対して2件のCNY 60返金が並行して要求されました。過剰返金をどう防ぎますか?
決済行または専用の返金可能残高行に対して条件付き更新を使用します。少なくともCNY 60が残っている場合にのみ、pending_refund_amountをCNY 60増やし、操作を作成します。両方のトランザクションが1つのバージョンで競合するため、一方が成功し、他方は再読み込み時に枠不足となります。不明な返金は枠確保を維持し、確定的な失敗で解放され、成功すると保留中から返金済みに移動します。プロバイダー側での制御はあくまで第2の防衛線にすぎません。
フォローアップ3:WebhookによりCAPTUREDがAUTHORIZEDより先に届きました。どのように処理されますか?
両方の受信記録を永続化し、重複排除します。CAPTUREDの署名、金額、通貨、およびプロバイダー参照が一致する場合、キャプチャ遷移と仕訳帳を適用します。後から届いたAUTHORIZEDイベントは過去の事実です。ステートマシンはロールバックを拒否し、Webhookの監査情報と遅延メトリクスのみを更新します。ペイロードに十分なバージョン情報が含まれていない場合は、到着順で上書きするのではなく、プロバイダーの現在の決済リソースを取得します。
フォローアップ4:なぜ決済テーブルと台帳の両方が必要なのですか?
決済テーブルは、キャプチャ、キャンセル、または返金が許可されるかどうかを判断するのに適したワークフローアグリゲートです。追記専用台帳は、残高がどのように形成され、どのビジネスイベントが各変更を引き起こしたかを説明するのに適した金銭記録です。決済はCAPTUREDからPARTIALLY_REFUNDEDに移行できますが、台帳は元のキャプチャと個々の独立したバランス仕訳帳を保持します。一意の操作参照がこれらを結合し、信頼できる遷移が1つのローカルトランザクション内で両方をコミットします。どちらの責務も他方を代替することはできません。
フォローアップ5:照合修復処理が2回転記されないことをどのように証明しますか?
各外部レポート行に、プロバイダー、レポートタイプ、トランザクション参照などの安定したソースキーを付与します。修復処理を不一致IDにバインドし、ユニークな(source_key, repair_type)制約を追加します。最初のトランザクションが仕訳帳を追記し、不一致にマークを付け、アウトボックスを書き込みます。クラッシュして再実行された場合は同じ修復レコードを検出し、その結果を再利用します。コミット前、コミット後、イベント発行後にプロセスを強制終了(キル)しても、仕訳数、残高、および下流イベント数が2回目に増加しないことを検証します。
フォローアップ6:後から2つ目のプロバイダーを追加する場合、フェイルオーバーはいつ許可されますか?
第1のプロバイダーが「操作は作成されなかった」または「確定的に失敗した」と明示的に回答し、ローカル操作に金銭の事実が存在しない場合にのみ、第2のプロバイダーで操作を作成します。接続タイムアウト、5xxエラー、不明状態はこの条件を満たしません。ルーティングの選択、プロバイダーの機能、金額、通貨、および理由を監査ログに記録します。両方の結果が存在する可能性がある場合は、問い合わせや照合によって二重請求の可能性が排除されるまで、履行と残高の解放を凍結します。自動返金によって不明な結果を隠蔽してはなりません。