feat: Import page — CSV and OFX/QFX file upload with parsing#14
Conversation
Agent-Logs-Url: https://github.com/plures/pares-modulus/sessions/2b6e6a6d-2d1c-461a-85f9-e535ff63d921 Co-authored-by: kayodebristol <3579196+kayodebristol@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds an Import workflow to the Financial Advisor plugin, enabling users to upload bank export files (CSV/OFX/QFX), preview parsed transactions with duplicate detection, and commit new records into the fa-transactions collection.
Changes:
- Introduces a new
/importUI with drag-and-drop upload, progress stages, preview table, and commit flow. - Adds CSV parsing with bank-format auto-detection + generic fallback, and OFX/QFX parsing for both SGML (OFX 1.x) and XML (OFX 2.x).
- Defines a
Transactionmodel plus helpers for ID generation and CSV dedup fingerprinting.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
| plugins/financial-advisor/src/pages/Import.svelte | New Import page UI + duplicate detection + commit logic into PluresDB collection. |
| plugins/financial-advisor/src/lib/transactions.ts | Introduces Transaction types and helpers (ID + CSV hash fingerprint). |
| plugins/financial-advisor/src/lib/parsers/csv.ts | Adds CSV parsing with bank-specific layouts and generic column heuristics. |
| plugins/financial-advisor/src/lib/parsers/ofx.ts | Adds OFX/QFX parsing (SGML + XML) and conversion to Transaction drafts. |
| stage = 'committing'; | ||
| const now = new Date().toISOString(); | ||
| const toSave = newRows; | ||
|
|
||
| let saved = 0; | ||
| try { | ||
| for (const draft of toSave) { | ||
| const txn: Transaction = { | ||
| ...draft, | ||
| accountId: selectedAccountId, | ||
| importedAt: now, | ||
| }; | ||
| await txnCollection.put(txn.id, txn); | ||
| saved++; | ||
| progress = Math.round((saved / toSave.length) * 100); | ||
| } |
There was a problem hiding this comment.
progress = Math.round((saved / toSave.length) * 100) will divide by zero if commitImport() is called when newRows.length is 0 (possible via programmatic calls or race conditions). Add a guard for toSave.length === 0 before entering the loop and set stage/progress accordingly.
| export function ofxTransactionsToTransactions( | ||
| rows: OfxTransaction[], | ||
| source: 'ofx' | 'qfx', | ||
| ): Omit<Transaction, 'accountId' | 'importedAt'>[] { | ||
| return rows.map(row => ({ | ||
| id: generateTransactionId(), | ||
| date: row.date, | ||
| description: row.description, | ||
| amount: row.amount, | ||
| type: mapTrnType(row.rawType), | ||
| source, | ||
| fitId: row.fitId, | ||
| })); |
There was a problem hiding this comment.
Transaction.id is documented as a stable ID (FITID for OFX/QFX), but this conversion generates a new random ID for every import. This breaks idempotency and makes retries/duplicate handling harder. Either make id deterministic for OFX/QFX (e.g., based on FITID and possibly account) or update the Transaction docs/usage so the contract matches behavior.
| if (isXml) { | ||
| // Use browser DOMParser (available in all modern browsers and jsdom) | ||
| // Strip the <?OFX?> PI if present since browsers may reject it | ||
| const xmlStr = text.replace(/<\?OFX[^>]*\?>/i, ''); | ||
| const parser = new DOMParser(); | ||
| const doc = parser.parseFromString(xmlStr, 'application/xml'); | ||
| return { | ||
| transactions: parseXmlTransactions(doc), | ||
| ...extractXmlMeta(doc), | ||
| }; |
There was a problem hiding this comment.
When treating input as XML, the parser result isn't validated. DOMParser.parseFromString() returns a document even on parse errors (often containing a <parsererror> element), which can lead to silently returning zero transactions. Detect XML parse errors and either throw a helpful error or fall back to the SGML parser.
kayodebristol
left a comment
There was a problem hiding this comment.
Auto-approved: CI green + Copilot code review complete.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
|
@copilot apply changes based on the comments in this thread |
… detection, divide-by-zero guard, safe retry on partial commit failure Agent-Logs-Url: https://github.com/plures/pares-modulus/sessions/792599a7-294c-49c6-befe-1710a62a2a6d Co-authored-by: kayodebristol <3579196+kayodebristol@users.noreply.github.com>
Applied in commit
|
|
@copilot apply changes based on the comments in this thread |
All changes from that review thread were already applied in commit
|
|
@copilot apply changes based on the comments in this thread |
All items from that review thread were applied in commit
|
Builds the Import page for the financial-advisor plugin: file upload, multi-format parsing, duplicate detection, preview, and immutable commit to
fa-transactions.New files
src/lib/transactions.ts—Transactioninterface,FA_TRANSACTIONS_COLLECTION,generateTransactionId(), andhashCsvTransaction()(64-bit SHA-256 prefix overdate|amount|descriptionfor CSV deduplication)src/lib/parsers/csv.ts— CSV parser with header-signature auto-detection for Chase, BofA, and Wells Fargo; generic column-heuristic fallback for other exportssrc/lib/parsers/ofx.ts— OFX/QFX parser covering OFX 1.x SGML (no closing tags) and OFX 2.x XML viaDOMParser; extractsFITID, date, amount, name/memo, and transaction typesrc/pages/Import.svelte— Full import UI wired to the above parsersImport page behaviour
.csv,.ofx,.qfx)fa-accountscollection.put(); no update path existsDuplicate detection example
The
/importroute's existingrequires: [{ type: 'accounts', minCount: 1 }]guard enforces the account prerequisite before the page renders.