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,125 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { generateAuditPrepBlueprint } from '../jobs/audit-prep.js';
3
+ import { JobValidationError } from '../jobs/validate.js';
4
+ describe('generateAuditPrepBlueprint (basic)', () => {
5
+ it('produces a valid JobBlueprint with correct jobType', () => {
6
+ const bp = generateAuditPrepBlueprint({ period: '2025' });
7
+ expect(bp.jobType).toBe('audit-prep');
8
+ });
9
+ it('has 4 phases and 14 total steps', () => {
10
+ const bp = generateAuditPrepBlueprint({ period: '2025' });
11
+ expect(bp.phases).toHaveLength(4);
12
+ expect(bp.summary.totalSteps).toBe(14);
13
+ });
14
+ it('phase names are correct', () => {
15
+ const bp = generateAuditPrepBlueprint({ period: '2025' });
16
+ expect(bp.phases.map(p => p.name)).toEqual([
17
+ 'Financial Statements',
18
+ 'Supporting Schedules',
19
+ 'Reconciliations',
20
+ 'Export & Compile Audit Pack',
21
+ ]);
22
+ });
23
+ it('step categories match expectations', () => {
24
+ const bp = generateAuditPrepBlueprint({ period: '2025' });
25
+ const categories = bp.phases.flatMap(p => p.steps.map(s => s.category));
26
+ expect(categories).toEqual([
27
+ 'report', // 1 — trial balance
28
+ 'report', // 2 — balance sheet
29
+ 'report', // 3 — P&L
30
+ 'report', // 4 — cash flow
31
+ 'report', // 5 — equity movement
32
+ 'report', // 6 — AR aging
33
+ 'report', // 7 — AP aging
34
+ 'report', // 8 — fixed assets
35
+ 'report', // 9 — tax ledger
36
+ 'verify', // 10 — bank reconciliation
37
+ 'verify', // 11 — intercompany
38
+ 'verify', // 12 — loan schedule
39
+ 'export', // 13 — export data
40
+ 'report', // 14 — compile audit pack
41
+ ]);
42
+ });
43
+ it('summary has non-empty apiCalls', () => {
44
+ const bp = generateAuditPrepBlueprint({ period: '2025' });
45
+ expect(bp.summary.apiCalls.length).toBeGreaterThan(0);
46
+ });
47
+ it('mode is standalone', () => {
48
+ const bp = generateAuditPrepBlueprint({ period: '2025' });
49
+ expect(bp.mode).toBe('standalone');
50
+ });
51
+ });
52
+ describe('generateAuditPrepBlueprint (defaults)', () => {
53
+ it('default currency is SGD', () => {
54
+ const bp = generateAuditPrepBlueprint({ period: '2025' });
55
+ expect(bp.currency).toBe('SGD');
56
+ });
57
+ });
58
+ describe('generateAuditPrepBlueprint (options)', () => {
59
+ it('custom currency works', () => {
60
+ const bp = generateAuditPrepBlueprint({ period: '2025', currency: 'PHP' });
61
+ expect(bp.currency).toBe('PHP');
62
+ });
63
+ it('full year period produces FY label', () => {
64
+ const bp = generateAuditPrepBlueprint({ period: '2025' });
65
+ expect(bp.period).toBe('FY2025');
66
+ });
67
+ it('full year period uses Jan 1 to Dec 31', () => {
68
+ const bp = generateAuditPrepBlueprint({ period: '2025' });
69
+ const step1 = bp.phases[0].steps[0];
70
+ const body = step1.apiBody;
71
+ expect(body.startDate).toBe('2025-01-01');
72
+ expect(body.endDate).toBe('2025-12-31');
73
+ });
74
+ it('quarter period produces Q label', () => {
75
+ const bp = generateAuditPrepBlueprint({ period: '2025-Q3' });
76
+ expect(bp.period).toBe('Q3 2025');
77
+ });
78
+ it('quarter period uses correct date range', () => {
79
+ const bp = generateAuditPrepBlueprint({ period: '2025-Q3' });
80
+ const step1 = bp.phases[0].steps[0];
81
+ const body = step1.apiBody;
82
+ expect(body.startDate).toBe('2025-07-01');
83
+ expect(body.endDate).toBe('2025-09-30');
84
+ });
85
+ });
86
+ describe('generateAuditPrepBlueprint (validation)', () => {
87
+ it('invalid period throws JobValidationError', () => {
88
+ expect(() => generateAuditPrepBlueprint({ period: '2025-06' }))
89
+ .toThrow(JobValidationError);
90
+ });
91
+ it('invalid quarter throws JobValidationError', () => {
92
+ expect(() => generateAuditPrepBlueprint({ period: '2025-Q5' }))
93
+ .toThrow(JobValidationError);
94
+ });
95
+ it('malformed period throws JobValidationError', () => {
96
+ expect(() => generateAuditPrepBlueprint({ period: 'FY2025' }))
97
+ .toThrow(JobValidationError);
98
+ });
99
+ it('empty period throws JobValidationError', () => {
100
+ expect(() => generateAuditPrepBlueprint({ period: '' }))
101
+ .toThrow(JobValidationError);
102
+ });
103
+ });
104
+ describe('generateAuditPrepBlueprint (summary)', () => {
105
+ it('recipeReferences include bank-reconciliation', () => {
106
+ const bp = generateAuditPrepBlueprint({ period: '2025' });
107
+ expect(bp.summary.recipeReferences).toContain('bank-reconciliation');
108
+ });
109
+ it('calcReferences include jaz calc loan', () => {
110
+ const bp = generateAuditPrepBlueprint({ period: '2025' });
111
+ expect(bp.summary.calcReferences).toContain('jaz calc loan');
112
+ });
113
+ it('apiCalls include key report endpoints', () => {
114
+ const bp = generateAuditPrepBlueprint({ period: '2025' });
115
+ expect(bp.summary.apiCalls).toContain('POST /generate-reports/trial-balance');
116
+ expect(bp.summary.apiCalls).toContain('POST /generate-reports/balance-sheet');
117
+ expect(bp.summary.apiCalls).toContain('POST /generate-reports/profit-and-loss');
118
+ expect(bp.summary.apiCalls).toContain('POST /generate-reports/cashflow');
119
+ expect(bp.summary.apiCalls).toContain('POST /generate-reports/equity-movement');
120
+ expect(bp.summary.apiCalls).toContain('POST /generate-reports/ar-aging');
121
+ expect(bp.summary.apiCalls).toContain('POST /generate-reports/ap-aging');
122
+ expect(bp.summary.apiCalls).toContain('POST /generate-reports/fixed-assets');
123
+ expect(bp.summary.apiCalls).toContain('POST /generate-reports/vat-ledger');
124
+ });
125
+ });
@@ -0,0 +1,108 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { generateBankReconBlueprint } from '../jobs/bank-recon.js';
3
+ describe('generateBankReconBlueprint (basic)', () => {
4
+ it('produces a valid JobBlueprint with correct jobType', () => {
5
+ const bp = generateBankReconBlueprint();
6
+ expect(bp.jobType).toBe('bank-recon');
7
+ });
8
+ it('has 4 phases and 10 total steps', () => {
9
+ const bp = generateBankReconBlueprint();
10
+ expect(bp.phases).toHaveLength(4);
11
+ expect(bp.summary.totalSteps).toBe(10);
12
+ });
13
+ it('phase names are correct', () => {
14
+ const bp = generateBankReconBlueprint();
15
+ expect(bp.phases.map(p => p.name)).toEqual([
16
+ 'Identify Bank Accounts',
17
+ 'Pull Unreconciled Records',
18
+ 'Resolve Unreconciled Items',
19
+ 'Verification',
20
+ ]);
21
+ });
22
+ it('step categories match expectations', () => {
23
+ const bp = generateBankReconBlueprint();
24
+ const categories = bp.phases.flatMap(p => p.steps.map(s => s.category));
25
+ expect(categories).toEqual([
26
+ 'verify', // 1 — list bank accounts
27
+ 'verify', // 2 — search unreconciled
28
+ 'verify', // 3 — check duplicates
29
+ 'resolve', // 4 — match bank records
30
+ 'resolve', // 5 — create transactions (import)
31
+ 'resolve', // 6 — create cash journals
32
+ 'review', // 7 — flag remaining items
33
+ 'verify', // 8 — re-check unreconciled count
34
+ 'verify', // 9 — review bank balance
35
+ 'report', // 10 — generate reconciliation report
36
+ ]);
37
+ });
38
+ it('summary has non-empty apiCalls', () => {
39
+ const bp = generateBankReconBlueprint();
40
+ expect(bp.summary.apiCalls.length).toBeGreaterThan(0);
41
+ });
42
+ it('mode is standalone', () => {
43
+ const bp = generateBankReconBlueprint();
44
+ expect(bp.mode).toBe('standalone');
45
+ });
46
+ });
47
+ describe('generateBankReconBlueprint (defaults)', () => {
48
+ it('default currency is SGD', () => {
49
+ const bp = generateBankReconBlueprint();
50
+ expect(bp.currency).toBe('SGD');
51
+ });
52
+ it('default period is current', () => {
53
+ const bp = generateBankReconBlueprint();
54
+ expect(bp.period).toBe('current');
55
+ });
56
+ });
57
+ describe('generateBankReconBlueprint (options)', () => {
58
+ it('custom currency works', () => {
59
+ const bp = generateBankReconBlueprint({ currency: 'PHP' });
60
+ expect(bp.currency).toBe('PHP');
61
+ });
62
+ it('account filter is applied', () => {
63
+ const bp = generateBankReconBlueprint({ account: 'DBS Current' });
64
+ const step1 = bp.phases[0].steps[0];
65
+ expect(step1.apiBody).toBeDefined();
66
+ expect(step1.apiBody.filters).toHaveProperty('accountName', 'DBS Current');
67
+ expect(step1.notes).toContain('DBS Current');
68
+ });
69
+ it('account filter absent by default', () => {
70
+ const bp = generateBankReconBlueprint();
71
+ const step1 = bp.phases[0].steps[0];
72
+ const filters = step1.apiBody.filters;
73
+ expect(filters).not.toHaveProperty('accountName');
74
+ });
75
+ it('period filter is applied to unreconciled search with correct end-of-month', () => {
76
+ const bp = generateBankReconBlueprint({ period: '2025-06' });
77
+ expect(bp.period).toBe('2025-06');
78
+ const step2 = bp.phases[1].steps[0];
79
+ const filters = step2.apiBody.filters;
80
+ expect(filters).toHaveProperty('valueDateFrom', '2025-06-01');
81
+ expect(filters).toHaveProperty('valueDateTo', '2025-06-30');
82
+ });
83
+ it('period filter handles February correctly', () => {
84
+ const bp = generateBankReconBlueprint({ period: '2025-02' });
85
+ const step2 = bp.phases[1].steps[0];
86
+ const filters = step2.apiBody.filters;
87
+ expect(filters).toHaveProperty('valueDateTo', '2025-02-28');
88
+ });
89
+ it('period filter handles leap year February', () => {
90
+ const bp = generateBankReconBlueprint({ period: '2024-02' });
91
+ const step2 = bp.phases[1].steps[0];
92
+ const filters = step2.apiBody.filters;
93
+ expect(filters).toHaveProperty('valueDateTo', '2024-02-29');
94
+ });
95
+ });
96
+ describe('generateBankReconBlueprint (summary)', () => {
97
+ it('recipeReferences include bank-reconciliation and cash-receipt', () => {
98
+ const bp = generateBankReconBlueprint();
99
+ expect(bp.summary.recipeReferences).toContain('bank-reconciliation');
100
+ expect(bp.summary.recipeReferences).toContain('cash-receipt');
101
+ });
102
+ it('apiCalls include key endpoints', () => {
103
+ const bp = generateBankReconBlueprint();
104
+ expect(bp.summary.apiCalls).toContain('POST /chart-of-accounts/search');
105
+ expect(bp.summary.apiCalls).toContain('POST /bank-records/search');
106
+ expect(bp.summary.apiCalls).toContain('POST /journals');
107
+ });
108
+ });
@@ -0,0 +1,98 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { generateCreditControlBlueprint } from '../jobs/credit-control.js';
3
+ import { JobValidationError } from '../jobs/validate.js';
4
+ describe('generateCreditControlBlueprint (basic)', () => {
5
+ it('produces a valid JobBlueprint with correct jobType', () => {
6
+ const bp = generateCreditControlBlueprint();
7
+ expect(bp.jobType).toBe('credit-control');
8
+ });
9
+ it('has 4 phases and 7 total steps', () => {
10
+ const bp = generateCreditControlBlueprint();
11
+ expect(bp.phases).toHaveLength(4);
12
+ expect(bp.summary.totalSteps).toBe(7);
13
+ });
14
+ it('phase names are correct', () => {
15
+ const bp = generateCreditControlBlueprint();
16
+ expect(bp.phases.map(p => p.name)).toEqual([
17
+ 'AR Analysis',
18
+ 'Chase List',
19
+ 'Bad Debt Assessment',
20
+ 'Verification',
21
+ ]);
22
+ });
23
+ it('step categories match expectations', () => {
24
+ const bp = generateCreditControlBlueprint();
25
+ const categories = bp.phases.flatMap(p => p.steps.map(s => s.category));
26
+ expect(categories).toEqual([
27
+ 'report', // 1 — AR aging report
28
+ 'verify', // 2 — identify overdue invoices
29
+ 'review', // 3 — group by customer priority
30
+ 'report', // 4 — follow-up action list
31
+ 'review', // 5 — identify doubtful debts
32
+ 'value', // 6 — calculate ECL provision
33
+ 'verify', // 7 — review AR aging after actions
34
+ ]);
35
+ });
36
+ it('summary has non-empty apiCalls', () => {
37
+ const bp = generateCreditControlBlueprint();
38
+ expect(bp.summary.apiCalls.length).toBeGreaterThan(0);
39
+ });
40
+ it('mode is standalone', () => {
41
+ const bp = generateCreditControlBlueprint();
42
+ expect(bp.mode).toBe('standalone');
43
+ });
44
+ });
45
+ describe('generateCreditControlBlueprint (defaults)', () => {
46
+ it('default currency is SGD', () => {
47
+ const bp = generateCreditControlBlueprint();
48
+ expect(bp.currency).toBe('SGD');
49
+ });
50
+ it('default overdueDays is 30', () => {
51
+ const bp = generateCreditControlBlueprint();
52
+ expect(bp.period).toBe('Overdue > 30 days');
53
+ });
54
+ });
55
+ describe('generateCreditControlBlueprint (options)', () => {
56
+ it('custom currency works', () => {
57
+ const bp = generateCreditControlBlueprint({ currency: 'PHP' });
58
+ expect(bp.currency).toBe('PHP');
59
+ });
60
+ it('custom overdueDays works', () => {
61
+ const bp = generateCreditControlBlueprint({ overdueDays: 60 });
62
+ expect(bp.period).toBe('Overdue > 60 days');
63
+ });
64
+ it('overdueDays is reflected in step description', () => {
65
+ const bp = generateCreditControlBlueprint({ overdueDays: 90 });
66
+ const step2 = bp.phases[0].steps[1];
67
+ expect(step2.description).toContain('90');
68
+ });
69
+ });
70
+ describe('generateCreditControlBlueprint (validation)', () => {
71
+ it('zero overdueDays throws JobValidationError', () => {
72
+ expect(() => generateCreditControlBlueprint({ overdueDays: 0 }))
73
+ .toThrow(JobValidationError);
74
+ });
75
+ it('negative overdueDays throws JobValidationError', () => {
76
+ expect(() => generateCreditControlBlueprint({ overdueDays: -10 }))
77
+ .toThrow(JobValidationError);
78
+ });
79
+ it('fractional overdueDays throws JobValidationError', () => {
80
+ expect(() => generateCreditControlBlueprint({ overdueDays: 30.5 }))
81
+ .toThrow(JobValidationError);
82
+ });
83
+ });
84
+ describe('generateCreditControlBlueprint (summary)', () => {
85
+ it('recipeReferences include bad-debt-provision', () => {
86
+ const bp = generateCreditControlBlueprint();
87
+ expect(bp.summary.recipeReferences).toContain('bad-debt-provision');
88
+ });
89
+ it('calcReferences include jaz calc ecl', () => {
90
+ const bp = generateCreditControlBlueprint();
91
+ expect(bp.summary.calcReferences).toContain('jaz calc ecl');
92
+ });
93
+ it('apiCalls include key endpoints', () => {
94
+ const bp = generateCreditControlBlueprint();
95
+ expect(bp.summary.apiCalls).toContain('POST /generate-reports/ar-aging');
96
+ expect(bp.summary.apiCalls).toContain('POST /invoices/search');
97
+ });
98
+ });
@@ -0,0 +1,104 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { generateFaReviewBlueprint } from '../jobs/fa-review.js';
3
+ describe('generateFaReviewBlueprint (basic)', () => {
4
+ it('produces a valid JobBlueprint with correct jobType', () => {
5
+ const bp = generateFaReviewBlueprint();
6
+ expect(bp.jobType).toBe('fa-review');
7
+ });
8
+ it('has 4 phases and 8 total steps', () => {
9
+ const bp = generateFaReviewBlueprint();
10
+ expect(bp.phases).toHaveLength(4);
11
+ expect(bp.summary.totalSteps).toBe(8);
12
+ });
13
+ it('phase names are correct', () => {
14
+ const bp = generateFaReviewBlueprint();
15
+ expect(bp.phases.map(p => p.name)).toEqual([
16
+ 'Register Review',
17
+ 'Depreciation Check',
18
+ 'Disposals & Write-offs',
19
+ 'Verification',
20
+ ]);
21
+ });
22
+ it('step categories match expectations', () => {
23
+ const bp = generateFaReviewBlueprint();
24
+ const categories = bp.phases.flatMap(p => p.steps.map(s => s.category));
25
+ expect(categories).toEqual([
26
+ 'report', // 1 — pull FA summary
27
+ 'verify', // 2 — verify against GL
28
+ 'verify', // 3 — verify depreciation runs
29
+ 'review', // 4 — check fully depreciated
30
+ 'adjust', // 5 — process disposals
31
+ 'adjust', // 6 — process write-offs
32
+ 'verify', // 7 — reconcile FA to TB
33
+ 'review', // 8 — check NBV reasonableness
34
+ ]);
35
+ });
36
+ it('summary has non-empty apiCalls', () => {
37
+ const bp = generateFaReviewBlueprint();
38
+ expect(bp.summary.apiCalls.length).toBeGreaterThan(0);
39
+ });
40
+ it('mode is standalone', () => {
41
+ const bp = generateFaReviewBlueprint();
42
+ expect(bp.mode).toBe('standalone');
43
+ });
44
+ });
45
+ describe('generateFaReviewBlueprint (defaults)', () => {
46
+ it('default currency is SGD', () => {
47
+ const bp = generateFaReviewBlueprint();
48
+ expect(bp.currency).toBe('SGD');
49
+ });
50
+ it('period is always current', () => {
51
+ const bp = generateFaReviewBlueprint();
52
+ expect(bp.period).toBe('current');
53
+ });
54
+ });
55
+ describe('generateFaReviewBlueprint (options)', () => {
56
+ it('custom currency works', () => {
57
+ const bp = generateFaReviewBlueprint({ currency: 'PHP' });
58
+ expect(bp.currency).toBe('PHP');
59
+ });
60
+ it('USD currency works', () => {
61
+ const bp = generateFaReviewBlueprint({ currency: 'USD' });
62
+ expect(bp.currency).toBe('USD');
63
+ });
64
+ });
65
+ describe('generateFaReviewBlueprint (summary)', () => {
66
+ it('recipeReferences include asset-disposal', () => {
67
+ const bp = generateFaReviewBlueprint();
68
+ expect(bp.summary.recipeReferences).toContain('asset-disposal');
69
+ });
70
+ it('calcReferences include depreciation and asset-disposal calcs', () => {
71
+ const bp = generateFaReviewBlueprint();
72
+ expect(bp.summary.calcReferences).toContain('jaz calc depreciation');
73
+ expect(bp.summary.calcReferences).toContain('jaz calc asset-disposal');
74
+ });
75
+ it('apiCalls include key endpoints', () => {
76
+ const bp = generateFaReviewBlueprint();
77
+ expect(bp.summary.apiCalls).toContain('POST /generate-reports/fixed-assets');
78
+ expect(bp.summary.apiCalls).toContain('POST /generate-reports/trial-balance');
79
+ expect(bp.summary.apiCalls).toContain('POST /journals');
80
+ });
81
+ });
82
+ describe('generateFaReviewBlueprint (phase structure)', () => {
83
+ it('phase 1 has 2 steps (register review)', () => {
84
+ const bp = generateFaReviewBlueprint();
85
+ expect(bp.phases[0].steps).toHaveLength(2);
86
+ });
87
+ it('phase 2 has 2 steps (depreciation check)', () => {
88
+ const bp = generateFaReviewBlueprint();
89
+ expect(bp.phases[1].steps).toHaveLength(2);
90
+ });
91
+ it('phase 3 has 2 steps (disposals & write-offs)', () => {
92
+ const bp = generateFaReviewBlueprint();
93
+ expect(bp.phases[2].steps).toHaveLength(2);
94
+ });
95
+ it('phase 4 has 2 steps (verification)', () => {
96
+ const bp = generateFaReviewBlueprint();
97
+ expect(bp.phases[3].steps).toHaveLength(2);
98
+ });
99
+ it('step orders are sequential 1-8', () => {
100
+ const bp = generateFaReviewBlueprint();
101
+ const orders = bp.phases.flatMap(p => p.steps.map(s => s.order));
102
+ expect(orders).toEqual([1, 2, 3, 4, 5, 6, 7, 8]);
103
+ });
104
+ });
@@ -0,0 +1,113 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { generateGstVatBlueprint } from '../jobs/gst-vat.js';
3
+ import { JobValidationError } from '../jobs/validate.js';
4
+ describe('generateGstVatBlueprint (basic)', () => {
5
+ it('produces a valid JobBlueprint with correct jobType', () => {
6
+ const bp = generateGstVatBlueprint({ period: '2025-Q1' });
7
+ expect(bp.jobType).toBe('gst-vat-filing');
8
+ });
9
+ it('has 6 phases and 10 total steps', () => {
10
+ const bp = generateGstVatBlueprint({ period: '2025-Q1' });
11
+ expect(bp.phases).toHaveLength(6);
12
+ expect(bp.summary.totalSteps).toBe(10);
13
+ });
14
+ it('phase names are correct', () => {
15
+ const bp = generateGstVatBlueprint({ period: '2025-Q1' });
16
+ expect(bp.phases.map(p => p.name)).toEqual([
17
+ 'Generate Tax Ledger',
18
+ 'Review Output Tax',
19
+ 'Review Input Tax',
20
+ 'Error Checks',
21
+ 'GST Return Summary',
22
+ 'Export & File',
23
+ ]);
24
+ });
25
+ it('step categories match expectations', () => {
26
+ const bp = generateGstVatBlueprint({ period: '2025-Q1' });
27
+ const categories = bp.phases.flatMap(p => p.steps.map(s => s.category));
28
+ expect(categories).toEqual([
29
+ 'report', // 1 — generate tax ledger
30
+ 'export', // 2 — export tax ledger
31
+ 'verify', // 3 — cross-reference invoices
32
+ 'verify', // 4 — check missing tax profiles
33
+ 'verify', // 5 — cross-reference bills
34
+ 'review', // 6 — identify blocked input tax
35
+ 'review', // 7 — review common errors
36
+ 'report', // 8 — compile F5 box mapping
37
+ 'report', // 9 — generate supporting reports
38
+ 'export', // 10 — export final tax ledger
39
+ ]);
40
+ });
41
+ it('summary has non-empty apiCalls', () => {
42
+ const bp = generateGstVatBlueprint({ period: '2025-Q1' });
43
+ expect(bp.summary.apiCalls.length).toBeGreaterThan(0);
44
+ });
45
+ it('mode is standalone', () => {
46
+ const bp = generateGstVatBlueprint({ period: '2025-Q1' });
47
+ expect(bp.mode).toBe('standalone');
48
+ });
49
+ });
50
+ describe('generateGstVatBlueprint (defaults)', () => {
51
+ it('default currency is SGD', () => {
52
+ const bp = generateGstVatBlueprint({ period: '2025-Q1' });
53
+ expect(bp.currency).toBe('SGD');
54
+ });
55
+ it('period label is formatted as Q label', () => {
56
+ const bp = generateGstVatBlueprint({ period: '2025-Q1' });
57
+ expect(bp.period).toBe('Q1 2025');
58
+ });
59
+ });
60
+ describe('generateGstVatBlueprint (options)', () => {
61
+ it('custom currency works', () => {
62
+ const bp = generateGstVatBlueprint({ period: '2025-Q2', currency: 'PHP' });
63
+ expect(bp.currency).toBe('PHP');
64
+ });
65
+ it('Q2 period generates correct date range', () => {
66
+ const bp = generateGstVatBlueprint({ period: '2025-Q2' });
67
+ expect(bp.period).toBe('Q2 2025');
68
+ // Verify date range is embedded in step apiBody
69
+ const step1 = bp.phases[0].steps[0];
70
+ const body = step1.apiBody;
71
+ expect(body.startDate).toBe('2025-04-01');
72
+ expect(body.endDate).toBe('2025-06-30');
73
+ });
74
+ it('Q4 period generates correct date range', () => {
75
+ const bp = generateGstVatBlueprint({ period: '2025-Q4' });
76
+ expect(bp.period).toBe('Q4 2025');
77
+ const step1 = bp.phases[0].steps[0];
78
+ const body = step1.apiBody;
79
+ expect(body.startDate).toBe('2025-10-01');
80
+ expect(body.endDate).toBe('2025-12-31');
81
+ });
82
+ });
83
+ describe('generateGstVatBlueprint (validation)', () => {
84
+ it('invalid period throws JobValidationError', () => {
85
+ expect(() => generateGstVatBlueprint({ period: '2025-06' }))
86
+ .toThrow(JobValidationError);
87
+ });
88
+ it('invalid quarter number throws JobValidationError', () => {
89
+ expect(() => generateGstVatBlueprint({ period: '2025-Q5' }))
90
+ .toThrow(JobValidationError);
91
+ });
92
+ it('malformed period throws JobValidationError', () => {
93
+ expect(() => generateGstVatBlueprint({ period: 'Q1-2025' }))
94
+ .toThrow(JobValidationError);
95
+ });
96
+ it('empty period throws JobValidationError', () => {
97
+ expect(() => generateGstVatBlueprint({ period: '' }))
98
+ .toThrow(JobValidationError);
99
+ });
100
+ });
101
+ describe('generateGstVatBlueprint (summary)', () => {
102
+ it('apiCalls include key endpoints', () => {
103
+ const bp = generateGstVatBlueprint({ period: '2025-Q1' });
104
+ expect(bp.summary.apiCalls).toContain('POST /generate-reports/vat-ledger');
105
+ expect(bp.summary.apiCalls).toContain('POST /invoices/search');
106
+ expect(bp.summary.apiCalls).toContain('POST /bills/search');
107
+ expect(bp.summary.apiCalls).toContain('POST /generate-reports/profit-and-loss');
108
+ });
109
+ it('no recipe references', () => {
110
+ const bp = generateGstVatBlueprint({ period: '2025-Q1' });
111
+ expect(bp.summary.recipeReferences).toHaveLength(0);
112
+ });
113
+ });