Capital One Account Changes

This commit is contained in:
2026-08-04 11:52:01 -04:00
parent 1400aa99d6
commit 2a4522a09a
3 changed files with 49 additions and 23 deletions

View File

@@ -187,20 +187,21 @@ model BalanceSnapshot {
All 4 user banks with exact column mappings. Implement in `src/lib/csv/bank-profiles.ts`. All 4 user banks with exact column mappings. Implement in `src/lib/csv/bank-profiles.ts`.
### Discover High Yield Savings → `BANK` ### Discover High Yield Savings → `BANK`
- **Strategy**: B (split debit/credit columns) - **Strategy**: C (single unsigned amount column + type column)
- `date` ← `Transaction Date` - `date` ← `Transaction Date`
- `description` ← `Transaction Description` - `description` ← `Transaction Description`
- `debitAmount` ← `Debit` - `amount` ← `Transaction Amount`
- `creditAmount` `Credit` - `type` ← `Transaction Type` — value is literally `Debit` or `Credit`
- `balance` ← `Balance` (reference only) - `balance` ← `Balance` (reference only)
- Ignore: `Transaction Type` - Ignore: `Account Number`
### Discover Credit Card → `CREDIT_CARD` ### Discover Credit Card → `CREDIT_CARD`
- **Strategy**: A (single signed column) - **Strategy**: B (split debit/credit columns)
- `date` ← `Trans. Date` - `date` ← `Transaction Date`
- `description` ← `Description` - `description` ← `Description`
- `amount` ← `Amount` — **positive = DEBIT (charge), negative = CREDIT (payment/refund)** - `debitAmount` ← `Debit`
- Ignore: `Post Date`, `Category` - `creditAmount` ← `Credit`
- Ignore: `Posted Date`, `Card No.`, `Category`
### Huntington Checking → `BANK` ### Huntington Checking → `BANK`
- **Strategy**: A (single signed column) - **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 // Sign convention varies per bank; each profile has invertAmountSign: boolean
function parseStrategyA(raw: string, invert: boolean): { amountCents: number; type: TransactionType } 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 // The non-empty/non-zero column determines type and amount
function parseStrategyB(debitRaw: string, creditRaw: string): { amountCents: number; type: TransactionType } 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: // All amounts via:
function parseCents(raw: string): number { function parseCents(raw: string): number {
return Math.round(parseFloat(raw.replace(/[$,\s]/g, '')) * 100) return Math.round(parseFloat(raw.replace(/[$,\s]/g, '')) * 100)

View File

@@ -1,4 +1,4 @@
export type ParseStrategy = 'A' | 'B' export type ParseStrategy = 'A' | 'B' | 'C'
export interface NormalizerConfig { export interface NormalizerConfig {
strategy: ParseStrategy strategy: ParseStrategy
@@ -11,6 +11,8 @@ export interface NormalizerConfig {
// Strategy B // Strategy B
debitColumn?: string debitColumn?: string
creditColumn?: string creditColumn?: string
// Strategy C
typeColumn?: string
} }
export interface BankProfile extends NormalizerConfig { export interface BankProfile extends NormalizerConfig {
@@ -25,24 +27,24 @@ export const bankProfiles: BankProfile[] = [
id: 'discover-savings', id: 'discover-savings',
name: 'Discover High Yield Savings', name: 'Discover High Yield Savings',
accountType: 'BANK', accountType: 'BANK',
strategy: 'B', strategy: 'C',
dateColumn: 'Transaction Date', dateColumn: 'Transaction Date',
descriptionColumn: 'Transaction Description', descriptionColumn: 'Transaction Description',
debitColumn: 'Debit', amountColumn: 'Transaction Amount',
creditColumn: 'Credit', typeColumn: 'Transaction Type',
detectColumns: ['Transaction Date', 'Transaction Description', 'Debit', 'Credit', 'Balance'], detectColumns: ['Transaction Date', 'Transaction Description', 'Transaction Type', 'Transaction Amount', 'Balance'],
}, },
{ {
id: 'discover-cc', id: 'discover-cc',
name: 'Discover Credit Card', name: 'Discover Credit Card',
accountType: 'CREDIT_CARD', accountType: 'CREDIT_CARD',
strategy: 'A', strategy: 'B',
dateColumn: 'Trans. Date', dateColumn: 'Transaction Date',
descriptionColumn: 'Description', descriptionColumn: 'Description',
amountColumn: 'Amount', debitColumn: 'Debit',
invertAmountSign: true, // negative = DEBIT (charge), positive = CREDIT (payment/refund) creditColumn: 'Credit',
categoryColumn: 'Category', categoryColumn: 'Category',
detectColumns: ['Trans. Date', 'Post Date', 'Description', 'Amount', 'Category'], detectColumns: ['Transaction Date', 'Posted Date', 'Card No.', 'Description', 'Category', 'Debit', 'Credit'],
}, },
{ {
id: 'huntington-checking', id: 'huntington-checking',

View File

@@ -16,10 +16,14 @@ export function parseCents(raw: string): number {
function parseDate(raw: string): Date { function parseDate(raw: string): Date {
const trimmed = raw.trim() const trimmed = raw.trim()
// MM/DD/YYYY // MM/DD/YYYY or MM/DD/YY
if (/^\d{1,2}\/\d{1,2}\/\d{4}$/.test(trimmed)) { const m = /^(\d{1,2})\/(\d{1,2})\/(\d{2}|\d{4})$/.exec(trimmed)
const [m, d, y] = trimmed.split('/') if (m) {
return new Date(parseInt(y), parseInt(m) - 1, parseInt(d)) 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) const date = new Date(trimmed)
if (!isNaN(date.getTime())) return date if (!isNaN(date.getTime())) return date
@@ -49,6 +53,17 @@ function strategyB(
return { amountCents: Math.abs(parseCents(creditRaw)), type: 'CREDIT' } 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( export function normalizeRows(
rows: Record<string, string>[], rows: Record<string, string>[],
accountId: string, accountId: string,
@@ -73,6 +88,10 @@ export function normalizeRows(
const r = strategyB(row[config.debitColumn] ?? '', row[config.creditColumn] ?? '') const r = strategyB(row[config.debitColumn] ?? '', row[config.creditColumn] ?? '')
amountCents = r.amountCents amountCents = r.amountCents
type = r.type 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 { } else {
continue continue
} }