クレジットカード明細CSVの取込の土台を実装した。今回は「画面からアップロードして使えるようにする」ところまでは進めず、まずはアプリケーション内部から安全に呼べる import サービスと、カード会社ごとの差異を吸収する parser 群、その周辺のモデルとテストを固めた回になる。
今回作ったもの
入口は CreditCardImportService。CreditCard と CSV文字列を渡すと、CreditCardStatement、CreditCardImportBatch、CreditCardStatementLine をまとめて保存する。
CreditCard- カード設定そのもの
parser_keyで、どの形式のCSVとして読むかを決めるCreditCardStatement- 請求月単位のヘッダ
CreditCardImportBatch- どのCSVを、いつ、どの parser で取り込んだかの履歴
CreditCardStatementLine- CSVの各明細行の原本
対応した parser は今のところ次の3つ。
- Orico
- AEON
- 楽天カード
加えて、generic_csv_v1 という自動判別モードも入れた。これはカード設定側で使うためのキーで、CSV内容から具体的な parser を選ぶ。
parser と保存を分けた
今回の実装で一番大きいのは、CSVを読む責務と、DBに保存する責務を分けたことだと思う。
各 parser は CSV 文字列を直接 Eloquent に流し込まず、いったん正規化済みの明細データを返すだけにした。保存は CreditCardImportService 側でまとめてやる。こうしておくと、parser のテストは「このCSVをどう読めるか」だけに集中できるし、import 側のテストは「読めた結果をどう保存するか」だけを見ればよくなる。
app/Data に追加した ParsedCreditCardStatement と ParsedCreditCardStatementLine は、その中間データを運ぶための薄い DTO。Eloquent ではなく、型付きの入れ物に近い。
会社ごとのズレは parser と override で吸収する
カード会社によって、
- 締日
- CSVがダウンロードできるようになる時期
- 1ファイルがカバーする利用期間
- CSV内に請求日や請求年が入っているかどうか
が違う。この差は parser 側で吸収しつつ、CSVだけでは確定できない値は import() の override 引数で補完する形にした。
たとえば楽天カードCSVは、支払月は入っていても請求年や請求日はCSV単体で確定できない場合がある。そのため、
statement_yearstatement_monthbilled_onpaid_onperiod_start_onperiod_end_on
を外から渡せるようにしてある。
ここを「とりあえずCSVの見た目から何とか推測する」で押し切ると、年またぎや遅延ダウンロード時に簡単に壊れるので、明示 override を残した。
同じ内容の行も潰さない
CSVはカード会社が出した原本なので、同じ店・同じ日・同じ金額の行が2件あれば、それは実際に2件あるものとして保存する。
以前は内容一致で duplicate 扱いに寄せる案もあったが、これは正当な複数利用を誤検知しやすい。今の実装では、取込時は全行を unreviewed で保存し、fingerprint は出現順込みの識別子として持つだけにした。自動 duplicate 判定は初版ではやっていない。
再取込は「置き換え」で扱う
同じ請求月を再取込した場合は、古い batch を inactive にして、その batch から作られた明細行と取引もまとめて無効化する。
これは「差分マージ」ではなく「新しいCSVを正として置き換える」方針。クレジットカード明細は、訂正版CSVを落とし直す方が自然で、途中の差分統合を頑張るより監査しやすい。
ただし、閉じた会計年度にぶら下がる取引があると、無効化できず例外で止まる。このあたりは既存の会計年度クローズのルールに従っている。
テストを厚めに入れた
今回は、正常系だけでなく異常系のテストをかなり増やした。
- 形式違いのCSVを渡したときに例外になる
generic_csv_v1で判別不能なCSVを渡したときに何も保存されない- 金額列や必須列が壊れているCSVで例外になる
- 同一内容行が複数あっても全件
unreviewedで保存される - クォート内改行を含むCSVでも壊れない
- 再取込時に旧 batch / lines / transactions が無効化される
後から画面やアップロード口をつなぐときに、入力導線の不具合と parser 自体の不具合を切り分けやすくするためにも、この段階でテストを厚くしておいた意味は大きい。
まだやっていないこと
今回のスコープ外にしたものも多い。
- HTTP ファイルアップロード
- Livewire 画面からの取込
- 原本CSVファイルの Storage 保存
- 明細レビューUI
Transaction登録UI- 自動
duplicate判定
つまり今の段階では、「CLIやサービス呼び出しからCSV文字列を渡せば、原本保存の基盤までは動く」という状態。エンドユーザー向けの操作導線はまだこれからになる。
次
次に考えるべき課題は、「こちらがまだ対応していないカード会社の明細をどう扱うか」だと思う。自分ひとりで使う前提なら、自分の持っているカード会社の種類だけparserを用意すれば十分。でも、多くの人に使ってもらうなら、それでは足りない。
世の中のクレジットカードは種類が多く、発行会社もCSV仕様もばらばらなので、「未対応です」で終わると入口がかなり狭くなる。かといって、最初からすべてのカード会社に専用 parser を用意するのも現実的ではない。
なので次のテーマは、専用 parser がない明細をどう受け止めるかになるはず。たとえば、
- 汎用CSVとして列対応をユーザーに設定させるのか
- 最低限のテンプレート変換を用意するのか
- いったんインポート前にプレビューして、どこが読めないかを案内するのか
このあたりを考えないと、「多くの人が使える」に進めないが、明細CSVのためにカードを契約するのも本末転倒な話なので、なかなか難しい。