Finance & Statements — Vault, Parsing, Reports
Upload password-protected bank statements. Vault decrypts transiently, deterministic parser validates totals, AI categorizes. All beginner-friendly with security deep-dive.
Overview — Finance Intelligence
What Finance Module Does
Located: /w/[workspace]/finance → Overview, Transactions, Subscriptions, Invoices. Handles sensitive data via SECRET_ENCRYPTION_KEY vault.
Bank Statement Pipeline (4 Steps)
1. Upload — Vault Hints
Finance → Import → choose PDF (SBI/HDFC/ICICI). Enter vault hints: PAN, DOB, phone last 4 (stored encrypted).
Route: POST /api/finance/import
Body: multipart/form-data { file, workspace_id }
Store: STORAGE_DIR/finance/<workspace>/<id>.pdf2. In-Memory Unlock (Secure)
Vault: AES-256 decrypt hints → try patterns:
- DDMMYYYY, MMDDYYYY, DDMMYY
- PAN(5)+DOB(4), Name+DOB
- Phone last 4 + combinations
Each try in-memory, not logged. Stops at first success.
If none → show "Try password" modal.🔒 Security: No Password Logging
activity_log or job payload. Vault blob encrypted at rest with SECRET_ENCRYPTION_KEY.3. Parse & Validate (Deterministic)
Parser: unpdf → text → table detection (regex for dates, amounts)
Rows: { date, description, amountMinor (int), currency, type: credit|debit }
Totals:
openingMinor + sum(credits) - sum(debits) === closingMinor
If mismatch → flag "Statement totals don't match" (red).
Integer math: ₹18,420.50 → 1842050 minor units (no float errors).4. Categorize & Review
AI proposes category per row; low-confidence (<0.7) shown in Review modal.
- Food, Travel, Shopping, Software, Subscriptions, Bills, Business, Salary, Transfers, ATM, Unknown
- Heuristic + LLM: merchant “Swiggy” → Food, “Adobe” → Software/Subscription
Vault & Security Model
Encryption:
- Key: SECRET_ENCRYPTION_KEY (32-byte hex, generate via `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`)
- Algorithm: AES-256-GCM (in lib/security/vault.ts)
- Stored: oauth_tokens, pan, dob, phone fragments encrypted
- Decrypt: only in worker, in-memory, per request
Tables:
- vault_items { id, workspace_id, kind, encrypted_blob }
- Only workspace owner can read/write via DAL guardProduction Warning
SECRET_ENCRYPTION_KEY breaks existing vault rows (can't decrypt). Migrate by re-encrypting with new key, don't just change env.Categorization & Deterministic Math
Why Deterministic > LLM for Totals
amountMinor ints, validates. LLM only labels merchant. This avoids “₹500 + ₹500 = ₹900” errors.Example transaction:
{ date: "2025-12-01", description: "UPI-ADOBE", amountMinor: 167500, type: "debit" }
→ Category: Software (Adobe keyword)
→ Subscription? Expense 1675 monthly → detect → create subscriptions rowSubscriptions & Invoices
Detection from emails ("Your Netflix receipt") + transactions ("NETFLIX" monthly):
subscriptions {
id, workspace_id, name: "Netflix", amountMinor, currency,
frequency: monthly|yearly, next_expected_payment, payment_method,
source: "gmail"|"transaction", status: active
}
UI: /finance/subscriptions → Upcoming payments: Adobe ₹1675 due 2 SepReminder automation: 3 days before due → notify.
Dashboard & Reports
/w/[workspace]/finance shows:
- Total spent this month:
₹18,420vs last month (+12%) - By category: Food ₹4,200 (22%), Software ₹2,400…
- By day chart (Recharts), by merchant, by account
All computed via SQL GROUP BY category on minor units, formatted with Intl.NumberFormat.
Database Schema
accounts { id, workspace_id, name, type: bank|wallet, openingMinor }
transactions { id, workspace_id, account_id, date, description, amountMinor, currency, type, category, confidence }
subscriptions { ... as above }
invoices { ... }
expenses { id, workspace_id, title, amountMinor, category, receipt_file_id }
audit: activity_log records every finance change