/* Shared, deterministic matching logic. No account identifiers are retained. */ (() => { const norm = value => String(value ?? '').normalize('NFKD').replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim(); const clean = value => typeof value === 'string' ? value.slice(0, 160).trim() : ''; function extract(payload, bank = 'capital-one') { const result = []; let visited = 0; function walk(value, depth) { if (!value || typeof value !== 'object' || depth > 16 || ++visited > 50000) return; const merchant = bank === 'rogers' ? value.merchant : value.transactionMerchant; const amount = bank === 'rogers' && /^-?\d+(?:\.\d+)?$/.test(String(value.amount?.value)) ? Number(value.amount.value) : value.transactionAmount; if (merchant && /^\d{4}$/.test(String(merchant.categoryCode)) && Number.isFinite(amount)) { if (bank === 'rogers') { const name = clean(merchant.name); if (name) result.push({bank, name, amount, last4: /\d{4}$/.exec(clean(value.cardNumber))?.[0] || '', holder: clean(value.name?.nameOnCard), date: clean(value.date).slice(0, 10), postedDate: clean(value.postedDate).slice(0, 10), state: clean(value.activityStatus), mcc: String(merchant.categoryCode), type: clean(merchant.category), category: clean(merchant.categoryDescription)}); } else { const name = clean(value.transactionDescription || merchant.name); if (name) result.push({ bank, name, amount: Math.abs(amount), last4: /^\d{4}$/.test(value.transactingCardLastFour) ? value.transactingCardLastFour : '', date: clean(value.transactionDisplayDate || value.transactionDate).slice(0, 10), state: clean(value.transactionState), mcc: String(merchant.categoryCode), type: clean(merchant.merchantType || merchant.parentCategory || value.displayCategory), category: clean(merchant.category) }); } } for (const child of Object.values(value)) walk(child, depth + 1); } walk(payload, 0); return result.slice(0, 5000); } function valid(record) { return record && typeof record.name === 'string' && record.name.length > 0 && record.name.length <= 160 && typeof record.amount === 'number' && Number.isFinite(record.amount) && (!record.bank || ['capital-one','rogers'].includes(record.bank)) && ['holder','postedDate'].every(k => record[k] === undefined || (typeof record[k] === 'string' && record[k].length <= 160)) && /^\d{4}$/.test(record.mcc) && ['last4','date','state','type','category'].every(k => typeof record[k] === 'string' && record[k].length <= 160); } function candidates(text, records, context = {}) { const normalized = ` ${norm(text)} `; const amounts = [...String(text).matchAll(/(?:-\s*)?\$?\s*(?:-\s*)?(?:\d{1,3}(?:,\d{3})+|\d+)\.\d{2}(?!\d)/g)] .filter(m => !/\d/.test(String(text)[m.index - 1] || '')) .map(m => Number(m[0].replace(/[$,\s]/g, ''))); const months = ['jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec']; function dateMatches(r) { return [r.date, r.postedDate].some(date => { if (!/^\d{4}-\d{2}-\d{2}$/.test(date || '')) return false; const [year,month,day] = date.split('-').map(Number); return normalized.includes(` ${year} ${String(month).padStart(2,'0')} ${String(day).padStart(2,'0')} `) || new RegExp(`(?:^| )${months[month-1]}[a-z]* ${day} ${year}(?: |$)`).test(norm(text)); }); } return records.filter(r => normalized.includes(` ${norm(r.name)} `) && amounts.some(a => Math.abs((r.bank === 'rogers' ? a : Math.abs(a)) - r.amount) < 0.005) && (r.bank === 'rogers' ? dateMatches(r) && (!context.holder || !r.holder || norm(context.holder) === norm(r.holder)) : (!r.last4 || new RegExp(`(^|\\D)${r.last4}(?!\\d)`).test(text)))); } function unique(records) { if (!records.length) return null; const signatures = new Set(records.map(r => JSON.stringify([r.mcc, r.type, r.category]))); return signatures.size === 1 ? records[0] : null; } const api = { norm, extract, valid, candidates, unique }; if (typeof module !== 'undefined' && module.exports) module.exports = api; else globalThis.MerchantLensCore = api; })();