From a89434e840815b7151948bebcfc7630c145bd4c0 Mon Sep 17 00:00:00 2001 From: ryanduguid Date: Thu, 6 Aug 2026 23:12:20 +1000 Subject: [PATCH 1/2] fix: preserve TOON output for reports --- src/commands/reports/aged-payables.ts | 2 +- src/commands/reports/aged-receivables.ts | 2 +- src/commands/reports/balance-sheet.ts | 2 +- src/commands/reports/profit-and-loss.ts | 2 +- src/commands/reports/trial-balance.ts | 2 +- test/commands/report-output.test.ts | 39 ++++++++++++++++++++++++ 6 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 test/commands/report-output.test.ts diff --git a/src/commands/reports/aged-payables.ts b/src/commands/reports/aged-payables.ts index 2737d73..0aa90b0 100644 --- a/src/commands/reports/aged-payables.ts +++ b/src/commands/reports/aged-payables.ts @@ -56,7 +56,7 @@ export default class ReportsAgedPayables extends BaseCommand { {key: 'paid', header: 'Paid', format: (v) => v ? formatCurrency(v) : ''}, {key: 'credited', header: 'Credited', format: (v) => v ? formatCurrency(v) : ''}, ], - {csv: flags.csv}, + flags, ) } diff --git a/src/commands/reports/aged-receivables.ts b/src/commands/reports/aged-receivables.ts index d459f28..9887f79 100644 --- a/src/commands/reports/aged-receivables.ts +++ b/src/commands/reports/aged-receivables.ts @@ -56,7 +56,7 @@ export default class ReportsAgedReceivables extends BaseCommand { {key: 'paid', header: 'Paid', format: (v) => v ? formatCurrency(v) : ''}, {key: 'credited', header: 'Credited', format: (v) => v ? formatCurrency(v) : ''}, ], - {csv: flags.csv}, + flags, ) } diff --git a/src/commands/reports/balance-sheet.ts b/src/commands/reports/balance-sheet.ts index a3be73b..3831570 100644 --- a/src/commands/reports/balance-sheet.ts +++ b/src/commands/reports/balance-sheet.ts @@ -60,7 +60,7 @@ export default class ReportsBalanceSheet extends BaseCommand { {key: 'account', header: 'Account'}, {key: 'amount', header: 'Amount', format: (v) => v ? formatCurrency(v) : ''}, ], - {csv: flags.csv}, + flags, ) } diff --git a/src/commands/reports/profit-and-loss.ts b/src/commands/reports/profit-and-loss.ts index e729bea..d6cb477 100644 --- a/src/commands/reports/profit-and-loss.ts +++ b/src/commands/reports/profit-and-loss.ts @@ -62,7 +62,7 @@ export default class ReportsProfitAndLoss extends BaseCommand { {key: 'account', header: 'Account'}, {key: 'amount', header: 'Amount', format: (v) => v ? formatCurrency(v) : ''}, ], - {csv: flags.csv}, + flags, ) } diff --git a/src/commands/reports/trial-balance.ts b/src/commands/reports/trial-balance.ts index 77add07..689b3df 100644 --- a/src/commands/reports/trial-balance.ts +++ b/src/commands/reports/trial-balance.ts @@ -52,7 +52,7 @@ export default class ReportsTrialBalance extends BaseCommand { {key: 'debit', header: 'Debit', format: (v) => v ? formatCurrency(v) : ''}, {key: 'credit', header: 'Credit', format: (v) => v ? formatCurrency(v) : ''}, ], - {csv: flags.csv}, + flags, ) } diff --git a/test/commands/report-output.test.ts b/test/commands/report-output.test.ts new file mode 100644 index 0000000..3593a03 --- /dev/null +++ b/test/commands/report-output.test.ts @@ -0,0 +1,39 @@ +import {describe, expect, it, vi} from 'vitest' +import ReportsAgedPayables from '../../src/commands/reports/aged-payables.js' +import ReportsAgedReceivables from '../../src/commands/reports/aged-receivables.js' +import ReportsBalanceSheet from '../../src/commands/reports/balance-sheet.js' +import ReportsProfitAndLoss from '../../src/commands/reports/profit-and-loss.js' +import ReportsTrialBalance from '../../src/commands/reports/trial-balance.js' + +type ReportCommand = new (argv: string[], config: object) => { + parse: (input: unknown) => Promise + run: () => Promise +} + +const reportCommands: Array<[string, ReportCommand, Record]> = [ + ['aged payables', ReportsAgedPayables, {'contact-id': 'contact-id', toon: true}], + ['aged receivables', ReportsAgedReceivables, {'contact-id': 'contact-id', toon: true}], + ['balance sheet', ReportsBalanceSheet, {toon: true}], + ['profit and loss', ReportsProfitAndLoss, {toon: true}], + ['trial balance', ReportsTrialBalance, {toon: true}], +] + +describe('report output formats', () => { + it.each(reportCommands)('%s passes --toon to the formatter', async (_name, Command, flags) => { + const command = new Command([], {}) as any + vi.spyOn(command, 'parse').mockResolvedValue({flags}) + vi.spyOn(command, 'xeroCall').mockImplementation(async (_flags: unknown, operation: Function) => + operation({ + accountingApi: new Proxy({}, { + get: () => vi.fn().mockResolvedValue({body: {reports: [{reportName: 'Report', rows: []}]}}), + }), + }, 'tenant-id'), + ) + vi.spyOn(command, 'log').mockImplementation(() => undefined) + const outputFormatted = vi.spyOn(command, 'outputFormatted').mockImplementation(() => undefined) + + await command.run() + + expect(outputFormatted.mock.calls[0]?.[2]).toEqual(flags) + }) +}) From 3fc2696b6acf2094ee827163d77175bdf088cd5f Mon Sep 17 00:00:00 2001 From: Ryan Duguid <152749594+ryanduguid@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:00:48 +1000 Subject: [PATCH 2/2] fix(reports): suppress the human-readable preamble for machine formats The five report commands logged the report name, date and blank lines to stdout before the formatted payload, so --toon and --csv output was not a parseable document. Gate the preamble on the table format. Also add a report-output test that runs the real outputFormatted path instead of mocking it, so getOutputFormat and formatOutput are actually exercised. --- src/commands/reports/aged-payables.ts | 6 ++- src/commands/reports/aged-receivables.ts | 6 ++- src/commands/reports/balance-sheet.ts | 6 ++- src/commands/reports/profit-and-loss.ts | 6 ++- src/commands/reports/trial-balance.ts | 8 ++-- test/commands/report-output.test.ts | 58 ++++++++++++++++++++++++ 6 files changed, 79 insertions(+), 11 deletions(-) diff --git a/src/commands/reports/aged-payables.ts b/src/commands/reports/aged-payables.ts index 0aa90b0..ec92f8a 100644 --- a/src/commands/reports/aged-payables.ts +++ b/src/commands/reports/aged-payables.ts @@ -43,8 +43,10 @@ export default class ReportsAgedPayables extends BaseCommand { return } - this.log(`\n${report.reportName as string}`) - this.log('') + if (this.getOutputFormat(flags) === 'table') { + this.log(`\n${report.reportName as string}`) + this.log('') + } const rows = this.extractReportRows(report) this.outputFormatted( diff --git a/src/commands/reports/aged-receivables.ts b/src/commands/reports/aged-receivables.ts index 9887f79..639ad17 100644 --- a/src/commands/reports/aged-receivables.ts +++ b/src/commands/reports/aged-receivables.ts @@ -43,8 +43,10 @@ export default class ReportsAgedReceivables extends BaseCommand { return } - this.log(`\n${report.reportName as string}`) - this.log('') + if (this.getOutputFormat(flags) === 'table') { + this.log(`\n${report.reportName as string}`) + this.log('') + } const rows = this.extractReportRows(report) this.outputFormatted( diff --git a/src/commands/reports/balance-sheet.ts b/src/commands/reports/balance-sheet.ts index 3831570..ad31528 100644 --- a/src/commands/reports/balance-sheet.ts +++ b/src/commands/reports/balance-sheet.ts @@ -50,8 +50,10 @@ export default class ReportsBalanceSheet extends BaseCommand { return } - this.log(`\n${report.reportName as string}`) - this.log('') + if (this.getOutputFormat(flags) === 'table') { + this.log(`\n${report.reportName as string}`) + this.log('') + } const rows = this.extractReportRows(report) this.outputFormatted( diff --git a/src/commands/reports/profit-and-loss.ts b/src/commands/reports/profit-and-loss.ts index d6cb477..6d1a9ed 100644 --- a/src/commands/reports/profit-and-loss.ts +++ b/src/commands/reports/profit-and-loss.ts @@ -52,8 +52,10 @@ export default class ReportsProfitAndLoss extends BaseCommand { return } - this.log(`\n${report.reportName as string}`) - this.log('') + if (this.getOutputFormat(flags) === 'table') { + this.log(`\n${report.reportName as string}`) + this.log('') + } const rows = this.extractReportRows(report) this.outputFormatted( diff --git a/src/commands/reports/trial-balance.ts b/src/commands/reports/trial-balance.ts index 689b3df..e0e84d9 100644 --- a/src/commands/reports/trial-balance.ts +++ b/src/commands/reports/trial-balance.ts @@ -40,9 +40,11 @@ export default class ReportsTrialBalance extends BaseCommand { return } - this.log(`\n${report.reportName as string}`) - this.log(`${(report.reportDate as string) ?? ''}`) - this.log('') + if (this.getOutputFormat(flags) === 'table') { + this.log(`\n${report.reportName as string}`) + this.log(`${(report.reportDate as string) ?? ''}`) + this.log('') + } const rows = this.extractReportRows(report) this.outputFormatted( diff --git a/test/commands/report-output.test.ts b/test/commands/report-output.test.ts index 3593a03..3b9388a 100644 --- a/test/commands/report-output.test.ts +++ b/test/commands/report-output.test.ts @@ -1,4 +1,5 @@ import {describe, expect, it, vi} from 'vitest' +import {encode} from '@toon-format/toon' import ReportsAgedPayables from '../../src/commands/reports/aged-payables.js' import ReportsAgedReceivables from '../../src/commands/reports/aged-receivables.js' import ReportsBalanceSheet from '../../src/commands/reports/balance-sheet.js' @@ -37,3 +38,60 @@ describe('report output formats', () => { expect(outputFormatted.mock.calls[0]?.[2]).toEqual(flags) }) }) + +const toonReport = { + reportName: 'Report', + reportDate: '31 January 2025', + rows: [ + { + rows: [ + { + cells: [ + {value: 'Cell 1'}, + {value: 'Cell 2'}, + {value: 'Cell 3'}, + {value: 'Cell 4'}, + {value: 'Cell 5'}, + ], + }, + ], + }, + ], +} + +const toonReportCommands: Array<[string, ReportCommand, Record, Record[]]> = [ + ['aged payables', ReportsAgedPayables, {'contact-id': 'contact-id', toon: true}, [ + {date: 'Cell 1', reference: 'Cell 2', due: 'Cell 3', paid: 'Cell 4', credited: 'Cell 5'}, + ]], + ['aged receivables', ReportsAgedReceivables, {'contact-id': 'contact-id', toon: true}, [ + {date: 'Cell 1', reference: 'Cell 2', due: 'Cell 3', paid: 'Cell 4', credited: 'Cell 5'}, + ]], + ['balance sheet', ReportsBalanceSheet, {toon: true}, [{account: 'Cell 1', amount: 'Cell 2'}]], + ['profit and loss', ReportsProfitAndLoss, {toon: true}, [{account: 'Cell 1', amount: 'Cell 2'}]], + ['trial balance', ReportsTrialBalance, {toon: true}, [ + {account: 'Cell 1', debit: 'Cell 2', credit: 'Cell 3'}, + ]], +] + +describe('report machine-readable output', () => { + it.each(toonReportCommands)( + '%s emits only the TOON document on stdout', + async (_name, Command, flags, expectedRows) => { + const command = new Command([], {}) as any + vi.spyOn(command, 'parse').mockResolvedValue({flags}) + vi.spyOn(command, 'xeroCall').mockImplementation(async (_flags: unknown, operation: Function) => + operation({ + accountingApi: new Proxy({}, { + get: () => vi.fn().mockResolvedValue({body: {reports: [toonReport]}}), + }), + }, 'tenant-id'), + ) + const log = vi.spyOn(command, 'log').mockImplementation(() => undefined) + + await command.run() + + expect(log).toHaveBeenCalledTimes(1) + expect(log.mock.calls[0]?.[0]).toBe(encode(expectedRows)) + }, + ) +})