jaz-cli 2.3.0 → 2.5.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.
@@ -0,0 +1,112 @@
1
+ /**
2
+ * IAS 37 Provision calculator — PV measurement + discount unwinding.
3
+ *
4
+ * Calculates the present value of a future obligation and generates
5
+ * the periodic unwinding schedule (Dr Finance Cost / Cr Provision).
6
+ *
7
+ * Compliance references:
8
+ * - IAS 37.36: Best estimate of expenditure required to settle
9
+ * - IAS 37.45: PV when effect of time value is material
10
+ * - IAS 37.60: Unwinding of discount = finance cost
11
+ *
12
+ * Uses `financial` package for PV calculation (same as lease calculator).
13
+ * Final period penny adjustment closes provision to the full nominal amount.
14
+ *
15
+ * Use cases:
16
+ * - Warranty obligations
17
+ * - Legal claims / litigation
18
+ * - Decommissioning / restoration
19
+ * - Restructuring provisions
20
+ * - Onerous contracts
21
+ */
22
+ import { pv } from 'financial';
23
+ import { round2, addMonths } from './types.js';
24
+ import { validatePositive, validatePositiveInteger, validateDateFormat, validateRate } from './validate.js';
25
+ import { journalStep, fmtCapsuleAmount } from './blueprint.js';
26
+ export function calculateProvision(inputs) {
27
+ const { amount, annualRate, termMonths, startDate, currency } = inputs;
28
+ validatePositive(amount, 'Estimated future outflow');
29
+ validateRate(annualRate, 'Discount rate');
30
+ validatePositiveInteger(termMonths, 'Term (months)');
31
+ validateDateFormat(startDate);
32
+ const monthlyRate = annualRate / 100 / 12;
33
+ // PV of a single future cash flow (IAS 37.45)
34
+ // pv(rate, nper, pmt, fv) — pmt=0, fv=amount → returns negative, negate
35
+ const presentValue = round2(-pv(monthlyRate, termMonths, 0, amount));
36
+ const totalUnwinding = round2(amount - presentValue);
37
+ // Initial recognition journal (Dr Expense / Cr Provision at PV)
38
+ const initialJournal = {
39
+ description: `Initial provision recognition at PV (IAS 37)`,
40
+ lines: [
41
+ { account: 'Provision Expense', debit: presentValue, credit: 0 },
42
+ { account: 'Provision for Obligations', debit: 0, credit: presentValue },
43
+ ],
44
+ };
45
+ // Unwinding schedule (effective interest method, IAS 37.60)
46
+ const schedule = [];
47
+ let provisionBalance = presentValue;
48
+ let totalInterest = 0;
49
+ for (let i = 1; i <= termMonths; i++) {
50
+ const openingBalance = round2(provisionBalance);
51
+ const isFinal = i === termMonths;
52
+ let interest;
53
+ if (isFinal) {
54
+ // Final period: close to nominal amount exactly
55
+ interest = round2(amount - openingBalance);
56
+ }
57
+ else {
58
+ interest = round2(openingBalance * monthlyRate);
59
+ }
60
+ provisionBalance = round2(openingBalance + interest);
61
+ totalInterest = round2(totalInterest + interest);
62
+ const date = startDate ? addMonths(startDate, i) : null;
63
+ const journal = {
64
+ description: `Provision unwinding — Month ${i} of ${termMonths}`,
65
+ lines: [
66
+ { account: 'Finance Cost — Unwinding', debit: interest, credit: 0 },
67
+ { account: 'Provision for Obligations', debit: 0, credit: interest },
68
+ ],
69
+ };
70
+ schedule.push({
71
+ period: i,
72
+ date,
73
+ openingBalance,
74
+ payment: 0, // no cash movement until settlement
75
+ interest,
76
+ principal: 0, // reuse ScheduleRow — principal not applicable
77
+ closingBalance: provisionBalance,
78
+ journal,
79
+ });
80
+ }
81
+ // Build blueprint for agent execution
82
+ let blueprint = null;
83
+ if (startDate) {
84
+ const steps = [
85
+ journalStep(1, initialJournal.description, startDate, initialJournal.lines),
86
+ ...schedule.map((row, idx) => journalStep(idx + 2, row.journal.description, row.date, row.journal.lines)),
87
+ ];
88
+ blueprint = {
89
+ capsuleType: 'Provisions',
90
+ capsuleName: `Provision — ${fmtCapsuleAmount(amount, currency)} — ${annualRate}% — ${termMonths} months`,
91
+ tags: ['Provision', 'IAS 37'],
92
+ customFields: { 'Obligation Type': null, 'Expected Settlement Date': null },
93
+ steps,
94
+ };
95
+ }
96
+ return {
97
+ type: 'provision',
98
+ currency: currency ?? null,
99
+ inputs: {
100
+ amount,
101
+ annualRate,
102
+ termMonths,
103
+ startDate: startDate ?? null,
104
+ },
105
+ presentValue,
106
+ nominalAmount: amount,
107
+ totalUnwinding,
108
+ initialJournal,
109
+ schedule,
110
+ blueprint,
111
+ };
112
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Shared types for jaz calc financial calculators.
3
+ */
4
+ /**
5
+ * Round to 2 decimal places (cents).
6
+ *
7
+ * Uses Math.round() (asymmetric, rounds 0.5 away from zero). This is the
8
+ * standard JavaScript rounding and works correctly for accounting amounts
9
+ * up to ~$90 trillion (Number.MAX_SAFE_INTEGER / 100). Final-period
10
+ * corrections in each calculator absorb any cumulative drift, ensuring
11
+ * balances always close to exactly $0.00.
12
+ */
13
+ export function round2(n) {
14
+ return Math.round(n * 100) / 100;
15
+ }
16
+ /** Advance a date by N months. Returns YYYY-MM-DD string. */
17
+ export function addMonths(dateStr, months) {
18
+ const d = new Date(dateStr + 'T00:00:00');
19
+ d.setMonth(d.getMonth() + months);
20
+ return d.toISOString().slice(0, 10);
21
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Input validation for jaz calc financial calculators.
3
+ * Throws CalcValidationError with user-friendly messages.
4
+ */
5
+ export class CalcValidationError extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = 'CalcValidationError';
9
+ }
10
+ }
11
+ export function validatePositive(value, name) {
12
+ if (!Number.isFinite(value) || value <= 0) {
13
+ throw new CalcValidationError(`${name} must be a positive number (got ${value})`);
14
+ }
15
+ }
16
+ export function validateNonNegative(value, name) {
17
+ if (!Number.isFinite(value) || value < 0) {
18
+ throw new CalcValidationError(`${name} must be zero or positive (got ${value})`);
19
+ }
20
+ }
21
+ export function validatePositiveInteger(value, name) {
22
+ if (!Number.isInteger(value) || value < 1) {
23
+ throw new CalcValidationError(`${name} must be a positive integer (got ${value})`);
24
+ }
25
+ }
26
+ export function validateSalvageLessThanCost(salvage, cost) {
27
+ if (salvage >= cost) {
28
+ throw new CalcValidationError(`Salvage value (${salvage}) must be less than cost (${cost})`);
29
+ }
30
+ }
31
+ export function validateDateFormat(date) {
32
+ if (!date)
33
+ return;
34
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
35
+ throw new CalcValidationError(`Date must be YYYY-MM-DD format (got "${date}")`);
36
+ }
37
+ const d = new Date(date + 'T00:00:00');
38
+ if (isNaN(d.getTime())) {
39
+ throw new CalcValidationError(`Invalid date: "${date}"`);
40
+ }
41
+ }
42
+ export function validateRate(rate, name = 'Rate') {
43
+ validateNonNegative(rate, name);
44
+ if (rate > 100) {
45
+ // Warning only — rates >100% are rare but legal (e.g. hyperinflation)
46
+ process.stderr.write(`Warning: ${name} is ${rate}% — are you sure this isn't a decimal? (e.g. 6 for 6%, not 0.06)\n`);
47
+ }
48
+ }
@@ -0,0 +1,200 @@
1
+ import chalk from 'chalk';
2
+ import { calculateLoan } from '../calc/loan.js';
3
+ import { calculateLease } from '../calc/lease.js';
4
+ import { calculateDepreciation } from '../calc/depreciation.js';
5
+ import { calculatePrepaidExpense, calculateDeferredRevenue } from '../calc/amortization.js';
6
+ import { calculateFxReval } from '../calc/fx-reval.js';
7
+ import { calculateEcl } from '../calc/ecl.js';
8
+ import { calculateProvision } from '../calc/provision.js';
9
+ import { printResult, printJson } from '../calc/format.js';
10
+ import { CalcValidationError } from '../calc/validate.js';
11
+ /** Wrap calc action with validation error handling. */
12
+ function calcAction(fn) {
13
+ return (opts) => {
14
+ try {
15
+ fn(opts);
16
+ }
17
+ catch (err) {
18
+ if (err instanceof CalcValidationError) {
19
+ console.error(chalk.red(`Error: ${err.message}`));
20
+ process.exit(1);
21
+ }
22
+ throw err;
23
+ }
24
+ };
25
+ }
26
+ export function registerCalcCommand(program) {
27
+ const calc = program
28
+ .command('calc')
29
+ .description('Financial calculators — loan, lease, depreciation, prepaid-expense, deferred-revenue, fx-reval, ecl, provision');
30
+ // ── jaz calc loan ──────────────────────────────────────────────
31
+ calc
32
+ .command('loan')
33
+ .description('Loan amortization schedule')
34
+ .requiredOption('--principal <amount>', 'Loan principal', parseFloat)
35
+ .requiredOption('--rate <percent>', 'Annual interest rate (%)', parseFloat)
36
+ .requiredOption('--term <months>', 'Loan term in months', parseInt)
37
+ .option('--start-date <date>', 'Start date (YYYY-MM-DD)')
38
+ .option('--currency <code>', 'Currency code (e.g. SGD, USD)')
39
+ .option('--json', 'Output as JSON')
40
+ .action(calcAction((opts) => {
41
+ const result = calculateLoan({
42
+ principal: opts.principal,
43
+ annualRate: opts.rate,
44
+ termMonths: opts.term,
45
+ startDate: opts.startDate,
46
+ currency: opts.currency,
47
+ });
48
+ opts.json ? printJson(result) : printResult(result);
49
+ }));
50
+ // ── jaz calc lease ─────────────────────────────────────────────
51
+ calc
52
+ .command('lease')
53
+ .description('IFRS 16 lease schedule (liability unwinding + ROU depreciation)')
54
+ .requiredOption('--payment <amount>', 'Monthly lease payment', parseFloat)
55
+ .requiredOption('--term <months>', 'Lease term in months', parseInt)
56
+ .requiredOption('--rate <percent>', 'Incremental borrowing rate (%)', parseFloat)
57
+ .option('--start-date <date>', 'Lease commencement date (YYYY-MM-DD)')
58
+ .option('--currency <code>', 'Currency code (e.g. SGD, USD)')
59
+ .option('--json', 'Output as JSON')
60
+ .action(calcAction((opts) => {
61
+ const result = calculateLease({
62
+ monthlyPayment: opts.payment,
63
+ termMonths: opts.term,
64
+ annualRate: opts.rate,
65
+ startDate: opts.startDate,
66
+ currency: opts.currency,
67
+ });
68
+ opts.json ? printJson(result) : printResult(result);
69
+ }));
70
+ // ── jaz calc depreciation ──────────────────────────────────────
71
+ calc
72
+ .command('depreciation')
73
+ .description('Depreciation schedule (straight-line, double declining, or 150% declining)')
74
+ .requiredOption('--cost <amount>', 'Asset cost', parseFloat)
75
+ .requiredOption('--salvage <amount>', 'Salvage value', parseFloat)
76
+ .requiredOption('--life <years>', 'Useful life in years', parseInt)
77
+ .option('--method <method>', 'Method: sl, ddb (default), or 150db', 'ddb')
78
+ .option('--frequency <freq>', 'Frequency: annual (default) or monthly', 'annual')
79
+ .option('--currency <code>', 'Currency code (e.g. SGD, USD)')
80
+ .option('--json', 'Output as JSON')
81
+ .action(calcAction((opts) => {
82
+ const result = calculateDepreciation({
83
+ cost: opts.cost,
84
+ salvageValue: opts.salvage,
85
+ usefulLifeYears: opts.life,
86
+ method: opts.method,
87
+ frequency: opts.frequency,
88
+ currency: opts.currency,
89
+ });
90
+ opts.json ? printJson(result) : printResult(result);
91
+ }));
92
+ // ── jaz calc prepaid-expense ──────────────────────────────────
93
+ calc
94
+ .command('prepaid-expense')
95
+ .description('Prepaid expense recognition schedule (e.g. insurance, rent, subscriptions)')
96
+ .requiredOption('--amount <amount>', 'Total prepaid amount', parseFloat)
97
+ .requiredOption('--periods <count>', 'Number of recognition periods', parseInt)
98
+ .option('--frequency <freq>', 'Frequency: monthly (default) or quarterly', 'monthly')
99
+ .option('--start-date <date>', 'Start date (YYYY-MM-DD)')
100
+ .option('--currency <code>', 'Currency code (e.g. SGD, USD)')
101
+ .option('--json', 'Output as JSON')
102
+ .action(calcAction((opts) => {
103
+ const result = calculatePrepaidExpense({
104
+ amount: opts.amount,
105
+ periods: opts.periods,
106
+ frequency: opts.frequency,
107
+ startDate: opts.startDate,
108
+ currency: opts.currency,
109
+ });
110
+ opts.json ? printJson(result) : printResult(result);
111
+ }));
112
+ // ── jaz calc deferred-revenue ───────────────────────────────────
113
+ calc
114
+ .command('deferred-revenue')
115
+ .description('Deferred revenue recognition schedule (e.g. annual contracts, retainers)')
116
+ .requiredOption('--amount <amount>', 'Total deferred amount', parseFloat)
117
+ .requiredOption('--periods <count>', 'Number of recognition periods', parseInt)
118
+ .option('--frequency <freq>', 'Frequency: monthly (default) or quarterly', 'monthly')
119
+ .option('--start-date <date>', 'Start date (YYYY-MM-DD)')
120
+ .option('--currency <code>', 'Currency code (e.g. SGD, USD)')
121
+ .option('--json', 'Output as JSON')
122
+ .action(calcAction((opts) => {
123
+ const result = calculateDeferredRevenue({
124
+ amount: opts.amount,
125
+ periods: opts.periods,
126
+ frequency: opts.frequency,
127
+ startDate: opts.startDate,
128
+ currency: opts.currency,
129
+ });
130
+ opts.json ? printJson(result) : printResult(result);
131
+ }));
132
+ // ── jaz calc fx-reval ───────────────────────────────────────────
133
+ calc
134
+ .command('fx-reval')
135
+ .description('FX revaluation — unrealized gain/loss on non-AR/AP foreign currency balances (IAS 21)')
136
+ .requiredOption('--amount <amount>', 'Foreign currency amount outstanding', parseFloat)
137
+ .requiredOption('--book-rate <rate>', 'Original booking exchange rate', parseFloat)
138
+ .requiredOption('--closing-rate <rate>', 'Period-end closing exchange rate', parseFloat)
139
+ .option('--currency <code>', 'Foreign currency code (e.g. USD)', 'USD')
140
+ .option('--base-currency <code>', 'Base (functional) currency code (e.g. SGD)', 'SGD')
141
+ .option('--json', 'Output as JSON')
142
+ .action(calcAction((opts) => {
143
+ const result = calculateFxReval({
144
+ amount: opts.amount,
145
+ bookRate: opts.bookRate,
146
+ closingRate: opts.closingRate,
147
+ currency: opts.currency,
148
+ baseCurrency: opts.baseCurrency,
149
+ });
150
+ opts.json ? printJson(result) : printResult(result);
151
+ }));
152
+ // ── jaz calc ecl ────────────────────────────────────────────────
153
+ calc
154
+ .command('ecl')
155
+ .description('Expected credit loss provision matrix (IFRS 9 simplified approach)')
156
+ .requiredOption('--current <amount>', 'Current (not overdue) receivables balance', parseFloat)
157
+ .requiredOption('--30d <amount>', '1-30 days overdue balance', parseFloat)
158
+ .requiredOption('--60d <amount>', '31-60 days overdue balance', parseFloat)
159
+ .requiredOption('--90d <amount>', '61-90 days overdue balance', parseFloat)
160
+ .requiredOption('--120d <amount>', '91+ days overdue balance', parseFloat)
161
+ .requiredOption('--rates <rates>', 'Loss rates per bucket (comma-separated %)', (v) => v.split(',').map(Number))
162
+ .option('--existing-provision <amount>', 'Existing provision balance', parseFloat, 0)
163
+ .option('--currency <code>', 'Currency code (e.g. SGD, USD)')
164
+ .option('--json', 'Output as JSON')
165
+ .action(calcAction((opts) => {
166
+ const rates = opts.rates;
167
+ const result = calculateEcl({
168
+ buckets: [
169
+ { name: 'Current', balance: opts.current, rate: rates[0] },
170
+ { name: '1-30 days', balance: opts['30d'], rate: rates[1] },
171
+ { name: '31-60 days', balance: opts['60d'], rate: rates[2] },
172
+ { name: '61-90 days', balance: opts['90d'], rate: rates[3] },
173
+ { name: '91+ days', balance: opts['120d'], rate: rates[4] },
174
+ ],
175
+ existingProvision: opts.existingProvision,
176
+ currency: opts.currency,
177
+ });
178
+ opts.json ? printJson(result) : printResult(result);
179
+ }));
180
+ // ── jaz calc provision ──────────────────────────────────────────
181
+ calc
182
+ .command('provision')
183
+ .description('IAS 37 provision PV measurement + discount unwinding schedule')
184
+ .requiredOption('--amount <amount>', 'Estimated future cash outflow', parseFloat)
185
+ .requiredOption('--rate <percent>', 'Discount rate (%)', parseFloat)
186
+ .requiredOption('--term <months>', 'Months until expected settlement', parseInt)
187
+ .option('--start-date <date>', 'Recognition date (YYYY-MM-DD)')
188
+ .option('--currency <code>', 'Currency code (e.g. SGD, USD)')
189
+ .option('--json', 'Output as JSON')
190
+ .action(calcAction((opts) => {
191
+ const result = calculateProvision({
192
+ amount: opts.amount,
193
+ annualRate: opts.rate,
194
+ termMonths: opts.term,
195
+ startDate: opts.startDate,
196
+ currency: opts.currency,
197
+ });
198
+ opts.json ? printJson(result) : printResult(result);
199
+ }));
200
+ }
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { dirname, join } from 'path';
6
6
  import { initCommand } from './commands/init.js';
7
7
  import { versionsCommand } from './commands/versions.js';
8
8
  import { updateCommand } from './commands/update.js';
9
+ import { registerCalcCommand } from './commands/calc.js';
9
10
  import { SKILL_TYPES } from './types/index.js';
10
11
  const __filename = fileURLToPath(import.meta.url);
11
12
  const __dirname = dirname(__filename);
@@ -49,4 +50,5 @@ program
49
50
  skill: options.skill,
50
51
  });
51
52
  });
53
+ registerCalcCommand(program);
52
54
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jaz-cli",
3
- "version": "2.3.0",
3
+ "version": "2.5.0",
4
4
  "description": "CLI to install Jaz AI skills for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,8 +36,9 @@
36
36
  "author": "Jaz Engineering <api-support@jaz.ai>",
37
37
  "license": "MIT",
38
38
  "dependencies": {
39
- "commander": "^12.1.0",
40
39
  "chalk": "^5.3.0",
40
+ "commander": "^12.1.0",
41
+ "financial": "^0.2.4",
41
42
  "ora": "^8.1.1",
42
43
  "prompts": "^2.4.2"
43
44
  },