振込1件を追跡する
振込情報の入力、依頼の保存、認証アプリTOTPによる認可、資金移動、複式記帳、履歴、管理者監視までを、現在の実装順に追跡します。
台帳、振込、KYC、権限、監査、照合をMinibankの実装と結び付ける。
先に学ぶ内容 — Prerequisite: 基礎 / 銀行業務学ぶこと — LEARNING GOALS
このページで学ぶこと
- TOTP認可付き振込の二段階処理
- 冪等性・状態機械・lock・transaction
- 振込先、お気に入り、履歴、監視のデータ分離
体験02 · TRANSFER(振込)
振込が失敗したとき、どんな記録が残る?
まずは予想をひと言。本文を読んだ後、お金の動きと、振込を頼んだ記録を順番に観察します。実際に失敗を起こす必要はありません。
- 01まず予想する — PREDICT
まずここだけ:思ったことをひと言書く
振込の途中で失敗したら、お金を動かした記録も、振込を頼んだ記録も、全部消えると思いますか?
正解でなくて大丈夫です。「〜だと思う」から始めてみましょう。
ヒント1を見る
お金を動かすことと、振込を頼むことは、同じタイミングでしょうか?
ヒント2を見る
失敗した理由を後から調べたいとき、どんな記録があると助かりますか?
もっと詳しく:この問いで使う専門用語
- Request(処理の依頼)は、振込内容を受け付けた記録です。
- MFA intent(振込への認可記録)は、振込内容とログイン中のsessionに結び付けた認可の期限・試行回数・使用状態を持ちます。コード自体は認証アプリとSupabase Authの間で確認され、銀行のDBには保存されません。
- Transaction(ひとまとまりとして確定・取消する処理)は、まとめて保存する更新の境界です。
- Rollback(途中の変更を元に戻すこと)は、失敗した処理の範囲にある変更を取り消します。
- Commit(変更の確定)は、その処理の変更を保存することです。
- State(状態)は依頼が待機・完了などのどの段階にあるかを表し、Lifecycle(状態の移り変わり)はその流れです。
- Ledger(会計台帳)はお金の正式な会計記録、Audit(監査証跡)は誰がいつ何をしたかを追う記録です。
- Idempotency(同じ依頼を繰り返しても結果を重複させない性質)は、二重の送金を防ぐための約束です。
CURRENT TRANSFER LIFECYCLE
どこまで進むと、お金が動くのか
緑の境界より前は「振込意思と認証アプリTOTPによる認可」の段階です。この振込に束縛した認可を消費した後、初めて資金移動へ入ります。
口座番号・金額・依頼人名・メモ
Auth・入力・Idempotency-Keyを確認
送金先を解決し、依頼を保存。確認コードは作らない
DBが返した銀行・支店・名義を見る
Authで検証済みのintentを、owner・session・内容hash・60秒以内で1回だけ消費
内部呼出し限定。両口座lock後に残高・冪等性・上限を再確認
Ledger・Transfer・残高cache・Audit
顧客は完了取引、管理者は全依頼状態
posted Ledgerを起点に、振込先詳細と金額を表示します。
transfer_requestsを起点に、認証待ち(pending_otp)から期限切れまで確認します。
残高不足や本人確認の失敗で、振込がどの段階で止まるかを3つのケースで体験します。
日常語で理解する — PLAIN LANGUAGE
まず、ここだけつかむ
振込の途中でお金の記録の更新に失敗すると、その部分だけ元の状態に戻します。ただし、振込を依頼した記録自体は消えず、「失敗した」という記録として残ります。この考え方を「ロールバック」と呼びます。
振込の本人確認には、スマートフォンの認証アプリ(TOTP)が表示する6桁のコードを使います。銀行側がコードを作って画面に表示することはなく、銀行のデータベースにコードは保存されません。確認は「この振込の内容」に結び付けられ、一度しか使えません。ただし、認証アプリのコード自体は振込先や金額を証明するものではありません。
現在の振込は二段階です。最初に振込依頼を保存しますが、この時点では残高を動かさず、確認コードも作りません。認証アプリのコードでこの振込への認可を得た後だけ資金移動を実行します。
銀行・支店・口座名義は画面入力を信用せず、DBが実際の送金先口座から解決します。お気に入りも利用者ごとに分離されます。
同じ振込を誤って二重に実行しないように確認し、関係する2つの口座を一時的に順番待ちさせてから、残高・記録・監査をまとめて更新します。この考え方を「冪等性」「ロック」と呼びます。
順番に理解する — DIFFICULT PARTS, STEP BY STEP
難しいポイントを、順番にほどく
専門用語を覚えることより、「なぜ必要か」「このシステムのどこで使うか」を理解することが目標です。
二段階振込
振込の依頼を保存する段階と、本当にお金を動かす段階を、認証アプリによる確認で分けます。
内容確認とTOTPによる認可が終わる前に資金が動かないようにするためです。
request_transfer()はpending_otpを作り、prepare_transfer_mfa()とtransfer_mfa_brokerで認可したintentを、confirm_transfer_otp()が消費してからexecute_transfer()を呼びます。
認可intentの状態機械
コードの正誤だけでなく、準備済み・challenge束縛済み・試行予約済み・認可済み・消費済み・期限切れを区別します。
一度使った認可や古いchallengeを再利用させず、試行回数も確実に残すためです。
transfer_auth.intentsがchallenge_id・attempts・authorized_at・consumed_at・expires_atを持ちます。旧デモOTPのactive/consumed/expired/failed/superseded分岐は#128以前の実装です。
冪等性
同じ依頼を何度送っても、実際のお金の移動は1回だけになる性質です。
二重クリック、タイムアウト後の再送、通信機器のretryは通常運用でも起こります。
idempotency keyとrequest fingerprintを保存し、同じ内容なら既存結果、異なる内容なら衝突として扱います。
request fingerprint
送金元・送金先・金額など、依頼内容から作る照合用の指紋です。
受付番号だけを見ると、同じ番号を別金額へ悪用・誤用できるからです。
同じkeyの保存済みfingerprintと今回の内容を比較します。
行lockと順序
更新対象の口座を一時的に他処理から守り、必ず同じ順で鍵を取ります。
同じ残高を2件が同時利用するlost updateや、互いの鍵を待つdeadlockを抑えます。
関係する口座を決定的な順序でFOR UPDATEし、その後に残高を検査します。
rollback
途中で失敗した変更を、処理前の状態へ全部戻すことです。
送金元だけ減る、業務記録だけ残る、といった半端な成功は金融事故になります。
confirm_transfer_otpが内側の金融更新の例外を捕捉すると、そのblockの更新はrollbackします。認可済みintentの消費・request=failed・TRANSFER_FAILEDは外側がcommitすれば残ります。
発展 — ADVANCEDもっと詳しく確認する最初の学習では読まなくても問題ありません。仕組みをもっと深く理解したい場合に開いてください。
金融更新を戻す範囲と、失敗後に残す記録
金融更新の仕訳・transfers・残高cache・成功audit(TRANSFER_COMPLETEDと内側の変更trigger)は、confirm_transfer_otpの内側の例外blockでまとめてrollbackします。先に保存済みのrequestは消えません。transfer_auth.consumeによる認可済みintentの消費(consumed_at)、request=failed、TRANSFER_FAILEDは内側のrollbackの対象外で、外側のDB transactionがcommitすると残ります。
TOTPコードの正誤はDBではなくSupabase Authが判定します。DBはNext.jsのtransfer_mfa_broker経由で検証前に試行を予約し、その時点でintentのattemptsを加算します(累積5回まで、間隔30秒以上)。認証に失敗しても変わるのはintentとAuth側の状態だけで、requestはpending_otpのまま、金融データも変わりません。新しい認可がないままconfirmすると、transfer_auth.consumeが42501で拒否し、その呼出し全体がrollbackします。request=failedは金融更新の例外を捕捉した経路です。未捕捉例外・deferred constraint・commit自体の失敗なら、その呼出し全体の変更がrollbackするため、失敗記録の永続化を無条件には保証しません。
| 場面 | 金融更新 | 外側commit後の状態・event |
|---|---|---|
| 依頼作成 | まだ記帳しない | pending_otp、TRANSFER_OTP_REQUESTED(event名は従来のまま)。確認コードは生成しない |
| TOTPの誤入力・Auth検証失敗 | 資金移動に入らない | 予約時点でintent.attemptsが加算済み(累積5回まで)。requestはpending_otp |
| 新しい認可なしのconfirm(未認可・60秒超過・別session・内容変更) | consumeが42501で拒否し、その呼出し全体をrollback | requestはpending_otp、intentは未消費のまま |
| 認可後の資金移動で捕捉した例外 | 内側の仕訳・transfer・cache・成功auditをrollback | intent消費済み、request=failed、TRANSFER_FAILED |
| 成功 | 金融更新と成功auditを保存 | request=completed、transfer_id、intent消費済み |
| 外側transaction/commit失敗 | その呼出し全体をrollback | その呼出しの失敗eventも保証しない。過去のcommit済み依頼とは別 |
振込intentに束縛した認証アプリTOTPと、再試行・上限契約
v7(migration 20260920074653と20260920083426)の振込認可は、Supabase Authの認証アプリTOTPを振込intentへ束縛する方式です。request_transferは確認コードを生成・返却せず(otp_plaintext列は応答互換のため残り、常にnull)、reissue_transfer_otpは常に42501で拒否します。prepare_transfer_mfaは所有者・Auth session・送金元・送金先・金額・memo・依頼人名・idempotency key・期限のhashをintentへ固定します。ブラウザはコードをSupabase Authへ直接送り、Next.jsとDBはコードも、そのhashも受け取らず保存もしません。DBはAuth所有のchallenge検証時刻、AAL2 session、同じsessionのTOTP AMRを読んで認可し、confirmは60秒以内の認可を1回だけ消費します。DEMO_OTP_DISPLAY_ENABLEDでDBのコード生成は復活しません。TOTPは時刻ごとの値で、振込先や金額への署名ではなく、フィッシング耐性も主張しません。稼働DBへの適用はこの教材ではUNKNOWNです。
| 対象 | コード上の契約 |
|---|---|
| intent | kind+requestごとに1件。owner・Auth session・支払内容のhashを固定。期限は作成から5分以内かつ依頼期限まで。内容やsessionが変わると使えない |
| 検証試行 | 検証前にserverが予約し、その時点で加算。intentごとに累積5回、試行間隔30秒以上。失敗・結果不明も回数に含む |
| challenge | intent作成後に作られ、束縛時点で作成30秒以内の未検証TOTP challengeだけを束縛。束縛は最大4件、間隔30秒以上。新しいchallengeで試行回数はリセットしない |
| 認可 | 予約より後にAuthが検証し(60秒以内)、intentのsessionがAAL2かつ同じsessionのTOTP AMRが予約・検証以降。別intentの認可から90秒以内は拒否 |
| 消費 | confirm_transfer_otpがtransfer_auth.consumeで1回だけ消費。認可から60秒以内・同じsession・未消費・内容hash一致が条件 |
| 旧再発行RPC | reissue_transfer_otpは常に42501。コードの再発行ではなく、新しいAuth challengeを束縛し直す |
| 期限 | 依頼は作成から15分。期限切れの依頼をconfirmするとrequest=expired。intentを別sessionへ移したり復活させたりしない |
| completedのconfirm再試行 | 所有者確認後、同じtransfer_idを返す。再送金も新しい認可の消費もしない |
| request冪等性 | 同一user+keyで送金先・金額・trim済み依頼人名・正規化memoを比較。相違はTR009。同内容pending_otp/completedは同じrequestを返す(otp_plaintextは常にnull) |
| 終了した依頼のkey | failed/expired/cancelled等でのrequest key再利用はTR009。confirmの完了再取得とは区別する |
| execute_transfer ACL | PUBLIC/anon/authenticated/service_roleのEXECUTEを剥奪。definerのconfirmから内部実行。旧認証済みPOST /transfersは409 OTP_REQUIRED |
| 振込上限 | 1回500,000円(依頼時にも検査)、送金元口座ごとJST日次1,000,000円。口座lock後、確定済みtransfersを集計 |
| 頻度 | 新規requestはuserごと直近1分5件まで。資金移動は送金元口座ごと直近1分10件まで。超過TR012 |
Java経路の実装と稼働確認を分ける
request/confirmはPAYMENT_BACKENDで選択します。未設定・空文字・legacyはNext.js→PostgREST RPC、javaはNext.js→Java Payment Service→PostgREST RPCです。不正な値はエラー、Javaのtimeout・5xx・通信断でもlegacyへ自動fallbackしません。旧OTP再発行はこの切替の対象外で、v7のRPCは常に42501で拒否します。TOTPの認可準備はNext.jsの/api/v1/transfers/mfaが担います。コードの存在は確認済みですが、本番・PreviewでJavaが選択されているかはUNKNOWNです。
- 01Next.jsがPAYMENT_BACKENDを読む
- 02legacy:呼出者tokenでPostgRESTへ / java:Java Payment ServiceへHTTP
- 03request_transfer / confirm_transfer_otpでDB処理
- 04Javaの@TransactionalではHTTP先のcommitを取り消せない
- 05応答消失時は結果不明。別backendへ自動fallbackしない
現在の振込フロー:依頼と実行を分ける
- 01振込情報入力:振込先口座・金額・依頼人名・メモ
- 02POST /api/v1/transfers/request:Bearer token、入力、Idempotency-Keyを検証
- 03request_transfer():振込依頼を作成。確認コードは作らず、この時点ではお金を動かさない
- 04確認画面:DBが解決した銀行・支店・預金種目・口座名義を表示
- 05POST /api/v1/transfers/mfa:prepare_transfer_mfa()が内容とAuth sessionをintentへ固定
- 06ブラウザがSupabase AuthでTOTP challengeを作り、serverがbrokerでintentへ束縛
- 07serverが検証試行を予約した後、ブラウザがTOTPコードをSupabase Authへ直接送る
- 08serverがbrokerで認可を完了。DBがAuth所有の検証記録・AAL2・同じsessionのTOTP AMRを確認
- 09POST /api/v1/transfers/otp/confirm → confirm_transfer_otp():owner・依頼状態・期限を確認し、transfer_auth.consumeで認可を1回だけ消費
- 10execute_transfer()を内部実行し、2口座をlockしてLedger・transfer・残高cache・Auditを確定
- 11入出金明細へ振込情報を表示し、管理者の振込監視から状態を確認
第1段階:request_transfer()は、依頼を固定する
このRPCは資金移動を行いません。callerをauth.uid()から特定し、KYC、送金元、送金先、自己送金、入力、1回500,000円の上限、利用者ごと直近1分5件の頻度を検査したうえで、transfer_requestsだけを作ります。v7では確認コードもtransfer_otp_challengesの行も作らず、応答のotp_plaintextは互換用で常にnullです。
銀行名・コードは『ミニ銀行 / 0001』、支店名・コードは『本店営業部 / 001』へserver側で固定され、口座名義は送金先customerから解決されます。利用者が名義を偽装して保存する経路はありません。
| 保存先 | 保存するもの | 意味 |
|---|---|---|
| transfer_requests | 送金元、送金先snapshot、金額、依頼人名、memo、status、fingerprint | TOTP認可前の認証済み業務依頼 |
| transfer_auth.intents | この段階ではまだ作られない。後でprepare_transfer_mfa()が作る | 内容hash・session・試行回数・認可/消費時刻を持つ非公開の認可記録 |
| transfers | まだ作られない | completedした資金移動だけを保存 |
| Ledger | まだ記帳しない | 認可の消費前には残高を動かさない |
TOTPコードをアプリとDBへ入れない:intentへの束縛
TOTPコードはブラウザからSupabase Authへ直接送られます。Next.jsのAPIとDBはコードもそのhashも受け取らず、保存もしません。代わりにDBは、Auth所有のchallengeの検証時刻、AAL2へ上がったsession、同じsessionのTOTP AMR(認証方法の記録)を読み取り専用で確認します。ブラウザからの『成功した』という申告やJWTのAAL2 claimだけでは認可になりません。
transfer_mfa_brokerはNext.jsのserverだけが呼べるよう、呼出者・sessionの一致に加え、環境ごとのserver secret(TRANSFER_MFA_SERVER_SECRET)のdigest照合を要求します。6桁のTOTPは総当たり可能な小さな空間なので、検証前にserverが試行を予約してattemptsを加算し(intentごと累積5回・間隔30秒以上)、challengeの束縛も最大4件・間隔30秒以上に制限します。新しいchallengeで試行回数はリセットしません。別intentの認可から90秒以内は、同じTOTPの時間枠を使い回せないよう拒否します。
旧デモOTP(salt付きhash保存、request/reissue RPCが平文を返しDEMO_OTP_DISPLAY_ENABLEDで画面表示)は#128以前の実装です。v7のDBはコードを生成せず、reissue_transfer_otpは常に42501を返します。
| intentの項目 | 意味 | confirm時の確認 |
|---|---|---|
| payload_hash / session_id | 作成時の支払内容とAuth session | 内容・sessionが変わっていれば消費できない |
| challenge_id / issue_count | 束縛したAuth challengeと束縛回数 | 作成後の新しい未検証challengeだけ。最大4件 |
| attempts / attempt_at | 予約済みの検証試行 | 累積5回。失敗や結果不明も数える |
| authorized_at | 予約後のAuth検証を確認した時刻 | 60秒以内でなければ消費できない |
| consumed_at / expires_at | 消費時刻と期限(5分以内かつ依頼期限まで) | 消費済み・期限切れは復活させない |
第2段階:confirm_transfer_otp()から資金移動する
confirm RPCはtransfer_requestsの行をFOR UPDATEして所有者を確認します。completedなら同じtransfer_idを返し、再度資金を動かしません。それ以外はpending_otpであることを確認し、依頼期限を過ぎていればrequest=expiredにしてrequest_expiredを返します。p_otp_code引数は互換のため残っていますが、v7の本体はコードを検証に使いません。
次にtransfer_auth.consume()が、同じ利用者・同じsession・認可から60秒以内・未消費・期限内・内容hash一致・AAL2のTOTP factorを確認してintentを消費します。その後requestをprocessingにして、例外block内でexecute_transfer()を呼びます。資金移動が失敗した場合、内側の実行変更はrollbackされますが、外側ではintentの消費とrequest=failed、TRANSFER_FAILEDを保存できます。
execute_transfer()が守る資金移動
- auth.uid()、金額、送金先、idempotency keyを確認
- 送金先と金額のfingerprintで、同じkeyの別内容を拒否
- 顧客、KYC、両口座のactive状態、自己送金を確認
- 2口座をUUID順にFOR UPDATEし、競合とdeadlock riskを抑える
- lock後に冪等性、Ledger残高、口座状態を再確認
- 送金元預金負債をdebit、送金先預金負債をcreditとして同額記帳
- transfersへ業務情報snapshotを追加
- Ledgerから両cached_balanceを再計算
- TRANSFER_COMPLETED business auditを追加
送金元 Customer Deposit Liability : debit 10,000
送金先 Customer Deposit Liability : credit 10,000
debit合計 = credit合計振込先情報とお気に入り
transfer_beneficiariesは利用者ごとの振込先masterです。登録時に実口座の存在・active状態・自己口座でないことをDBが検査し、銀行・支店・名義をserver側で固定します。利用者が更新できるのはlabelとis_favoriteだけで、振込先口座自体の変更は削除と再登録です。
tableはRLS有効かつanon/authenticated/service_roleの直接権限がなく、list/create/update/deleteのSECURITY DEFINER RPCがauth.uid()でownerを確認します。別利用者のIDはnot foundへ畳み、存在列挙も抑えます。
| 機能 | DBでの防御 |
|---|---|
| 一覧 | owner_user_id = auth.uid()、favorite・最終利用順 |
| 登録 | 実口座を再解決、自己口座拒否、owner+口座番号をunique |
| 編集 | 対象行をlockしowner確認、label/favoriteだけ更新 |
| 削除 | 対象行をlockしowner確認 |
| 最終利用 | transfer_requests INSERT triggerでlast_used_at更新 |
履歴と管理者監視は、見る目的が違う
顧客履歴はLedgerへpostedされた取引だけを返すためstatusはcompletedです。認証待ち(pending_otp)や失敗も運用監視する管理者画面は、資金移動前を含むtransfer_requestsのlifecycleを読みます。admin wrapperはauth.uid()からactive operatorを確認し、検索とcursor paginationをprivate ops functionへ委譲します。
| 画面 | source | 見える状態・情報 |
|---|---|---|
| 顧客の入出金明細 | get_account_transactions() → posted journal | completed取引、方向、相手口座、銀行・支店・種目・名義・依頼人・fee・組戻し |
| 管理者の振込監視 | admin_list_transfer_requests() → transfer_requests | pending_otp / processing / completed / failed / expired / cancelled、失敗理由、組戻し |
| 監査画面 | audit_logs | transfer_requestsを含む対象の操作履歴 |
実際にテストが発見した3つのバグ
| 事象 | 根本原因 | 修正と学び |
|---|---|---|
| PGRST203で旧呼出しが失敗 | 引数を3個からdefault付き11個へ増やしたCREATE OR REPLACEは旧3引数版を置換せずoverloadを追加した | 旧signatureをDROP。PostgreSQL function identityは名前+引数型で決まる |
| 5回失敗後にotp_expired | WHERE status='active'でfailed行を検索対象から消し、存在しないchallengeと区別できなかった | 最新行をstatus不問でlockし、取得後に状態分岐。状態機械をWHERE句で消さない |
| 生DB messageがclientへ露出 | message_textをfailure_reasonとしてRPC responseへ返した | RPCはSQLSTATEだけ返し、TransferServiceのmapRpcError()で安全な日本語へ変換 |
実際のMinibankでは
現在の振込はrequest/confirm経路を通ります。認証済みの旧POST /api/v1/transfersは409 OTP_REQUIREDを返します。execute_transferはPUBLIC・anon・authenticated・service_roleからEXECUTEを剥奪し、所有者権限のconfirm_transfer_otpから内部実行します。古いcreateTransferのコードが残っていても、顧客の直接実行権限を意味しません。振込先情報は業務recordへ保存し、Ledgerの形は従来どおり借貸2行のままです。
体験学習の続き · LEARNING LOOP
ここまで読めましたか?続けて確認してみましょう
振込の流れを読んだところで、実際の画面・記録・コードを見て、最初の予想と見比べながら自分の言葉で説明してみます。
- 02実際に操作する — OPERATE
同じ仕組みを、動く画面から観察する
まず顧客画面を開いて観察し、この教材へ戻ります。リンクは新しいタブで開きます。ログインが必要です。
アカウントがない場合は新規登録が必要です。本人確認・口座開設には管理者承認が必要です。承認前や操作できない場合も、この教材内の図・説明・Source Trail(教材の参照元)で代替学習し、次へ進めます。
ログイン・新規登録の入口を開く ↗操作できない場合:教材の参照元で確認する ↓利用者の画面 · MINIBANK振込画面を開く ↗学習用のデモ情報で、依頼・確認コード・結果の順を観察します。実在する情報は入力しません。
利用者の画面 · MINIBANK顧客の入出金明細 ↗振込が完了した後に、金額と取引日時がどう表示されるか見ます。
もっと詳しく:管理者の画面で観察する
管理者ログインが必要です。権限がない場合は、次の「証拠を確認する」の教材による確認で進められます。
- 03証拠を確認する — EVIDENCE
何を見たか、1つずつ確かめる
Evidence(判断の根拠となる記録)を確認したらチェックします。各項目の「何を見るか」を手がかりにしてください。
画面を開けない項目は、Source Trail(教材の参照元)と教材の説明で確認できます。失敗を実際に起こす必要はありません。
- 04コードの処理経路を追う — CODE TRAIL
コードを書く必要はありません
ここでは「説明されている仕組みが、実際のMini Bankのどこに実装されているか」を確認します。まずは日本語の役割説明を読み、入力した依頼がどこへ渡るかをつかみましょう。コードの全文を読む必要はありません。
基本の順序は、画面 → API Route(依頼を受ける窓口)→ TransferService(処理をつなぐサービス)→ DB / RPC(記録の保存場所と、その処理の呼び出し)です。まず最初の2つを見て、続くサービスは「もっと詳しく」で確認します。
利用者が振込を入力し、確認コードを送る画面。依頼と確認を分けている箇所を探します。
src/components/customer/TransferForm.tsxAPI Route(依頼を受ける窓口)。確認コードの入力を検査し、サービスへ渡します。
src/app/api/v1/transfers/otp/confirm/route.ts教材の参照元を確認する — Source Trail ↓もっと詳しく見る:残りのファイルと処理の約束
API Routeから受け取った依頼と振込処理をつなぐサービス。依頼作成と確認の呼び出しを探します。
src/services/TransferService.tsJava backend(Java側の処理サービス)への接続。設定で選ばれた場合だけ利用され、自動で別経路へ切り替わりません。稼働環境での選択はUNKNOWN(未確認)です。
src/services/JavaPaymentBackend.tsMigration(DB定義の変更履歴)。金融更新を取り消す範囲と依頼状態の保存条件を確認します。
supabase/migrations/20260907102613_banking_authorization_and_integrity.sqlRegression test(変更後も約束が保たれるかの確認)。#128以前のデモ確認コードと振込の失敗・再送を検査します。v7のTOTP認可は対象外です。
scripts/regression/suites/transferOtp.mjsLock(同時更新を制御する仕組み)は競合を防ぎ、Fingerprint(依頼内容の照合用の値)は内容の違いを見分けます。
参照元のCURRENT IMPLEMENTATION(現在の実装)とHISTORICAL IMPLEMENTATION(過去の実装)を区別します。古い変更履歴だけでは現在の定義を判断できません。
- 05自分の言葉で説明する — EXPLAIN
LEVEL 1 — やさしい説明(まずここだけ)
振込のお金の更新で失敗したとき、元に戻るものと、残り得るものを分けて、短い2文を目安に説明してください。残るための条件も、日常語で書いてみましょう。
日常語だけで完了できます。専門用語の数や正誤を自動採点するものではありません。
ヒント1を見る
「お金の変更」と「頼んだことの記録」を分けて書いてみましょう。
ヒント2を見る
残る記録について、いつでも必ず残るのか、処理の確定が必要なのかを教材で確かめましょう。
もっと詳しく:LEVEL 2 — 専門用語を使った説明(任意)
Rollback(途中の変更の取消)とCommit(変更の確定)を使い、Request(依頼)・OTP challenge(確認コードの検証記録)・Ledger(会計台帳)・TRANSFER_FAILED(振込失敗の出来事)の扱いを説明してみましょう。
挑戦する場合は、上の説明欄に追記できます。追加しなくても完了できます。
説明の確認ポイント — Checkpoint- 振込を頼む段階と、お金を動かす段階を分けて説明できる
- お金の変更が戻っても、依頼や失敗の記録は処理が確定すれば残り得ると説明できる
理解を確かめる — CHECK YOUR UNDERSTANDING
7問で理解チェック
答えを頭の中で考えてから、各項目を開いて確認してください。
Q1金融更新のrollbackで依頼や失敗記録も全部消えますか?
金融更新の仕訳・transfers・残高cache・成功audit(TRANSFER_COMPLETEDと内側の変更trigger)は、confirm_transfer_otpの内側の例外blockでまとめてrollbackします。先に保存済みのrequestは消えません。transfer_auth.consumeによる認可済みintentの消費(consumed_at)、request=failed、TRANSFER_FAILEDは内側のrollbackの対象外で、外側のDB transactionがcommitすると残ります。
Q2振込の確認コードは銀行が作って画面に表示し、DBに保存しますか?
v7(migration 20260920074653と20260920083426)の振込認可は、Supabase Authの認証アプリTOTPを振込intentへ束縛する方式です。request_transferは確認コードを生成・返却せず(otp_plaintext列は応答互換のため残り、常にnull)、reissue_transfer_otpは常に42501で拒否します。prepare_transfer_mfaは所有者・Auth session・送金元・送金先・金額・memo・依頼人名・idempotency key・期限のhashをintentへ固定します。ブラウザはコードをSupabase Authへ直接送り、Next.jsとDBはコードも、そのhashも受け取らず保存もしません。DBはAuth所有のchallenge検証時刻、AAL2 session、同じsessionのTOTP AMRを読んで認可し、confirmは60秒以内の認可を1回だけ消費します。DEMO_OTP_DISPLAY_ENABLEDでDBのコード生成は復活しません。TOTPは時刻ごとの値で、振込先や金額への署名ではなく、フィッシング耐性も主張しません。稼働DBへの適用はこの教材ではUNKNOWNです。
Q3振込内容の確認画面が出た時点で、お金は動いていますか?
動いていません。transfer_requestsが作られただけです。TOTPで認可したintentをconfirm_transfer_otp()が消費した後に、execute_transfer()が資金を動かします。
Q4#128以前のデモOTP実装で、failed状態のchallengeをWHERE status='active'で探すと何が起きましたか?
行が存在しないように見え、『失敗済み』と『最初から存在しない』を区別できなくなります。
Q5送金成功の応答を受け取れず再送したら、どうなるべきですか?
同じidempotency keyと同じ内容なら、再送金せず最初の結果を返すべきです。
Q6なぜ残高確認より先にlockが必要ですか?
確認後から減額までの間に別処理が残高を使う競合を防ぐためです。
Q7同じkeyで金額だけ変えた場合は?
既存結果を返さず、fingerprint不一致として拒否すべきです。
完全に暗記する必要はありません。大切な考え方を説明できそうなら押してください。
教材の参照元 — SOURCE TRAIL
根拠と関連教材
CURRENT IMPLEMENTATIONは確認したrepository内の実装です。HISTORICAL IMPLEMENTATIONは導入・修正の履歴で、後続migrationによる再定義があり得ます。RELATED LESSONは教材への案内で、実装の一次Sourceではありません。ASSUMPTIONは仮定、UNKNOWNは未確認を表します。
確認基点: d02eae8914f39cbf06bfe422dc39c71886f82b3e / 2026-09-11。稼働環境への適用確認: UNKNOWN。パス実在の自動検査と、記述した技術的事実の確認は別です。
- 現在の実装 — CURRENT IMPLEMENTATION — Primary Source
supabase/migrations/20260920074653_transfer_totp_intent_authorization.sqlpublic.request_transfer / public.confirm_transfer_otp / public.reissue_transfer_otp / public.prepare_transfer_mfa#128。依頼は確認コードを生成せずotp_plaintext=null。confirmはtransfer_auth.consumeで認可済みintentを消費してからexecute_transferを呼ぶ。reissueは常に42501。稼働DBへの適用はUNKNOWN。 - 現在の実装 — CURRENT IMPLEMENTATION — Primary Source
supabase/migrations/20260920083426_require_fresh_totp_on_intent_session.sqlpublic.transfer_mfa_broker#129。intentのAuth session自身に、予約・検証より後のTOTP AMRを要求する最新broker定義。 - 現在の実装 — CURRENT IMPLEMENTATION — Primary Source
supabase/migrations/20260920115556_scalar_journal_posting_atomic_cache.sqlpublic.execute_transfer最新定義。ledger.post_journal_entryの戻り値をscalar uuidで受ける。検証・lock・上限・冪等性は従来どおり。既存ACLは維持。 - 現在の実装 — CURRENT IMPLEMENTATION
src/app/api/v1/transfers/mfa/route.tsintent準備・challenge束縛・試行予約・認可完了のserver入口。TOTPコードは受け取らない。 - 現在の実装 — CURRENT IMPLEMENTATION
src/components/auth/TransferMfa.tsxブラウザがSupabase Authへchallenge/verifyを直接送る部分。成功の自己申告は認可にならない。 - 現在の実装 — CURRENT IMPLEMENTATION
src/app/api/v1/transfers/route.ts認証済みの旧直接振込入口はOTP_REQUIREDを返す。 - 現在の実装 — CURRENT IMPLEMENTATION
src/lib/errors/apiError.tsOTP_REQUIREDのHTTP statusは409。 - 現在の実装 — CURRENT IMPLEMENTATION
scripts/mfa/intent-db.test.mjsintent束縛・回数/期限・単回消費・session検査のSQL受入test。Auth行はfixtureで、実Authの証明ではない。当日のPASSも意味しない。 - 過去の実装 — HISTORICAL IMPLEMENTATION
src/lib/config/demoOtpDisplay.ts#128以前のデモコード表示フラグ。v7ではDBがコードを返さず、Productionでは常にfalse。 - 過去の実装 — HISTORICAL IMPLEMENTATION
scripts/regression/suites/transferOtp.mjs#128以前のデモOTP(誤入力・再発行・期限)の回帰test。v7の認可方式の根拠にしない。 - 現在の実装 — CURRENT IMPLEMENTATION — Primary Source
src/lib/config/paymentBackend.tsgetPaymentBackend切替の許容値・default・不正値拒否。稼働環境の値はUNKNOWN。 - 現在の実装 — CURRENT IMPLEMENTATION — Primary Source
src/services/TransferService.tsrequestTransfer / confirmTransferOtp / reissueTransferOtprequest/confirmのみbackend切替。残存createTransfer関数のコメントは現ACLの根拠にしない。 - 現在の実装 — CURRENT IMPLEMENTATION
src/services/JavaPaymentBackend.tsJavaへのHTTP adapterとエラー処理。自動fallbackなし。 - 現在の実装 — CURRENT IMPLEMENTATION
src/components/customer/TransferForm.tsx入力・確認・本人認証・完了の4画面state。otp_plaintextがない場合にTransferMfaを表示 - 現在の実装 — CURRENT IMPLEMENTATION
src/app/api/v1/transfers/request/route.tsPOST()依頼作成。旧デモ表示分岐が残るが、v7のRPCはotp_plaintext=nullを返す - 現在の実装 — CURRENT IMPLEMENTATION
src/app/api/v1/transfers/otp/confirm/route.tsPOST()認可済みintentを消費させるconfirmのAPI境界 - 現在の実装 — CURRENT IMPLEMENTATION
src/services/TransferService.tsrequestTransfer() / confirmTransferOtp()RPC adapterと安全なerror変換 - 過去の実装 — HISTORICAL IMPLEMENTATION
supabase/migrations/20260823170001_create_transfer_beneficiary_and_otp.sqlrequest_transfer() / execute_transfer()振込先snapshotとOTP二段階処理 - 過去の実装 — HISTORICAL IMPLEMENTATION
supabase/migrations/20260823170002_drop_old_execute_transfer_overload.sqlPGRST203 overload競合の修正 - 過去の実装 — HISTORICAL IMPLEMENTATION
supabase/migrations/20260823170003_fix_confirm_and_reissue_transfer_otp_bugs.sqlOTP状態判定と曖昧列参照の修正 - 過去の実装 — HISTORICAL IMPLEMENTATION
supabase/migrations/20260823170004_sanitize_confirm_transfer_otp_failure_reason.sql生DB error漏洩の修正 - 過去の実装 — HISTORICAL IMPLEMENTATION
supabase/migrations/20260823190001_create_transfer_beneficiaries.sql振込先お気に入り - 過去の実装 — HISTORICAL IMPLEMENTATION
supabase/migrations/20260823180001_extend_get_account_transactions_beneficiary_fields.sql顧客履歴詳細 - 過去の実装 — HISTORICAL IMPLEMENTATION
supabase/migrations/20260823180002_create_admin_transfer_requests_read_api.sql管理者振込監視 - 過去の実装 — HISTORICAL IMPLEMENTATION
scripts/regression/suites/transferOtp.mjs#128以前のデモOTP前提の回帰(OTP・状態・認可・error境界)。v7のTOTP認可はscripts/mfa/intent-db.test.mjsが対象 - 現在の実装 — CURRENT IMPLEMENTATION
scripts/regression/suites/transferBeneficiaries.mjsお気に入り所有権・権限・auditの回帰 - 現在の実装 — CURRENT IMPLEMENTATION
e2e/full-journey.spec.ts画面からOTP、Ledger反映、履歴までのE2E