jaz-cli 2.7.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/assets/skills/api/SKILL.md +1 -1
  2. package/assets/skills/conversion/SKILL.md +1 -1
  3. package/assets/skills/jobs/SKILL.md +104 -0
  4. package/assets/skills/jobs/references/audit-prep.md +319 -0
  5. package/assets/skills/jobs/references/bank-recon.md +234 -0
  6. package/assets/skills/jobs/references/building-blocks.md +135 -0
  7. package/assets/skills/jobs/references/credit-control.md +273 -0
  8. package/assets/skills/jobs/references/fa-review.md +267 -0
  9. package/assets/skills/jobs/references/gst-vat-filing.md +250 -0
  10. package/assets/skills/jobs/references/month-end-close.md +308 -0
  11. package/assets/skills/jobs/references/payment-run.md +246 -0
  12. package/assets/skills/jobs/references/quarter-end-close.md +268 -0
  13. package/assets/skills/jobs/references/supplier-recon.md +330 -0
  14. package/assets/skills/jobs/references/year-end-close.md +341 -0
  15. package/assets/skills/transaction-recipes/SKILL.md +1 -1
  16. package/dist/__tests__/jobs-audit-prep.test.js +125 -0
  17. package/dist/__tests__/jobs-bank-recon.test.js +108 -0
  18. package/dist/__tests__/jobs-credit-control.test.js +98 -0
  19. package/dist/__tests__/jobs-fa-review.test.js +104 -0
  20. package/dist/__tests__/jobs-gst-vat.test.js +113 -0
  21. package/dist/__tests__/jobs-month-end.test.js +162 -0
  22. package/dist/__tests__/jobs-payment-run.test.js +106 -0
  23. package/dist/__tests__/jobs-quarter-end.test.js +155 -0
  24. package/dist/__tests__/jobs-supplier-recon.test.js +115 -0
  25. package/dist/__tests__/jobs-validate.test.js +181 -0
  26. package/dist/__tests__/jobs-year-end.test.js +149 -0
  27. package/dist/commands/jobs.js +184 -0
  28. package/dist/index.js +2 -0
  29. package/dist/jobs/audit-prep.js +211 -0
  30. package/dist/jobs/bank-recon.js +163 -0
  31. package/dist/jobs/credit-control.js +126 -0
  32. package/dist/jobs/fa-review.js +121 -0
  33. package/dist/jobs/format.js +102 -0
  34. package/dist/jobs/gst-vat.js +187 -0
  35. package/dist/jobs/month-end.js +232 -0
  36. package/dist/jobs/payment-run.js +199 -0
  37. package/dist/jobs/quarter-end.js +135 -0
  38. package/dist/jobs/supplier-recon.js +132 -0
  39. package/dist/jobs/types.js +36 -0
  40. package/dist/jobs/validate.js +115 -0
  41. package/dist/jobs/year-end.js +153 -0
  42. package/dist/types/index.js +2 -1
  43. package/package.json +1 -1
@@ -0,0 +1,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
+ });