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,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
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { generateSupplierReconBlueprint } from '../jobs/supplier-recon.js';
|
|
3
|
+
describe('generateSupplierReconBlueprint (basic)', () => {
|
|
4
|
+
it('produces a valid JobBlueprint with correct jobType', () => {
|
|
5
|
+
const bp = generateSupplierReconBlueprint();
|
|
6
|
+
expect(bp.jobType).toBe('supplier-recon');
|
|
7
|
+
});
|
|
8
|
+
it('has 4 phases and 7 total steps', () => {
|
|
9
|
+
const bp = generateSupplierReconBlueprint();
|
|
10
|
+
expect(bp.phases).toHaveLength(4);
|
|
11
|
+
expect(bp.summary.totalSteps).toBe(7);
|
|
12
|
+
});
|
|
13
|
+
it('phase names are correct', () => {
|
|
14
|
+
const bp = generateSupplierReconBlueprint();
|
|
15
|
+
expect(bp.phases.map(p => p.name)).toEqual([
|
|
16
|
+
'Pull AP Data',
|
|
17
|
+
'Compare Against Supplier Statement',
|
|
18
|
+
'Resolve Discrepancies',
|
|
19
|
+
'Verification',
|
|
20
|
+
]);
|
|
21
|
+
});
|
|
22
|
+
it('step categories match expectations', () => {
|
|
23
|
+
const bp = generateSupplierReconBlueprint();
|
|
24
|
+
const categories = bp.phases.flatMap(p => p.steps.map(s => s.category));
|
|
25
|
+
expect(categories).toEqual([
|
|
26
|
+
'report', // 1 — AP aging report
|
|
27
|
+
'verify', // 2 — list bills for supplier
|
|
28
|
+
'verify', // 3 — match against statement
|
|
29
|
+
'review', // 4 — identify mismatches
|
|
30
|
+
'resolve', // 5 — create missing bills
|
|
31
|
+
'adjust', // 6 — record adjustments
|
|
32
|
+
'verify', // 7 — re-check AP aging
|
|
33
|
+
]);
|
|
34
|
+
});
|
|
35
|
+
it('summary has non-empty apiCalls', () => {
|
|
36
|
+
const bp = generateSupplierReconBlueprint();
|
|
37
|
+
expect(bp.summary.apiCalls.length).toBeGreaterThan(0);
|
|
38
|
+
});
|
|
39
|
+
it('mode is standalone', () => {
|
|
40
|
+
const bp = generateSupplierReconBlueprint();
|
|
41
|
+
expect(bp.mode).toBe('standalone');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
describe('generateSupplierReconBlueprint (defaults)', () => {
|
|
45
|
+
it('default currency is SGD', () => {
|
|
46
|
+
const bp = generateSupplierReconBlueprint();
|
|
47
|
+
expect(bp.currency).toBe('SGD');
|
|
48
|
+
});
|
|
49
|
+
it('default period is current', () => {
|
|
50
|
+
const bp = generateSupplierReconBlueprint();
|
|
51
|
+
expect(bp.period).toBe('current');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe('generateSupplierReconBlueprint (options)', () => {
|
|
55
|
+
it('custom currency works', () => {
|
|
56
|
+
const bp = generateSupplierReconBlueprint({ currency: 'PHP' });
|
|
57
|
+
expect(bp.currency).toBe('PHP');
|
|
58
|
+
});
|
|
59
|
+
it('supplier filter is applied to bills search', () => {
|
|
60
|
+
const bp = generateSupplierReconBlueprint({ supplier: 'Acme Corp' });
|
|
61
|
+
const step2 = bp.phases[0].steps[1];
|
|
62
|
+
const filters = step2.apiBody.filters;
|
|
63
|
+
expect(filters).toHaveProperty('supplierName', 'Acme Corp');
|
|
64
|
+
});
|
|
65
|
+
it('supplier filter absent by default', () => {
|
|
66
|
+
const bp = generateSupplierReconBlueprint();
|
|
67
|
+
const step2 = bp.phases[0].steps[1];
|
|
68
|
+
const filters = step2.apiBody.filters;
|
|
69
|
+
expect(filters).not.toHaveProperty('supplierName');
|
|
70
|
+
});
|
|
71
|
+
it('supplier is reflected in AP aging notes', () => {
|
|
72
|
+
const bp = generateSupplierReconBlueprint({ supplier: 'Acme Corp' });
|
|
73
|
+
const step1 = bp.phases[0].steps[0];
|
|
74
|
+
expect(step1.notes).toContain('Acme Corp');
|
|
75
|
+
});
|
|
76
|
+
it('supplier is reflected in verification notes', () => {
|
|
77
|
+
const bp = generateSupplierReconBlueprint({ supplier: 'Acme Corp' });
|
|
78
|
+
const verifyStep = bp.phases[3].steps[0];
|
|
79
|
+
expect(verifyStep.notes).toContain('Acme Corp');
|
|
80
|
+
});
|
|
81
|
+
it('period filter is applied to bills search with correct end-of-month', () => {
|
|
82
|
+
const bp = generateSupplierReconBlueprint({ period: '2025-06' });
|
|
83
|
+
expect(bp.period).toBe('2025-06');
|
|
84
|
+
const step2 = bp.phases[0].steps[1];
|
|
85
|
+
const filters = step2.apiBody.filters;
|
|
86
|
+
expect(filters).toHaveProperty('dateFrom', '2025-06-01');
|
|
87
|
+
expect(filters).toHaveProperty('dateTo', '2025-06-30');
|
|
88
|
+
});
|
|
89
|
+
it('period filter handles February correctly', () => {
|
|
90
|
+
const bp = generateSupplierReconBlueprint({ period: '2025-02' });
|
|
91
|
+
const step2 = bp.phases[0].steps[1];
|
|
92
|
+
const filters = step2.apiBody.filters;
|
|
93
|
+
expect(filters).toHaveProperty('dateTo', '2025-02-28');
|
|
94
|
+
});
|
|
95
|
+
it('period filter absent by default', () => {
|
|
96
|
+
const bp = generateSupplierReconBlueprint();
|
|
97
|
+
const step2 = bp.phases[0].steps[1];
|
|
98
|
+
const filters = step2.apiBody.filters;
|
|
99
|
+
expect(filters).not.toHaveProperty('dateFrom');
|
|
100
|
+
expect(filters).not.toHaveProperty('dateTo');
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe('generateSupplierReconBlueprint (summary)', () => {
|
|
104
|
+
it('recipeReferences include standard-bill', () => {
|
|
105
|
+
const bp = generateSupplierReconBlueprint();
|
|
106
|
+
expect(bp.summary.recipeReferences).toContain('standard-bill');
|
|
107
|
+
});
|
|
108
|
+
it('apiCalls include key endpoints', () => {
|
|
109
|
+
const bp = generateSupplierReconBlueprint();
|
|
110
|
+
expect(bp.summary.apiCalls).toContain('POST /generate-reports/ap-aging');
|
|
111
|
+
expect(bp.summary.apiCalls).toContain('POST /bills/search');
|
|
112
|
+
expect(bp.summary.apiCalls).toContain('POST /bills');
|
|
113
|
+
expect(bp.summary.apiCalls).toContain('POST /journals');
|
|
114
|
+
});
|
|
115
|
+
});
|