ボタンを押した後、システムの中で何が起こるか
口座開設を一件だけ追いかける。画面、API、サービス、データベースの分担と、途中で失敗したときの境界。
本文の説明・図・掲載コードだけで読み進められます。出典リンクは、権限のある人向け(非公開GitHub)です。GitHubで閲覧権限がある方だけ開けます。
この記事でわかること
- 画面は依頼を送り、APIは認証と入力形式を確かめる
- 口座と対応する元帳勘定を一つのDB処理で作る
- 画面の成功表示とDBの確定は別で、制約が最後の守りになる
この記事の目次
山田さんが「口座を開設する」を押す
架空の山田さんはログインし、氏名を登録し、本人確認の審査がverifiedになったところです。まだ普通預金口座はありません。口座画面には開設ボタンが現れます。この章は、その一回の操作を追います。開設の例を選ぶのは、お金を動かす前にも「誰の記録を、どこまで一緒に作るか」という銀行システムの基本が現れるからです。
画面を出す時点では、Next.jsのサーバー側のページが顧客情報と口座一覧を読み、表示部品へ渡しています。顧客登録がまだなら、名前の登録を案内します。口座がなければ「未開設」と表示し、本人確認が完了した場合に開設ボタンを置きます。つまり、空の残高をゼロ円と見せるのではなく、口座自体が存在するかを区別しています。
ただしボタンの表示条件は、利用者を迷わせないための案内です。利用者の端末で表示されるものだけを信用して、開設を許可するわけにはいきません。画面を経由しない依頼が来ても、サーバーとデータベースが同じ境界を守る必要があります。
画面は「依頼中」を持ち、結果を待つ
OpenAccountButtonという部品は、押された直後に送信中の状態へ変え、ボタンを無効にします。これは二度押しを減らす工夫です。ログイン中のセッションが見つからなければ、期限切れの案内を出して止めます。セッションがあれば、口座APIへPOSTという種類の通信を送ります。POSTは、この場合「新しく作ってほしい」という依頼です。
送る本文は空のオブジェクトです。利用者に顧客ID、好きな口座番号、最初の残高を入力させる仕組みではありません。認証情報はヘッダーと呼ばれる通信の付属情報に添えます。口座の持ち主は、その認証情報からサーバー側でたどるので、「この顧客の口座を作って」と他人のIDを本文に指定する必要がありません。
APIから失敗の応答が返れば、部品はメッセージを表示します。成功時はrouter.refreshでページのデータを読み直します。手元だけで「開設済み」に塗り替えるのではなく、保存された口座を再取得して表示につなげるのです。一方、通信そのものが切れる状況まで、この部品の表示だけでDBの成否を断定できるわけではありません。
APIは受付票を作り、本人と入力を確かめる
APIの入口はsrc/app/api/v1/accounts/route.tsにあります。到着した依頼にはrequest_idという照会用の番号を付けます。次に認証情報を検証し、本文が開設用の形かを調べます。認証とは「いま誰が依頼しているか」の確認です。口座を作ってよいという業務上の判断のすべてを、この確認だけで終えるわけではありません。
入力の形を調べる仕組みは、普通預金ordinaryと通貨JPYだけを受け付け、知らない項目を拒否します。たとえば本文にcached_balance: 1000000を紛れ込ませても、残高付きの開設として進める設計ではありません。入力の許可リストを持つことで、画面にはない項目を勝手に送り込む経路を狭めています。
const auth = await requireAuthContext(request);
const client = getSupabaseRequestClient(auth.accessToken, requestId);
const account = await createAccount(client, parsed.data);これは実装から抜き出した三行です。本人を確かめ、その本人としてデータベースに問い合わせる道具を作り、サービスへ渡しています。API自身は、口座と元帳勘定の二つのINSERTをばらばらに実行しません。
サービスは、受付の言葉をDBの呼び出しへ変える
AccountServiceのcreateAccountは、create_bank_accountというRPCを呼び出します。指定がなければordinaryとJPYを渡し、戻り値があれば画面向けの口座データへ整えます。返答が空なら「結果が返らなかった」というエラーにします。単に通信できたことを、口座作成の成功と取り違えないためです。
この層が薄いことには理由があります。もしサービスで「本人確認済みかを読む」「口座を作る」「会計用勘定を作る」と三回に分けて送ったら、二回目の後で失敗し、口座だけが残る可能性があります。重要な一組の作業をDB側へまとめれば、その境界をデータベースの取引処理に任せられます。これをトランザクションと呼びます。
応答を整える仕事も重要です。内部ではcached_balanceという列ですが、利用者向けにはbalanceという名前で返します。内部のcustomer_idやupdated_atなど、その画面に不要な項目は含めません。サービスは何でも入ったDBの行をそのまま外へ渡すのではなく、利用者へ見せる約束を作っています。
データベースは口座と会計用勘定を一組で作る
口座開設の基盤SQLは、ログインした本人から顧客を探し、本人確認、既存口座、口座種別と通貨を確かめます。そのうえで口座番号を発行し、bank_accountsに口座を追加します。続いてledger.ledger_accountsに、その口座と結び付いた預金勘定を作ります。顧客が見る「普通預金口座」と、銀行が仕訳を書く「預金の勘定」は、用途の異なる二つの記録です。
山田さんの口座番号が架空の1234567なら、基盤の定義では対応する勘定コードはDEP-1234567になります。預金勘定の区分はliability、つまり銀行側の負債です。山田さんにとっての預金は、銀行にとって返す義務だからです。開設自体では、ここへ一万円を自動で載せるわけではありません。残高が増えるには、別の適切な仕訳が要ります。
二つの追加は同じ関数の同じトランザクションで行われます。後半の勘定追加が失敗すれば、前半の口座追加も取り消されます。また顧客ごとの口座の重複や、口座ごとの預金勘定の重複は、一意制約というDBの禁止事項でも守ります。画面の二度押し対策とは別の、保存場所での防御です。
失敗を四つの場所に分けて考える
同じ「開設できなかった」でも、原因の場所は違います。画面にセッションがないならログインのやり直しです。APIで知らない入力項目が拒否されたなら、依頼の形式を直す必要があります。DBで本人確認未完了や既存口座が判明したなら、業務上の条件を見ます。応答が届かなかっただけなら、保存結果が不明なままです。最後のケースを、DBが必ず失敗したという意味へ置き換えてはいけません。
APIはエラーを共通の形にまとめ、request_idを添えて返します。知らない内部エラーをそのまま利用者へ出さず、一般的なメッセージに変える作りもあります。内部のSQLや秘密情報を画面に出すことなく、調査の手掛かりを残すためです。利用者向けの言葉と、開発・運用側が追う番号を一緒に持つ設計と言えます。
この章で追ったのは固定ソースの口座開設経路です。現在のDB全体には後の版で追加された営業日や復旧の保護もあり、この章の基盤SQLだけが許可条件のすべてではありません。また、入出金や振込には、重複依頼の識別や残高・同時実行の確認が加わります。役割の並びは似ていても、口座開設を読んだだけで送金の安全性を確認したことにはなりません。
出典と、確認できた範囲
以下のリンクは非公開GitHubへの出典で、閲覧権限が必要です。本文はリンク先を開かなくても読める構成です。固定コード・設計資料を参照していますが、今回、この読み物のために銀行の取引やDB操作、本番試験は実行していません。実装や資料にある検査と、こちらで実施した検証を混同しないようにしています。
- 開設ボタン ↗(非公開GitHub・要権限)
クライアントが持つ送信中状態と通信 - 口座API ↗(非公開GitHub・要権限)
認証、入力検査、サービス呼出し - 口座入力の形 ↗(非公開GitHub・要権限)
普通預金・円と余分な項目の拒否 - 口座サービス ↗(非公開GitHub・要権限)
RPC呼出しと表示用応答 - 口座開設の基盤SQL ↗(非公開GitHub・要権限)
口座と会計用勘定の同時作成 - APIの共通応答 ↗(非公開GitHub・要権限)
安全なエラーとrequest_id
MiniBankは学習・検証用の銀行アプリです。架空の金額や人物は、仕組みを説明するための例です。
つながる読み物
ここまで読んだ記録を、ひとつ。
自分のペースで。あとから取り消すこともできます。