2026-05-29 18:06:12 -05:00
|
|
|
'use strict';
|
|
|
|
|
|
2026-07-06 10:47:16 -05:00
|
|
|
import type { Db } from '../types/db';
|
|
|
|
|
|
2026-05-29 18:06:12 -05:00
|
|
|
const { getDb } = require('../db/database');
|
|
|
|
|
|
|
|
|
|
// Lazy-loaded in-memory cache — loaded once on first use
|
2026-07-06 10:47:16 -05:00
|
|
|
let _patterns: any[] | null = null;
|
|
|
|
|
let _overrideTerms: string[] | null = null;
|
2026-05-29 18:06:12 -05:00
|
|
|
|
2026-07-06 10:47:16 -05:00
|
|
|
function normalize(text: unknown): string {
|
2026-05-29 18:06:12 -05:00
|
|
|
if (!text) return '';
|
|
|
|
|
return String(text)
|
|
|
|
|
.toLowerCase()
|
|
|
|
|
.replace(/&/g, 'and')
|
|
|
|
|
.replace(/\s+/g, ' ')
|
|
|
|
|
.trim();
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 10:47:16 -05:00
|
|
|
function loadCache(): void {
|
2026-05-29 18:06:12 -05:00
|
|
|
if (_patterns !== null) return;
|
2026-07-06 10:47:16 -05:00
|
|
|
const db: Db = getDb();
|
2026-05-29 18:06:12 -05:00
|
|
|
_patterns = db.prepare(
|
|
|
|
|
'SELECT pattern, confidence, category, rationale FROM advisory_non_bill_filters'
|
|
|
|
|
).all();
|
|
|
|
|
_overrideTerms = db.prepare(
|
|
|
|
|
'SELECT term FROM advisory_bill_like_overrides'
|
2026-07-06 10:47:16 -05:00
|
|
|
).all().map((r: any) => r.term);
|
2026-05-29 18:06:12 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check a transaction title against advisory filter patterns.
|
|
|
|
|
* Returns null if Create Bill should be shown normally, or
|
|
|
|
|
* { confidence: 'high'|'medium', category, rationale } if it should be suppressed.
|
|
|
|
|
*/
|
2026-07-06 10:47:16 -05:00
|
|
|
function checkTransaction(title: unknown): { confidence: string; category: any; rationale: any } | null {
|
2026-05-29 18:06:12 -05:00
|
|
|
if (!title) return null;
|
|
|
|
|
try {
|
|
|
|
|
loadCache();
|
|
|
|
|
} catch {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const normalized = normalize(title);
|
|
|
|
|
if (!normalized) return null;
|
|
|
|
|
|
|
|
|
|
// Bill-like override terms take priority — always show Create Bill
|
2026-07-06 10:47:16 -05:00
|
|
|
for (const term of _overrideTerms!) {
|
2026-05-29 18:06:12 -05:00
|
|
|
if (normalized.includes(term)) return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find the highest-confidence matching pattern
|
2026-07-06 10:47:16 -05:00
|
|
|
let highMatch: any = null;
|
|
|
|
|
let mediumMatch: any = null;
|
2026-05-29 18:06:12 -05:00
|
|
|
|
2026-07-06 10:47:16 -05:00
|
|
|
for (const row of _patterns!) {
|
2026-05-29 18:06:12 -05:00
|
|
|
if (normalized.includes(row.pattern)) {
|
|
|
|
|
if (row.confidence === 'high') {
|
|
|
|
|
highMatch = row;
|
|
|
|
|
break; // high confidence — no need to keep looking
|
|
|
|
|
} else if (!mediumMatch) {
|
|
|
|
|
mediumMatch = row;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const match = highMatch || mediumMatch;
|
|
|
|
|
if (!match) return null;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
confidence: match.confidence,
|
|
|
|
|
category: match.category,
|
|
|
|
|
rationale: match.rationale || null,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Clear the in-memory cache (used after re-seeding in tests). */
|
2026-07-06 10:47:16 -05:00
|
|
|
function clearCache(): void {
|
2026-05-29 18:06:12 -05:00
|
|
|
_patterns = null;
|
|
|
|
|
_overrideTerms = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
module.exports = { checkTransaction, clearCache };
|