クレジットカード明細CSVの取込だけ先に作ってあったので、その次の段階として、CreditCardStatementLine から Transaction を登録できるようにした。
今回やりたかったのは、単に「明細から仕訳を作る」ではなく、
- 既存の
TransactionRegistrarを正規ルートとして再利用する - カード明細固有の前処理は別の責務に分ける
- 再取込時に old batch 配下の取引がちゃんと無効化される
- 間違えた登録は取消してやり直せる
というあたりまで含めて、あとで崩れにくい形にすることだった。
今回追加したもの
入口としては、CreditCardStatementLine に次の2つを追加した。
registerTransaction()cancelTransactionRegistration()
ただし、モデルに重いロジックを全部書いたわけではない。実処理の本体は CreditCardStatementLineRegistrar に置いている。
役割分担はこうなった。
CreditCardStatementLine- 薄い入口
CreditCardStatementLineRegistrar- 明細の状態確認
- 取引日の決定
- 会計年度の解決
- カード設定からの貸方補助科目解決
TransactionRegistrarへ渡す入力の組み立て- 登録後の明細更新
TransactionRegistrarTransaction/JournalEntryの正規化、検証、保存
つまり、TransactionRegistrar は保存コアのままにして、カード明細由来のユースケースだけを薄いアダプタ層で包んだ形になる。
なぜ Registrar を増やしたのか
最初は「CreditCardStatementLine からそのまま TransactionRegistrar を呼べばよいのでは」と思ったのだけど、実際に必要だったのは保存の前段の方だった。
TransactionRegistrar が元から持っていたのは、
- 貸借一致の検証
- 税区分の解釈
- 家事按分の展開
- 所属整合
Transaction/JournalEntryの保存
であって、
- この明細は今登録してよい状態か
used_onとposted_onのどちらを取引日にするか- その日付がどの
FiscalYearに属するか - 個人カードなら
owner_draw_sub_account_id、事業用カードならliability_sub_account_idを使う - 登録後に
CreditCardStatementLineをregisteredに更新する
といった、カード明細固有の前処理は持っていなかった。
なので、「TransactionRegistrar に実装されていなかったから StatementLineRegistrar に書いた」というよりは、「保存コアに混ぜるべきでないカード明細固有の処理を、専用の Registrar に置いた」というのが近い。
ただし、今回やっていて、将来的に共通化してもよさそうな処理も見えた。
- 取引日から
FiscalYearを解決する処理 - 一部の transaction data 組み立て補助
このへんは別の入口が増えてきたら、また整理したい。
description と remarks
カード明細の文字列を Transaction にどう持たせるかも少し迷った。
最終的には、
description- 帳簿表示に使う会計上の摘要
remarks- カード明細の原文や補足
という分け方にした。
元データの正本は当然 CreditCardStatementLine にあるけれど、帳簿表示や検索のたびに relation だけを辿って摘要を復元するのは不便だし、Transaction 単体で意味が通らなくなる。なので、relation は relation として残しつつ、Transaction 側にも要約と原文の一部を保存する方針にしている。
個人カードと事業用カードの貸方
カード設定のどの項目を貸方に使うかも、テストを少し丁寧にした。
- 個人カード
owner_draw_sub_account_id- 事業用カード
liability_sub_account_id
を使うのが仕様なのだけど、単に「期待した科目が入っていた」だけだと、たまたま同じ値を返していても通ってしまう。そこで、両方の ID を明示的にセットしたうえで、どちらが選ばれたかまで確認するテストにしている。
こういう分岐は、実装時よりリファクタ時に壊れやすいので、先に固定しておいた方がよい。
間違えた登録はどう直すか
今回の機能を作ると、カード明細由来の Transaction は credit_card_import_batch_id を持つようになる。これは再取込時に old batch 配下の取引をまとめて無効化するために必要な情報。
一方で、既存の TransactionRevisor は credit_card_import_batch_id !== null の取引を修正対象外にしている。つまり、この機能を入れた瞬間に、カード明細由来の取引は改訂機能から外れる。
ここは最初に仕様を決めて、初期実装では「改訂」ではなく「登録取消して再登録」で扱うことにした。
そのために cancelTransactionRegistration() を用意して、
- 紐づく
Transactionをdeactivate() - 明細行を
unreviewedに戻す transaction_idを外す
という逆遷移を許可している。
これは「将来ずっとこの仕様で行く」というより、まず安全に回る最小構成を先に入れた感じに近い。
再取込と決算済み年度
もう一つ見落としやすかったのが、再取込と決算済み年度の関係。
CreditCardImportBatch::deactivate() は配下の取引をまとめて deactivate() するけれど、Transaction::deactivate() は決算済み年度の取引を拒否する。つまり、決算済み年度に属する登録済み取引を含む batch は再取込できない。
この挙動自体は既存ルールに沿っているのだけど、batch 由来の Transaction を今回初めて実際に作るようになったので、ようやく到達可能な経路になった。
設計上は、
- 一部だけスキップして続行しない
- 失敗したら再取込全体をロールバックする
という方針にしている。
この話は、年またぎのカード明細だとさらに起こりやすい。
たとえば、
- 利用日が 2024年12月28日
- 請求月が 2025年1月
- 実際にCSVを取り込むのが 2025年1月末や2月
というケースでも、会計上は 2024年度の取引になる。今回の実装は請求月ではなく used_on を基準に FiscalYear を決めるので、これは意図どおり。
ただし、その後 2024年度を締めてから「2025年1月請求分の修正版CSVを取り込み直したい」となると、batch 配下の取引の一部が 2024年度に属していて、しかもその年度はもう決算済み、という状態が起こりうる。そうなると、その取引を無効化できず再取込全体が止まる。
なので運用としては、
- カードの請求月ではなく
used_onがどの年度に属するかで考える - 12月利用分が翌年1月請求で届くカードは、前年の締め前確認対象に含める
- 年度を締める前に、その年度に属するカード明細の取込とレビューを終える
という前提になる。
これは今の実装の制約でもあるし、同時に帳簿としては自然な扱いでもある。年またぎのカード明細は「1月の請求だから翌年度のもの」と見てしまいやすいので、ここは明示しておいた方がよいと思う。
テスト
今回追加したテストでは、少なくとも次を見ている。
- 未レビュー行から登録できる
used_onがなくてもposted_onで年度解決できる- 決算済み年度では登録できない
- 個人カードは
owner_draw_sub_account_idを使う - 事業用カードは
liability_sub_account_idを使う business_ratio < 100で家事按分行が増える- 非費用科目に
business_ratioを渡すと弾かれる credit_card_import_batch_idがない行は登録できない- 0円明細は登録できない
- 登録取消で
unreviewedに戻せる
lockForUpdate() を使っているので、SQLite だけでなく MySQL 設定でも対象テストを流した。
次
これで「CSVを保存するだけ」から「明細レビューして帳簿へ登録する」まで一歩進んだ。
ただ、まだやっていないことも多い。
- Livewire などの画面導線
- tax_type を UI 上どう出し分けるか
private/ignored/duplicateのレビュー操作- 再取込時のレビュー結果引き継ぎ
- カード明細由来取引を将来的に改訂対象へ入れるかどうか
特に最後の点は、今回わざと先送りした仕様バグに近いところでもある。とはいえ、まずは「登録できる」「取消してやり直せる」「再取込と整合する」を先に固めたので、次の段階に進む土台としては悪くないと思う。