• #tax-assistant
  • #仕訳
  • #マネーフォワード
  • #自動化
  • #設計
開発tax-assistantアクティブ

クレカ明細への自動仕訳ルールマッチング機能の設計

クレジットカード明細に対して、マネーフォワードからエクスポートした仕訳ルールを自動適用し、勘定科目を提案する機能の設計。

概要

やりたいこと

  1. クレカ明細の「利用先」を仕訳ルールの「明細一致条件」と照合
  2. マッチしたルールの勘定科目を候補として表示
  3. ユーザーが確定すれば、その勘定科目で仕訳を生成

マッチタイプ

タイプ説明用途
完全一致文字列が完全に一致固定名称の店舗
部分一致パターンが含まれる店舗名の一部
レーベンシュタイン編集距離で類似度判定OCRの誤認識対応
類似度単語の重複率語順が入れ替わるケース

データフロー

[仕訳ルールCSV] → /import-shiwake-rules → [shiwake_rules テーブル]
                                              ↓
[クレカ明細] → 突き合わせチェック → [マッチング処理]
                                              ↓
                                    [matched_rules JSON保存]
                                              ↓
                                    [UI: 候補表示 → ユーザー確定]
                                              ↓
                                    [仕訳生成・エクスポート]

DBスキーマ

creditcard_transactions(追加カラム)

ALTER TABLE creditcard_transactions ADD COLUMN matched_rules TEXT;
ALTER TABLE creditcard_transactions ADD COLUMN selected_rule_hash TEXT;
ALTER TABLE creditcard_transactions ADD COLUMN selected_rule_row INTEGER;

matched_rules JSON構造

[
  {
    "row_number": 15,
    "rule_hash": "a1b2c3d4e5f6...",
    "pattern": "スターバックス",
    "match_type": "partial",
    "similarity": 100,
    "account": "会議費",
    "sub_account": "",
    "tax_type": "課税仕入10%",
    "credit_account": "未払金",
    "summary": "打合せ"
  }
]

check_status の拡張

ステータス説明
unchecked未チェックグレー
matchedレシート一致
unmatchedレシート不一致
rule_matchedルールで候補あり
rule_confirmedルール確定済み
private私的利用
refund返金オレンジ

マッチングロジック

def match_shiwake_rules(description: str, rules: list[dict]) -> list[dict]:
    """クレカ明細の説明文に対して仕訳ルールをマッチング"""
    description_normalized = normalize_text(description)
    matches = []

    for rule in rules:
        pattern = rule['pattern']
        pattern_normalized = normalize_text(pattern)
        match_type = rule.get('match_type', 'partial')
        threshold = rule.get('threshold', 80)

        # Phase 1 では正規表現は無効
        if rule.get('regex_enabled') == '1':
            continue

        similarity = 0
        if match_type == 'exact':
            if description_normalized == pattern_normalized:
                similarity = 100
        elif match_type == 'partial':
            if pattern_normalized in description_normalized:
                similarity = 100
        elif match_type == 'levenshtein':
            similarity = calc_levenshtein_similarity(
                description_normalized, pattern_normalized
            )
        elif match_type == 'token':
            similarity = calc_token_similarity(
                description_normalized, pattern_normalized
            )

        if similarity >= threshold:
            matches.append({
                'row_number': rule['row_number'],
                'rule_hash': calc_rule_hash(rule),
                'pattern': pattern,
                'match_type': match_type,
                'similarity': similarity,
                'account': rule['account'],
                'sub_account': rule.get('sub_account', ''),
                'tax_type': rule.get('tax_type', ''),
                'credit_account': rule.get('credit_account', ''),
                'summary': rule.get('summary', '')
            })

    # 類似度の高い順にソート、重複排除
    matches = sorted(matches, key=lambda x: -x['similarity'])
    seen_hashes = set()
    unique_matches = []
    for m in matches:
        if m['rule_hash'] not in seen_hashes:
            seen_hashes.add(m['rule_hash'])
            unique_matches.append(m)

    return unique_matches

API設計

ルールマッチング実行

POST /api/creditcard/match-rules
Request: { client_id: string }
Response: {
  total: number,
  rule_matched: number,
  unchanged: number
}

ルール確定

POST /api/creditcard/{id}/confirm-rule
Request: {
  rule_hash: string,
  rule_row: number
}
Response: { success: boolean }

UI設計

クレカ明細タブ

  • check_statusrule_matchedの行は青背景
  • 「勘定科目」列に候補を表示
  • 複数候補がある場合は「会議費 他2件」のように表示
  • クリックでドロップダウンから選択

ルール確定フロー

  1. 候補をクリック → ドロップダウン表示
  2. ルールを選択 → selected_rule_hash/selected_rule_rowを保存
  3. check_statusrule_confirmedに更新

Codexレビューで固めたポイント

  • rule_hash: CSV更新時の整合性検証に使用(row_numberだけでは行がずれる)
  • 正規表現はPhase 2: Phase 1では完全/部分/類似度マッチのみ
  • ReDoS対策: 正規表現を使う場合はタイムアウト(100ms)を設定
  • 履歴ポリシー: 再マッチ時はselected_rule_*をクリアして再判定

TODO

  • /import-shiwake-rulesスラッシュコマンドの実装
  • マッチングロジックの実装
  • APIエンドポイントの追加
  • フロントエンドUIの実装
  • 正規表現対応(Phase 2)