From 2a4522a09adcba07fdf5cce97975d565971373e9 Mon Sep 17 00:00:00 2001 From: jerick Date: Tue, 4 Aug 2026 11:52:01 -0400 Subject: [PATCH] Capital One Account Changes --- CLAUDE.md | 23 ++++++++++++++--------- src/lib/csv/bank-profiles.ts | 22 ++++++++++++---------- src/lib/csv/normalizer.ts | 27 +++++++++++++++++++++++---- 3 files changed, 49 insertions(+), 23 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a47bf16..1c7bdbb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -187,20 +187,21 @@ model BalanceSnapshot { All 4 user banks with exact column mappings. Implement in `src/lib/csv/bank-profiles.ts`. ### Discover High Yield Savings → `BANK` -- **Strategy**: B (split debit/credit columns) +- **Strategy**: C (single unsigned amount column + type column) - `date` ← `Transaction Date` - `description` ← `Transaction Description` -- `debitAmount` ← `Debit` -- `creditAmount` ← `Credit` +- `amount` ← `Transaction Amount` +- `type` ← `Transaction Type` — value is literally `Debit` or `Credit` - `balance` ← `Balance` (reference only) -- Ignore: `Transaction Type` +- Ignore: `Account Number` ### Discover Credit Card → `CREDIT_CARD` -- **Strategy**: A (single signed column) -- `date` ← `Trans. Date` +- **Strategy**: B (split debit/credit columns) +- `date` ← `Transaction Date` - `description` ← `Description` -- `amount` ← `Amount` — **positive = DEBIT (charge), negative = CREDIT (payment/refund)** -- Ignore: `Post Date`, `Category` +- `debitAmount` ← `Debit` +- `creditAmount` ← `Credit` +- Ignore: `Posted Date`, `Card No.`, `Category` ### Huntington Checking → `BANK` - **Strategy**: A (single signed column) @@ -224,10 +225,14 @@ All 4 user banks with exact column mappings. Implement in `src/lib/csv/bank-prof // Sign convention varies per bank; each profile has invertAmountSign: boolean function parseStrategyA(raw: string, invert: boolean): { amountCents: number; type: TransactionType } -// Strategy B — separate debit/credit columns (Discover Savings) +// Strategy B — separate debit/credit columns (Discover Credit Card) // The non-empty/non-zero column determines type and amount function parseStrategyB(debitRaw: string, creditRaw: string): { amountCents: number; type: TransactionType } +// Strategy C — single unsigned amount column + separate type column (Discover Savings) +// The type column's literal value ("Debit" / "Credit") determines type +function parseStrategyC(amountRaw: string, typeRaw: string): { amountCents: number; type: TransactionType } + // All amounts via: function parseCents(raw: string): number { return Math.round(parseFloat(raw.replace(/[$,\s]/g, '')) * 100) diff --git a/src/lib/csv/bank-profiles.ts b/src/lib/csv/bank-profiles.ts index cb6b8aa..0f39e4b 100644 --- a/src/lib/csv/bank-profiles.ts +++ b/src/lib/csv/bank-profiles.ts @@ -1,4 +1,4 @@ -export type ParseStrategy = 'A' | 'B' +export type ParseStrategy = 'A' | 'B' | 'C' export interface NormalizerConfig { strategy: ParseStrategy @@ -11,6 +11,8 @@ export interface NormalizerConfig { // Strategy B debitColumn?: string creditColumn?: string + // Strategy C + typeColumn?: string } export interface BankProfile extends NormalizerConfig { @@ -25,24 +27,24 @@ export const bankProfiles: BankProfile[] = [ id: 'discover-savings', name: 'Discover High Yield Savings', accountType: 'BANK', - strategy: 'B', + strategy: 'C', dateColumn: 'Transaction Date', descriptionColumn: 'Transaction Description', - debitColumn: 'Debit', - creditColumn: 'Credit', - detectColumns: ['Transaction Date', 'Transaction Description', 'Debit', 'Credit', 'Balance'], + amountColumn: 'Transaction Amount', + typeColumn: 'Transaction Type', + detectColumns: ['Transaction Date', 'Transaction Description', 'Transaction Type', 'Transaction Amount', 'Balance'], }, { id: 'discover-cc', name: 'Discover Credit Card', accountType: 'CREDIT_CARD', - strategy: 'A', - dateColumn: 'Trans. Date', + strategy: 'B', + dateColumn: 'Transaction Date', descriptionColumn: 'Description', - amountColumn: 'Amount', - invertAmountSign: true, // negative = DEBIT (charge), positive = CREDIT (payment/refund) + debitColumn: 'Debit', + creditColumn: 'Credit', categoryColumn: 'Category', - detectColumns: ['Trans. Date', 'Post Date', 'Description', 'Amount', 'Category'], + detectColumns: ['Transaction Date', 'Posted Date', 'Card No.', 'Description', 'Category', 'Debit', 'Credit'], }, { id: 'huntington-checking', diff --git a/src/lib/csv/normalizer.ts b/src/lib/csv/normalizer.ts index 20aeee5..1549294 100644 --- a/src/lib/csv/normalizer.ts +++ b/src/lib/csv/normalizer.ts @@ -16,10 +16,14 @@ export function parseCents(raw: string): number { function parseDate(raw: string): Date { const trimmed = raw.trim() - // MM/DD/YYYY - if (/^\d{1,2}\/\d{1,2}\/\d{4}$/.test(trimmed)) { - const [m, d, y] = trimmed.split('/') - return new Date(parseInt(y), parseInt(m) - 1, parseInt(d)) + // MM/DD/YYYY or MM/DD/YY + const m = /^(\d{1,2})\/(\d{1,2})\/(\d{2}|\d{4})$/.exec(trimmed) + if (m) { + const month = parseInt(m[1]) + const day = parseInt(m[2]) + let year = parseInt(m[3]) + if (year < 100) year += 2000 + return new Date(year, month - 1, day) } const date = new Date(trimmed) if (!isNaN(date.getTime())) return date @@ -49,6 +53,17 @@ function strategyB( return { amountCents: Math.abs(parseCents(creditRaw)), type: 'CREDIT' } } +function strategyC( + amountRaw: string, + typeRaw: string, +): { amountCents: number; type: 'DEBIT' | 'CREDIT' } { + const cents = Math.abs(parseCents(amountRaw)) + const typeNormalized = typeRaw.trim().toUpperCase() + if (typeNormalized === 'CREDIT') return { amountCents: cents, type: 'CREDIT' } + if (typeNormalized === 'DEBIT') return { amountCents: cents, type: 'DEBIT' } + throw new Error(`Unknown transaction type: ${typeRaw}`) +} + export function normalizeRows( rows: Record[], accountId: string, @@ -73,6 +88,10 @@ export function normalizeRows( const r = strategyB(row[config.debitColumn] ?? '', row[config.creditColumn] ?? '') amountCents = r.amountCents type = r.type + } else if (config.strategy === 'C' && config.amountColumn && config.typeColumn) { + const r = strategyC(row[config.amountColumn] ?? '0', row[config.typeColumn] ?? '') + amountCents = r.amountCents + type = r.type } else { continue }