開発tax-assistant完了
クレジットカード明細に仕訳ルールを自動マッチングする機能を実装
結論
クレジットカード明細に対して、マネーフォワードからエクスポートした仕訳ルールCSVを自動マッチングし、勘定科目を提案する機能を実装した。マッチしたルールは右側パネルに詳細表示され、ルール編集画面への遷移も可能になった。
実装内容
1. check_statusに「候補」ステータスを追加
既存のcheck_statusにrule_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) | 勘定科目別のレシート確認 |
マッチングロジックの概要
マッチングは以下の順序で行われる。
- クレカ明細の「利用先」を正規化(全角→半角、スペース除去など)
- 仕訳ルールの「明細一致条件」と照合
- マッチタイプに応じて類似度を計算
- 閾値を超えたルールを候補として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対策としてタイムアウト設定)
- 学習機能: 手動で確定したルールを新規ルールとして登録
関連記事
- クレカ明細への自動仕訳ルールマッチング機能の設計 - 設計ドキュメント
- Vue.jsで帳票画像ナビゲーションUIを共通コンポーネント化する - ImageNavigationPanelの詳細
- 仕訳ルールUI改善:書類タイプ別の件数表示と条件付きカラム表示 - ルール編集画面のUI改善