Capital One Account Changes
This commit is contained in:
23
CLAUDE.md
23
CLAUDE.md
@@ -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)
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user