jaz-cli 2.6.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 (62) hide show
  1. package/assets/skills/api/SKILL.md +12 -2
  2. package/assets/skills/api/references/dependencies.md +3 -2
  3. package/assets/skills/api/references/endpoints.md +78 -0
  4. package/assets/skills/api/references/feature-glossary.md +4 -4
  5. package/assets/skills/api/references/field-map.md +17 -0
  6. package/assets/skills/api/references/full-api-surface.md +1 -1
  7. package/assets/skills/conversion/SKILL.md +1 -1
  8. package/assets/skills/jobs/SKILL.md +104 -0
  9. package/assets/skills/jobs/references/audit-prep.md +319 -0
  10. package/assets/skills/jobs/references/bank-recon.md +234 -0
  11. package/assets/skills/jobs/references/building-blocks.md +135 -0
  12. package/assets/skills/jobs/references/credit-control.md +273 -0
  13. package/assets/skills/jobs/references/fa-review.md +267 -0
  14. package/assets/skills/jobs/references/gst-vat-filing.md +250 -0
  15. package/assets/skills/jobs/references/month-end-close.md +308 -0
  16. package/assets/skills/jobs/references/payment-run.md +246 -0
  17. package/assets/skills/jobs/references/quarter-end-close.md +268 -0
  18. package/assets/skills/jobs/references/supplier-recon.md +330 -0
  19. package/assets/skills/jobs/references/year-end-close.md +341 -0
  20. package/assets/skills/transaction-recipes/SKILL.md +1 -1
  21. package/dist/__tests__/amortization.test.js +101 -0
  22. package/dist/__tests__/asset-disposal.test.js +249 -0
  23. package/dist/__tests__/blueprint.test.js +72 -0
  24. package/dist/__tests__/depreciation.test.js +125 -0
  25. package/dist/__tests__/ecl.test.js +134 -0
  26. package/dist/__tests__/fixed-deposit.test.js +214 -0
  27. package/dist/__tests__/fx-reval.test.js +115 -0
  28. package/dist/__tests__/jobs-audit-prep.test.js +125 -0
  29. package/dist/__tests__/jobs-bank-recon.test.js +108 -0
  30. package/dist/__tests__/jobs-credit-control.test.js +98 -0
  31. package/dist/__tests__/jobs-fa-review.test.js +104 -0
  32. package/dist/__tests__/jobs-gst-vat.test.js +113 -0
  33. package/dist/__tests__/jobs-month-end.test.js +162 -0
  34. package/dist/__tests__/jobs-payment-run.test.js +106 -0
  35. package/dist/__tests__/jobs-quarter-end.test.js +155 -0
  36. package/dist/__tests__/jobs-supplier-recon.test.js +115 -0
  37. package/dist/__tests__/jobs-validate.test.js +181 -0
  38. package/dist/__tests__/jobs-year-end.test.js +149 -0
  39. package/dist/__tests__/lease.test.js +96 -0
  40. package/dist/__tests__/loan.test.js +80 -0
  41. package/dist/__tests__/provision.test.js +141 -0
  42. package/dist/__tests__/validate.test.js +81 -0
  43. package/dist/calc/asset-disposal.js +17 -13
  44. package/dist/calc/fixed-deposit.js +26 -17
  45. package/dist/calc/lease.js +7 -3
  46. package/dist/commands/jobs.js +184 -0
  47. package/dist/index.js +2 -0
  48. package/dist/jobs/audit-prep.js +211 -0
  49. package/dist/jobs/bank-recon.js +163 -0
  50. package/dist/jobs/credit-control.js +126 -0
  51. package/dist/jobs/fa-review.js +121 -0
  52. package/dist/jobs/format.js +102 -0
  53. package/dist/jobs/gst-vat.js +187 -0
  54. package/dist/jobs/month-end.js +232 -0
  55. package/dist/jobs/payment-run.js +199 -0
  56. package/dist/jobs/quarter-end.js +135 -0
  57. package/dist/jobs/supplier-recon.js +132 -0
  58. package/dist/jobs/types.js +36 -0
  59. package/dist/jobs/validate.js +115 -0
  60. package/dist/jobs/year-end.js +153 -0
  61. package/dist/types/index.js +2 -1
  62. package/package.json +5 -2
@@ -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
+ });
@@ -0,0 +1,162 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { generateMonthEndBlueprint } from '../jobs/month-end.js';
3
+ import { JobValidationError } from '../jobs/validate.js';
4
+ describe('generateMonthEndBlueprint', () => {
5
+ const base = { period: '2025-01' };
6
+ // Structure
7
+ it('produces 5 phases', () => {
8
+ const r = generateMonthEndBlueprint(base);
9
+ expect(r.phases).toHaveLength(5);
10
+ });
11
+ it('produces 18 total steps', () => {
12
+ const r = generateMonthEndBlueprint(base);
13
+ expect(r.summary.totalSteps).toBe(18);
14
+ });
15
+ it('has correct jobType', () => {
16
+ const r = generateMonthEndBlueprint(base);
17
+ expect(r.jobType).toBe('month-end-close');
18
+ });
19
+ it('mode is standalone', () => {
20
+ const r = generateMonthEndBlueprint(base);
21
+ expect(r.mode).toBe('standalone');
22
+ });
23
+ it('period label is "Jan 2025"', () => {
24
+ const r = generateMonthEndBlueprint(base);
25
+ expect(r.period).toBe('Jan 2025');
26
+ });
27
+ // Phase names
28
+ it('phase names are correct', () => {
29
+ const r = generateMonthEndBlueprint(base);
30
+ expect(r.phases[0].name).toBe('Phase 1: Pre-Close Preparation');
31
+ expect(r.phases[1].name).toBe('Phase 2: Accruals & Adjustments');
32
+ expect(r.phases[2].name).toBe('Phase 3: Period-End Valuations');
33
+ expect(r.phases[3].name).toBe('Phase 4: Verification');
34
+ expect(r.phases[4].name).toBe('Phase 5: Close & Lock');
35
+ });
36
+ // Step categories per phase
37
+ it('Phase 1 steps are all "verify"', () => {
38
+ const r = generateMonthEndBlueprint(base);
39
+ for (const step of r.phases[0].steps) {
40
+ expect(step.category).toBe('verify');
41
+ }
42
+ });
43
+ it('Phase 2 steps are all "accrue"', () => {
44
+ const r = generateMonthEndBlueprint(base);
45
+ for (const step of r.phases[1].steps) {
46
+ expect(step.category).toBe('accrue');
47
+ }
48
+ });
49
+ it('Phase 3 steps are all "value"', () => {
50
+ const r = generateMonthEndBlueprint(base);
51
+ for (const step of r.phases[2].steps) {
52
+ expect(step.category).toBe('value');
53
+ }
54
+ });
55
+ it('Phase 4 steps are all "report"', () => {
56
+ const r = generateMonthEndBlueprint(base);
57
+ for (const step of r.phases[3].steps) {
58
+ expect(step.category).toBe('report');
59
+ }
60
+ });
61
+ it('Phase 5 steps are all "lock"', () => {
62
+ const r = generateMonthEndBlueprint(base);
63
+ for (const step of r.phases[4].steps) {
64
+ expect(step.category).toBe('lock');
65
+ }
66
+ });
67
+ // API dates
68
+ it('API calls contain correct dates for "2025-01"', () => {
69
+ const r = generateMonthEndBlueprint(base);
70
+ const allSteps = r.phases.flatMap(p => p.steps);
71
+ const stepsWithBody = allSteps.filter(s => s.apiBody);
72
+ for (const step of stepsWithBody) {
73
+ const body = step.apiBody;
74
+ // Check that date fields reference the correct period
75
+ if (body.filter && typeof body.filter === 'object') {
76
+ const filter = body.filter;
77
+ if (filter.valueDate && typeof filter.valueDate === 'object') {
78
+ const vd = filter.valueDate;
79
+ expect(vd.from).toBe('2025-01-01');
80
+ expect(vd.to).toBe('2025-01-31');
81
+ }
82
+ }
83
+ if (body.endDate) {
84
+ expect(body.endDate).toBe('2025-01-31');
85
+ }
86
+ if (body.startDate) {
87
+ expect(body.startDate).toBe('2025-01-01');
88
+ }
89
+ }
90
+ });
91
+ // Summary counts
92
+ it('summary has recipe references', () => {
93
+ const r = generateMonthEndBlueprint(base);
94
+ expect(r.summary.recipeReferences.length).toBeGreaterThan(0);
95
+ // Sorted array — verify it is sorted
96
+ const sorted = [...r.summary.recipeReferences].sort();
97
+ expect(r.summary.recipeReferences).toEqual(sorted);
98
+ });
99
+ it('summary has calc references', () => {
100
+ const r = generateMonthEndBlueprint(base);
101
+ expect(r.summary.calcReferences.length).toBeGreaterThan(0);
102
+ });
103
+ it('summary has API calls', () => {
104
+ const r = generateMonthEndBlueprint(base);
105
+ expect(r.summary.apiCalls.length).toBeGreaterThan(0);
106
+ });
107
+ it('summary totalSteps matches actual step count', () => {
108
+ const r = generateMonthEndBlueprint(base);
109
+ const actual = r.phases.reduce((sum, p) => sum + p.steps.length, 0);
110
+ expect(r.summary.totalSteps).toBe(actual);
111
+ });
112
+ // Currency
113
+ it('currency defaults to "SGD"', () => {
114
+ const r = generateMonthEndBlueprint(base);
115
+ expect(r.currency).toBe('SGD');
116
+ });
117
+ it('custom currency "USD" passes through', () => {
118
+ const r = generateMonthEndBlueprint({ period: '2025-01', currency: 'USD' });
119
+ expect(r.currency).toBe('USD');
120
+ });
121
+ // Validation
122
+ it('invalid period throws JobValidationError', () => {
123
+ expect(() => generateMonthEndBlueprint({ period: 'bad' })).toThrow(JobValidationError);
124
+ });
125
+ it('invalid month 13 throws JobValidationError', () => {
126
+ expect(() => generateMonthEndBlueprint({ period: '2025-13' })).toThrow(JobValidationError);
127
+ });
128
+ // Prior month calculation
129
+ it('for "2025-01" the prior month is Dec 2024', () => {
130
+ const r = generateMonthEndBlueprint({ period: '2025-01' });
131
+ // Phase 4, step 4 ("Compare P&L to prior month") contains prior month dates
132
+ const compareStep = r.phases[3].steps[3];
133
+ expect(compareStep.notes).toContain('2024-12-01');
134
+ expect(compareStep.notes).toContain('2024-12-31');
135
+ expect(compareStep.apiBody).toBeDefined();
136
+ expect(compareStep.apiBody.secondarySnapshotDate).toBe('2024-12-01');
137
+ });
138
+ it('for "2025-06" the prior month is May 2025', () => {
139
+ const r = generateMonthEndBlueprint({ period: '2025-06' });
140
+ const compareStep = r.phases[3].steps[3];
141
+ expect(compareStep.notes).toContain('2025-05-01');
142
+ expect(compareStep.notes).toContain('2025-05-31');
143
+ expect(compareStep.apiBody.secondarySnapshotDate).toBe('2025-05-01');
144
+ });
145
+ // Step ordering
146
+ it('step ordering is continuous 1..N', () => {
147
+ const r = generateMonthEndBlueprint(base);
148
+ const allOrders = r.phases.flatMap(p => p.steps.map(s => s.order));
149
+ for (let i = 0; i < allOrders.length; i++) {
150
+ expect(allOrders[i]).toBe(i + 1);
151
+ }
152
+ });
153
+ // Different period
154
+ it('"2025-06" produces correct dates and label', () => {
155
+ const r = generateMonthEndBlueprint({ period: '2025-06' });
156
+ expect(r.period).toBe('Jun 2025');
157
+ // Check report body dates
158
+ const trialBalance = r.phases[3].steps[0];
159
+ expect(trialBalance.apiBody.startDate).toBe('2025-06-01');
160
+ expect(trialBalance.apiBody.endDate).toBe('2025-06-30');
161
+ });
162
+ });
@@ -0,0 +1,106 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { generatePaymentRunBlueprint } from '../jobs/payment-run.js';
3
+ import { JobValidationError } from '../jobs/validate.js';
4
+ describe('generatePaymentRunBlueprint (basic)', () => {
5
+ it('produces a valid JobBlueprint with correct jobType', () => {
6
+ const bp = generatePaymentRunBlueprint();
7
+ expect(bp.jobType).toBe('payment-run');
8
+ });
9
+ it('has 6 phases and 10 total steps', () => {
10
+ const bp = generatePaymentRunBlueprint();
11
+ expect(bp.phases).toHaveLength(6);
12
+ expect(bp.summary.totalSteps).toBe(10);
13
+ });
14
+ it('phase names are correct', () => {
15
+ const bp = generatePaymentRunBlueprint();
16
+ expect(bp.phases.map(p => p.name)).toEqual([
17
+ 'Identify Outstanding Bills',
18
+ 'Summarize by Supplier',
19
+ 'Select & Approve Payment Batch',
20
+ 'Record Payments',
21
+ 'FX Payments',
22
+ 'Verification',
23
+ ]);
24
+ });
25
+ it('step categories match expectations', () => {
26
+ const bp = generatePaymentRunBlueprint();
27
+ const categories = bp.phases.flatMap(p => p.steps.map(s => s.category));
28
+ expect(categories).toEqual([
29
+ 'verify', // 1 — list unpaid bills
30
+ 'verify', // 2 — filter by due date
31
+ 'report', // 3 — group by supplier
32
+ 'report', // 4 — AP aging
33
+ 'review', // 5 — check cash availability
34
+ 'resolve', // 6 — record full payments
35
+ 'resolve', // 7 — record partial payments
36
+ 'resolve', // 8 — FX payments
37
+ 'verify', // 9 — verify AP aging
38
+ 'verify', // 10 — verify bank balance
39
+ ]);
40
+ });
41
+ it('summary has non-empty apiCalls', () => {
42
+ const bp = generatePaymentRunBlueprint();
43
+ expect(bp.summary.apiCalls.length).toBeGreaterThan(0);
44
+ });
45
+ it('mode is standalone', () => {
46
+ const bp = generatePaymentRunBlueprint();
47
+ expect(bp.mode).toBe('standalone');
48
+ });
49
+ });
50
+ describe('generatePaymentRunBlueprint (defaults)', () => {
51
+ it('default currency is SGD', () => {
52
+ const bp = generatePaymentRunBlueprint();
53
+ expect(bp.currency).toBe('SGD');
54
+ });
55
+ it('default period is current', () => {
56
+ const bp = generatePaymentRunBlueprint();
57
+ expect(bp.period).toBe('current');
58
+ });
59
+ });
60
+ describe('generatePaymentRunBlueprint (options)', () => {
61
+ it('custom currency works', () => {
62
+ const bp = generatePaymentRunBlueprint({ currency: 'PHP' });
63
+ expect(bp.currency).toBe('PHP');
64
+ });
65
+ it('dueBefore filter is applied', () => {
66
+ const bp = generatePaymentRunBlueprint({ dueBefore: '2025-06-30' });
67
+ expect(bp.period).toBe('Due before 2025-06-30');
68
+ const step1 = bp.phases[0].steps[0];
69
+ const filters = step1.apiBody.filters;
70
+ expect(filters).toHaveProperty('dueDateTo', '2025-06-30');
71
+ });
72
+ it('dueBefore absent by default', () => {
73
+ const bp = generatePaymentRunBlueprint();
74
+ const step1 = bp.phases[0].steps[0];
75
+ const filters = step1.apiBody.filters;
76
+ expect(filters).not.toHaveProperty('dueDateTo');
77
+ });
78
+ });
79
+ describe('generatePaymentRunBlueprint (validation)', () => {
80
+ it('invalid dueBefore format throws JobValidationError', () => {
81
+ expect(() => generatePaymentRunBlueprint({ dueBefore: '30-06-2025' }))
82
+ .toThrow(JobValidationError);
83
+ });
84
+ it('malformed date throws JobValidationError', () => {
85
+ expect(() => generatePaymentRunBlueprint({ dueBefore: '2025/06/30' }))
86
+ .toThrow(JobValidationError);
87
+ });
88
+ });
89
+ describe('generatePaymentRunBlueprint (summary)', () => {
90
+ it('recipeReferences include bill-payment and fx-bill-payment', () => {
91
+ const bp = generatePaymentRunBlueprint();
92
+ expect(bp.summary.recipeReferences).toContain('bill-payment');
93
+ expect(bp.summary.recipeReferences).toContain('fx-bill-payment');
94
+ });
95
+ it('calcReferences include jaz calc fx-reval', () => {
96
+ const bp = generatePaymentRunBlueprint();
97
+ expect(bp.summary.calcReferences).toContain('jaz calc fx-reval');
98
+ });
99
+ it('apiCalls include key endpoints', () => {
100
+ const bp = generatePaymentRunBlueprint();
101
+ expect(bp.summary.apiCalls).toContain('POST /bills/search');
102
+ expect(bp.summary.apiCalls).toContain('POST /bills/{id}/payments');
103
+ expect(bp.summary.apiCalls).toContain('POST /generate-reports/ap-aging');
104
+ expect(bp.summary.apiCalls).toContain('POST /generate-reports/trial-balance');
105
+ });
106
+ });
@@ -0,0 +1,155 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { generateQuarterEndBlueprint } from '../jobs/quarter-end.js';
3
+ import { JobValidationError } from '../jobs/validate.js';
4
+ describe('generateQuarterEndBlueprint', () => {
5
+ // Standalone mode (default)
6
+ describe('standalone mode', () => {
7
+ const base = { period: '2025-Q1' };
8
+ it('includes month-end phases for Jan, Feb, Mar plus quarterly extras and verification', () => {
9
+ const r = generateQuarterEndBlueprint(base);
10
+ // 3 months x 5 phases each = 15 month phases + Phase 6 (extras) + Phase 7 (verification)
11
+ expect(r.phases).toHaveLength(17);
12
+ });
13
+ it('has correct jobType', () => {
14
+ const r = generateQuarterEndBlueprint(base);
15
+ expect(r.jobType).toBe('quarter-end-close');
16
+ });
17
+ it('mode is standalone', () => {
18
+ const r = generateQuarterEndBlueprint(base);
19
+ expect(r.mode).toBe('standalone');
20
+ });
21
+ it('period label is "Q1 2025"', () => {
22
+ const r = generateQuarterEndBlueprint(base);
23
+ expect(r.period).toBe('Q1 2025');
24
+ });
25
+ it('currency defaults to "SGD"', () => {
26
+ const r = generateQuarterEndBlueprint(base);
27
+ expect(r.currency).toBe('SGD');
28
+ });
29
+ it('month-end phases are prefixed with month labels', () => {
30
+ const r = generateQuarterEndBlueprint(base);
31
+ // First 5 phases are Jan month-end
32
+ expect(r.phases[0].name).toContain('Jan 2025');
33
+ expect(r.phases[0].name).toContain('Phase 1');
34
+ // Phases 5-9 are Feb month-end
35
+ expect(r.phases[5].name).toContain('Feb 2025');
36
+ // Phases 10-14 are Mar month-end
37
+ expect(r.phases[10].name).toContain('Mar 2025');
38
+ });
39
+ it('quarterly extras is Phase 6', () => {
40
+ const r = generateQuarterEndBlueprint(base);
41
+ const extras = r.phases[15];
42
+ expect(extras.name).toBe('Phase 6: Quarterly Extras');
43
+ });
44
+ it('quarterly verification is Phase 7', () => {
45
+ const r = generateQuarterEndBlueprint(base);
46
+ const verification = r.phases[16];
47
+ expect(verification.name).toBe('Phase 7: Quarterly Verification');
48
+ });
49
+ it('quarterly extras has 5 steps', () => {
50
+ const r = generateQuarterEndBlueprint(base);
51
+ const extras = r.phases[15];
52
+ expect(extras.steps).toHaveLength(5);
53
+ });
54
+ it('quarterly verification has 3 steps', () => {
55
+ const r = generateQuarterEndBlueprint(base);
56
+ const verification = r.phases[16];
57
+ expect(verification.steps).toHaveLength(3);
58
+ });
59
+ it('each month group has internally continuous ordering 1..18', () => {
60
+ const r = generateQuarterEndBlueprint(base);
61
+ // Each of the 3 months has 5 phases x 18 steps with ordering 1..18
62
+ for (let m = 0; m < 3; m++) {
63
+ const monthPhases = r.phases.slice(m * 5, m * 5 + 5);
64
+ const orders = monthPhases.flatMap(p => p.steps.map(s => s.order));
65
+ for (let i = 0; i < orders.length; i++) {
66
+ expect(orders[i]).toBe(i + 1);
67
+ }
68
+ }
69
+ });
70
+ it('quarterly extras ordering continues from last month max order', () => {
71
+ const r = generateQuarterEndBlueprint(base);
72
+ const extras = r.phases[15];
73
+ // Month-end phases each have 18 steps; quarterly extras starts at 19
74
+ expect(extras.steps[0].order).toBe(19);
75
+ });
76
+ it('summary totalSteps matches actual step count', () => {
77
+ const r = generateQuarterEndBlueprint(base);
78
+ const actual = r.phases.reduce((sum, p) => sum + p.steps.length, 0);
79
+ expect(r.summary.totalSteps).toBe(actual);
80
+ });
81
+ it('summary includes recipe references from both monthly and quarterly steps', () => {
82
+ const r = generateQuarterEndBlueprint(base);
83
+ // Monthly recipes
84
+ expect(r.summary.recipeReferences).toContain('accrued-expenses');
85
+ expect(r.summary.recipeReferences).toContain('bank-loan');
86
+ // Quarterly extras recipes
87
+ expect(r.summary.recipeReferences).toContain('bad-debt-provision');
88
+ expect(r.summary.recipeReferences).toContain('intercompany');
89
+ expect(r.summary.recipeReferences).toContain('provisions');
90
+ });
91
+ it('summary recipe references are sorted', () => {
92
+ const r = generateQuarterEndBlueprint(base);
93
+ const sorted = [...r.summary.recipeReferences].sort();
94
+ expect(r.summary.recipeReferences).toEqual(sorted);
95
+ });
96
+ });
97
+ // Incremental mode
98
+ describe('incremental mode', () => {
99
+ const base = { period: '2025-Q1', incremental: true };
100
+ it('has only quarterly extras and verification phases (no month-end)', () => {
101
+ const r = generateQuarterEndBlueprint(base);
102
+ expect(r.phases).toHaveLength(2);
103
+ expect(r.phases[0].name).toBe('Phase 6: Quarterly Extras');
104
+ expect(r.phases[1].name).toBe('Phase 7: Quarterly Verification');
105
+ });
106
+ it('mode is incremental', () => {
107
+ const r = generateQuarterEndBlueprint(base);
108
+ expect(r.mode).toBe('incremental');
109
+ });
110
+ it('step ordering starts at 1 for extras', () => {
111
+ const r = generateQuarterEndBlueprint(base);
112
+ expect(r.phases[0].steps[0].order).toBe(1);
113
+ });
114
+ it('step ordering is continuous across extras and verification', () => {
115
+ const r = generateQuarterEndBlueprint(base);
116
+ const allOrders = r.phases.flatMap(p => p.steps.map(s => s.order));
117
+ for (let i = 0; i < allOrders.length; i++) {
118
+ expect(allOrders[i]).toBe(i + 1);
119
+ }
120
+ });
121
+ it('total steps = 5 extras + 3 verification = 8', () => {
122
+ const r = generateQuarterEndBlueprint(base);
123
+ expect(r.summary.totalSteps).toBe(8);
124
+ });
125
+ });
126
+ // Q4 dates
127
+ it('"2025-Q4" has correct dates Oct-Dec', () => {
128
+ const r = generateQuarterEndBlueprint({ period: '2025-Q4' });
129
+ // Quarterly extras phase uses quarter-level dates
130
+ const extras = r.phases[r.phases.length - 2];
131
+ const gstStep = extras.steps[0];
132
+ expect(gstStep.apiBody.startDate).toBe('2025-10-01');
133
+ expect(gstStep.apiBody.endDate).toBe('2025-12-31');
134
+ });
135
+ // Quarterly verification reports reference correct dates
136
+ it('quarterly verification reports reference full quarter dates', () => {
137
+ const r = generateQuarterEndBlueprint({ period: '2025-Q2', incremental: true });
138
+ const verification = r.phases[1];
139
+ const trialBalance = verification.steps[0];
140
+ expect(trialBalance.apiBody.startDate).toBe('2025-04-01');
141
+ expect(trialBalance.apiBody.endDate).toBe('2025-06-30');
142
+ });
143
+ // Custom currency
144
+ it('custom currency "USD" passes through', () => {
145
+ const r = generateQuarterEndBlueprint({ period: '2025-Q1', currency: 'USD' });
146
+ expect(r.currency).toBe('USD');
147
+ });
148
+ // Validation
149
+ it('invalid period throws JobValidationError', () => {
150
+ expect(() => generateQuarterEndBlueprint({ period: 'bad' })).toThrow(JobValidationError);
151
+ });
152
+ it('Q5 throws JobValidationError', () => {
153
+ expect(() => generateQuarterEndBlueprint({ period: '2025-Q5' })).toThrow(JobValidationError);
154
+ });
155
+ });