import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { parseOfx } from '../src/import/ofx';
import { makeApp, setUpAdmin, type Client, type TestApp } from './helpers';
// Made-up OFX files in both flavors banks send: the older SGML style with
// unclosed tags (bank account), and XML (credit card, as in a .qfx).
const SGML = `OFXHEADER:100
DATA:OFXSGML
VERSION:102
SECURITY:NONE
ENCODING:USASCII
CHARSET:1252
0INFO20261007120000ENGBank of America5959
10INFO
USD000000000000099991234CHECKING
2026090120260930
CREDIT20260905120000.000[-5:EST]3000.00A1ACME CORP PAYROLLPPD ID: 123
DEBIT20260912-54.20A2KROGER #123 CHARLOTTE NC
XFER20260915-300.00A3Online Banking transfer to SAV5678 Confirmation# 1
DEBIT20260920-12,50A4JOES COFFEE & BAGELS
4633.30202609304633.3020260930
`;
const XML = `
1USD
4111XXXXXXXX9876
2026090120260930
DEBIT2026090320260902-23.99C1AMAZON MKTPL*AB12CD3E4
CREDIT2026090718.50C2AMAZON MKTPL*QR56ST7U8Return
DEBIT20260912-15.49C3NETFLIX.COM
-412.3320260930
`;
describe('reading OFX', () => {
it('reads the older SGML style', () => {
const s = parseOfx(SGML);
expect(s).toMatchObject({ kind: 'checking', institution: 'Bank of America', accountLast4: '1234', ledger: { cents: 463330, date: '2026-09-30' } });
expect(s.transactions.map((t) => [t.date, t.amountCents, t.name])).toEqual([
['2026-09-05', 300000, 'ACME CORP PAYROLL'],
['2026-09-12', -5420, 'KROGER #123 CHARLOTTE NC'],
['2026-09-15', -30000, 'Online Banking transfer to SAV'],
['2026-09-20', -1250, 'JOES COFFEE & BAGELS'],
]);
});
it('reads XML, using the purchase date when there is one', () => {
const s = parseOfx(XML);
expect(s).toMatchObject({ kind: 'credit_card', accountLast4: '9876' });
expect(s.transactions[0]).toMatchObject({ date: '2026-09-02', postedDate: '2026-09-03', amountCents: -2399 });
expect(s.transactions[1]).toMatchObject({ amountCents: 1850, memo: 'Return' });
});
});
describe('importing OFX', () => {
let t: TestApp;
let admin: Client;
beforeEach(async () => {
t = await makeApp();
admin = await setUpAdmin(t);
});
afterEach(() => t.cleanup());
it('imports with no mapping, matches the account by its last digits, and skips repeats', async () => {
const checking = (await admin.post('/api/v1/accounts', { name: 'Checking', kind: 'checking', institution: 'Bank of America', last4: '1234' })).json.id;
await admin.post('/api/v1/accounts', { name: 'Prime Visa', kind: 'credit_card', institution: 'Chase', last4: '9876' });
const preview = await admin.upload('/api/v1/imports/files', 'export.qfx', SGML);
expect(preview.json).toMatchObject({ layout: { key: 'ofx' }, account: { id: checking, confidence: 'certain' }, counts: { new: 4 } });
const done = await admin.post('/api/v1/imports', { stagingId: preview.json.stagingId, accountId: checking });
expect(done.status).toBe(200);
const page = (await admin.get(`/api/v1/transactions?accountId=${checking}`)).json;
expect(page.items.map((x: { merchant: string }) => x.merchant)).toContain('Kroger');
const account = ((await admin.get('/api/v1/accounts')).json as { id: number; balanceCents: number; balanceConfirmedOn: string }[]).find((a) => a.id === checking)!;
expect(account).toMatchObject({ balanceCents: 463330, balanceConfirmedOn: '2026-09-20' });
const again = await admin.upload('/api/v1/imports/files', 'export.qfx', SGML);
expect(again.json.counts).toMatchObject({ new: 0, duplicates: 4 });
const card = await admin.upload('/api/v1/imports/files', 'Statement.qfx', XML);
expect(card.json.account.confidence).toBe('certain');
expect(card.json.accountHint).toMatchObject({ kind: 'credit_card' });
});
});