'use strict'; // Batch 3: OFX/QFX import — parser handles SGML (OFX 1.x, unclosed leaf tags) and // XML (OFX 2.x / QFX); commit inserts with the same dedupe scope as the CSV path. const test = require('node:test'); const assert = require('node:assert/strict'); const os = require('node:os'); const path = require('node:path'); const fs = require('node:fs'); const dbPath = path.join(os.tmpdir(), `bill-tracker-ofx-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; const { getDb, closeDb } = require('../db/database'); const { parseOfx, previewOfxTransactions, commitOfxTransactions } = require('../services/ofxImportService'); // OFX 1.x SGML — container tags closed, leaf tags unclosed. const OFX_SGML = `OFXHEADER:100 DATA:OFXSGML VERSION:102 USD 000123456789 DEBIT 20260615120000 -42.50 TXN-1001 Netflix Monthly subscription CREDIT 20260616 1500.00 TXN-1002 Payroll `; // OFX 2.x / QFX — XML (closed leaf tags) + Intuit tag we must ignore. const QFX_XML = ` 00123 DEBIT 20260701 -9.99 X-1 Spotify & Co `; let userId; test.before(() => { const db = getDb(); userId = db.prepare("INSERT INTO users (username, password_hash, role, active) VALUES ('ofx-user','x','user',1)").run().lastInsertRowid; }); test.after(() => { closeDb(); for (const s of ['', '-wal', '-shm']) { try { fs.unlinkSync(dbPath + s); } catch {} } }); test('parseOfx reads SGML transactions (date, signed cents, fitid, name/memo)', () => { const txns = parseOfx(OFX_SGML); assert.equal(txns.length, 2); const netflix = txns[0]; assert.equal(netflix.posted_date, '2026-06-15'); assert.equal(netflix.amount, -4250, 'signed cents'); assert.equal(netflix.provider_transaction_id, 'ofx:id:TXN-1001'); assert.equal(netflix.payee, 'Netflix'); assert.equal(netflix.memo, 'Monthly subscription'); assert.equal(txns[1].amount, 150000, 'credit is positive cents'); }); test('parseOfx reads XML/QFX + decodes entities + ignores Intuit tags', () => { const txns = parseOfx(QFX_XML); assert.equal(txns.length, 1); assert.equal(txns[0].amount, -999); assert.equal(txns[0].posted_date, '2026-07-01'); assert.equal(txns[0].payee, 'Spotify & Co'); }); test('parseOfx rejects non-OFX and empty files', () => { assert.throws(() => parseOfx('just some text'), /does not look like|OFX/i); assert.throws(() => parseOfx(''), /No transactions/i); }); test('preview → commit inserts, and re-commit dedupes', () => { const db = getDb(); const before = db.prepare('SELECT COUNT(*) n FROM transactions WHERE user_id=?').get(userId).n; const p1 = previewOfxTransactions(userId, Buffer.from(OFX_SGML), { original_filename: 'stmt.ofx' }); assert.equal(p1.count, 2); const r1 = commitOfxTransactions(userId, p1.import_session_id); assert.equal(r1.imported, 2); assert.equal(r1.skipped, 0); const mid = db.prepare('SELECT COUNT(*) n FROM transactions WHERE user_id=?').get(userId).n; assert.equal(mid - before, 2); // amounts persisted as integer cents const amt = db.prepare("SELECT amount FROM transactions WHERE provider_transaction_id='ofx:id:TXN-1001' AND user_id=?").get(userId).amount; assert.equal(amt, -4250); // re-import the same file → all skipped, no new rows const p2 = previewOfxTransactions(userId, Buffer.from(OFX_SGML), { original_filename: 'stmt.ofx' }); const r2 = commitOfxTransactions(userId, p2.import_session_id); assert.equal(r2.imported, 0); assert.equal(r2.skipped, 2); const after = db.prepare('SELECT COUNT(*) n FROM transactions WHERE user_id=?').get(userId).n; assert.equal(after, mid, 'no duplicates on re-import'); // logged to import_history const hist = db.prepare("SELECT file_type, rows_created, rows_skipped FROM import_history WHERE user_id=? ORDER BY id DESC LIMIT 1").get(userId); assert.equal(hist.file_type, 'ofx_transactions'); });