CSV明細からTransaction作成機能を追加

クレジットカード明細CSVの取込だけ先に作ってあったので、その次の段階として、CreditCardStatementLine から Transaction を登録できるようにした。

今回やりたかったのは、単に「明細から仕訳を作る」ではなく、

  • 既存の TransactionRegistrar を正規ルートとして再利用する
  • カード明細固有の前処理は別の責務に分ける
  • 再取込時に old batch 配下の取引がちゃんと無効化される
  • 間違えた登録は取消してやり直せる

というあたりまで含めて、あとで崩れにくい形にすることだった。

今回追加したもの

入口としては、CreditCardStatementLine に次の2つを追加した。

  • registerTransaction()
  • cancelTransactionRegistration()

ただし、モデルに重いロジックを全部書いたわけではない。実処理の本体は CreditCardStatementLineRegistrar に置いている。

役割分担はこうなった。

  • CreditCardStatementLine
  • 薄い入口
  • CreditCardStatementLineRegistrar
  • 明細の状態確認
  • 取引日の決定
  • 会計年度の解決
  • カード設定からの貸方補助科目解決
  • TransactionRegistrar へ渡す入力の組み立て
  • 登録後の明細更新
  • TransactionRegistrar
  • Transaction / JournalEntry の正規化、検証、保存

つまり、TransactionRegistrar は保存コアのままにして、カード明細由来のユースケースだけを薄いアダプタ層で包んだ形になる。

なぜ Registrar を増やしたのか

最初は「CreditCardStatementLine からそのまま TransactionRegistrar を呼べばよいのでは」と思ったのだけど、実際に必要だったのは保存の前段の方だった。

TransactionRegistrar が元から持っていたのは、

  • 貸借一致の検証
  • 税区分の解釈
  • 家事按分の展開
  • 所属整合
  • Transaction / JournalEntry の保存

であって、

  • この明細は今登録してよい状態か
  • used_onposted_on のどちらを取引日にするか
  • その日付がどの FiscalYear に属するか
  • 個人カードなら owner_draw_sub_account_id、事業用カードなら liability_sub_account_id を使う
  • 登録後に CreditCardStatementLineregistered に更新する

といった、カード明細固有の前処理は持っていなかった。

なので、「TransactionRegistrar に実装されていなかったから StatementLineRegistrar に書いた」というよりは、「保存コアに混ぜるべきでないカード明細固有の処理を、専用の Registrar に置いた」というのが近い。

ただし、今回やっていて、将来的に共通化してもよさそうな処理も見えた。

  • 取引日から FiscalYear を解決する処理
  • 一部の transaction data 組み立て補助

このへんは別の入口が増えてきたら、また整理したい。

descriptionremarks

カード明細の文字列を Transaction にどう持たせるかも少し迷った。

最終的には、

  • description
  • 帳簿表示に使う会計上の摘要
  • remarks
  • カード明細の原文や補足

という分け方にした。

元データの正本は当然 CreditCardStatementLine にあるけれど、帳簿表示や検索のたびに relation だけを辿って摘要を復元するのは不便だし、Transaction 単体で意味が通らなくなる。なので、relation は relation として残しつつ、Transaction 側にも要約と原文の一部を保存する方針にしている。

個人カードと事業用カードの貸方

カード設定のどの項目を貸方に使うかも、テストを少し丁寧にした。

  • 個人カード
  • owner_draw_sub_account_id
  • 事業用カード
  • liability_sub_account_id

を使うのが仕様なのだけど、単に「期待した科目が入っていた」だけだと、たまたま同じ値を返していても通ってしまう。そこで、両方の ID を明示的にセットしたうえで、どちらが選ばれたかまで確認するテストにしている。

こういう分岐は、実装時よりリファクタ時に壊れやすいので、先に固定しておいた方がよい。

間違えた登録はどう直すか

今回の機能を作ると、カード明細由来の Transactioncredit_card_import_batch_id を持つようになる。これは再取込時に old batch 配下の取引をまとめて無効化するために必要な情報。

一方で、既存の TransactionRevisorcredit_card_import_batch_id !== null の取引を修正対象外にしている。つまり、この機能を入れた瞬間に、カード明細由来の取引は改訂機能から外れる。

ここは最初に仕様を決めて、初期実装では「改訂」ではなく「登録取消して再登録」で扱うことにした。

そのために cancelTransactionRegistration() を用意して、

  • 紐づく Transactiondeactivate()
  • 明細行を 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 のレビュー操作
  • 再取込時のレビュー結果引き継ぎ
  • カード明細由来取引を将来的に改訂対象へ入れるかどうか

特に最後の点は、今回わざと先送りした仕様バグに近いところでもある。とはいえ、まずは「登録できる」「取消してやり直せる」「再取込と整合する」を先に固めたので、次の段階に進む土台としては悪くないと思う。