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`.
### 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)

View File

@@ -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',

View File

@@ -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<string, string>[],
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
}