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,181 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parseMonthPeriod, parseQuarterPeriod, parseYearPeriod, validateDateString, validateOverdueDays, JobValidationError, } from '../jobs/validate.js';
|
|
3
|
+
describe('parseMonthPeriod', () => {
|
|
4
|
+
it('parses "2025-01" → Jan 2025 with correct dates', () => {
|
|
5
|
+
const r = parseMonthPeriod('2025-01');
|
|
6
|
+
expect(r.type).toBe('month');
|
|
7
|
+
expect(r.year).toBe(2025);
|
|
8
|
+
expect(r.month).toBe(1);
|
|
9
|
+
expect(r.startDate).toBe('2025-01-01');
|
|
10
|
+
expect(r.endDate).toBe('2025-01-31');
|
|
11
|
+
expect(r.label).toBe('Jan 2025');
|
|
12
|
+
});
|
|
13
|
+
it('parses "2025-12" → Dec 2025 with correct dates', () => {
|
|
14
|
+
const r = parseMonthPeriod('2025-12');
|
|
15
|
+
expect(r.type).toBe('month');
|
|
16
|
+
expect(r.year).toBe(2025);
|
|
17
|
+
expect(r.month).toBe(12);
|
|
18
|
+
expect(r.startDate).toBe('2025-12-01');
|
|
19
|
+
expect(r.endDate).toBe('2025-12-31');
|
|
20
|
+
expect(r.label).toBe('Dec 2025');
|
|
21
|
+
});
|
|
22
|
+
it('handles February in a leap year', () => {
|
|
23
|
+
const r = parseMonthPeriod('2024-02');
|
|
24
|
+
expect(r.endDate).toBe('2024-02-29');
|
|
25
|
+
});
|
|
26
|
+
it('handles February in a non-leap year', () => {
|
|
27
|
+
const r = parseMonthPeriod('2025-02');
|
|
28
|
+
expect(r.endDate).toBe('2025-02-28');
|
|
29
|
+
});
|
|
30
|
+
it('rejects month 13 with JobValidationError', () => {
|
|
31
|
+
expect(() => parseMonthPeriod('2025-13')).toThrow(JobValidationError);
|
|
32
|
+
});
|
|
33
|
+
it('rejects non-date string "abc"', () => {
|
|
34
|
+
expect(() => parseMonthPeriod('abc')).toThrow(JobValidationError);
|
|
35
|
+
});
|
|
36
|
+
it('rejects single-digit month "2025-1"', () => {
|
|
37
|
+
expect(() => parseMonthPeriod('2025-1')).toThrow(JobValidationError);
|
|
38
|
+
});
|
|
39
|
+
it('rejects month 00', () => {
|
|
40
|
+
expect(() => parseMonthPeriod('2025-00')).toThrow(JobValidationError);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
describe('parseQuarterPeriod', () => {
|
|
44
|
+
it('parses "2025-Q1" → Jan-Mar 2025', () => {
|
|
45
|
+
const r = parseQuarterPeriod('2025-Q1');
|
|
46
|
+
expect(r.type).toBe('quarter');
|
|
47
|
+
expect(r.year).toBe(2025);
|
|
48
|
+
expect(r.quarter).toBe(1);
|
|
49
|
+
expect(r.startDate).toBe('2025-01-01');
|
|
50
|
+
expect(r.endDate).toBe('2025-03-31');
|
|
51
|
+
expect(r.label).toBe('Q1 2025');
|
|
52
|
+
expect(r.months).toHaveLength(3);
|
|
53
|
+
expect(r.months[0].month).toBe(1);
|
|
54
|
+
expect(r.months[1].month).toBe(2);
|
|
55
|
+
expect(r.months[2].month).toBe(3);
|
|
56
|
+
});
|
|
57
|
+
it('parses "2025-Q4" → Oct-Dec 2025', () => {
|
|
58
|
+
const r = parseQuarterPeriod('2025-Q4');
|
|
59
|
+
expect(r.type).toBe('quarter');
|
|
60
|
+
expect(r.year).toBe(2025);
|
|
61
|
+
expect(r.quarter).toBe(4);
|
|
62
|
+
expect(r.startDate).toBe('2025-10-01');
|
|
63
|
+
expect(r.endDate).toBe('2025-12-31');
|
|
64
|
+
expect(r.label).toBe('Q4 2025');
|
|
65
|
+
expect(r.months).toHaveLength(3);
|
|
66
|
+
expect(r.months[0].month).toBe(10);
|
|
67
|
+
expect(r.months[1].month).toBe(11);
|
|
68
|
+
expect(r.months[2].month).toBe(12);
|
|
69
|
+
});
|
|
70
|
+
it('parses "2025-Q2" → Apr-Jun 2025', () => {
|
|
71
|
+
const r = parseQuarterPeriod('2025-Q2');
|
|
72
|
+
expect(r.startDate).toBe('2025-04-01');
|
|
73
|
+
expect(r.endDate).toBe('2025-06-30');
|
|
74
|
+
});
|
|
75
|
+
it('is case-insensitive for the Q prefix', () => {
|
|
76
|
+
const r = parseQuarterPeriod('2025-q3');
|
|
77
|
+
expect(r.quarter).toBe(3);
|
|
78
|
+
expect(r.startDate).toBe('2025-07-01');
|
|
79
|
+
});
|
|
80
|
+
it('rejects Q5 with JobValidationError', () => {
|
|
81
|
+
expect(() => parseQuarterPeriod('2025-Q5')).toThrow(JobValidationError);
|
|
82
|
+
});
|
|
83
|
+
it('rejects Q0 with JobValidationError', () => {
|
|
84
|
+
expect(() => parseQuarterPeriod('2025-q0')).toThrow(JobValidationError);
|
|
85
|
+
});
|
|
86
|
+
it('rejects invalid format "2025Q1"', () => {
|
|
87
|
+
expect(() => parseQuarterPeriod('2025Q1')).toThrow(JobValidationError);
|
|
88
|
+
});
|
|
89
|
+
it('each month in the quarter has correct start and end dates', () => {
|
|
90
|
+
const r = parseQuarterPeriod('2025-Q1');
|
|
91
|
+
expect(r.months[0].startDate).toBe('2025-01-01');
|
|
92
|
+
expect(r.months[0].endDate).toBe('2025-01-31');
|
|
93
|
+
expect(r.months[1].startDate).toBe('2025-02-01');
|
|
94
|
+
expect(r.months[1].endDate).toBe('2025-02-28');
|
|
95
|
+
expect(r.months[2].startDate).toBe('2025-03-01');
|
|
96
|
+
expect(r.months[2].endDate).toBe('2025-03-31');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
describe('parseYearPeriod', () => {
|
|
100
|
+
it('parses "2025" → full year Jan-Dec with 4 quarters', () => {
|
|
101
|
+
const r = parseYearPeriod('2025');
|
|
102
|
+
expect(r.type).toBe('year');
|
|
103
|
+
expect(r.year).toBe(2025);
|
|
104
|
+
expect(r.startDate).toBe('2025-01-01');
|
|
105
|
+
expect(r.endDate).toBe('2025-12-31');
|
|
106
|
+
expect(r.label).toBe('FY2025');
|
|
107
|
+
expect(r.quarters).toHaveLength(4);
|
|
108
|
+
});
|
|
109
|
+
it('quarters span the correct months', () => {
|
|
110
|
+
const r = parseYearPeriod('2025');
|
|
111
|
+
expect(r.quarters[0].startDate).toBe('2025-01-01');
|
|
112
|
+
expect(r.quarters[0].endDate).toBe('2025-03-31');
|
|
113
|
+
expect(r.quarters[1].startDate).toBe('2025-04-01');
|
|
114
|
+
expect(r.quarters[1].endDate).toBe('2025-06-30');
|
|
115
|
+
expect(r.quarters[2].startDate).toBe('2025-07-01');
|
|
116
|
+
expect(r.quarters[2].endDate).toBe('2025-09-30');
|
|
117
|
+
expect(r.quarters[3].startDate).toBe('2025-10-01');
|
|
118
|
+
expect(r.quarters[3].endDate).toBe('2025-12-31');
|
|
119
|
+
});
|
|
120
|
+
it('each quarter has 3 months', () => {
|
|
121
|
+
const r = parseYearPeriod('2025');
|
|
122
|
+
for (const q of r.quarters) {
|
|
123
|
+
expect(q.months).toHaveLength(3);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
it('rejects non-numeric string "abcd"', () => {
|
|
127
|
+
expect(() => parseYearPeriod('abcd')).toThrow(JobValidationError);
|
|
128
|
+
});
|
|
129
|
+
it('rejects partial year "25"', () => {
|
|
130
|
+
expect(() => parseYearPeriod('25')).toThrow(JobValidationError);
|
|
131
|
+
});
|
|
132
|
+
it('rejects YYYY-MM format "2025-01"', () => {
|
|
133
|
+
expect(() => parseYearPeriod('2025-01')).toThrow(JobValidationError);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
describe('validateDateString', () => {
|
|
137
|
+
it('accepts a valid date "2025-06-15"', () => {
|
|
138
|
+
expect(() => validateDateString('2025-06-15', 'testDate')).not.toThrow();
|
|
139
|
+
});
|
|
140
|
+
it('accepts undefined (optional field)', () => {
|
|
141
|
+
expect(() => validateDateString(undefined, 'testDate')).not.toThrow();
|
|
142
|
+
});
|
|
143
|
+
it('rejects "not-a-date"', () => {
|
|
144
|
+
expect(() => validateDateString('not-a-date', 'testDate')).toThrow(JobValidationError);
|
|
145
|
+
});
|
|
146
|
+
it('rejects invalid month "2025-13-01"', () => {
|
|
147
|
+
expect(() => validateDateString('2025-13-01', 'testDate')).toThrow(JobValidationError);
|
|
148
|
+
});
|
|
149
|
+
it('rejects impossible calendar date "2025-02-31"', () => {
|
|
150
|
+
expect(() => validateDateString('2025-02-31', 'testDate')).toThrow(JobValidationError);
|
|
151
|
+
});
|
|
152
|
+
it('rejects impossible calendar date "2025-06-31"', () => {
|
|
153
|
+
expect(() => validateDateString('2025-06-31', 'testDate')).toThrow(JobValidationError);
|
|
154
|
+
});
|
|
155
|
+
it('rejects wrong format "06/15/2025"', () => {
|
|
156
|
+
expect(() => validateDateString('06/15/2025', 'testDate')).toThrow(JobValidationError);
|
|
157
|
+
});
|
|
158
|
+
it('error message includes the field name', () => {
|
|
159
|
+
expect(() => validateDateString('bad', 'myField')).toThrow(/myField/);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
describe('validateOverdueDays', () => {
|
|
163
|
+
it('accepts a valid positive integer 30', () => {
|
|
164
|
+
expect(() => validateOverdueDays(30)).not.toThrow();
|
|
165
|
+
});
|
|
166
|
+
it('accepts 1 (minimum valid)', () => {
|
|
167
|
+
expect(() => validateOverdueDays(1)).not.toThrow();
|
|
168
|
+
});
|
|
169
|
+
it('rejects 0', () => {
|
|
170
|
+
expect(() => validateOverdueDays(0)).toThrow(JobValidationError);
|
|
171
|
+
});
|
|
172
|
+
it('rejects negative values', () => {
|
|
173
|
+
expect(() => validateOverdueDays(-1)).toThrow(JobValidationError);
|
|
174
|
+
});
|
|
175
|
+
it('rejects non-integer 1.5', () => {
|
|
176
|
+
expect(() => validateOverdueDays(1.5)).toThrow(JobValidationError);
|
|
177
|
+
});
|
|
178
|
+
it('rejects non-integer 0.5', () => {
|
|
179
|
+
expect(() => validateOverdueDays(0.5)).toThrow(JobValidationError);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { generateYearEndBlueprint } from '../jobs/year-end.js';
|
|
3
|
+
import { JobValidationError } from '../jobs/validate.js';
|
|
4
|
+
describe('generateYearEndBlueprint', () => {
|
|
5
|
+
// Standalone mode (default)
|
|
6
|
+
describe('standalone mode', () => {
|
|
7
|
+
const base = { period: '2025' };
|
|
8
|
+
it('includes all quarterly phases plus annual extras and verification', () => {
|
|
9
|
+
const r = generateYearEndBlueprint(base);
|
|
10
|
+
// 4 quarters x 17 phases each (15 month-end + 2 quarterly) = 68
|
|
11
|
+
// + Phase 8 (annual extras) + Phase 9 (annual verification) = 70
|
|
12
|
+
expect(r.phases.length).toBeGreaterThan(0);
|
|
13
|
+
// Last two phases are annual extras and verification
|
|
14
|
+
const last = r.phases[r.phases.length - 1];
|
|
15
|
+
const secondLast = r.phases[r.phases.length - 2];
|
|
16
|
+
expect(secondLast.name).toBe('Phase 8: Annual Extras');
|
|
17
|
+
expect(last.name).toBe('Phase 9: Annual Verification');
|
|
18
|
+
});
|
|
19
|
+
it('has correct jobType', () => {
|
|
20
|
+
const r = generateYearEndBlueprint(base);
|
|
21
|
+
expect(r.jobType).toBe('year-end-close');
|
|
22
|
+
});
|
|
23
|
+
it('mode is standalone', () => {
|
|
24
|
+
const r = generateYearEndBlueprint(base);
|
|
25
|
+
expect(r.mode).toBe('standalone');
|
|
26
|
+
});
|
|
27
|
+
it('period label is "FY2025"', () => {
|
|
28
|
+
const r = generateYearEndBlueprint(base);
|
|
29
|
+
expect(r.period).toBe('FY2025');
|
|
30
|
+
});
|
|
31
|
+
it('currency defaults to "SGD"', () => {
|
|
32
|
+
const r = generateYearEndBlueprint(base);
|
|
33
|
+
expect(r.currency).toBe('SGD');
|
|
34
|
+
});
|
|
35
|
+
it('phase names are prefixed with quarter labels', () => {
|
|
36
|
+
const r = generateYearEndBlueprint(base);
|
|
37
|
+
// First batch of phases should contain Q1 prefix
|
|
38
|
+
expect(r.phases[0].name).toContain('Q1 2025');
|
|
39
|
+
});
|
|
40
|
+
it('annual extras has 7 steps', () => {
|
|
41
|
+
const r = generateYearEndBlueprint(base);
|
|
42
|
+
const extras = r.phases[r.phases.length - 2];
|
|
43
|
+
expect(extras.name).toBe('Phase 8: Annual Extras');
|
|
44
|
+
expect(extras.steps).toHaveLength(7);
|
|
45
|
+
});
|
|
46
|
+
it('annual verification has 3 steps', () => {
|
|
47
|
+
const r = generateYearEndBlueprint(base);
|
|
48
|
+
const verification = r.phases[r.phases.length - 1];
|
|
49
|
+
expect(verification.name).toBe('Phase 9: Annual Verification');
|
|
50
|
+
expect(verification.steps).toHaveLength(3);
|
|
51
|
+
});
|
|
52
|
+
it('annual extras ordering continues from last quarterly max order', () => {
|
|
53
|
+
const r = generateYearEndBlueprint(base);
|
|
54
|
+
// Each quarter ends with quarterly verification at order 26
|
|
55
|
+
// Annual extras starts at 27
|
|
56
|
+
const extras = r.phases[r.phases.length - 2];
|
|
57
|
+
expect(extras.steps[0].order).toBe(27);
|
|
58
|
+
});
|
|
59
|
+
it('annual verification follows annual extras ordering', () => {
|
|
60
|
+
const r = generateYearEndBlueprint(base);
|
|
61
|
+
const extras = r.phases[r.phases.length - 2];
|
|
62
|
+
const verification = r.phases[r.phases.length - 1];
|
|
63
|
+
const lastExtraOrder = extras.steps[extras.steps.length - 1].order;
|
|
64
|
+
expect(verification.steps[0].order).toBe(lastExtraOrder + 1);
|
|
65
|
+
});
|
|
66
|
+
it('summary totalSteps matches actual step count', () => {
|
|
67
|
+
const r = generateYearEndBlueprint(base);
|
|
68
|
+
const actual = r.phases.reduce((sum, p) => sum + p.steps.length, 0);
|
|
69
|
+
expect(r.summary.totalSteps).toBe(actual);
|
|
70
|
+
});
|
|
71
|
+
it('summary includes recipe references from monthly, quarterly, and annual steps', () => {
|
|
72
|
+
const r = generateYearEndBlueprint(base);
|
|
73
|
+
// Monthly recipe
|
|
74
|
+
expect(r.summary.recipeReferences).toContain('accrued-expenses');
|
|
75
|
+
// Quarterly recipe
|
|
76
|
+
expect(r.summary.recipeReferences).toContain('intercompany');
|
|
77
|
+
// Annual recipe
|
|
78
|
+
expect(r.summary.recipeReferences).toContain('dividend');
|
|
79
|
+
});
|
|
80
|
+
it('summary recipe references are sorted', () => {
|
|
81
|
+
const r = generateYearEndBlueprint(base);
|
|
82
|
+
const sorted = [...r.summary.recipeReferences].sort();
|
|
83
|
+
expect(r.summary.recipeReferences).toEqual(sorted);
|
|
84
|
+
});
|
|
85
|
+
it('annual extras API calls use full year dates', () => {
|
|
86
|
+
const r = generateYearEndBlueprint(base);
|
|
87
|
+
const extras = r.phases[r.phases.length - 2];
|
|
88
|
+
const gstStep = extras.steps.find(s => s.description.includes('GST'));
|
|
89
|
+
expect(gstStep).toBeDefined();
|
|
90
|
+
expect(gstStep.apiBody.startDate).toBe('2025-01-01');
|
|
91
|
+
expect(gstStep.apiBody.endDate).toBe('2025-12-31');
|
|
92
|
+
});
|
|
93
|
+
it('annual verification reports use full year dates', () => {
|
|
94
|
+
const r = generateYearEndBlueprint(base);
|
|
95
|
+
const verification = r.phases[r.phases.length - 1];
|
|
96
|
+
const trialBalance = verification.steps[0];
|
|
97
|
+
expect(trialBalance.apiBody.startDate).toBe('2025-01-01');
|
|
98
|
+
expect(trialBalance.apiBody.endDate).toBe('2025-12-31');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
// Incremental mode
|
|
102
|
+
describe('incremental mode', () => {
|
|
103
|
+
const base = { period: '2025', incremental: true };
|
|
104
|
+
it('has only annual extras and verification (no quarter/month phases)', () => {
|
|
105
|
+
const r = generateYearEndBlueprint(base);
|
|
106
|
+
expect(r.phases).toHaveLength(2);
|
|
107
|
+
expect(r.phases[0].name).toBe('Phase 8: Annual Extras');
|
|
108
|
+
expect(r.phases[1].name).toBe('Phase 9: Annual Verification');
|
|
109
|
+
});
|
|
110
|
+
it('mode is incremental', () => {
|
|
111
|
+
const r = generateYearEndBlueprint(base);
|
|
112
|
+
expect(r.mode).toBe('incremental');
|
|
113
|
+
});
|
|
114
|
+
it('step ordering starts at 1 for extras', () => {
|
|
115
|
+
const r = generateYearEndBlueprint(base);
|
|
116
|
+
expect(r.phases[0].steps[0].order).toBe(1);
|
|
117
|
+
});
|
|
118
|
+
it('step ordering is continuous across extras and verification', () => {
|
|
119
|
+
const r = generateYearEndBlueprint(base);
|
|
120
|
+
const allOrders = r.phases.flatMap(p => p.steps.map(s => s.order));
|
|
121
|
+
for (let i = 0; i < allOrders.length; i++) {
|
|
122
|
+
expect(allOrders[i]).toBe(i + 1);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
it('total steps = 7 extras + 3 verification = 10', () => {
|
|
126
|
+
const r = generateYearEndBlueprint(base);
|
|
127
|
+
expect(r.summary.totalSteps).toBe(10);
|
|
128
|
+
});
|
|
129
|
+
it('annual extras has 7 steps', () => {
|
|
130
|
+
const r = generateYearEndBlueprint(base);
|
|
131
|
+
expect(r.phases[0].steps).toHaveLength(7);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
// Custom currency
|
|
135
|
+
it('custom currency "MYR" passes through', () => {
|
|
136
|
+
const r = generateYearEndBlueprint({ period: '2025', currency: 'MYR' });
|
|
137
|
+
expect(r.currency).toBe('MYR');
|
|
138
|
+
});
|
|
139
|
+
// Validation
|
|
140
|
+
it('invalid period throws JobValidationError', () => {
|
|
141
|
+
expect(() => generateYearEndBlueprint({ period: 'bad' })).toThrow(JobValidationError);
|
|
142
|
+
});
|
|
143
|
+
it('"2025-Q1" is not a valid year period', () => {
|
|
144
|
+
expect(() => generateYearEndBlueprint({ period: '2025-Q1' })).toThrow(JobValidationError);
|
|
145
|
+
});
|
|
146
|
+
it('"abcd" throws JobValidationError', () => {
|
|
147
|
+
expect(() => generateYearEndBlueprint({ period: 'abcd' })).toThrow(JobValidationError);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { generateMonthEndBlueprint } from '../jobs/month-end.js';
|
|
3
|
+
import { generateQuarterEndBlueprint } from '../jobs/quarter-end.js';
|
|
4
|
+
import { generateYearEndBlueprint } from '../jobs/year-end.js';
|
|
5
|
+
import { generateBankReconBlueprint } from '../jobs/bank-recon.js';
|
|
6
|
+
import { generateGstVatBlueprint } from '../jobs/gst-vat.js';
|
|
7
|
+
import { generatePaymentRunBlueprint } from '../jobs/payment-run.js';
|
|
8
|
+
import { generateCreditControlBlueprint } from '../jobs/credit-control.js';
|
|
9
|
+
import { generateSupplierReconBlueprint } from '../jobs/supplier-recon.js';
|
|
10
|
+
import { generateAuditPrepBlueprint } from '../jobs/audit-prep.js';
|
|
11
|
+
import { generateFaReviewBlueprint } from '../jobs/fa-review.js';
|
|
12
|
+
import { printBlueprint, printBlueprintJson } from '../jobs/format.js';
|
|
13
|
+
import { JobValidationError } from '../jobs/validate.js';
|
|
14
|
+
/** Wrap job action with validation error handling. */
|
|
15
|
+
function jobAction(fn) {
|
|
16
|
+
return (opts) => {
|
|
17
|
+
try {
|
|
18
|
+
fn(opts);
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
if (err instanceof JobValidationError) {
|
|
22
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function registerJobsCommand(program) {
|
|
30
|
+
const jobs = program
|
|
31
|
+
.command('jobs')
|
|
32
|
+
.description('Accounting job blueprints — month-end, quarter-end, year-end, bank-recon, gst-vat, payment-run, credit-control, supplier-recon, audit-prep, fa-review');
|
|
33
|
+
// ── jaz jobs month-end ──────────────────────────────────────────
|
|
34
|
+
jobs
|
|
35
|
+
.command('month-end')
|
|
36
|
+
.description('Month-end close blueprint (5 phases, 18 steps)')
|
|
37
|
+
.requiredOption('--period <YYYY-MM>', 'Month period (e.g., 2025-01)')
|
|
38
|
+
.option('--currency <code>', 'Currency code (e.g. SGD, USD)')
|
|
39
|
+
.option('--json', 'Output as JSON')
|
|
40
|
+
.action(jobAction((opts) => {
|
|
41
|
+
const bp = generateMonthEndBlueprint({
|
|
42
|
+
period: opts.period,
|
|
43
|
+
currency: opts.currency,
|
|
44
|
+
});
|
|
45
|
+
opts.json ? printBlueprintJson(bp) : printBlueprint(bp);
|
|
46
|
+
}));
|
|
47
|
+
// ── jaz jobs quarter-end ────────────────────────────────────────
|
|
48
|
+
jobs
|
|
49
|
+
.command('quarter-end')
|
|
50
|
+
.description('Quarter-end close blueprint (monthly × 3 + quarterly extras)')
|
|
51
|
+
.requiredOption('--period <YYYY-QN>', 'Quarter period (e.g., 2025-Q1)')
|
|
52
|
+
.option('--incremental', 'Generate only quarterly extras (assumes months already closed)')
|
|
53
|
+
.option('--currency <code>', 'Currency code (e.g. SGD, USD)')
|
|
54
|
+
.option('--json', 'Output as JSON')
|
|
55
|
+
.action(jobAction((opts) => {
|
|
56
|
+
const bp = generateQuarterEndBlueprint({
|
|
57
|
+
period: opts.period,
|
|
58
|
+
currency: opts.currency,
|
|
59
|
+
incremental: opts.incremental,
|
|
60
|
+
});
|
|
61
|
+
opts.json ? printBlueprintJson(bp) : printBlueprint(bp);
|
|
62
|
+
}));
|
|
63
|
+
// ── jaz jobs year-end ───────────────────────────────────────────
|
|
64
|
+
jobs
|
|
65
|
+
.command('year-end')
|
|
66
|
+
.description('Year-end close blueprint (quarterly × 4 + annual extras)')
|
|
67
|
+
.requiredOption('--period <YYYY>', 'Fiscal year (e.g., 2025)')
|
|
68
|
+
.option('--incremental', 'Generate only annual extras (assumes quarters already closed)')
|
|
69
|
+
.option('--currency <code>', 'Currency code (e.g. SGD, USD)')
|
|
70
|
+
.option('--json', 'Output as JSON')
|
|
71
|
+
.action(jobAction((opts) => {
|
|
72
|
+
const bp = generateYearEndBlueprint({
|
|
73
|
+
period: opts.period,
|
|
74
|
+
currency: opts.currency,
|
|
75
|
+
incremental: opts.incremental,
|
|
76
|
+
});
|
|
77
|
+
opts.json ? printBlueprintJson(bp) : printBlueprint(bp);
|
|
78
|
+
}));
|
|
79
|
+
// ── jaz jobs bank-recon ─────────────────────────────────────────
|
|
80
|
+
jobs
|
|
81
|
+
.command('bank-recon')
|
|
82
|
+
.description('Bank reconciliation catch-up blueprint')
|
|
83
|
+
.option('--account <name>', 'Specific bank account name')
|
|
84
|
+
.option('--period <YYYY-MM>', 'Month period to reconcile')
|
|
85
|
+
.option('--currency <code>', 'Currency code (e.g. SGD, USD)')
|
|
86
|
+
.option('--json', 'Output as JSON')
|
|
87
|
+
.action(jobAction((opts) => {
|
|
88
|
+
const bp = generateBankReconBlueprint({
|
|
89
|
+
account: opts.account,
|
|
90
|
+
period: opts.period,
|
|
91
|
+
currency: opts.currency,
|
|
92
|
+
});
|
|
93
|
+
opts.json ? printBlueprintJson(bp) : printBlueprint(bp);
|
|
94
|
+
}));
|
|
95
|
+
// ── jaz jobs gst-vat ────────────────────────────────────────────
|
|
96
|
+
jobs
|
|
97
|
+
.command('gst-vat')
|
|
98
|
+
.description('GST/VAT filing preparation blueprint')
|
|
99
|
+
.requiredOption('--period <YYYY-QN>', 'Quarter period (e.g., 2025-Q1)')
|
|
100
|
+
.option('--currency <code>', 'Currency code (e.g. SGD, USD)')
|
|
101
|
+
.option('--json', 'Output as JSON')
|
|
102
|
+
.action(jobAction((opts) => {
|
|
103
|
+
const bp = generateGstVatBlueprint({
|
|
104
|
+
period: opts.period,
|
|
105
|
+
currency: opts.currency,
|
|
106
|
+
});
|
|
107
|
+
opts.json ? printBlueprintJson(bp) : printBlueprint(bp);
|
|
108
|
+
}));
|
|
109
|
+
// ── jaz jobs payment-run ────────────────────────────────────────
|
|
110
|
+
jobs
|
|
111
|
+
.command('payment-run')
|
|
112
|
+
.description('Payment run blueprint (bulk bill payments)')
|
|
113
|
+
.option('--due-before <YYYY-MM-DD>', 'Pay bills due on or before this date')
|
|
114
|
+
.option('--currency <code>', 'Currency code (e.g. SGD, USD)')
|
|
115
|
+
.option('--json', 'Output as JSON')
|
|
116
|
+
.action(jobAction((opts) => {
|
|
117
|
+
const bp = generatePaymentRunBlueprint({
|
|
118
|
+
dueBefore: opts.dueBefore,
|
|
119
|
+
currency: opts.currency,
|
|
120
|
+
});
|
|
121
|
+
opts.json ? printBlueprintJson(bp) : printBlueprint(bp);
|
|
122
|
+
}));
|
|
123
|
+
// ── jaz jobs credit-control ─────────────────────────────────────
|
|
124
|
+
jobs
|
|
125
|
+
.command('credit-control')
|
|
126
|
+
.description('Credit control / AR chase blueprint')
|
|
127
|
+
.option('--overdue-days <days>', 'Minimum overdue days to include', (v) => {
|
|
128
|
+
const n = Number.parseInt(v, 10);
|
|
129
|
+
if (!Number.isFinite(n))
|
|
130
|
+
throw new JobValidationError(`overdue-days must be an integer (got "${v}")`);
|
|
131
|
+
return n;
|
|
132
|
+
})
|
|
133
|
+
.option('--currency <code>', 'Currency code (e.g. SGD, USD)')
|
|
134
|
+
.option('--json', 'Output as JSON')
|
|
135
|
+
.action(jobAction((opts) => {
|
|
136
|
+
const bp = generateCreditControlBlueprint({
|
|
137
|
+
overdueDays: opts.overdueDays,
|
|
138
|
+
currency: opts.currency,
|
|
139
|
+
});
|
|
140
|
+
opts.json ? printBlueprintJson(bp) : printBlueprint(bp);
|
|
141
|
+
}));
|
|
142
|
+
// ── jaz jobs supplier-recon ─────────────────────────────────────
|
|
143
|
+
jobs
|
|
144
|
+
.command('supplier-recon')
|
|
145
|
+
.description('Supplier statement reconciliation blueprint')
|
|
146
|
+
.option('--supplier <name>', 'Specific supplier name')
|
|
147
|
+
.option('--period <YYYY-MM>', 'Month period')
|
|
148
|
+
.option('--currency <code>', 'Currency code (e.g. SGD, USD)')
|
|
149
|
+
.option('--json', 'Output as JSON')
|
|
150
|
+
.action(jobAction((opts) => {
|
|
151
|
+
const bp = generateSupplierReconBlueprint({
|
|
152
|
+
supplier: opts.supplier,
|
|
153
|
+
period: opts.period,
|
|
154
|
+
currency: opts.currency,
|
|
155
|
+
});
|
|
156
|
+
opts.json ? printBlueprintJson(bp) : printBlueprint(bp);
|
|
157
|
+
}));
|
|
158
|
+
// ── jaz jobs audit-prep ─────────────────────────────────────────
|
|
159
|
+
jobs
|
|
160
|
+
.command('audit-prep')
|
|
161
|
+
.description('Audit preparation pack blueprint')
|
|
162
|
+
.requiredOption('--period <YYYY>', 'Fiscal year (e.g., 2025)')
|
|
163
|
+
.option('--currency <code>', 'Currency code (e.g. SGD, USD)')
|
|
164
|
+
.option('--json', 'Output as JSON')
|
|
165
|
+
.action(jobAction((opts) => {
|
|
166
|
+
const bp = generateAuditPrepBlueprint({
|
|
167
|
+
period: opts.period,
|
|
168
|
+
currency: opts.currency,
|
|
169
|
+
});
|
|
170
|
+
opts.json ? printBlueprintJson(bp) : printBlueprint(bp);
|
|
171
|
+
}));
|
|
172
|
+
// ── jaz jobs fa-review ──────────────────────────────────────────
|
|
173
|
+
jobs
|
|
174
|
+
.command('fa-review')
|
|
175
|
+
.description('Fixed asset register review blueprint')
|
|
176
|
+
.option('--currency <code>', 'Currency code (e.g. SGD, USD)')
|
|
177
|
+
.option('--json', 'Output as JSON')
|
|
178
|
+
.action(jobAction((opts) => {
|
|
179
|
+
const bp = generateFaReviewBlueprint({
|
|
180
|
+
currency: opts.currency,
|
|
181
|
+
});
|
|
182
|
+
opts.json ? printBlueprintJson(bp) : printBlueprint(bp);
|
|
183
|
+
}));
|
|
184
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { initCommand } from './commands/init.js';
|
|
|
7
7
|
import { versionsCommand } from './commands/versions.js';
|
|
8
8
|
import { updateCommand } from './commands/update.js';
|
|
9
9
|
import { registerCalcCommand } from './commands/calc.js';
|
|
10
|
+
import { registerJobsCommand } from './commands/jobs.js';
|
|
10
11
|
import { SKILL_TYPES } from './types/index.js';
|
|
11
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
13
|
const __dirname = dirname(__filename);
|
|
@@ -51,4 +52,5 @@ program
|
|
|
51
52
|
});
|
|
52
53
|
});
|
|
53
54
|
registerCalcCommand(program);
|
|
55
|
+
registerJobsCommand(program);
|
|
54
56
|
program.parse();
|