← 一覧に戻る

Zaim クレカ二重記録防止 — 設計

2026年5月7日 08:24 更新 / 設計タスク (j-20260507-001)

一言でいうと

Suica チャージ行を取込時に捨てる。1ファイル数行追加で済む。

なぜこれが必要なのか

家計簿アプリ Zaim には、健人が使ってる取込ルートが 2 系統 あるの。

この 2 つは普段はキレイに棲み分けるんだけど、Suica オートチャージ(VIEW カードから自動でチャージされる仕組み)が走るとき、同じ金額が両方のルートから入ってきて二重カウントされる。放置すると家計簿の総支出が嘘の数字になって、月次の予算判断が歪むのよ。

具体例:VIEW カードから ¥3,000 オートチャージ → クレカ連携が「クレカ支出 ¥3,000(オートチャージ)」を記録、Suica CSV が「Suica 入金 ¥3,000(電子マネー入金)」を記録。
→ 同じ ¥3,000 の動きが「支出」と「収入」で別々に膨らむ。本来は振替(口座間のお金の移動。家計の支出ではない)として 1 件で済ませるべき動きなの。

何をしたのか

このページでやるのは 設計のみ(実装は別ジョブで起票する)。具体的には、二重記録の発生条件を整理して、防止戦略を 5 案出して、推奨 1 案を選んで実装方針を固める、までを担当するわ。

重複パターンの整理

Suica CSV 取込で発生しうる行は 3 種類。実際に二重記録になるのは パターン A だけと特定できた。

🔴 パターン A — Suica オートチャージ(実際に発生する)

[VIEWカード]──オートチャージ ¥3,000──→[Suica残高] │ │ ↓ クレカ連携① ↓ Suica CSV取込② 支出 ¥3,000(オートチャージ) 収入 ¥3,000(電子マネー入金)

クレカ会社からは「VIEW カードで ¥3,000 の請求」、Suica 履歴からは「Suica に ¥3,000 入金」として記録される。同じ動きが「支出」と「収入」で 2 回登場するため、家計簿全体で見たときに支出も収入も嵩上げされる。これが 本タスクで防ぐべき唯一のパターン

🟢 パターン B — Suica 電子マネー利用(重複しない)

[Suica残高]──電子マネー支払い ¥320──→[駅売店/改札] │ ↓ Suica CSV取込②のみ 支出 ¥320(食費/交通) *クレカ連携①には来ない

Suica 残高で支払った時点でクレカは関与しない。だからクレカ連携には絶対に出ない。取込しても重複は起きない安全な行で、これは普通に支出として記録するべき。

🟡 パターン C — 現金 / 駅券売機チャージ(運用上発生しない)

現金で券売機からチャージした場合、クレカ連携には出てこない。この場合チャージ行を取り込まないと、現金の動きが家計簿から消える。
ただし健人の現運用は モバイル Suica(オートチャージ運用) のみで現金チャージは発生しないため、YAGNI(You Aren't Gonna Need It=必要になってから足せばいい原則)で当面サポートしないと判断する。

ステップ / 詳細

防止戦略の 5 案を比較

方針実装コスト副作用
推奨
案1
取込側でチャージ行を skip(収入+カテゴリ「その他」+内訳「電子マネー入金」を検知して捨てる)小(数行)現金チャージは記録から漏れる(運用上発生しない)
代替
案2
チャージ行を「振替」として変換登録(クレカ→Suica の口座間移動として 1 件で記録)クレカ連携の支出と相殺ロジックが必要、設計が複雑化
案3金額+日付±1 日でクレカ取引と突合(Zaim API /v2/home/money=Zaim の取引一覧を取得するエンドポイント で同期間取得 → 一致したら skip)突合の閾値判定が脆い。同金額が連続する日は誤検知
案4Suica CSV 出力段階で除外(fetch-history.sh(Suica履歴取得シェルスクリプト)で「電子マネー入金」行をフィルタ)家計簿ロジックがシェルに散らばる(責務分散の悪化)
案5そのまま記録 + 月次レポートで突合警告家計簿の数字は嘘のまま、人間の目視に依存

推奨:案 1(取込時 skip)

選定理由
① クレカ連携で「ほんとの支出」が既に記録されている → Suica 側のチャージ行を捨てても家計簿の総支出は変わらない(むしろ正しい数字に戻る)
② 既存コードの「振替行は YAGNI で skip」と同じパターン。一貫性が保てて、新しい概念を持ち込まなくて済む
③ シンプル(KISS 原則:Keep It Simple, Stupid=できるだけシンプルに)。1 ファイル数行追加 + テスト 3 本追加で完結

実装方針(ファイル / 関数 / 行レベル)

変更対象ファイル: ~/ghq/github.com/ramenumaiwhy/claude-code-zaim/src/handlers/importCsvHandler.ts(CSV 取込のリクエストを受け取って 1 行ずつ Zaim API に投げるハンドラ=「Webからの取込リクエストを受け付ける窓口」の役割)

変更箇所: 関数 importCsvHandler の行ループ内、現状の method === "振替" スキップ判定(行 289–293 付近)の直後に同型の if を 1 つ足すだけ。

// 現状 (行 289–293):
if (method === "振替") {
  // Suica振替はYAGNIでサポートしない
  skipped += 1;
  continue;
}

// ↓ ここに追加 ↓
// Suica チャージ行 (収入 / その他 / 電子マネー入金) は VIEW カード等の
// クレカ連携で支出として既に取込済 → 二重記録防止のため skip。
// 現金チャージは現状のモバイル Suica 運用では発生しないため YAGNI。
if (
  method === "収入" &&
  category === "その他" &&
  subCategory === "電子マネー入金"
) {
  skipped += 1;
  continue;
}

判定キーの根拠: ~/.claude/skills/suica-browser/scripts/fetch-history.sh(Suica 履歴を取得して Zaim 形式 CSV に変換するシェルスクリプト)の出力ロジック(行 203–207)で、入金行は必ず カテゴリ=その他 + カテゴリの内訳=電子マネー入金 でラベル付けされる。3 つすべて完全一致を要求することで、健人が将来手動で同カテゴリを使った場合の誤検知を防ぐ。

テスト追加方針(テスト駆動の最小ケース)

変更対象: src/handlers/importCsvHandler.test.ts(取込ハンドラのテストファイル)

影響範囲チェックリスト

項目影響
既存 dedup(imported:suica:<hash> KV キー:「同一行を二度取り込まない」ための既存の重複防止機構)変更なし。skip 行は KV 書き込みもしない(既存の振替 skip と同じ挙動)
Zaim API 呼び出し回数減る(チャージ行分の /v2/home/money/income 呼び出しが消える)。レート制限的にも有利
クレカ連携側の挙動無関係(Zaim 側の自動連携には触れない)
過去取込済データ影響なし(今後の取込のみ)。既に二重計上された分は手動削除推奨

ポイント

案 2(振替変換)を見送った理由: 「正しい家計簿表現」としては案 2 が理論上ベスト(クレカ→Suica の振替で 1 件記録すれば実態に一致する)。ただし Zaim 振替 API の調査と相殺ロジックの設計コストが高く、案 1 でも家計簿の総支出は正しい数字になる(過剰計上が消えるだけ)。過剰計上の解消が今回のゴールなので、案 1 で十分。将来「Suica の中身を独立した口座として可視化したい」要件が出てきたら案 2 に発展させればいい。

残課題(実装後に詰めるべき)

next_action:この設計で OK なら、別ジョブで実装起票(importCsvHandler.ts への数行追加 + テスト 3 ケース追加 + dry-run デプロイ確認)。NG ならフィードバックもらって修正 → 再レビュー。

📝 質問モード — テキストを選択してね
✓ 質問を送信しました