jaz-cli 2.7.0 → 2.9.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 (68) 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 +161 -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/sg-tax/add-backs-guide.md +354 -0
  14. package/assets/skills/jobs/references/sg-tax/capital-allowances-guide.md +343 -0
  15. package/assets/skills/jobs/references/sg-tax/data-extraction.md +408 -0
  16. package/assets/skills/jobs/references/sg-tax/enhanced-deductions.md +248 -0
  17. package/assets/skills/jobs/references/sg-tax/exemptions-and-rebates.md +197 -0
  18. package/assets/skills/jobs/references/sg-tax/form-cs-fields.md +191 -0
  19. package/assets/skills/jobs/references/sg-tax/ifrs16-tax-adjustment.md +194 -0
  20. package/assets/skills/jobs/references/sg-tax/losses-and-carry-forwards.md +269 -0
  21. package/assets/skills/jobs/references/sg-tax/overview.md +207 -0
  22. package/assets/skills/jobs/references/sg-tax/wizard-workflow.md +391 -0
  23. package/assets/skills/jobs/references/supplier-recon.md +330 -0
  24. package/assets/skills/jobs/references/year-end-close.md +341 -0
  25. package/assets/skills/transaction-recipes/SKILL.md +1 -1
  26. package/dist/__tests__/jobs-audit-prep.test.js +125 -0
  27. package/dist/__tests__/jobs-bank-recon.test.js +108 -0
  28. package/dist/__tests__/jobs-credit-control.test.js +98 -0
  29. package/dist/__tests__/jobs-fa-review.test.js +104 -0
  30. package/dist/__tests__/jobs-gst-vat.test.js +113 -0
  31. package/dist/__tests__/jobs-month-end.test.js +162 -0
  32. package/dist/__tests__/jobs-payment-run.test.js +106 -0
  33. package/dist/__tests__/jobs-quarter-end.test.js +155 -0
  34. package/dist/__tests__/jobs-supplier-recon.test.js +115 -0
  35. package/dist/__tests__/jobs-validate.test.js +181 -0
  36. package/dist/__tests__/jobs-year-end.test.js +149 -0
  37. package/dist/__tests__/tax-sg-capital-allowances.test.js +389 -0
  38. package/dist/__tests__/tax-sg-exemptions.test.js +232 -0
  39. package/dist/__tests__/tax-sg-form-cs.test.js +687 -0
  40. package/dist/__tests__/tax-validate.test.js +208 -0
  41. package/dist/commands/init.js +7 -2
  42. package/dist/commands/jobs.js +184 -0
  43. package/dist/commands/tax.js +195 -0
  44. package/dist/index.js +4 -0
  45. package/dist/jobs/audit-prep.js +211 -0
  46. package/dist/jobs/bank-recon.js +163 -0
  47. package/dist/jobs/credit-control.js +126 -0
  48. package/dist/jobs/fa-review.js +121 -0
  49. package/dist/jobs/format.js +102 -0
  50. package/dist/jobs/gst-vat.js +187 -0
  51. package/dist/jobs/month-end.js +232 -0
  52. package/dist/jobs/payment-run.js +199 -0
  53. package/dist/jobs/quarter-end.js +135 -0
  54. package/dist/jobs/supplier-recon.js +132 -0
  55. package/dist/jobs/types.js +36 -0
  56. package/dist/jobs/validate.js +115 -0
  57. package/dist/jobs/year-end.js +153 -0
  58. package/dist/tax/format.js +18 -0
  59. package/dist/tax/sg/capital-allowances.js +160 -0
  60. package/dist/tax/sg/constants.js +63 -0
  61. package/dist/tax/sg/exemptions.js +76 -0
  62. package/dist/tax/sg/form-cs.js +349 -0
  63. package/dist/tax/sg/format-sg.js +134 -0
  64. package/dist/tax/types.js +9 -0
  65. package/dist/tax/validate.js +124 -0
  66. package/dist/types/index.js +2 -1
  67. package/dist/utils/template.js +1 -1
  68. package/package.json +1 -1
@@ -0,0 +1,208 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { TaxValidationError, validateYa, validateNonNegative, validateExemptionType, validateAssetCategory, validateDateFormat, validateFormCsInput, validateCaInput, } from '../tax/validate.js';
3
+ // ── validateYa ──────────────────────────────────────────────────
4
+ describe('validateYa', () => {
5
+ it('accepts valid YA 2026', () => {
6
+ expect(() => validateYa(2026)).not.toThrow();
7
+ });
8
+ it('rejects YA below range (2019)', () => {
9
+ expect(() => validateYa(2019)).toThrow('between 2020 and 2100');
10
+ });
11
+ it('rejects YA above range (2101)', () => {
12
+ expect(() => validateYa(2101)).toThrow('between 2020 and 2100');
13
+ });
14
+ it('rejects NaN', () => {
15
+ expect(() => validateYa(NaN)).toThrow('must be an integer');
16
+ });
17
+ it('rejects non-integer (2025.5)', () => {
18
+ expect(() => validateYa(2025.5)).toThrow('must be an integer');
19
+ });
20
+ });
21
+ // ── validateNonNegative ─────────────────────────────────────────
22
+ describe('validateNonNegative', () => {
23
+ it('accepts zero', () => {
24
+ expect(() => validateNonNegative(0, 'amount')).not.toThrow();
25
+ });
26
+ it('accepts positive number', () => {
27
+ expect(() => validateNonNegative(100, 'amount')).not.toThrow();
28
+ });
29
+ it('rejects negative number', () => {
30
+ expect(() => validateNonNegative(-1, 'amount')).toThrow('must be zero or positive');
31
+ });
32
+ it('rejects NaN', () => {
33
+ expect(() => validateNonNegative(NaN, 'amount')).toThrow('must be zero or positive');
34
+ });
35
+ it('rejects Infinity', () => {
36
+ expect(() => validateNonNegative(Infinity, 'amount')).toThrow('must be zero or positive');
37
+ });
38
+ });
39
+ // ── validateExemptionType ───────────────────────────────────────
40
+ describe('validateExemptionType', () => {
41
+ it('accepts sute', () => {
42
+ expect(() => validateExemptionType('sute')).not.toThrow();
43
+ });
44
+ it('accepts pte', () => {
45
+ expect(() => validateExemptionType('pte')).not.toThrow();
46
+ });
47
+ it('accepts none', () => {
48
+ expect(() => validateExemptionType('none')).not.toThrow();
49
+ });
50
+ it('rejects invalid type', () => {
51
+ expect(() => validateExemptionType('invalid')).toThrow('Must be one of: sute, pte, none');
52
+ });
53
+ });
54
+ // ── validateAssetCategory ───────────────────────────────────────
55
+ describe('validateAssetCategory', () => {
56
+ it('accepts computer', () => {
57
+ expect(() => validateAssetCategory('computer')).not.toThrow();
58
+ });
59
+ it('rejects invalid category', () => {
60
+ expect(() => validateAssetCategory('invalid')).toThrow('Must be one of: computer, automation, low-value, general, ip, renovation');
61
+ });
62
+ });
63
+ // ── validateDateFormat ──────────────────────────────────────────
64
+ describe('validateDateFormat', () => {
65
+ it('accepts valid date 2025-01-01', () => {
66
+ expect(() => validateDateFormat('2025-01-01', 'startDate')).not.toThrow();
67
+ });
68
+ it('rejects invalid month (2025-13-01)', () => {
69
+ expect(() => validateDateFormat('2025-13-01', 'startDate')).toThrow(TaxValidationError);
70
+ });
71
+ it('rejects invalid day (2025-02-30)', () => {
72
+ expect(() => validateDateFormat('2025-02-30', 'startDate')).toThrow(TaxValidationError);
73
+ });
74
+ it('rejects non-date string', () => {
75
+ expect(() => validateDateFormat('not-a-date', 'startDate')).toThrow('YYYY-MM-DD');
76
+ });
77
+ });
78
+ // ── validateFormCsInput ─────────────────────────────────────────
79
+ describe('validateFormCsInput', () => {
80
+ const validInput = {
81
+ ya: 2026,
82
+ basisPeriodStart: '2025-01-01',
83
+ basisPeriodEnd: '2025-12-31',
84
+ revenue: 500000,
85
+ accountingProfit: 100000,
86
+ exemptionType: 'pte',
87
+ addBacks: {
88
+ depreciation: 0,
89
+ amortization: 0,
90
+ rouDepreciation: 0,
91
+ leaseInterest: 0,
92
+ generalProvisions: 0,
93
+ donations: 0,
94
+ entertainment: 0,
95
+ penalties: 0,
96
+ privateCar: 0,
97
+ capitalExpOnPnl: 0,
98
+ unrealizedFxLoss: 0,
99
+ otherNonDeductible: 0,
100
+ },
101
+ deductions: {
102
+ actualLeasePayments: 0,
103
+ unrealizedFxGain: 0,
104
+ exemptDividends: 0,
105
+ exemptIncome: 0,
106
+ otherDeductions: 0,
107
+ },
108
+ capitalAllowances: {
109
+ currentYearClaim: 0,
110
+ balanceBroughtForward: 0,
111
+ },
112
+ enhancedDeductions: {
113
+ rdExpenditure: 0,
114
+ rdMultiplier: 2.5,
115
+ ipRegistration: 0,
116
+ ipMultiplier: 2.0,
117
+ donations250Base: 0,
118
+ s14qRenovation: 0,
119
+ },
120
+ losses: { broughtForward: 0 },
121
+ donationsCarryForward: { broughtForward: 0 },
122
+ };
123
+ it('accepts valid complete input', () => {
124
+ expect(() => validateFormCsInput(validInput)).not.toThrow();
125
+ });
126
+ it('rejects negative depreciation add-back', () => {
127
+ const bad = {
128
+ ...validInput,
129
+ addBacks: { ...validInput.addBacks, depreciation: -100 },
130
+ };
131
+ expect(() => validateFormCsInput(bad)).toThrow('must be zero or positive');
132
+ });
133
+ it('rejects basisPeriodStart after basisPeriodEnd', () => {
134
+ const bad = {
135
+ ...validInput,
136
+ basisPeriodStart: '2025-12-31',
137
+ basisPeriodEnd: '2025-01-01',
138
+ };
139
+ expect(() => validateFormCsInput(bad)).toThrow('must be on or before');
140
+ });
141
+ it('accepts basisPeriodStart equal to basisPeriodEnd', () => {
142
+ const same = {
143
+ ...validInput,
144
+ basisPeriodStart: '2025-06-15',
145
+ basisPeriodEnd: '2025-06-15',
146
+ };
147
+ expect(() => validateFormCsInput(same)).not.toThrow();
148
+ });
149
+ });
150
+ // ── validateCaInput ─────────────────────────────────────────────
151
+ describe('validateCaInput', () => {
152
+ const validAsset = {
153
+ description: 'Laptop',
154
+ cost: 3000,
155
+ acquisitionDate: '2025-03-15',
156
+ category: 'computer',
157
+ priorYearsClaimed: 0,
158
+ };
159
+ const validInput = {
160
+ ya: 2026,
161
+ assets: [validAsset],
162
+ unabsorbedBroughtForward: 0,
163
+ };
164
+ it('accepts valid input with one asset', () => {
165
+ expect(() => validateCaInput(validInput)).not.toThrow();
166
+ });
167
+ it('rejects empty assets array', () => {
168
+ expect(() => validateCaInput({ ...validInput, assets: [] })).toThrow('At least one asset');
169
+ });
170
+ it('rejects priorYearsClaimed exceeding cost', () => {
171
+ const bad = {
172
+ ...validInput,
173
+ assets: [{ ...validAsset, cost: 1000, priorYearsClaimed: 1500 }],
174
+ };
175
+ expect(() => validateCaInput(bad)).toThrow('exceeds cost');
176
+ });
177
+ it('rejects low-value asset with cost above $5,000', () => {
178
+ const bad = {
179
+ ...validInput,
180
+ assets: [{ ...validAsset, category: 'low-value', cost: 6000 }],
181
+ };
182
+ expect(() => validateCaInput(bad)).toThrow('exceeds $5,000 threshold');
183
+ });
184
+ it('rejects IP asset with invalid ipWriteOffYears (7)', () => {
185
+ const bad = {
186
+ ...validInput,
187
+ assets: [{
188
+ ...validAsset,
189
+ category: 'ip',
190
+ ipWriteOffYears: 7,
191
+ }],
192
+ };
193
+ expect(() => validateCaInput(bad)).toThrow('must be 5, 10, or 15');
194
+ });
195
+ it('accepts IP asset with valid ipWriteOffYears (5, 10, 15)', () => {
196
+ for (const years of [5, 10, 15]) {
197
+ const ok = {
198
+ ...validInput,
199
+ assets: [{
200
+ ...validAsset,
201
+ category: 'ip',
202
+ ipWriteOffYears: years,
203
+ }],
204
+ };
205
+ expect(() => validateCaInput(ok)).not.toThrow();
206
+ }
207
+ });
208
+ });
@@ -16,7 +16,7 @@ export async function initCommand(options) {
16
16
  choices: [
17
17
  {
18
18
  title: `All (Recommended)`,
19
- description: 'API reference + data conversion + transaction recipes',
19
+ description: 'API reference + data conversion + transaction recipes + accounting jobs',
20
20
  value: 'all',
21
21
  },
22
22
  {
@@ -34,6 +34,11 @@ export async function initCommand(options) {
34
34
  description: SKILL_DESCRIPTIONS['transaction-recipes'],
35
35
  value: 'transaction-recipes',
36
36
  },
37
+ {
38
+ title: 'Jobs only',
39
+ description: SKILL_DESCRIPTIONS.jobs,
40
+ value: 'jobs',
41
+ },
37
42
  ],
38
43
  initial: 0,
39
44
  });
@@ -44,7 +49,7 @@ export async function initCommand(options) {
44
49
  skillType = response.skill;
45
50
  }
46
51
  const skillLabel = skillType === 'all'
47
- ? 'api + conversion + transaction-recipes'
52
+ ? 'api + conversion + transaction-recipes + jobs'
48
53
  : skillType;
49
54
  logger.info(`Installing: ${chalk.cyan(skillLabel)}`);
50
55
  const spinner = ora('Installing skill files...').start();
@@ -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|YYYY-QN>', 'Fiscal year or quarter (e.g., 2025 or 2025-Q3)')
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
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * `jaz tax` command group — Singapore corporate income tax.
3
+ *
4
+ * Commands:
5
+ * jaz tax sg-cs — Form C-S / C-S Lite computation
6
+ * jaz tax sg-ca — Capital allowance schedule
7
+ */
8
+ import chalk from 'chalk';
9
+ import { readFileSync } from 'fs';
10
+ import { TaxValidationError } from '../tax/validate.js';
11
+ import { computeFormCs } from '../tax/sg/form-cs.js';
12
+ import { computeCapitalAllowances } from '../tax/sg/capital-allowances.js';
13
+ import { printTaxResult, printTaxJson } from '../tax/format.js';
14
+ /** Wrap tax action with validation error handling. */
15
+ function taxAction(fn) {
16
+ return (opts) => {
17
+ try {
18
+ fn(opts);
19
+ }
20
+ catch (err) {
21
+ if (err instanceof TaxValidationError) {
22
+ console.error(chalk.red(`Error: ${err.message}`));
23
+ process.exit(1);
24
+ }
25
+ throw err;
26
+ }
27
+ };
28
+ }
29
+ /** Read JSON input from --input file or stdin. */
30
+ function readJsonInput(opts) {
31
+ const inputFile = opts.input;
32
+ if (inputFile) {
33
+ const raw = readFileSync(inputFile, 'utf-8');
34
+ return JSON.parse(raw);
35
+ }
36
+ // Check if stdin has data (piped input) — use fd 0 for cross-platform support
37
+ if (!process.stdin.isTTY) {
38
+ try {
39
+ const raw = readFileSync(0, 'utf-8').trim();
40
+ if (raw)
41
+ return JSON.parse(raw);
42
+ }
43
+ catch {
44
+ // No stdin data available
45
+ }
46
+ }
47
+ return null;
48
+ }
49
+ /** Build a default SgFormCsInput structure with zeros. */
50
+ function buildDefaultInput(opts) {
51
+ return {
52
+ ya: opts.ya,
53
+ basisPeriodStart: opts.basisStart ?? `${opts.ya - 1}-01-01`,
54
+ basisPeriodEnd: opts.basisEnd ?? `${opts.ya - 1}-12-31`,
55
+ currency: opts.currency,
56
+ revenue: opts.revenue ?? 0,
57
+ accountingProfit: opts.profit ?? 0,
58
+ addBacks: {
59
+ depreciation: opts.depreciation ?? 0,
60
+ amortization: opts.amortization ?? 0,
61
+ rouDepreciation: opts.rouDepreciation ?? 0,
62
+ leaseInterest: opts.leaseInterest ?? 0,
63
+ generalProvisions: opts.provisions ?? 0,
64
+ donations: opts.donations ?? 0,
65
+ entertainment: opts.entertainment ?? 0,
66
+ penalties: opts.penalties ?? 0,
67
+ privateCar: opts.privateCar ?? 0,
68
+ capitalExpOnPnl: 0,
69
+ unrealizedFxLoss: 0,
70
+ otherNonDeductible: 0,
71
+ },
72
+ deductions: {
73
+ actualLeasePayments: opts.leasePayments ?? 0,
74
+ unrealizedFxGain: 0,
75
+ exemptDividends: 0,
76
+ exemptIncome: 0,
77
+ otherDeductions: 0,
78
+ },
79
+ capitalAllowances: {
80
+ currentYearClaim: opts.ca ?? 0,
81
+ balanceBroughtForward: opts.caBf ?? 0,
82
+ },
83
+ enhancedDeductions: {
84
+ rdExpenditure: 0,
85
+ rdMultiplier: 2.5,
86
+ ipRegistration: 0,
87
+ ipMultiplier: 2.0,
88
+ donations250Base: opts.donations ?? 0,
89
+ s14qRenovation: 0,
90
+ },
91
+ losses: {
92
+ broughtForward: opts.lossesBf ?? 0,
93
+ },
94
+ donationsCarryForward: {
95
+ broughtForward: opts.donationsBf ?? 0,
96
+ },
97
+ exemptionType: opts.exemption ?? 'pte',
98
+ };
99
+ }
100
+ export function registerTaxCommand(program) {
101
+ const tax = program
102
+ .command('tax')
103
+ .description('Corporate income tax computation — Singapore Form C-S / C-S Lite');
104
+ // ── jaz tax sg-cs ────────────────────────────────────────────────
105
+ tax
106
+ .command('sg-cs')
107
+ .description('Singapore Form C-S / C-S Lite corporate income tax computation')
108
+ .option('--input <file>', 'JSON input file (full SgFormCsInput structure)')
109
+ .option('--ya <year>', 'Year of Assessment', parseInt)
110
+ .option('--revenue <amount>', 'Total revenue', parseFloat)
111
+ .option('--profit <amount>', 'Accounting net profit/loss', parseFloat)
112
+ .option('--depreciation <amount>', 'Accounting depreciation (add-back)', parseFloat)
113
+ .option('--amortization <amount>', 'Intangible amortization (add-back)', parseFloat)
114
+ .option('--rou-depreciation <amount>', 'IFRS 16 ROU depreciation (add-back)', parseFloat)
115
+ .option('--lease-interest <amount>', 'IFRS 16 lease interest (add-back)', parseFloat)
116
+ .option('--lease-payments <amount>', 'Actual lease payments (deduction)', parseFloat)
117
+ .option('--provisions <amount>', 'General provisions (add-back)', parseFloat)
118
+ .option('--donations <amount>', 'IPC donations (add-back, claimed at 250%)', parseFloat)
119
+ .option('--entertainment <amount>', 'Non-deductible entertainment', parseFloat)
120
+ .option('--penalties <amount>', 'Penalties & fines', parseFloat)
121
+ .option('--private-car <amount>', 'S-plated vehicle expenses', parseFloat)
122
+ .option('--ca <amount>', 'Current year capital allowances', parseFloat)
123
+ .option('--ca-bf <amount>', 'Unabsorbed CA brought forward', parseFloat)
124
+ .option('--losses-bf <amount>', 'Unabsorbed trade losses b/f', parseFloat)
125
+ .option('--donations-bf <amount>', 'Unabsorbed donations b/f', parseFloat)
126
+ .option('--exemption <type>', 'Exemption type: sute, pte (default), none', 'pte')
127
+ .option('--basis-start <date>', 'Basis period start (YYYY-MM-DD)')
128
+ .option('--basis-end <date>', 'Basis period end (YYYY-MM-DD)')
129
+ .option('--currency <code>', 'Currency code (default: SGD)')
130
+ .option('--json', 'Output as JSON')
131
+ .action(taxAction((opts) => {
132
+ // Full JSON input mode (from file or stdin)
133
+ const jsonInput = readJsonInput(opts);
134
+ let input;
135
+ if (jsonInput) {
136
+ input = jsonInput;
137
+ }
138
+ else {
139
+ // Simple flag mode
140
+ if (!opts.ya) {
141
+ console.error(chalk.red('Error: --ya (Year of Assessment) is required'));
142
+ process.exit(1);
143
+ }
144
+ input = buildDefaultInput(opts);
145
+ }
146
+ const result = computeFormCs(input);
147
+ opts.json ? printTaxJson(result) : printTaxResult(result);
148
+ }));
149
+ // ── jaz tax sg-ca ────────────────────────────────────────────────
150
+ tax
151
+ .command('sg-ca')
152
+ .description('Singapore capital allowance schedule (per-asset computation)')
153
+ .option('--input <file>', 'JSON input file (full SgCapitalAllowanceInput structure)')
154
+ .option('--ya <year>', 'Year of Assessment', parseInt)
155
+ .option('--cost <amount>', 'Asset cost (simple single-asset mode)', parseFloat)
156
+ .option('--category <cat>', 'Asset category: computer, automation, low-value, general, ip, renovation')
157
+ .option('--acquired <date>', 'Acquisition date (YYYY-MM-DD)')
158
+ .option('--prior-claimed <amount>', 'CA already claimed in prior YAs', parseFloat, 0)
159
+ .option('--ip-years <years>', 'IP write-off period (5, 10, or 15)', parseInt)
160
+ .option('--unabsorbed-bf <amount>', 'Unabsorbed CA brought forward', parseFloat, 0)
161
+ .option('--currency <code>', 'Currency code (default: SGD)')
162
+ .option('--json', 'Output as JSON')
163
+ .action(taxAction((opts) => {
164
+ const jsonInput = readJsonInput(opts);
165
+ let input;
166
+ if (jsonInput) {
167
+ input = jsonInput;
168
+ }
169
+ else {
170
+ if (!opts.ya) {
171
+ console.error(chalk.red('Error: --ya (Year of Assessment) is required'));
172
+ process.exit(1);
173
+ }
174
+ if (!opts.cost || !opts.category || !opts.acquired) {
175
+ console.error(chalk.red('Error: --cost, --category, and --acquired are required in simple mode'));
176
+ process.exit(1);
177
+ }
178
+ input = {
179
+ ya: opts.ya,
180
+ currency: opts.currency,
181
+ unabsorbedBroughtForward: opts.unabsorbedBf,
182
+ assets: [{
183
+ description: `${opts.category.charAt(0).toUpperCase() + opts.category.slice(1)} asset`,
184
+ cost: opts.cost,
185
+ acquisitionDate: opts.acquired,
186
+ category: opts.category,
187
+ priorYearsClaimed: opts.priorClaimed,
188
+ ipWriteOffYears: opts.ipYears,
189
+ }],
190
+ };
191
+ }
192
+ const result = computeCapitalAllowances(input);
193
+ opts.json ? printTaxJson(result) : printTaxResult(result);
194
+ }));
195
+ }
package/dist/index.js CHANGED
@@ -7,6 +7,8 @@ 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';
11
+ import { registerTaxCommand } from './commands/tax.js';
10
12
  import { SKILL_TYPES } from './types/index.js';
11
13
  const __filename = fileURLToPath(import.meta.url);
12
14
  const __dirname = dirname(__filename);
@@ -51,4 +53,6 @@ program
51
53
  });
52
54
  });
53
55
  registerCalcCommand(program);
56
+ registerJobsCommand(program);
57
+ registerTaxCommand(program);
54
58
  program.parse();