基本の14章 · 06

1万円の振込は、いつ本当に成立するのか。

ボタンを押した瞬間? 確認コードを入れた時? 画面の言葉とデータベースの出来事をつなぎ、お金を動かすための約束を読み解きます。

本文の説明・図・掲載コードだけで読み進められます。出典リンクは、権限のある人向け(非公開GitHub)です。GitHubで閲覧権限がある方だけ開けます。

ONE TRANSFER, FOUR MOMENTS

01 INPUT振込内容を入力誰に・いくら
02 REQUEST依頼を受け付けるまだお金は動かない
03 AUTHORIZE本人が確認する今回の振込を認証
04 COMMIT確定・記帳する残高と記録がそろう

受付と成立は、別の出来事。 この違いが、この記事の出発点です。

この記事でわかること

  • 画面、API、データベースが分担している仕事
  • 振込をひとまとまりで記録する理由
  • 二重クリックや通信断でも、元の依頼を確認する設計
この記事の目次
01 / FOLLOW THE MONEY

まず、1万円の行き先を見る

Aさんの口座に5万円、Bさんの口座に2万円があるとします。AさんがBさんへ1万円を振り込むと、Aさんは4万円、Bさんは3万円。二人の合計は7万円のままです。

ここでは、MiniBank内の普通預金どうしの振込を扱います。金額と残高は仕組みを説明するための架空の例です。実際に振込を実行した結果ではありません。

図解:AさんからBさんへ、1万円
1万円→

振込後は、Aさん 40,000円 / Bさん 30,000円。合計は 70,000円。

難しいのは、引き算と足し算そのものではありません。片方だけ成功させず、同じ依頼を二度実行せず、本人が意図した内容で動かすこと。この三つを、画面だけに任せず、奥の仕組みまでつないで守ります。

この例で扱わないこと

他行宛振込、現金の移動、手数料は含めません。現在の対応先はミニ銀行・本店営業部の普通預金です。MiniBankは学習・検証用のアプリであり、実在の銀行サービスではありません。

根拠:対応先とサービスの分岐 ↗(非公開GitHub・要権限)

02 / REQUEST IS NOT COMMIT

「受け付けた」は、まだ途中

Aさんが口座番号と金額を入力すると、画面は振込の依頼を送ります。ここで作るのは「この内容で振り込みたい」という記録です。依頼を受け付けたことと、お金を動かし終えたことを分けています。

  1. 画面:内容と、依頼の目印を送る

    振込先、金額、依頼人名などに、同じ依頼を見分けるための一意のキーを添えます。

  2. API:入口で、形を確かめる

    ログインしているか、口座番号は7桁か、金額などの形式は正しいか。受け付ける項目を限定します。

  3. サービス:処理先へつなぐ

    設定された処理基盤へ依頼を渡し、戻ってきた結果が想定した形かを確認します。

  4. データベース:依頼を保存する

    既定の処理経路では、request_transfer が振込依頼を作ります。この受付だけで資金は移動しません。

画面で確認できても、奥でも確かめる

画面のチェックは入力を助けるため。残高、口座の持ち主、口座の状態といった重要な条件は、確定する側でも検証します。画面から来た値を、そのまま信用しない設計です。

実装をもう一段だけ読む:入口から依頼保存まで

以下は呼び出し関係を短く整理した読み取り用の図です。実行できるコードの抜粋ではありません。

TransferForm
  → POST /api/v1/transfers/request
  → requestTransfer
  → request_transfer(既定の legacy 経路)
  → 振込依頼を保存

金額は画面から文字列として送られます。Idempotency-Key は、同じ依頼の再試行を区別する目印です。画面はHTTPの成功だけでなく、INSTRUCTION_ACKNOWLEDGED という「依頼を受理した」状態も確かめます。

TransferService は PAYMENT_BACKEND の設定に応じて legacy / Java を切り替えます。これは障害時に自動でもう片方へ流す仕組みではありません。この記事のSQLの説明は legacy 経路を対象にします。稼働環境の設定は今回確認していません。

TransferForm の依頼処理 ↗(非公開GitHub・要権限) / 入力スキーマ ↗(非公開GitHub・要権限) / TransferService ↗(非公開GitHub・要権限)

03 / AUTHORIZE THIS TRANSFER

本人の確認で、確定へ進む

ログイン済みであることと、今回の振込内容を本人が確認したことは、同じではありません。現行の画面は、依頼を作ったあとに認証アプリの確認コード(TOTP)による確認へ進みます。

認証が済むと、画面は確定用のAPIを呼びます。データベース側は、依頼の持ち主や期限、今回の依頼に結び付いた認証を確かめてから、資金を動かす処理へ進みます。

大切なのは、ブラウザが「認証できました」と言うだけでは通れないことです。サーバー側で、依頼とセッションに対応する認証の記録を検証します。

成立する場所は、確認画面のさらに奥

本人確認は、確定へ進むための条件です。その後の残高チェックや記帳まで成功し、データベースの処理がまとまって確定して、初めて振込が成立します。

実装をもう一段だけ読む:認証と確定の境界
TransferMfa で今回の依頼を認証
  → onAuthorized
  → 確定用 API
  → confirmTransferOtp
  → confirm_transfer_otp
  → 認証の記録を消費
  → execute_transfer
  → completed

関数名には otp が残っていますが、現行の確認処理は、依頼に結び付いたTOTP認証の記録を使います。confirm_transfer_otp は対象の依頼をロックし、持ち主、完了済みかどうか、期限を確認します。完了済みの同じ依頼なら、再度資金を動かさず同じ結果を返します。

確定中の金融処理でエラーが起きた場合、その内側の処理を取り消してから、依頼を失敗状態にし、失敗の監査記録を残します。「失敗したら何の記録も残らない」という意味ではありません。

MFA から確定への接続 ↗(非公開GitHub・要権限) / MFA API ↗(非公開GitHub・要権限) / 依頼の確定と失敗処理 ↗(非公開GitHub・要権限)

04 / TWO ENTRIES, ONE PROMISE

残高の裏に、二つの記録

画面には「残高4万円」と一つの数字が出ます。でも、数字を書き換えるだけでは、なぜ4万円になったのか説明できません。そこで、お金が動いた理由を帳簿に残し、その帳簿から残高を確かめるようにします。

Aさんの預金は、銀行から見ると「Aさんに返す義務のあるお金」です。会計では負債として扱います。銀行内でAさんからBさんへ1万円を振り込むと、銀行が返す相手の内訳が変わります。

1万円の振込を、銀行側の帳簿で見る
記録するもの帳簿の記入この例での意味
Aさんの預金に対応する口座借方 10,000円Aさんへの預金債務が1万円減る
Bさんの預金に対応する口座貸方 10,000円Bさんへの預金債務が1万円増える

借方と貸方には、同じ1万円を記録します。ここで「借方なら減る、貸方なら増える」と丸暗記するのは避けましょう。今回は預金という負債の口座だから、この向きになるのです。資産など、口座の種類が違えば読み方も変わります。

二つの記録を、一つのまとまりにする

Aさんの分だけを先に保存し、その後でBさんの分を保存する途中に処理が止まると、片方だけお金が減ったように見えてしまいます。そこで、二つの帳簿の記録、振込の記録、表示用の残高の再計算を、同じデータベースのトランザクションで扱います。

トランザクション = 途中を「完成」にしない単位

必要な処理がそろったら確定し、途中で失敗したら金融処理をまとめて取り消す。このまとまりが、片方だけの振込を防ぐ基礎になります。

実装をもう一段だけ読む:ロック、元帳、残高

execute_transfer は関係する口座をUUID順にロックします。複数の処理が同時に同じ口座を変えないよう、順番をそろえて待ち合わせるためです。

口座を決まった順序でロック
  → 同じキーで完了していないか再確認
  → 元帳由来の残高・口座状態・利用条件を検証
  → ledger.post_journal_entry で記帳
  → transfers に振込を記録
  → cached_balance を再計算
  → TRANSFER_COMPLETED の監査記録

表示用に保持する cached_balance は、帳簿の代わりになる正本ではありません。後続の修正では、記帳前から元帳との不一致があった場合も LG018 で拒否します。「今回の借方と貸方が合った」だけで、すでにある不一致を見逃さないためです。

post_journal_entry の戻り値はUUID単体です。この内部関数だけを直接呼ぶことは、認証と重複防止を備えた振込操作の代わりにはなりません。

ロックから記帳まで ↗(非公開GitHub・要権限) / 既存の残高不一致を拒否する修正 ↗(非公開GitHub・要権限)

05 / WHEN THINGS GO WRONG

うまくいかない時こそ、設計が見える

正常な振込の道順だけでは、安全は説明しきれません。よくある四つの場面から、何を区別しているかを見てみます。

01 残高が足りない

入力時に十分でも、確定までに別の取引が動くかもしれません。確定処理では、ロックした状態で元帳由来の残高を確認します。

02 二度クリックした

同じ依頼を示すキーと依頼IDを使います。完了済みなら同じ結果を返す仕組みで、同じ依頼を重ねて記帳しないようにします。

03 確認の期限が切れた

期限を過ぎた依頼を、そのまま確定させません。やり直す場合も、期限切れかつ未計上であることを確認する流れがあります。

04 送信後に通信が切れた

「返事がない」と「失敗した」は別です。画面は結果不明として扱い、元の依頼の状態を照会します。自動で新しい振込を作りません。

返事が消えても、処理は終わっているかもしれない

たとえば記帳の直後に通信が切れると、振込は成立していても完了画面を受け取れません。状態確認で FINANCIAL_COMMITTED が返る場合は、送金済みとして案内します。ここで新しい依頼を作り直さないことが重要です。

同じものを何度受け取っても、金融上の結果を重ねない。こうした性質を冪等性(べきとうせい)と呼びます。ただし、毎回まったく新しいキーで送れば、同じ依頼とは見なせません。画面とサーバーが、元の依頼を見失わないよう連携する必要があります。

実装をもう一段だけ読む:結果不明からの戻り方

TransferForm は request_unknown を区別します。所有者の認証を伴う状態確認APIで元の依頼を調べ、確定済みならその結果を案内します。再読み込みだけでは自動再送しません。

期限切れで未計上と確認できた依頼について、ブラウザに保存した手がかりを消して入力に戻れるかは、canRestartExpiredTransfer で判断します。データベースの依頼を削除する操作ではありません。単なる「古い画面だから」という理由で、ブラウザ上の手がかりを消す設計ではありません。

結果照会と再開の判断 ↗(非公開GitHub・要権限) / 依頼の確定時の再確認 ↗(非公開GitHub・要権限)

06 / WHAT CHANGED SINCE V1

v1から、何が変わった?

既存の制作記事は、v1.0.0のコードを土台にしています。振込の入口から、サービス、データベースへ進む読み方は、今も役立ちます。ただし、現在の実装と、同じ呼び出し順ではありません。

v1の記録

振込の骨組みを理解する

旧記事では、POST /api/v1/transfers から createTransfer、execute_transfer へ進む道順を説明しています。当時の設計を読むための記録です。

現行コード

受付・認証・確定を分ける

現在は依頼を作り、今回の振込を認証してから確定します。旧APIは、認証済みの呼び出しにも OTP_REQUIRED を返す移行用の入口です。

だから、記事中の「v1」を一括で「v20」に書き換えるだけでは、正しい説明になりません。何が増えたかだけでなく、何を防ぐために経路が変わったかを読むことが、制作の過程を学ぶ意味になります。

根拠:旧APIの現在の動作 ↗(非公開GitHub・要権限) / 既存サイトが扱う版 ↗(非公開GitHub・要権限)

ここまでの要点

受付は、成立ではない。本人確認は、確定への条件。振込の記録と残高は、データベースでひとまとまりにする。そして結果が不明なら、元の依頼を確かめる。ひとつの振込は、このつながりで支えられています。

SOURCE NOTES

出典と、確認できた範囲

コードの説明と、稼働環境の検証を分けます

以下のリンクは非公開GitHubへの出典で、閲覧権限が必要です。本文はリンク先を開かなくても読める構成です。固定コード・設計資料を参照していますが、今回、この読み物のために銀行の取引やDB操作、本番試験は実行していません。実装や資料にある検査と、こちらで実施した検証を混同しないようにしています。

  1. 画面から依頼送信 ↗(非公開GitHub・要権限)
    金額と一意キーを送り、受付状態を確かめる
  2. 入力のスキーマ ↗(非公開GitHub・要権限)
    APIの受け付ける形
  3. サービスと処理基盤 ↗(非公開GitHub・要権限)
    request/confirmとlegacy/Javaの分岐
  4. 依頼の確定 ↗(非公開GitHub・要権限)
    認証、ロック、期限、完了と失敗
  5. 記帳の金融処理 ↗(非公開GitHub・要権限)
    ロック、残高、記帳とキャッシュ
  6. 既存不一致の検知 ↗(非公開GitHub・要権限)
    元帳とキャッシュの不一致を拒否
  7. 結果不明からの照会 ↗(非公開GitHub・要権限)
    元の依頼と再開条件
  8. 旧振込APIの応答 ↗(非公開GitHub・要権限)
    認証後OTP_REQUIRED

MiniBankは学習・検証用の銀行アプリです。架空の金額や人物は、仕組みを説明するための例です。

つながる読み物

ここまで読んだ記録を、ひとつ。

自分のペースで。あとから取り消すこともできます。

次の読み物元帳は、出来事を説明できる数字へ変える
→