jaz-cli 2.7.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/assets/skills/api/SKILL.md +1 -1
  2. package/assets/skills/conversion/SKILL.md +1 -1
  3. package/assets/skills/jobs/SKILL.md +104 -0
  4. package/assets/skills/jobs/references/audit-prep.md +319 -0
  5. package/assets/skills/jobs/references/bank-recon.md +234 -0
  6. package/assets/skills/jobs/references/building-blocks.md +135 -0
  7. package/assets/skills/jobs/references/credit-control.md +273 -0
  8. package/assets/skills/jobs/references/fa-review.md +267 -0
  9. package/assets/skills/jobs/references/gst-vat-filing.md +250 -0
  10. package/assets/skills/jobs/references/month-end-close.md +308 -0
  11. package/assets/skills/jobs/references/payment-run.md +246 -0
  12. package/assets/skills/jobs/references/quarter-end-close.md +268 -0
  13. package/assets/skills/jobs/references/supplier-recon.md +330 -0
  14. package/assets/skills/jobs/references/year-end-close.md +341 -0
  15. package/assets/skills/transaction-recipes/SKILL.md +1 -1
  16. package/dist/__tests__/jobs-audit-prep.test.js +125 -0
  17. package/dist/__tests__/jobs-bank-recon.test.js +108 -0
  18. package/dist/__tests__/jobs-credit-control.test.js +98 -0
  19. package/dist/__tests__/jobs-fa-review.test.js +104 -0
  20. package/dist/__tests__/jobs-gst-vat.test.js +113 -0
  21. package/dist/__tests__/jobs-month-end.test.js +162 -0
  22. package/dist/__tests__/jobs-payment-run.test.js +106 -0
  23. package/dist/__tests__/jobs-quarter-end.test.js +155 -0
  24. package/dist/__tests__/jobs-supplier-recon.test.js +115 -0
  25. package/dist/__tests__/jobs-validate.test.js +181 -0
  26. package/dist/__tests__/jobs-year-end.test.js +149 -0
  27. package/dist/commands/jobs.js +184 -0
  28. package/dist/index.js +2 -0
  29. package/dist/jobs/audit-prep.js +211 -0
  30. package/dist/jobs/bank-recon.js +163 -0
  31. package/dist/jobs/credit-control.js +126 -0
  32. package/dist/jobs/fa-review.js +121 -0
  33. package/dist/jobs/format.js +102 -0
  34. package/dist/jobs/gst-vat.js +187 -0
  35. package/dist/jobs/month-end.js +232 -0
  36. package/dist/jobs/payment-run.js +199 -0
  37. package/dist/jobs/quarter-end.js +135 -0
  38. package/dist/jobs/supplier-recon.js +132 -0
  39. package/dist/jobs/types.js +36 -0
  40. package/dist/jobs/validate.js +115 -0
  41. package/dist/jobs/year-end.js +153 -0
  42. package/dist/types/index.js +2 -1
  43. package/package.json +1 -1
@@ -0,0 +1,181 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { parseMonthPeriod, parseQuarterPeriod, parseYearPeriod, validateDateString, validateOverdueDays, JobValidationError, } from '../jobs/validate.js';
3
+ describe('parseMonthPeriod', () => {
4
+ it('parses "2025-01" → Jan 2025 with correct dates', () => {
5
+ const r = parseMonthPeriod('2025-01');
6
+ expect(r.type).toBe('month');
7
+ expect(r.year).toBe(2025);
8
+ expect(r.month).toBe(1);
9
+ expect(r.startDate).toBe('2025-01-01');
10
+ expect(r.endDate).toBe('2025-01-31');
11
+ expect(r.label).toBe('Jan 2025');
12
+ });
13
+ it('parses "2025-12" → Dec 2025 with correct dates', () => {
14
+ const r = parseMonthPeriod('2025-12');
15
+ expect(r.type).toBe('month');
16
+ expect(r.year).toBe(2025);
17
+ expect(r.month).toBe(12);
18
+ expect(r.startDate).toBe('2025-12-01');
19
+ expect(r.endDate).toBe('2025-12-31');
20
+ expect(r.label).toBe('Dec 2025');
21
+ });
22
+ it('handles February in a leap year', () => {
23
+ const r = parseMonthPeriod('2024-02');
24
+ expect(r.endDate).toBe('2024-02-29');
25
+ });
26
+ it('handles February in a non-leap year', () => {
27
+ const r = parseMonthPeriod('2025-02');
28
+ expect(r.endDate).toBe('2025-02-28');
29
+ });
30
+ it('rejects month 13 with JobValidationError', () => {
31
+ expect(() => parseMonthPeriod('2025-13')).toThrow(JobValidationError);
32
+ });
33
+ it('rejects non-date string "abc"', () => {
34
+ expect(() => parseMonthPeriod('abc')).toThrow(JobValidationError);
35
+ });
36
+ it('rejects single-digit month "2025-1"', () => {
37
+ expect(() => parseMonthPeriod('2025-1')).toThrow(JobValidationError);
38
+ });
39
+ it('rejects month 00', () => {
40
+ expect(() => parseMonthPeriod('2025-00')).toThrow(JobValidationError);
41
+ });
42
+ });
43
+ describe('parseQuarterPeriod', () => {
44
+ it('parses "2025-Q1" → Jan-Mar 2025', () => {
45
+ const r = parseQuarterPeriod('2025-Q1');
46
+ expect(r.type).toBe('quarter');
47
+ expect(r.year).toBe(2025);
48
+ expect(r.quarter).toBe(1);
49
+ expect(r.startDate).toBe('2025-01-01');
50
+ expect(r.endDate).toBe('2025-03-31');
51
+ expect(r.label).toBe('Q1 2025');
52
+ expect(r.months).toHaveLength(3);
53
+ expect(r.months[0].month).toBe(1);
54
+ expect(r.months[1].month).toBe(2);
55
+ expect(r.months[2].month).toBe(3);
56
+ });
57
+ it('parses "2025-Q4" → Oct-Dec 2025', () => {
58
+ const r = parseQuarterPeriod('2025-Q4');
59
+ expect(r.type).toBe('quarter');
60
+ expect(r.year).toBe(2025);
61
+ expect(r.quarter).toBe(4);
62
+ expect(r.startDate).toBe('2025-10-01');
63
+ expect(r.endDate).toBe('2025-12-31');
64
+ expect(r.label).toBe('Q4 2025');
65
+ expect(r.months).toHaveLength(3);
66
+ expect(r.months[0].month).toBe(10);
67
+ expect(r.months[1].month).toBe(11);
68
+ expect(r.months[2].month).toBe(12);
69
+ });
70
+ it('parses "2025-Q2" → Apr-Jun 2025', () => {
71
+ const r = parseQuarterPeriod('2025-Q2');
72
+ expect(r.startDate).toBe('2025-04-01');
73
+ expect(r.endDate).toBe('2025-06-30');
74
+ });
75
+ it('is case-insensitive for the Q prefix', () => {
76
+ const r = parseQuarterPeriod('2025-q3');
77
+ expect(r.quarter).toBe(3);
78
+ expect(r.startDate).toBe('2025-07-01');
79
+ });
80
+ it('rejects Q5 with JobValidationError', () => {
81
+ expect(() => parseQuarterPeriod('2025-Q5')).toThrow(JobValidationError);
82
+ });
83
+ it('rejects Q0 with JobValidationError', () => {
84
+ expect(() => parseQuarterPeriod('2025-q0')).toThrow(JobValidationError);
85
+ });
86
+ it('rejects invalid format "2025Q1"', () => {
87
+ expect(() => parseQuarterPeriod('2025Q1')).toThrow(JobValidationError);
88
+ });
89
+ it('each month in the quarter has correct start and end dates', () => {
90
+ const r = parseQuarterPeriod('2025-Q1');
91
+ expect(r.months[0].startDate).toBe('2025-01-01');
92
+ expect(r.months[0].endDate).toBe('2025-01-31');
93
+ expect(r.months[1].startDate).toBe('2025-02-01');
94
+ expect(r.months[1].endDate).toBe('2025-02-28');
95
+ expect(r.months[2].startDate).toBe('2025-03-01');
96
+ expect(r.months[2].endDate).toBe('2025-03-31');
97
+ });
98
+ });
99
+ describe('parseYearPeriod', () => {
100
+ it('parses "2025" → full year Jan-Dec with 4 quarters', () => {
101
+ const r = parseYearPeriod('2025');
102
+ expect(r.type).toBe('year');
103
+ expect(r.year).toBe(2025);
104
+ expect(r.startDate).toBe('2025-01-01');
105
+ expect(r.endDate).toBe('2025-12-31');
106
+ expect(r.label).toBe('FY2025');
107
+ expect(r.quarters).toHaveLength(4);
108
+ });
109
+ it('quarters span the correct months', () => {
110
+ const r = parseYearPeriod('2025');
111
+ expect(r.quarters[0].startDate).toBe('2025-01-01');
112
+ expect(r.quarters[0].endDate).toBe('2025-03-31');
113
+ expect(r.quarters[1].startDate).toBe('2025-04-01');
114
+ expect(r.quarters[1].endDate).toBe('2025-06-30');
115
+ expect(r.quarters[2].startDate).toBe('2025-07-01');
116
+ expect(r.quarters[2].endDate).toBe('2025-09-30');
117
+ expect(r.quarters[3].startDate).toBe('2025-10-01');
118
+ expect(r.quarters[3].endDate).toBe('2025-12-31');
119
+ });
120
+ it('each quarter has 3 months', () => {
121
+ const r = parseYearPeriod('2025');
122
+ for (const q of r.quarters) {
123
+ expect(q.months).toHaveLength(3);
124
+ }
125
+ });
126
+ it('rejects non-numeric string "abcd"', () => {
127
+ expect(() => parseYearPeriod('abcd')).toThrow(JobValidationError);
128
+ });
129
+ it('rejects partial year "25"', () => {
130
+ expect(() => parseYearPeriod('25')).toThrow(JobValidationError);
131
+ });
132
+ it('rejects YYYY-MM format "2025-01"', () => {
133
+ expect(() => parseYearPeriod('2025-01')).toThrow(JobValidationError);
134
+ });
135
+ });
136
+ describe('validateDateString', () => {
137
+ it('accepts a valid date "2025-06-15"', () => {
138
+ expect(() => validateDateString('2025-06-15', 'testDate')).not.toThrow();
139
+ });
140
+ it('accepts undefined (optional field)', () => {
141
+ expect(() => validateDateString(undefined, 'testDate')).not.toThrow();
142
+ });
143
+ it('rejects "not-a-date"', () => {
144
+ expect(() => validateDateString('not-a-date', 'testDate')).toThrow(JobValidationError);
145
+ });
146
+ it('rejects invalid month "2025-13-01"', () => {
147
+ expect(() => validateDateString('2025-13-01', 'testDate')).toThrow(JobValidationError);
148
+ });
149
+ it('rejects impossible calendar date "2025-02-31"', () => {
150
+ expect(() => validateDateString('2025-02-31', 'testDate')).toThrow(JobValidationError);
151
+ });
152
+ it('rejects impossible calendar date "2025-06-31"', () => {
153
+ expect(() => validateDateString('2025-06-31', 'testDate')).toThrow(JobValidationError);
154
+ });
155
+ it('rejects wrong format "06/15/2025"', () => {
156
+ expect(() => validateDateString('06/15/2025', 'testDate')).toThrow(JobValidationError);
157
+ });
158
+ it('error message includes the field name', () => {
159
+ expect(() => validateDateString('bad', 'myField')).toThrow(/myField/);
160
+ });
161
+ });
162
+ describe('validateOverdueDays', () => {
163
+ it('accepts a valid positive integer 30', () => {
164
+ expect(() => validateOverdueDays(30)).not.toThrow();
165
+ });
166
+ it('accepts 1 (minimum valid)', () => {
167
+ expect(() => validateOverdueDays(1)).not.toThrow();
168
+ });
169
+ it('rejects 0', () => {
170
+ expect(() => validateOverdueDays(0)).toThrow(JobValidationError);
171
+ });
172
+ it('rejects negative values', () => {
173
+ expect(() => validateOverdueDays(-1)).toThrow(JobValidationError);
174
+ });
175
+ it('rejects non-integer 1.5', () => {
176
+ expect(() => validateOverdueDays(1.5)).toThrow(JobValidationError);
177
+ });
178
+ it('rejects non-integer 0.5', () => {
179
+ expect(() => validateOverdueDays(0.5)).toThrow(JobValidationError);
180
+ });
181
+ });
@@ -0,0 +1,149 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { generateYearEndBlueprint } from '../jobs/year-end.js';
3
+ import { JobValidationError } from '../jobs/validate.js';
4
+ describe('generateYearEndBlueprint', () => {
5
+ // Standalone mode (default)
6
+ describe('standalone mode', () => {
7
+ const base = { period: '2025' };
8
+ it('includes all quarterly phases plus annual extras and verification', () => {
9
+ const r = generateYearEndBlueprint(base);
10
+ // 4 quarters x 17 phases each (15 month-end + 2 quarterly) = 68
11
+ // + Phase 8 (annual extras) + Phase 9 (annual verification) = 70
12
+ expect(r.phases.length).toBeGreaterThan(0);
13
+ // Last two phases are annual extras and verification
14
+ const last = r.phases[r.phases.length - 1];
15
+ const secondLast = r.phases[r.phases.length - 2];
16
+ expect(secondLast.name).toBe('Phase 8: Annual Extras');
17
+ expect(last.name).toBe('Phase 9: Annual Verification');
18
+ });
19
+ it('has correct jobType', () => {
20
+ const r = generateYearEndBlueprint(base);
21
+ expect(r.jobType).toBe('year-end-close');
22
+ });
23
+ it('mode is standalone', () => {
24
+ const r = generateYearEndBlueprint(base);
25
+ expect(r.mode).toBe('standalone');
26
+ });
27
+ it('period label is "FY2025"', () => {
28
+ const r = generateYearEndBlueprint(base);
29
+ expect(r.period).toBe('FY2025');
30
+ });
31
+ it('currency defaults to "SGD"', () => {
32
+ const r = generateYearEndBlueprint(base);
33
+ expect(r.currency).toBe('SGD');
34
+ });
35
+ it('phase names are prefixed with quarter labels', () => {
36
+ const r = generateYearEndBlueprint(base);
37
+ // First batch of phases should contain Q1 prefix
38
+ expect(r.phases[0].name).toContain('Q1 2025');
39
+ });
40
+ it('annual extras has 7 steps', () => {
41
+ const r = generateYearEndBlueprint(base);
42
+ const extras = r.phases[r.phases.length - 2];
43
+ expect(extras.name).toBe('Phase 8: Annual Extras');
44
+ expect(extras.steps).toHaveLength(7);
45
+ });
46
+ it('annual verification has 3 steps', () => {
47
+ const r = generateYearEndBlueprint(base);
48
+ const verification = r.phases[r.phases.length - 1];
49
+ expect(verification.name).toBe('Phase 9: Annual Verification');
50
+ expect(verification.steps).toHaveLength(3);
51
+ });
52
+ it('annual extras ordering continues from last quarterly max order', () => {
53
+ const r = generateYearEndBlueprint(base);
54
+ // Each quarter ends with quarterly verification at order 26
55
+ // Annual extras starts at 27
56
+ const extras = r.phases[r.phases.length - 2];
57
+ expect(extras.steps[0].order).toBe(27);
58
+ });
59
+ it('annual verification follows annual extras ordering', () => {
60
+ const r = generateYearEndBlueprint(base);
61
+ const extras = r.phases[r.phases.length - 2];
62
+ const verification = r.phases[r.phases.length - 1];
63
+ const lastExtraOrder = extras.steps[extras.steps.length - 1].order;
64
+ expect(verification.steps[0].order).toBe(lastExtraOrder + 1);
65
+ });
66
+ it('summary totalSteps matches actual step count', () => {
67
+ const r = generateYearEndBlueprint(base);
68
+ const actual = r.phases.reduce((sum, p) => sum + p.steps.length, 0);
69
+ expect(r.summary.totalSteps).toBe(actual);
70
+ });
71
+ it('summary includes recipe references from monthly, quarterly, and annual steps', () => {
72
+ const r = generateYearEndBlueprint(base);
73
+ // Monthly recipe
74
+ expect(r.summary.recipeReferences).toContain('accrued-expenses');
75
+ // Quarterly recipe
76
+ expect(r.summary.recipeReferences).toContain('intercompany');
77
+ // Annual recipe
78
+ expect(r.summary.recipeReferences).toContain('dividend');
79
+ });
80
+ it('summary recipe references are sorted', () => {
81
+ const r = generateYearEndBlueprint(base);
82
+ const sorted = [...r.summary.recipeReferences].sort();
83
+ expect(r.summary.recipeReferences).toEqual(sorted);
84
+ });
85
+ it('annual extras API calls use full year dates', () => {
86
+ const r = generateYearEndBlueprint(base);
87
+ const extras = r.phases[r.phases.length - 2];
88
+ const gstStep = extras.steps.find(s => s.description.includes('GST'));
89
+ expect(gstStep).toBeDefined();
90
+ expect(gstStep.apiBody.startDate).toBe('2025-01-01');
91
+ expect(gstStep.apiBody.endDate).toBe('2025-12-31');
92
+ });
93
+ it('annual verification reports use full year dates', () => {
94
+ const r = generateYearEndBlueprint(base);
95
+ const verification = r.phases[r.phases.length - 1];
96
+ const trialBalance = verification.steps[0];
97
+ expect(trialBalance.apiBody.startDate).toBe('2025-01-01');
98
+ expect(trialBalance.apiBody.endDate).toBe('2025-12-31');
99
+ });
100
+ });
101
+ // Incremental mode
102
+ describe('incremental mode', () => {
103
+ const base = { period: '2025', incremental: true };
104
+ it('has only annual extras and verification (no quarter/month phases)', () => {
105
+ const r = generateYearEndBlueprint(base);
106
+ expect(r.phases).toHaveLength(2);
107
+ expect(r.phases[0].name).toBe('Phase 8: Annual Extras');
108
+ expect(r.phases[1].name).toBe('Phase 9: Annual Verification');
109
+ });
110
+ it('mode is incremental', () => {
111
+ const r = generateYearEndBlueprint(base);
112
+ expect(r.mode).toBe('incremental');
113
+ });
114
+ it('step ordering starts at 1 for extras', () => {
115
+ const r = generateYearEndBlueprint(base);
116
+ expect(r.phases[0].steps[0].order).toBe(1);
117
+ });
118
+ it('step ordering is continuous across extras and verification', () => {
119
+ const r = generateYearEndBlueprint(base);
120
+ const allOrders = r.phases.flatMap(p => p.steps.map(s => s.order));
121
+ for (let i = 0; i < allOrders.length; i++) {
122
+ expect(allOrders[i]).toBe(i + 1);
123
+ }
124
+ });
125
+ it('total steps = 7 extras + 3 verification = 10', () => {
126
+ const r = generateYearEndBlueprint(base);
127
+ expect(r.summary.totalSteps).toBe(10);
128
+ });
129
+ it('annual extras has 7 steps', () => {
130
+ const r = generateYearEndBlueprint(base);
131
+ expect(r.phases[0].steps).toHaveLength(7);
132
+ });
133
+ });
134
+ // Custom currency
135
+ it('custom currency "MYR" passes through', () => {
136
+ const r = generateYearEndBlueprint({ period: '2025', currency: 'MYR' });
137
+ expect(r.currency).toBe('MYR');
138
+ });
139
+ // Validation
140
+ it('invalid period throws JobValidationError', () => {
141
+ expect(() => generateYearEndBlueprint({ period: 'bad' })).toThrow(JobValidationError);
142
+ });
143
+ it('"2025-Q1" is not a valid year period', () => {
144
+ expect(() => generateYearEndBlueprint({ period: '2025-Q1' })).toThrow(JobValidationError);
145
+ });
146
+ it('"abcd" throws JobValidationError', () => {
147
+ expect(() => generateYearEndBlueprint({ period: 'abcd' })).toThrow(JobValidationError);
148
+ });
149
+ });
@@ -0,0 +1,184 @@
1
+ import chalk from 'chalk';
2
+ import { generateMonthEndBlueprint } from '../jobs/month-end.js';
3
+ import { generateQuarterEndBlueprint } from '../jobs/quarter-end.js';
4
+ import { generateYearEndBlueprint } from '../jobs/year-end.js';
5
+ import { generateBankReconBlueprint } from '../jobs/bank-recon.js';
6
+ import { generateGstVatBlueprint } from '../jobs/gst-vat.js';
7
+ import { generatePaymentRunBlueprint } from '../jobs/payment-run.js';
8
+ import { generateCreditControlBlueprint } from '../jobs/credit-control.js';
9
+ import { generateSupplierReconBlueprint } from '../jobs/supplier-recon.js';
10
+ import { generateAuditPrepBlueprint } from '../jobs/audit-prep.js';
11
+ import { generateFaReviewBlueprint } from '../jobs/fa-review.js';
12
+ import { printBlueprint, printBlueprintJson } from '../jobs/format.js';
13
+ import { JobValidationError } from '../jobs/validate.js';
14
+ /** Wrap job action with validation error handling. */
15
+ function jobAction(fn) {
16
+ return (opts) => {
17
+ try {
18
+ fn(opts);
19
+ }
20
+ catch (err) {
21
+ if (err instanceof JobValidationError) {
22
+ console.error(chalk.red(`Error: ${err.message}`));
23
+ process.exit(1);
24
+ }
25
+ throw err;
26
+ }
27
+ };
28
+ }
29
+ export function registerJobsCommand(program) {
30
+ const jobs = program
31
+ .command('jobs')
32
+ .description('Accounting job blueprints — month-end, quarter-end, year-end, bank-recon, gst-vat, payment-run, credit-control, supplier-recon, audit-prep, fa-review');
33
+ // ── jaz jobs month-end ──────────────────────────────────────────
34
+ jobs
35
+ .command('month-end')
36
+ .description('Month-end close blueprint (5 phases, 18 steps)')
37
+ .requiredOption('--period <YYYY-MM>', 'Month period (e.g., 2025-01)')
38
+ .option('--currency <code>', 'Currency code (e.g. SGD, USD)')
39
+ .option('--json', 'Output as JSON')
40
+ .action(jobAction((opts) => {
41
+ const bp = generateMonthEndBlueprint({
42
+ period: opts.period,
43
+ currency: opts.currency,
44
+ });
45
+ opts.json ? printBlueprintJson(bp) : printBlueprint(bp);
46
+ }));
47
+ // ── jaz jobs quarter-end ────────────────────────────────────────
48
+ jobs
49
+ .command('quarter-end')
50
+ .description('Quarter-end close blueprint (monthly × 3 + quarterly extras)')
51
+ .requiredOption('--period <YYYY-QN>', 'Quarter period (e.g., 2025-Q1)')
52
+ .option('--incremental', 'Generate only quarterly extras (assumes months already closed)')
53
+ .option('--currency <code>', 'Currency code (e.g. SGD, USD)')
54
+ .option('--json', 'Output as JSON')
55
+ .action(jobAction((opts) => {
56
+ const bp = generateQuarterEndBlueprint({
57
+ period: opts.period,
58
+ currency: opts.currency,
59
+ incremental: opts.incremental,
60
+ });
61
+ opts.json ? printBlueprintJson(bp) : printBlueprint(bp);
62
+ }));
63
+ // ── jaz jobs year-end ───────────────────────────────────────────
64
+ jobs
65
+ .command('year-end')
66
+ .description('Year-end close blueprint (quarterly × 4 + annual extras)')
67
+ .requiredOption('--period <YYYY>', 'Fiscal year (e.g., 2025)')
68
+ .option('--incremental', 'Generate only annual extras (assumes quarters already closed)')
69
+ .option('--currency <code>', 'Currency code (e.g. SGD, USD)')
70
+ .option('--json', 'Output as JSON')
71
+ .action(jobAction((opts) => {
72
+ const bp = generateYearEndBlueprint({
73
+ period: opts.period,
74
+ currency: opts.currency,
75
+ incremental: opts.incremental,
76
+ });
77
+ opts.json ? printBlueprintJson(bp) : printBlueprint(bp);
78
+ }));
79
+ // ── jaz jobs bank-recon ─────────────────────────────────────────
80
+ jobs
81
+ .command('bank-recon')
82
+ .description('Bank reconciliation catch-up blueprint')
83
+ .option('--account <name>', 'Specific bank account name')
84
+ .option('--period <YYYY-MM>', 'Month period to reconcile')
85
+ .option('--currency <code>', 'Currency code (e.g. SGD, USD)')
86
+ .option('--json', 'Output as JSON')
87
+ .action(jobAction((opts) => {
88
+ const bp = generateBankReconBlueprint({
89
+ account: opts.account,
90
+ period: opts.period,
91
+ currency: opts.currency,
92
+ });
93
+ opts.json ? printBlueprintJson(bp) : printBlueprint(bp);
94
+ }));
95
+ // ── jaz jobs gst-vat ────────────────────────────────────────────
96
+ jobs
97
+ .command('gst-vat')
98
+ .description('GST/VAT filing preparation blueprint')
99
+ .requiredOption('--period <YYYY-QN>', 'Quarter period (e.g., 2025-Q1)')
100
+ .option('--currency <code>', 'Currency code (e.g. SGD, USD)')
101
+ .option('--json', 'Output as JSON')
102
+ .action(jobAction((opts) => {
103
+ const bp = generateGstVatBlueprint({
104
+ period: opts.period,
105
+ currency: opts.currency,
106
+ });
107
+ opts.json ? printBlueprintJson(bp) : printBlueprint(bp);
108
+ }));
109
+ // ── jaz jobs payment-run ────────────────────────────────────────
110
+ jobs
111
+ .command('payment-run')
112
+ .description('Payment run blueprint (bulk bill payments)')
113
+ .option('--due-before <YYYY-MM-DD>', 'Pay bills due on or before this date')
114
+ .option('--currency <code>', 'Currency code (e.g. SGD, USD)')
115
+ .option('--json', 'Output as JSON')
116
+ .action(jobAction((opts) => {
117
+ const bp = generatePaymentRunBlueprint({
118
+ dueBefore: opts.dueBefore,
119
+ currency: opts.currency,
120
+ });
121
+ opts.json ? printBlueprintJson(bp) : printBlueprint(bp);
122
+ }));
123
+ // ── jaz jobs credit-control ─────────────────────────────────────
124
+ jobs
125
+ .command('credit-control')
126
+ .description('Credit control / AR chase blueprint')
127
+ .option('--overdue-days <days>', 'Minimum overdue days to include', (v) => {
128
+ const n = Number.parseInt(v, 10);
129
+ if (!Number.isFinite(n))
130
+ throw new JobValidationError(`overdue-days must be an integer (got "${v}")`);
131
+ return n;
132
+ })
133
+ .option('--currency <code>', 'Currency code (e.g. SGD, USD)')
134
+ .option('--json', 'Output as JSON')
135
+ .action(jobAction((opts) => {
136
+ const bp = generateCreditControlBlueprint({
137
+ overdueDays: opts.overdueDays,
138
+ currency: opts.currency,
139
+ });
140
+ opts.json ? printBlueprintJson(bp) : printBlueprint(bp);
141
+ }));
142
+ // ── jaz jobs supplier-recon ─────────────────────────────────────
143
+ jobs
144
+ .command('supplier-recon')
145
+ .description('Supplier statement reconciliation blueprint')
146
+ .option('--supplier <name>', 'Specific supplier name')
147
+ .option('--period <YYYY-MM>', 'Month period')
148
+ .option('--currency <code>', 'Currency code (e.g. SGD, USD)')
149
+ .option('--json', 'Output as JSON')
150
+ .action(jobAction((opts) => {
151
+ const bp = generateSupplierReconBlueprint({
152
+ supplier: opts.supplier,
153
+ period: opts.period,
154
+ currency: opts.currency,
155
+ });
156
+ opts.json ? printBlueprintJson(bp) : printBlueprint(bp);
157
+ }));
158
+ // ── jaz jobs audit-prep ─────────────────────────────────────────
159
+ jobs
160
+ .command('audit-prep')
161
+ .description('Audit preparation pack blueprint')
162
+ .requiredOption('--period <YYYY>', 'Fiscal year (e.g., 2025)')
163
+ .option('--currency <code>', 'Currency code (e.g. SGD, USD)')
164
+ .option('--json', 'Output as JSON')
165
+ .action(jobAction((opts) => {
166
+ const bp = generateAuditPrepBlueprint({
167
+ period: opts.period,
168
+ currency: opts.currency,
169
+ });
170
+ opts.json ? printBlueprintJson(bp) : printBlueprint(bp);
171
+ }));
172
+ // ── jaz jobs fa-review ──────────────────────────────────────────
173
+ jobs
174
+ .command('fa-review')
175
+ .description('Fixed asset register review blueprint')
176
+ .option('--currency <code>', 'Currency code (e.g. SGD, USD)')
177
+ .option('--json', 'Output as JSON')
178
+ .action(jobAction((opts) => {
179
+ const bp = generateFaReviewBlueprint({
180
+ currency: opts.currency,
181
+ });
182
+ opts.json ? printBlueprintJson(bp) : printBlueprint(bp);
183
+ }));
184
+ }
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ import { initCommand } from './commands/init.js';
7
7
  import { versionsCommand } from './commands/versions.js';
8
8
  import { updateCommand } from './commands/update.js';
9
9
  import { registerCalcCommand } from './commands/calc.js';
10
+ import { registerJobsCommand } from './commands/jobs.js';
10
11
  import { SKILL_TYPES } from './types/index.js';
11
12
  const __filename = fileURLToPath(import.meta.url);
12
13
  const __dirname = dirname(__filename);
@@ -51,4 +52,5 @@ program
51
52
  });
52
53
  });
53
54
  registerCalcCommand(program);
55
+ registerJobsCommand(program);
54
56
  program.parse();