入金と出金は、残高の足し算・引き算だけではない
一万円を預け、三千円を引き出す例から、二つの会計行、重複防止、同時操作、記録の原子性を読む。
本文の説明・図・掲載コードだけで読み進められます。出典リンクは、権限のある人向け(非公開GitHub)です。GitHubで閲覧権限がある方だけ開けます。
この記事でわかること
- 入金・出金は顧客預金と相手勘定を同額で動かす
- 出金の可否はロック後に元帳残高で確かめる
- 業務記録・仕訳・残高・監査をつなげて一回の操作を説明する
この記事の目次
この「入出金」は何を表しているのか
MiniBankの入出金は、架空の預金へ資金を入れたり、そこから払い出したりする会計上の操作です。実物のATMや現金輸送、銀行ネットワークへ接続するものではありません。ソースにはcore.depositとcore.withdrawalという内部の業務処理があり、権限を確認する外側の窓口から呼ばれる構造になっています。利用者が自由に残高を書き換えるための仕組みではありません。
この章では、山田さんの残高がゼロ円から始まり、一万円の入金後に三千円を出金する架空の例を使います。最後の残高が七千円になること自体は簡単です。難しいのは、一万円がどこから来た扱いか、三千円を二度払っていないか、途中で止まったときにどの記録が残るかを説明できることです。
旧入出金の相手勘定はSYSTEM_FUNDINGという名前です。v10の勘定科目対応では、これを現金のGL1000へまとめます。名前から本物の資金調達サービスを想像する必要はありません。学習用の入出金の相手を、会計上どこへ記録するかを示す箱です。
一万円の入金は、銀行の資産と負債を増やす
山田さんへ一万円を入金すると、山田さんの預金勘定には貸方一万円を記録します。顧客預金は銀行の返済義務なので、銀行側の負債が増える向きです。同時に、SYSTEM_FUNDINGには借方一万円を記録します。v10の見方では現金という資産が増える向きになります。
ここで「借方は減少、貸方は増加」と丸暗記すると、現金の説明で矛盾します。増減の向きは勘定の種類によって変わります。資産は借方で増え、顧客預金という負債は貸方で増えます。二つの行の金額は同じでも、同じものを二回増やしているわけではありません。
入金処理は金額が正か、口座があるか、操作を識別するキーがあるかを検査し、口座の状態がactiveかを確認してから仕訳を作ります。凍結中や閉鎖済みの口座へ、数値だけ強制的に足すという動作にはしません。会計の左右が合うことと、業務上その口座へ入金してよいことは、両方必要です。
三千円の出金は、残高を確かめてから逆向きに動かす
次に三千円を出金すると、山田さんの預金勘定は借方三千円です。銀行が山田さんへ返す義務が減ります。相手勘定は貸方三千円となり、現金が減ります。山田さんの預金勘定に積み重なった数字は、貸方一万円、借方三千円。貸方合計から借方合計を引くと、残高七千円になります。
出金処理では、この元帳残高を計算し、出金額以上あるかを確かめます。たとえば七千円になった後で八千円を出金しようとすれば、不足として拒否する設計です。画面に表示された古い残高や、依頼者が本文へ書いた残高を根拠にはしません。業務処理の中で、正式な記録から確かめます。
select coalesce(sum(credit_amount), 0) - coalesce(sum(debit_amount), 0) into v_ledger_balance
from ledger.journal_lines where ledger_account_id = v_ledger_account_id;
if v_ledger_balance < p_amount then
raise exception 'insufficient funds' using errcode = 'WD010';
end if;これはcore.withdrawalの抜粋です。coalesceは、まだ行がないときの空の結果をゼロとして扱います。後半は、合計が出金額より小さければ止める条件です。コードの短さより、「何を残高の根拠としているか」を読み取ることが重要です。
二人が同じ残高を見たとき、どう順番を付けるか
残高一万円に対し、八千円の出金が二件ほぼ同時に届いたとします。両方が一万円を見て「足りる」と判断してから引き落とすと、合計一万六千円を払ってしまいます。これを防ぐには、残高を読むだけでは足りません。その口座を使う処理に順番を付け、前の処理が確定した後の残高で次を判断する必要があります。
core.withdrawalは、対象のbank_accounts行をFOR UPDATEでロックしてから、状態と元帳残高を確かめます。ロックは、同じ対象の更新処理を途中で割り込ませないための待ち合わせです。先の出金が成功した後、後の出金は残り二千円を前提に確認するので、八千円は不足となります。順番を付ける対象は、画面のボタンではなくDBの口座です。
ロックを取ると、待ち時間は発生し得ます。しかし安全性を壊して速く処理することはできません。複数口座を同時に扱う振込では、ロックする順番を揃える問題も加わります。入出金の一口座の例は、より複雑な送金の同時実行を考える出発点になります。実際の待ち時間や高負荷での性能は、このソース読解だけでは判断できません。
再送は、同じ依頼かどうかまで確かめる
入金一万円の依頼はDBで成功したのに、返事が届かなかったとします。同じ入金をもう一度計上すれば、残高は二万円になってしまいます。そこで入出金にはidempotency_key、つまり同じ業務依頼を識別するキーがあります。旧入出金経路では、操作した主体、操作の種類、キーを使って、すでに処理した記録を探します。
さらに口座と金額からrequest_fingerprintを作り、依頼内容の要約を比較します。同じキーで同じ口座・同じ一万円なら、保存済みの結果を返します。同じキーを使いながら二万円へ変えたら、別内容との衝突として拒否します。「同じキーなら何でも同じ結果」は、誤った依頼を黙って通してしまうためです。
実装は、口座をロックする前だけでなく、取得した後にも重複を確認します。待っている間に、先行する同じ依頼が完了するかもしれないからです。また再送時に新しいキーを作ると、同じ業務だと認識できません。キーはクリック回数の番号ではなく、一回として扱いたい業務の意図に結び付けるものです。画面を連打できなくすることと、保存側の冪等性は別の防御です。
仕訳、業務記録、残高、監査を一緒に確定する
入出金は、仕訳の二行を作って終わりではありません。financial_operationsに入金・出金という業務結果を残し、その行からjournal_entry_idで仕訳を指します。口座のcached_balanceも元帳から再計算し、業務監査の記録へ金額・口座・仕訳を結び付けます。これによって「明細に見える数字」と「何を実行した結果か」をたどれます。
これらを同じDB処理の中で行うと、後半で例外が発生した場合、途中の書込みだけを残さず取り消すことができます。原子性とは、ここでは「一組として全部確定するか、確定しないか」という性質です。業務の記録だけ成功扱いになり、仕訳がない状態や、仕訳があるのに対応する結果がない状態を避けるために使います。
計上の基盤にも守りがあります。借方合計と貸方合計の一致を調べ、対象口座の派生残高を同じ取引内で再計算します。さらに、計上前からキャッシュと元帳に差がある場合は、通常の取引を止めて照合を求めます。新しい入出金で差を上書きすれば直ったように見えますが、それでは古い不整合の原因が分からなくなるためです。
間違いを直すときも、過去の一万円を消さない
もし一万円の入金が誤りだったなら、仕訳行を直接削除して見えなくするのではなく、逆向きの仕訳を追加して打ち消す設計です。元の一万円、打ち消した一万円、その理由を残せば、後から経過を追えます。ただし山田さんがすでに一部を使っていれば、元に戻す操作にも残高の条件が関わります。取消しは、どんな状況でも成功する消しゴムではありません。また、内部の逆仕訳関数があることは、現行の管理画面で任意の取引を取り消せる意味ではありません。v17の契約は、汎用の管理入口admin_reverse_transactionをRV001で拒否すると明記しています。利用できる訂正経路は、業務ごとの現行仕様を確かめる必要があります。
この章で説明したのは旧来の入出金を担うcoreの経路です。v8の事象ベースの計上機構が追加されても、これらがすべてbusiness_eventsへ移されたわけではありません。また学習用の相手勘定に対する計上は、実際の現金が到着したという外部証明にはなりません。実銀行なら外部の受渡し確認や現金管理が必要になりますが、この模型の範囲外です。
この教材を読むために、実口座や共有DBで試しに入出金する必要はありません。ソースに書かれた条件と、架空の数字で作った仕訳を照らし合わせれば、記録の意味を学べます。本章はDBを実行して検証した報告ではなく、固定ソースの業務処理を説明したものです。
根拠:逆仕訳と残高条件を持つcore.reverse_transaction(非公開GitHub・要権限)
出典と、確認できた範囲
以下のリンクは非公開GitHubへの出典で、閲覧権限が必要です。本文はリンク先を開かなくても読める構成です。固定コード・設計資料を参照していますが、今回、この読み物のために銀行の取引やDB操作、本番試験は実行していません。実装や資料にある検査と、こちらで実施した検証を混同しないようにしています。
- 入出金と逆仕訳の処理 ↗(非公開GitHub・要権限)
同一処理内の記帳・業務結果・残高・監査 - 計上基盤の差異検査 ↗(非公開GitHub・要権限)
貸借一致、ロック、キャッシュ再計算 - 内部業務処理の境界 ↗(非公開GitHub・要権限)
coreと認可する窓口の分離 - 銀行会計の勘定対応 ↗(非公開GitHub・要権限)
入出金の相手勘定を現金へ集計 - v8の範囲 ↗(非公開GitHub・要権限)
入出金の旧経路を事象表へ移していない - v17の訂正と汎用取消しの制限 ↗(非公開GitHub・要権限)
内部の逆仕訳処理と、現行の公開管理入口の可否を区別する
MiniBankは学習・検証用の銀行アプリです。架空の金額や人物は、仕組みを説明するための例です。
つながる読み物
ここまで読んだ記録を、ひとつ。
自分のペースで。あとから取り消すこともできます。