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.
- package/assets/skills/api/SKILL.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__/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/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 +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
|
+
});
|