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.
- package/assets/skills/api/SKILL.md +12 -2
- package/assets/skills/api/references/dependencies.md +3 -2
- package/assets/skills/api/references/endpoints.md +78 -0
- package/assets/skills/api/references/feature-glossary.md +4 -4
- package/assets/skills/api/references/field-map.md +17 -0
- package/assets/skills/api/references/full-api-surface.md +1 -1
- package/assets/skills/conversion/SKILL.md +1 -1
- package/assets/skills/jobs/SKILL.md +104 -0
- package/assets/skills/jobs/references/audit-prep.md +319 -0
- package/assets/skills/jobs/references/bank-recon.md +234 -0
- package/assets/skills/jobs/references/building-blocks.md +135 -0
- package/assets/skills/jobs/references/credit-control.md +273 -0
- package/assets/skills/jobs/references/fa-review.md +267 -0
- package/assets/skills/jobs/references/gst-vat-filing.md +250 -0
- package/assets/skills/jobs/references/month-end-close.md +308 -0
- package/assets/skills/jobs/references/payment-run.md +246 -0
- package/assets/skills/jobs/references/quarter-end-close.md +268 -0
- package/assets/skills/jobs/references/supplier-recon.md +330 -0
- package/assets/skills/jobs/references/year-end-close.md +341 -0
- package/assets/skills/transaction-recipes/SKILL.md +1 -1
- package/dist/__tests__/amortization.test.js +101 -0
- package/dist/__tests__/asset-disposal.test.js +249 -0
- package/dist/__tests__/blueprint.test.js +72 -0
- package/dist/__tests__/depreciation.test.js +125 -0
- package/dist/__tests__/ecl.test.js +134 -0
- package/dist/__tests__/fixed-deposit.test.js +214 -0
- package/dist/__tests__/fx-reval.test.js +115 -0
- package/dist/__tests__/jobs-audit-prep.test.js +125 -0
- package/dist/__tests__/jobs-bank-recon.test.js +108 -0
- package/dist/__tests__/jobs-credit-control.test.js +98 -0
- package/dist/__tests__/jobs-fa-review.test.js +104 -0
- package/dist/__tests__/jobs-gst-vat.test.js +113 -0
- package/dist/__tests__/jobs-month-end.test.js +162 -0
- package/dist/__tests__/jobs-payment-run.test.js +106 -0
- package/dist/__tests__/jobs-quarter-end.test.js +155 -0
- package/dist/__tests__/jobs-supplier-recon.test.js +115 -0
- package/dist/__tests__/jobs-validate.test.js +181 -0
- package/dist/__tests__/jobs-year-end.test.js +149 -0
- package/dist/__tests__/lease.test.js +96 -0
- package/dist/__tests__/loan.test.js +80 -0
- package/dist/__tests__/provision.test.js +141 -0
- package/dist/__tests__/validate.test.js +81 -0
- package/dist/calc/asset-disposal.js +17 -13
- package/dist/calc/fixed-deposit.js +26 -17
- package/dist/calc/lease.js +7 -3
- package/dist/commands/jobs.js +184 -0
- package/dist/index.js +2 -0
- package/dist/jobs/audit-prep.js +211 -0
- package/dist/jobs/bank-recon.js +163 -0
- package/dist/jobs/credit-control.js +126 -0
- package/dist/jobs/fa-review.js +121 -0
- package/dist/jobs/format.js +102 -0
- package/dist/jobs/gst-vat.js +187 -0
- package/dist/jobs/month-end.js +232 -0
- package/dist/jobs/payment-run.js +199 -0
- package/dist/jobs/quarter-end.js +135 -0
- package/dist/jobs/supplier-recon.js +132 -0
- package/dist/jobs/types.js +36 -0
- package/dist/jobs/validate.js +115 -0
- package/dist/jobs/year-end.js +153 -0
- package/dist/types/index.js +2 -1
- 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
|
+
});
|