Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions backend/services/billing/__tests__/accountingExportService.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import {
streamExport,
reconcile,
createExportSchedule,
getExportSchedules,
updateExportSchedule,
deleteExportSchedule,
toggleExportSchedule,
runDueExports,
recordExportDownload,
getExportHistory,
getExportAnalytics,
TransactionRecord,
TransactionType,
} from '../accountingExportService';
Expand Down Expand Up @@ -141,4 +150,143 @@ describe('accountingExportService', () => {
expect(result.mismatches[0]?.reason).toContain('type mismatch');
});
});

describe('PDF streaming', () => {
it('streams valid PDF content', () => {
const chunks: string[] = [];
const { totalRecords } = streamExport([makeRecord()], {
format: 'pdf',
filter: { merchantId: 'merchant-1' },
onChunk: (c) => chunks.push(c),
});

const output = chunks.join('');
expect(totalRecords).toBe(1);
expect(output).toContain('%PDF-1.4');
expect(output).toContain('merchant-1');
expect(output).toContain('Slack');
expect(output).toContain('%%EOF');
});
});

describe('JSON schema wrapping', () => {
it('wraps JSON output in schema envelope when includeSchema is true', () => {
const chunks: string[] = [];
streamExport([makeRecord()], {
format: 'json',
filter: { merchantId: 'merchant-1' },
includeSchema: true,
onChunk: (c) => chunks.push(c),
});

const output = chunks.join('');
const parsed = JSON.parse(output);
expect(parsed.$schema).toContain('subtrackr.app');
expect(parsed.schemaVersion).toBe('1.0.0');
expect(parsed.recordCount).toBe(1);
expect(Array.isArray(parsed.records)).toBe(true);
});

it('returns bare JSON array by default', () => {
const chunks: string[] = [];
streamExport([makeRecord()], {
format: 'json',
onChunk: (c) => chunks.push(c),
});

const output = chunks.join('');
const parsed = JSON.parse(output);
expect(Array.isArray(parsed)).toBe(true);
expect(parsed[0].id).toBe('txn_1');
});
});

describe('custom field mappings in CSV', () => {
it('applies custom field mappings to CSV rows', () => {
const chunks: string[] = [];
streamExport([makeRecord()], {
format: 'csv',
onChunk: (c) => chunks.push(c),
fieldMappings: [
{ targetField: 'ID', sourceField: 'id' },
{ targetField: 'Total', sourceField: 'amount', transform: 'currency' },
],
});

const output = chunks.join('');
expect(output).toContain('"ID"');
expect(output).toContain('"Total"');
expect(output).toContain('"txn_1"');
expect(output).toContain('"12.50"');
});
});

describe('schedule management', () => {
const baseInput = {
merchantId: 'merchant-sched',
format: 'csv' as const,
frequency: 'weekly' as const,
enabled: true,
includeInactive: false,
nextRunAt: Date.now() + 1000,
};

it('creates, reads, updates, and deletes schedules', () => {
const sched = createExportSchedule(baseInput);
expect(sched.id).toBeTruthy();

const schedules = getExportSchedules('merchant-sched');
expect(schedules.some((s) => s.id === sched.id)).toBe(true);

const updated = updateExportSchedule(sched.id, { frequency: 'monthly' });
expect(updated?.frequency).toBe('monthly');

const deleted = deleteExportSchedule(sched.id);
expect(deleted).toBe(true);

const afterDelete = getExportSchedules('merchant-sched');
expect(afterDelete.find((s) => s.id === sched.id)).toBeUndefined();
});

it('toggleExportSchedule enables and disables', () => {
const sched = createExportSchedule({ ...baseInput, enabled: true });
const paused = toggleExportSchedule(sched.id, false);
expect(paused?.enabled).toBe(false);
const resumed = toggleExportSchedule(sched.id, true);
expect(resumed?.enabled).toBe(true);
});

it('runDueExports processes due schedules', () => {
const past = Date.now() - 5000;
createExportSchedule({ ...baseInput, merchantId: 'merchant-due', nextRunAt: past });
const results = runDueExports([makeRecord()], Date.now());
expect(results.length).toBeGreaterThanOrEqual(1);
});
});

describe('analytics and history', () => {
it('getExportAnalytics aggregates history', () => {
// runDueExports populates history; analytics should reflect counts
const analytics = getExportAnalytics();
expect(typeof analytics.totalExports).toBe('number');
expect(typeof analytics.totalDownloads).toBe('number');
expect(typeof analytics.formatBreakdown.csv).toBe('number');
});

it('recordExportDownload increments download count', () => {
const history = getExportHistory();
if (history.length === 0) {
// No history to test against; pass
return;
}
const entry = history[0]!;
const updated = recordExportDownload(entry.id);
expect(updated?.downloadCount).toBe((entry.downloadCount ?? 0) + 1);
});

it('recordExportDownload returns null for unknown id', () => {
const result = recordExportDownload('unknown_backend_id');
expect(result).toBeNull();
});
});
});
Loading