jaz-cli 2.8.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.
- package/assets/skills/api/SKILL.md +1 -1
- package/assets/skills/conversion/SKILL.md +1 -1
- package/assets/skills/jobs/SKILL.md +58 -1
- package/assets/skills/jobs/references/sg-tax/add-backs-guide.md +354 -0
- package/assets/skills/jobs/references/sg-tax/capital-allowances-guide.md +343 -0
- package/assets/skills/jobs/references/sg-tax/data-extraction.md +408 -0
- package/assets/skills/jobs/references/sg-tax/enhanced-deductions.md +248 -0
- package/assets/skills/jobs/references/sg-tax/exemptions-and-rebates.md +197 -0
- package/assets/skills/jobs/references/sg-tax/form-cs-fields.md +191 -0
- package/assets/skills/jobs/references/sg-tax/ifrs16-tax-adjustment.md +194 -0
- package/assets/skills/jobs/references/sg-tax/losses-and-carry-forwards.md +269 -0
- package/assets/skills/jobs/references/sg-tax/overview.md +207 -0
- package/assets/skills/jobs/references/sg-tax/wizard-workflow.md +391 -0
- package/assets/skills/transaction-recipes/SKILL.md +1 -1
- package/dist/__tests__/jobs-audit-prep.test.js +3 -3
- package/dist/__tests__/jobs-bank-recon.test.js +5 -5
- package/dist/__tests__/jobs-credit-control.test.js +1 -1
- package/dist/__tests__/jobs-fa-review.test.js +1 -1
- package/dist/__tests__/jobs-payment-run.test.js +3 -3
- package/dist/__tests__/jobs-supplier-recon.test.js +6 -6
- package/dist/__tests__/tax-sg-capital-allowances.test.js +389 -0
- package/dist/__tests__/tax-sg-exemptions.test.js +232 -0
- package/dist/__tests__/tax-sg-form-cs.test.js +687 -0
- package/dist/__tests__/tax-validate.test.js +208 -0
- package/dist/commands/init.js +7 -2
- package/dist/commands/jobs.js +1 -1
- package/dist/commands/tax.js +195 -0
- package/dist/index.js +2 -0
- package/dist/jobs/audit-prep.js +4 -4
- package/dist/jobs/bank-recon.js +4 -4
- package/dist/jobs/credit-control.js +5 -5
- package/dist/jobs/fa-review.js +4 -4
- package/dist/jobs/gst-vat.js +3 -3
- package/dist/jobs/payment-run.js +4 -4
- package/dist/jobs/supplier-recon.js +3 -3
- package/dist/tax/format.js +18 -0
- package/dist/tax/sg/capital-allowances.js +160 -0
- package/dist/tax/sg/constants.js +63 -0
- package/dist/tax/sg/exemptions.js +76 -0
- package/dist/tax/sg/form-cs.js +349 -0
- package/dist/tax/sg/format-sg.js +134 -0
- package/dist/tax/types.js +9 -0
- package/dist/tax/validate.js +124 -0
- package/dist/utils/template.js +1 -1
- 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
|
+
});
|
package/dist/commands/init.js
CHANGED
|
@@ -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();
|
package/dist/commands/jobs.js
CHANGED
|
@@ -159,7 +159,7 @@ export function registerJobsCommand(program) {
|
|
|
159
159
|
jobs
|
|
160
160
|
.command('audit-prep')
|
|
161
161
|
.description('Audit preparation pack blueprint')
|
|
162
|
-
.requiredOption('--period <YYYY>', 'Fiscal year (e.g., 2025)')
|
|
162
|
+
.requiredOption('--period <YYYY|YYYY-QN>', 'Fiscal year or quarter (e.g., 2025 or 2025-Q3)')
|
|
163
163
|
.option('--currency <code>', 'Currency code (e.g. SGD, USD)')
|
|
164
164
|
.option('--json', 'Output as JSON')
|
|
165
165
|
.action(jobAction((opts) => {
|
|
@@ -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
|
@@ -8,6 +8,7 @@ import { versionsCommand } from './commands/versions.js';
|
|
|
8
8
|
import { updateCommand } from './commands/update.js';
|
|
9
9
|
import { registerCalcCommand } from './commands/calc.js';
|
|
10
10
|
import { registerJobsCommand } from './commands/jobs.js';
|
|
11
|
+
import { registerTaxCommand } from './commands/tax.js';
|
|
11
12
|
import { SKILL_TYPES } from './types/index.js';
|
|
12
13
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
14
|
const __dirname = dirname(__filename);
|
|
@@ -53,4 +54,5 @@ program
|
|
|
53
54
|
});
|
|
54
55
|
registerCalcCommand(program);
|
|
55
56
|
registerJobsCommand(program);
|
|
57
|
+
registerTaxCommand(program);
|
|
56
58
|
program.parse();
|
package/dist/jobs/audit-prep.js
CHANGED
|
@@ -84,7 +84,7 @@ export function generateAuditPrepBlueprint(opts) {
|
|
|
84
84
|
order: 6,
|
|
85
85
|
description: 'Generate AR aging schedule',
|
|
86
86
|
category: 'report',
|
|
87
|
-
apiCall: 'POST /generate-reports/ar-
|
|
87
|
+
apiCall: 'POST /generate-reports/ar-report',
|
|
88
88
|
apiBody: {
|
|
89
89
|
endDate: p.endDate,
|
|
90
90
|
},
|
|
@@ -94,7 +94,7 @@ export function generateAuditPrepBlueprint(opts) {
|
|
|
94
94
|
order: 7,
|
|
95
95
|
description: 'Generate AP aging schedule',
|
|
96
96
|
category: 'report',
|
|
97
|
-
apiCall: 'POST /generate-reports/ap-
|
|
97
|
+
apiCall: 'POST /generate-reports/ap-report',
|
|
98
98
|
apiBody: {
|
|
99
99
|
endDate: p.endDate,
|
|
100
100
|
},
|
|
@@ -104,7 +104,7 @@ export function generateAuditPrepBlueprint(opts) {
|
|
|
104
104
|
order: 8,
|
|
105
105
|
description: 'Generate Fixed Asset register',
|
|
106
106
|
category: 'report',
|
|
107
|
-
apiCall: 'POST /generate-reports/fixed-assets',
|
|
107
|
+
apiCall: 'POST /generate-reports/fixed-assets-summary',
|
|
108
108
|
apiBody: {
|
|
109
109
|
endDate: p.endDate,
|
|
110
110
|
},
|
|
@@ -136,7 +136,7 @@ export function generateAuditPrepBlueprint(opts) {
|
|
|
136
136
|
category: 'verify',
|
|
137
137
|
apiCall: 'POST /bank-records/search',
|
|
138
138
|
apiBody: {
|
|
139
|
-
|
|
139
|
+
filter: {
|
|
140
140
|
reconciliationStatus: 'UNRECONCILED',
|
|
141
141
|
},
|
|
142
142
|
},
|
package/dist/jobs/bank-recon.js
CHANGED
|
@@ -28,7 +28,7 @@ export function generateBankReconBlueprint(opts = {}) {
|
|
|
28
28
|
category: 'verify',
|
|
29
29
|
apiCall: 'POST /chart-of-accounts/search',
|
|
30
30
|
apiBody: {
|
|
31
|
-
|
|
31
|
+
filter: {
|
|
32
32
|
classificationType: 'Bank Accounts',
|
|
33
33
|
...accountFilter,
|
|
34
34
|
},
|
|
@@ -50,7 +50,7 @@ export function generateBankReconBlueprint(opts = {}) {
|
|
|
50
50
|
category: 'verify',
|
|
51
51
|
apiCall: 'POST /bank-records/search',
|
|
52
52
|
apiBody: {
|
|
53
|
-
|
|
53
|
+
filter: {
|
|
54
54
|
reconciliationStatus: 'UNRECONCILED',
|
|
55
55
|
...accountFilter,
|
|
56
56
|
...periodFilter,
|
|
@@ -64,7 +64,7 @@ export function generateBankReconBlueprint(opts = {}) {
|
|
|
64
64
|
category: 'verify',
|
|
65
65
|
apiCall: 'POST /bank-records/search',
|
|
66
66
|
apiBody: {
|
|
67
|
-
|
|
67
|
+
filter: {
|
|
68
68
|
reconciliationStatus: 'UNRECONCILED',
|
|
69
69
|
...accountFilter,
|
|
70
70
|
},
|
|
@@ -126,7 +126,7 @@ export function generateBankReconBlueprint(opts = {}) {
|
|
|
126
126
|
category: 'verify',
|
|
127
127
|
apiCall: 'POST /bank-records/search',
|
|
128
128
|
apiBody: {
|
|
129
|
-
|
|
129
|
+
filter: {
|
|
130
130
|
reconciliationStatus: 'UNRECONCILED',
|
|
131
131
|
...accountFilter,
|
|
132
132
|
},
|
|
@@ -21,7 +21,7 @@ export function generateCreditControlBlueprint(opts = {}) {
|
|
|
21
21
|
order: 1,
|
|
22
22
|
description: 'Generate AR aging report',
|
|
23
23
|
category: 'report',
|
|
24
|
-
apiCall: 'POST /generate-reports/ar-
|
|
24
|
+
apiCall: 'POST /generate-reports/ar-report',
|
|
25
25
|
notes: 'Current, 1-30, 31-60, 61-90, 90+ day buckets. Focus on buckets beyond threshold.',
|
|
26
26
|
verification: 'AR aging report generated — note total receivables and overdue amount',
|
|
27
27
|
},
|
|
@@ -31,7 +31,7 @@ export function generateCreditControlBlueprint(opts = {}) {
|
|
|
31
31
|
category: 'verify',
|
|
32
32
|
apiCall: 'POST /invoices/search',
|
|
33
33
|
apiBody: {
|
|
34
|
-
|
|
34
|
+
filter: {
|
|
35
35
|
status: ['APPROVED'],
|
|
36
36
|
overdue: true,
|
|
37
37
|
},
|
|
@@ -52,7 +52,7 @@ export function generateCreditControlBlueprint(opts = {}) {
|
|
|
52
52
|
category: 'review',
|
|
53
53
|
apiCall: 'POST /invoices/search',
|
|
54
54
|
apiBody: {
|
|
55
|
-
|
|
55
|
+
filter: {
|
|
56
56
|
status: ['APPROVED'],
|
|
57
57
|
overdue: true,
|
|
58
58
|
},
|
|
@@ -84,7 +84,7 @@ export function generateCreditControlBlueprint(opts = {}) {
|
|
|
84
84
|
order: 5,
|
|
85
85
|
description: 'Identify doubtful debts',
|
|
86
86
|
category: 'review',
|
|
87
|
-
apiCall: 'POST /generate-reports/ar-
|
|
87
|
+
apiCall: 'POST /generate-reports/ar-report',
|
|
88
88
|
notes: 'Review 90+ day bucket for potential bad debts. Consider: customer financial health, dispute status, collateral, historical write-off rates.',
|
|
89
89
|
verification: 'Doubtful debt candidates identified and documented',
|
|
90
90
|
},
|
|
@@ -108,7 +108,7 @@ export function generateCreditControlBlueprint(opts = {}) {
|
|
|
108
108
|
order: 7,
|
|
109
109
|
description: 'Review AR aging after credit control actions',
|
|
110
110
|
category: 'verify',
|
|
111
|
-
apiCall: 'POST /generate-reports/ar-
|
|
111
|
+
apiCall: 'POST /generate-reports/ar-report',
|
|
112
112
|
notes: 'Compare to opening AR aging. Document: payments received, payment plans agreed, write-offs processed, provisions updated.',
|
|
113
113
|
verification: 'AR aging reviewed, all actions documented, follow-up dates set',
|
|
114
114
|
},
|
package/dist/jobs/fa-review.js
CHANGED
|
@@ -15,7 +15,7 @@ export function generateFaReviewBlueprint(opts = {}) {
|
|
|
15
15
|
order: 1,
|
|
16
16
|
description: 'Pull fixed asset summary report',
|
|
17
17
|
category: 'report',
|
|
18
|
-
apiCall: 'POST /generate-reports/fixed-assets',
|
|
18
|
+
apiCall: 'POST /generate-reports/fixed-assets-summary',
|
|
19
19
|
notes: 'Review: asset description, cost, accumulated depreciation, NBV, useful life remaining, depreciation method per asset',
|
|
20
20
|
verification: 'FA register generated — note total cost, total accumulated depreciation, total NBV',
|
|
21
21
|
},
|
|
@@ -38,7 +38,7 @@ export function generateFaReviewBlueprint(opts = {}) {
|
|
|
38
38
|
order: 3,
|
|
39
39
|
description: 'Verify depreciation runs are up to date',
|
|
40
40
|
category: 'verify',
|
|
41
|
-
apiCall: 'POST /generate-reports/fixed-assets',
|
|
41
|
+
apiCall: 'POST /generate-reports/fixed-assets-summary',
|
|
42
42
|
calcCommand: 'jaz calc depreciation',
|
|
43
43
|
notes: 'Check that monthly/annual depreciation has been posted for all assets through the current period. Recalculate sample of assets to verify amounts.',
|
|
44
44
|
verification: 'Depreciation is current — no missed periods. Sample recalculations match posted amounts.',
|
|
@@ -47,7 +47,7 @@ export function generateFaReviewBlueprint(opts = {}) {
|
|
|
47
47
|
order: 4,
|
|
48
48
|
description: 'Check fully depreciated assets',
|
|
49
49
|
category: 'review',
|
|
50
|
-
apiCall: 'POST /generate-reports/fixed-assets',
|
|
50
|
+
apiCall: 'POST /generate-reports/fixed-assets-summary',
|
|
51
51
|
notes: 'Identify assets with NBV = 0 or NBV = salvage value. Review whether: (1) asset is still in use, (2) asset should be disposed/written off, (3) useful life estimate needs revision.',
|
|
52
52
|
verification: 'All fully depreciated assets reviewed — disposals and write-offs identified',
|
|
53
53
|
},
|
|
@@ -96,7 +96,7 @@ export function generateFaReviewBlueprint(opts = {}) {
|
|
|
96
96
|
order: 8,
|
|
97
97
|
description: 'Check NBV reasonableness',
|
|
98
98
|
category: 'review',
|
|
99
|
-
apiCall: 'POST /generate-reports/fixed-assets',
|
|
99
|
+
apiCall: 'POST /generate-reports/fixed-assets-summary',
|
|
100
100
|
notes: [
|
|
101
101
|
'Review remaining NBV for reasonableness:',
|
|
102
102
|
'(1) No negative NBV,',
|
package/dist/jobs/gst-vat.js
CHANGED
|
@@ -48,7 +48,7 @@ export function generateGstVatBlueprint(opts) {
|
|
|
48
48
|
category: 'verify',
|
|
49
49
|
apiCall: 'POST /invoices/search',
|
|
50
50
|
apiBody: {
|
|
51
|
-
|
|
51
|
+
filter: {
|
|
52
52
|
dateFrom: qp.startDate,
|
|
53
53
|
dateTo: qp.endDate,
|
|
54
54
|
status: ['APPROVED', 'PAID'],
|
|
@@ -63,7 +63,7 @@ export function generateGstVatBlueprint(opts) {
|
|
|
63
63
|
category: 'verify',
|
|
64
64
|
apiCall: 'POST /invoices/search',
|
|
65
65
|
apiBody: {
|
|
66
|
-
|
|
66
|
+
filter: {
|
|
67
67
|
dateFrom: qp.startDate,
|
|
68
68
|
dateTo: qp.endDate,
|
|
69
69
|
},
|
|
@@ -84,7 +84,7 @@ export function generateGstVatBlueprint(opts) {
|
|
|
84
84
|
category: 'verify',
|
|
85
85
|
apiCall: 'POST /bills/search',
|
|
86
86
|
apiBody: {
|
|
87
|
-
|
|
87
|
+
filter: {
|
|
88
88
|
dateFrom: qp.startDate,
|
|
89
89
|
dateTo: qp.endDate,
|
|
90
90
|
status: ['APPROVED', 'PAID'],
|