• #Vue.js
  • #tax-assistant
  • #仕訳
  • #自動化
  • #CSV
開発tax-assistant完了

クレジットカード明細に仕訳ルールを自動マッチングする機能を実装

結論

クレジットカード明細に対して、マネーフォワードからエクスポートした仕訳ルールCSVを自動マッチングし、勘定科目を提案する機能を実装した。マッチしたルールは右側パネルに詳細表示され、ルール編集画面への遷移も可能になった。

実装内容

1. check_statusに「候補」ステータスを追加

既存のcheck_statusrule_matchedを追加し、ルールマッチした明細を識別できるようにした。

// check_status の定義
type CheckStatus =
  | 'unchecked'      // 未チェック
  | 'matched'        // レシート一致
  | 'unmatched'      // レシート不一致
  | 'rule_matched'   // ルールで候補あり(新規追加)
  | 'rule_confirmed' // ルール確定済み
  | 'private'        // 私的利用
  | 'refund'         // 返金

// 表示用ラベル
const statusLabels: Record<CheckStatus, string> = {
  unchecked: '未チェック',
  matched: '一致',
  unmatched: '不一致',
  rule_matched: '候補',  // 新規追加
  rule_confirmed: '確定',
  private: '私的',
  refund: '返金',
}

2. マッチしたルールの詳細表示

右側パネルにマッチしたルールの詳細を表示するUIを追加した。

<template>
  <div class="rule-match-panel" v-if="selectedTransaction?.matched_rules">
    <h3>マッチしたルール</h3>
    <div
      v-for="rule in parsedMatchedRules"
      :key="rule.rule_hash"
      class="rule-item"
      :class="{ selected: rule.rule_hash === selectedRuleHash }"
      @click="selectRule(rule)"
    >
      <div class="rule-pattern">{{ rule.pattern }}</div>
      <div class="rule-account">
        {{ rule.account }}
        <span v-if="rule.sub_account">/ {{ rule.sub_account }}</span>
      </div>
      <div class="rule-tax">{{ rule.tax_type }}</div>
      <div class="rule-similarity">類似度: {{ rule.similarity }}%</div>
    </div>

    <NuxtLink
      :to="`/shiwake-rules?account=${encodeURIComponent(selectedRule?.account || '')}`"
      class="edit-rules-link"
    >
      仕訳ルール編集
    </NuxtLink>
  </div>
</template>

<script setup lang="ts">
interface MatchedRule {
  row_number: number
  rule_hash: string
  pattern: string
  match_type: 'exact' | 'partial' | 'levenshtein' | 'token'
  similarity: number
  account: string
  sub_account: string
  tax_type: string
  credit_account: string
  summary: string
}

const parsedMatchedRules = computed<MatchedRule[]>(() => {
  if (!selectedTransaction.value?.matched_rules) return []
  try {
    return JSON.parse(selectedTransaction.value.matched_rules)
  } catch {
    return []
  }
})
</script>

3. URLクエリパラメータでの状態管理

クレカ明細タブの選択状態をURLで管理し、ブラウザの戻る/進むやブックマークに対応した。

// クエリパラメータの定義
interface CreditCardQueryParams {
  ccYear?: string   // 年(例: "2026")
  ccMonth?: string  // 月(例: "01")
  ccIndex?: string  // 選択行のインデックス(0始まり)
}

// URLからの状態復元
const route = useRoute()
const router = useRouter()

const restoreFromQuery = () => {
  const { ccYear, ccMonth, ccIndex } = route.query as CreditCardQueryParams

  if (ccYear && ccMonth) {
    selectedYear.value = parseInt(ccYear)
    selectedMonth.value = parseInt(ccMonth)
  }

  if (ccIndex !== undefined) {
    const index = parseInt(ccIndex)
    if (!isNaN(index) && index >= 0) {
      nextTick(() => {
        selectRow(index)
      })
    }
  }
}

// 状態変更時のURL更新
const updateQueryParams = () => {
  router.replace({
    query: {
      ...route.query,
      ccYear: String(selectedYear.value),
      ccMonth: String(selectedMonth.value),
      ccIndex: selectedIndex.value >= 0 ? String(selectedIndex.value) : undefined,
    },
  })
}

// 行選択時
const selectRow = (index: number) => {
  selectedIndex.value = index
  updateQueryParams()
}

4. ImageNavigationPanelコンポーネントの共通化

帳票プレビュー機能を複数ビューで統一するため、共通コンポーネントを作成した。詳細は Vue.jsで帳票画像ナビゲーションUIを共通コンポーネント化する を参照。

<!-- CreditCardMatchingView.vue -->
<ImageNavigationPanel
  :current-index="selectedIndex"
  :total-count="filteredTransactions.length"
  @prev="goPrev"
  @next="goNext"
>
  <div class="preview-content">
    <!-- マッチしたルールの詳細 -->
    <div v-if="selectedTransaction?.matched_rules" class="rule-details">
      ...
    </div>
    <!-- レシート画像プレビュー -->
    <img v-if="previewImageUrl" :src="previewImageUrl" />
  </div>
</ImageNavigationPanel>

5. 仕訳ルール編集画面への遷移

マッチしたルールから直接ルール編集画面に遷移できるリンクを追加した。

<NuxtLink
  :to="{
    path: '/shiwake-rules',
    query: {
      account: selectedRule?.account,
      subAccount: selectedRule?.sub_account,
      taxType: selectedRule?.tax_type,
    }
  }"
  class="edit-rules-link"
>
  仕訳ルール編集
</NuxtLink>

ルール編集画面側では、クエリパラメータを受け取ってミラーカラムUIの初期選択状態を設定する。

帳票プレビュー機能の統一

以下の4つのビューで同じナビゲーションUIを使用するようになった。

ビュー用途
読取一覧(MillerColumnsView)OCR読取済みレシートの確認
クレカ明細(CreditCardMatchingView)クレカ明細とレシートの突き合わせ
Square明細(SquareMatchingView)Square決済とレシートの突き合わせ
科目別一覧(AccountView)勘定科目別のレシート確認

マッチングロジックの概要

マッチングは以下の順序で行われる。

  1. クレカ明細の「利用先」を正規化(全角→半角、スペース除去など)
  2. 仕訳ルールの「明細一致条件」と照合
  3. マッチタイプに応じて類似度を計算
  4. 閾値を超えたルールを候補としてJSON保存
# マッチングの疑似コード
for transaction in creditcard_transactions:
    matches = []
    for rule in shiwake_rules:
        similarity = calculate_similarity(
            transaction.description,
            rule.pattern,
            rule.match_type
        )
        if similarity >= rule.threshold:
            matches.append({
                'rule_hash': rule.hash,
                'pattern': rule.pattern,
                'similarity': similarity,
                'account': rule.account,
                ...
            })

    if matches:
        transaction.matched_rules = json.dumps(matches)
        transaction.check_status = 'rule_matched'

今後の拡張予定

  • ルール確定フロー: 候補から選択して確定するとrule_confirmedに変更
  • 正規表現対応: Phase 2で正規表現マッチを追加(ReDoS対策としてタイムアウト設定)
  • 学習機能: 手動で確定したルールを新規ルールとして登録

関連記事