開発tax-assistantメモ
クレジットカード明細インポートとレシート突き合わせ機能の実装
Square明細の突き合わせ機能に続いて、クレジットカード明細のインポートとレシート突き合わせ機能を実装した。経費計上漏れを検出する目的で、クレカ明細とスキャン済みレシートを日付・金額ベースでマッチングする。
背景と目的
解決したい課題
- 経費計上漏れの検出: クレカで支払ったのにレシートをスキャンし忘れているケース
- 私的利用の除外: 事業経費ではないクレカ利用を明示的にマーク
- 返金処理の追跡: キャンセル・返品による返金を適切に処理
Square明細との違い
| 項目 | Square明細 | クレカ明細 |
|---|---|---|
| 対象 | 売上(入金) | 経費(支出) |
| 一致判定 | Square ≤ 売上伝票 | クレカ = レシート |
| 私的利用 | なし | あり(privateステータス) |
| 返金 | なし | あり(refundステータス) |
実装内容
Phase 1: データベース設計
creditcard_transactionsテーブルを追加。Square明細と同様の構造だが、私的利用(private)と返金(refund)のステータスを追加。
CREATE TABLE IF NOT EXISTS creditcard_transactions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tx_date TEXT NOT NULL,
amount INTEGER NOT NULL,
description TEXT,
category TEXT,
source_file TEXT NOT NULL,
row_index INTEGER NOT NULL,
check_status TEXT DEFAULT 'unchecked',
check_message TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(source_file, row_index)
);
CREATE INDEX idx_creditcard_tx_date ON creditcard_transactions(tx_date);
CREATE INDEX idx_creditcard_check_status ON creditcard_transactions(check_status);
Phase 2: インポート機能
TSV形式のクレカ明細をパースしてインポート。重複はINSERT OR IGNOREでスキップ。
# import_creditcard.py の抜粋
def parse_creditcard_tsv(file_path: str) -> list[dict]:
"""TSV形式のクレカ明細をパース"""
transactions = []
with open(file_path, 'r', encoding='utf-8') as f:
reader = csv.reader(f, delimiter='\t')
for row_index, row in enumerate(reader):
if len(row) < 5:
continue
# 金額はマイナス値で記録されている
amount = abs(int(row[3].replace(',', '')))
transactions.append({
'tx_date': row[1], # YYYY/MM/DD形式
'amount': amount,
'description': row[2],
'category': row[4],
'source_file': os.path.basename(file_path),
'row_index': row_index
})
return transactions
Phase 3: 突き合わせロジック
レシートが存在する日付範囲内のクレカ明細のみをチェック対象にする。
def check_creditcard_against_receipts(db_path: str) -> dict:
"""クレカ明細とレシートの突き合わせチェック"""
# 確定レシートの日付範囲を取得
date_range_sql = """
SELECT MIN(date) as min_date, MAX(date) as max_date
FROM receipts
WHERE confirmed = 1 AND date IS NOT NULL
"""
# 日付範囲内のクレカ明細のみをチェック
check_sql = """
WITH receipt_dates AS (
SELECT strftime('%Y-%m-%d', date) as receipt_date, amount
FROM receipts
WHERE confirmed = 1
)
UPDATE creditcard_transactions
SET check_status = CASE
WHEN check_status IN ('private', 'refund') THEN check_status
WHEN EXISTS (
SELECT 1 FROM receipt_dates
WHERE receipt_date = strftime('%Y-%m-%d', tx_date)
AND receipt_dates.amount = creditcard_transactions.amount
) THEN 'matched'
ELSE 'unmatched'
END
WHERE tx_date BETWEEN ? AND ?
AND check_status NOT IN ('private', 'refund')
"""
Phase 4: フロントエンド
CreditCardMatchingView.vueを新規作成。Square明細タブと同様のUI構造。
特徴:
- 種別フィルター: 「支払い」「返金」
- 年月スプリットビュー
- サマリーバー: 一致/不一致/私的利用/返金/未チェック件数
- チェック対象日付範囲の表示
<template>
<div class="creditcard-matching">
<!-- 種別カラム -->
<div class="type-column">
<div class="column-header">種別</div>
<div
v-for="type in types"
:key="type.value"
:class="['type-item', { active: selectedType === type.value }]"
@click="selectType(type.value)"
>
{{ type.label }}
</div>
</div>
<!-- 年月カラム -->
<YearFilterColumn :years="years" ... />
<MonthFilterColumn :months="months" ... />
<!-- データテーブル -->
<div class="data-table">
<div class="summary-bar">
全{{ total }}件 | 一致{{ matched }} 不一致{{ unmatched }}
私的{{ private }} 返金{{ refund }} 未チェック{{ unchecked }}
<span v-if="checkDateRange" class="date-range">
(チェック対象: {{ checkDateRange.min }} 〜 {{ checkDateRange.max }})
</span>
</div>
<!-- テーブル本体 -->
</div>
</div>
</template>
Codexレビューで反映した改善点
GPT-5.2(Codex)によるレビューで指摘された問題点を修正:
- 一意キーでの更新:
date, amountではなくsource_file, row_indexで更新対象を特定 - 返金の自動判定: 金額がマイナスの場合は自動で
refundステータスを設定 - 日付範囲フィルタ: レシートが存在する日付範囲外のクレカ明細はチェック対象外
- 件数の一時データ化: DBには件数を保存せず、APIレスポンスで集計結果として返す
使い方
1. クレカ明細のインポート
# スラッシュコマンドでインポート
/import-creditcard
inbox/ディレクトリ内のTSVファイルを自動検出してインポート。
2. 突き合わせチェック
「クレカ明細」タブで「レシートとチェック」ボタンをクリック。
3. 不一致の確認
不一致リストを確認し、必要に応じて:
- レシートをスキャンして追加
- 私的利用としてマーク
今後の課題
- 私的利用のマーク機能(UI)
- マネーフォワード仕訳プレビューとの連携
- 複数クレカの管理(ソース別フィルタ)