'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');
});