jaz-cli 2.5.0 → 2.7.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 (35) hide show
  1. package/assets/skills/api/SKILL.md +12 -2
  2. package/assets/skills/api/references/dependencies.md +3 -2
  3. package/assets/skills/api/references/endpoints.md +78 -0
  4. package/assets/skills/api/references/feature-glossary.md +5 -5
  5. package/assets/skills/api/references/field-map.md +17 -0
  6. package/assets/skills/api/references/full-api-surface.md +1 -1
  7. package/assets/skills/conversion/SKILL.md +1 -1
  8. package/assets/skills/transaction-recipes/SKILL.md +53 -19
  9. package/assets/skills/transaction-recipes/references/asset-disposal.md +174 -0
  10. package/assets/skills/transaction-recipes/references/fixed-deposit.md +164 -0
  11. package/assets/skills/transaction-recipes/references/hire-purchase.md +190 -0
  12. package/dist/__tests__/amortization.test.js +101 -0
  13. package/dist/__tests__/asset-disposal.test.js +249 -0
  14. package/dist/__tests__/blueprint.test.js +72 -0
  15. package/dist/__tests__/depreciation.test.js +125 -0
  16. package/dist/__tests__/ecl.test.js +134 -0
  17. package/dist/__tests__/fixed-deposit.test.js +214 -0
  18. package/dist/__tests__/fx-reval.test.js +115 -0
  19. package/dist/__tests__/lease.test.js +96 -0
  20. package/dist/__tests__/loan.test.js +80 -0
  21. package/dist/__tests__/provision.test.js +141 -0
  22. package/dist/__tests__/validate.test.js +81 -0
  23. package/dist/calc/amortization.js +21 -3
  24. package/dist/calc/asset-disposal.js +155 -0
  25. package/dist/calc/blueprint.js +26 -1
  26. package/dist/calc/depreciation.js +24 -1
  27. package/dist/calc/ecl.js +13 -1
  28. package/dist/calc/fixed-deposit.js +178 -0
  29. package/dist/calc/format.js +107 -2
  30. package/dist/calc/fx-reval.js +11 -1
  31. package/dist/calc/lease.js +42 -9
  32. package/dist/calc/loan.js +12 -2
  33. package/dist/calc/provision.js +17 -1
  34. package/dist/commands/calc.js +54 -2
  35. package/package.json +5 -2
@@ -11,6 +11,17 @@ const fmtR = (n) => fmt(n).padStart(14);
11
11
  const fmtPct = (n) => `${n}%`;
12
12
  const currTag = (c) => c ? ` (${c})` : '';
13
13
  const line = (w) => chalk.dim('─'.repeat(w));
14
+ function printWorkings(result) {
15
+ const bp = result.blueprint;
16
+ if (!bp?.capsuleDescription)
17
+ return;
18
+ console.log(chalk.bold('Workings (capsule description)'));
19
+ console.log(line(60));
20
+ for (const l of bp.capsuleDescription.split('\n')) {
21
+ console.log(chalk.dim(` ${l}`));
22
+ }
23
+ console.log();
24
+ }
14
25
  function printJournal(journal) {
15
26
  console.log(chalk.dim(` ${journal.description}`));
16
27
  for (const l of journal.lines) {
@@ -77,18 +88,24 @@ function printLoanTable(result) {
77
88
  console.log(chalk.dim(` Per period (example — Month 1):`));
78
89
  printJournal(result.schedule[0].journal);
79
90
  console.log();
91
+ printWorkings(result);
80
92
  }
81
93
  // ── Lease ─────────────────────────────────────────────────────────
82
94
  function printLeaseTable(result) {
83
95
  const W = 90;
96
+ const hp = result.isHirePurchase;
97
+ const title = hp ? 'Hire Purchase Schedule (IFRS 16)' : 'IFRS 16 Lease Schedule';
84
98
  console.log();
85
- console.log(chalk.bold(`IFRS 16 Lease Schedule${currTag(result.currency)}`));
99
+ console.log(chalk.bold(`${title}${currTag(result.currency)}`));
86
100
  console.log(line(W));
87
101
  console.log(chalk.bold(` Monthly Payment: ${fmt(result.inputs.monthlyPayment)}`));
88
102
  console.log(chalk.bold(` Lease Term: ${result.inputs.termMonths} months`));
103
+ if (hp) {
104
+ console.log(chalk.bold(` Useful Life: ${result.depreciationMonths} months`));
105
+ }
89
106
  console.log(chalk.bold(` Discount Rate (IBR): ${fmtPct(result.inputs.annualRate)}`));
90
107
  console.log(chalk.bold(` Present Value: ${fmt(result.presentValue)}`));
91
- console.log(chalk.bold(` Monthly ROU Dep: ${fmt(result.monthlyRouDepreciation)}`));
108
+ console.log(chalk.bold(` Monthly ROU Dep: ${fmt(result.monthlyRouDepreciation)}${hp ? ` (over ${result.depreciationMonths} months, not ${result.inputs.termMonths})` : ''}`));
92
109
  console.log(line(W));
93
110
  console.log(chalk.bold('\nInitial Recognition'));
94
111
  printJournal(result.initialJournal);
@@ -138,6 +155,7 @@ function printLeaseTable(result) {
138
155
  console.log(` Total ROU depreciation: ${fmt(result.totalDepreciation)}`);
139
156
  console.log(` Total P&L impact: ${fmt(result.totalInterest + result.totalDepreciation)}`);
140
157
  console.log();
158
+ printWorkings(result);
141
159
  }
142
160
  // ── Depreciation ──────────────────────────────────────────────────
143
161
  function printDepreciationTable(result) {
@@ -209,6 +227,7 @@ function printDepreciationTable(result) {
209
227
  console.log(chalk.bold('Journal Entry (per period)'));
210
228
  printJournal(result.schedule[0].journal);
211
229
  console.log();
230
+ printWorkings(result);
212
231
  }
213
232
  // ── Prepaid Expense / Deferred Revenue ────────────────────────────
214
233
  function printRecognitionTable(result) {
@@ -248,6 +267,7 @@ function printRecognitionTable(result) {
248
267
  console.log(chalk.bold('Journal Entry (per period)'));
249
268
  printJournal(result.schedule[0].journal);
250
269
  console.log();
270
+ printWorkings(result);
251
271
  }
252
272
  // ── FX Revaluation ────────────────────────────────────────────────
253
273
  function printFxRevalTable(result) {
@@ -276,6 +296,7 @@ function printFxRevalTable(result) {
276
296
  console.log(chalk.bold('Day 1 Reversal'));
277
297
  printJournal(result.reversalJournal);
278
298
  console.log();
299
+ printWorkings(result);
279
300
  }
280
301
  // ── ECL Provision ─────────────────────────────────────────────────
281
302
  function printEclTable(result) {
@@ -323,6 +344,7 @@ function printEclTable(result) {
323
344
  console.log(chalk.bold('Journal Entry'));
324
345
  printJournal(result.journal);
325
346
  console.log();
347
+ printWorkings(result);
326
348
  }
327
349
  // ── Provision PV Unwinding ────────────────────────────────────────
328
350
  function printProvisionTable(result) {
@@ -370,6 +392,87 @@ function printProvisionTable(result) {
370
392
  console.log(chalk.bold('Journal Entry (per period)'));
371
393
  printJournal(result.schedule[0].journal);
372
394
  console.log();
395
+ printWorkings(result);
396
+ }
397
+ // ── Fixed Deposit ────────────────────────────────────────────────
398
+ function printFixedDepositTable(result) {
399
+ const W = 75;
400
+ console.log();
401
+ console.log(chalk.bold(`Fixed Deposit — Interest Accrual Schedule${currTag(result.currency)}`));
402
+ console.log(line(W));
403
+ console.log(chalk.bold(` Principal: ${fmt(result.inputs.principal)}`));
404
+ console.log(chalk.bold(` Annual Rate: ${fmtPct(result.inputs.annualRate)}`));
405
+ console.log(chalk.bold(` Term: ${result.inputs.termMonths} months`));
406
+ console.log(chalk.bold(` Compounding: ${result.inputs.compounding}`));
407
+ console.log(chalk.bold(` Total Interest: ${fmt(result.totalInterest)}`));
408
+ console.log(chalk.bold(` Maturity Value: ${fmt(result.maturityValue)}`));
409
+ if (result.inputs.compounding !== 'none') {
410
+ console.log(chalk.bold(` Effective Rate: ${result.effectiveRate}% p.a.`));
411
+ }
412
+ console.log(line(W));
413
+ const header = [
414
+ 'Period'.padStart(6),
415
+ result.inputs.startDate ? 'Date'.padStart(12) : null,
416
+ 'Carrying Amt'.padStart(14),
417
+ 'Interest'.padStart(14),
418
+ result.inputs.compounding !== 'none' ? 'New Balance'.padStart(14) : null,
419
+ ].filter(Boolean).join(' ');
420
+ console.log(chalk.dim(header));
421
+ console.log(line(W));
422
+ for (const row of result.schedule) {
423
+ const cols = [
424
+ String(row.period).padStart(6),
425
+ row.date ? row.date.padStart(12) : null,
426
+ fmtR(row.openingBalance),
427
+ fmtR(row.interest),
428
+ result.inputs.compounding !== 'none' ? fmtR(row.closingBalance) : null,
429
+ ].filter(Boolean).join(' ');
430
+ console.log(cols);
431
+ }
432
+ console.log(line(W));
433
+ console.log(chalk.bold(` Total interest: ${fmt(result.totalInterest)}`));
434
+ console.log();
435
+ console.log(chalk.bold('Journal Entries'));
436
+ console.log(line(60));
437
+ console.log(chalk.dim(' Placement:'));
438
+ printJournal(result.placementJournal);
439
+ console.log();
440
+ console.log(chalk.dim(' Monthly accrual (example — Month 1):'));
441
+ printJournal(result.schedule[0].journal);
442
+ console.log();
443
+ console.log(chalk.dim(' Maturity:'));
444
+ printJournal(result.maturityJournal);
445
+ console.log();
446
+ printWorkings(result);
447
+ }
448
+ // ── Asset Disposal ───────────────────────────────────────────────
449
+ function printAssetDisposalTable(result) {
450
+ const W = 60;
451
+ console.log();
452
+ console.log(chalk.bold(`Asset Disposal — IAS 16${currTag(result.currency)}`));
453
+ console.log(line(W));
454
+ console.log(chalk.bold(` Asset Cost: ${fmt(result.inputs.cost)}`));
455
+ console.log(chalk.bold(` Salvage Value: ${fmt(result.inputs.salvageValue)}`));
456
+ console.log(chalk.bold(` Useful Life: ${result.inputs.usefulLifeYears} years`));
457
+ console.log(chalk.bold(` Method: ${result.inputs.method.toUpperCase()}`));
458
+ console.log(chalk.bold(` Acquired: ${result.inputs.acquisitionDate}`));
459
+ console.log(chalk.bold(` Disposed: ${result.inputs.disposalDate}`));
460
+ console.log(chalk.bold(` Months Held: ${result.monthsHeld}`));
461
+ console.log(line(W));
462
+ console.log();
463
+ console.log(` Accumulated Depreciation: ${fmt(result.accumulatedDepreciation)}`);
464
+ console.log(` Net Book Value: ${fmt(result.netBookValue)}`);
465
+ console.log(` Disposal Proceeds: ${fmt(result.inputs.proceeds)}`);
466
+ console.log(line(W));
467
+ const label = result.isGain
468
+ ? (result.gainOrLoss > 0 ? chalk.green('GAIN on Disposal') : 'AT BOOK VALUE')
469
+ : chalk.red('LOSS on Disposal');
470
+ console.log(` ${label}: ${fmt(Math.abs(result.gainOrLoss))}`);
471
+ console.log();
472
+ console.log(chalk.bold('Disposal Journal'));
473
+ printJournal(result.disposalJournal);
474
+ console.log();
475
+ printWorkings(result);
373
476
  }
374
477
  // ── Dispatch ──────────────────────────────────────────────────────
375
478
  export function printResult(result) {
@@ -382,6 +485,8 @@ export function printResult(result) {
382
485
  case 'fx-reval': return printFxRevalTable(result);
383
486
  case 'ecl': return printEclTable(result);
384
487
  case 'provision': return printProvisionTable(result);
488
+ case 'fixed-deposit': return printFixedDepositTable(result);
489
+ case 'asset-disposal': return printAssetDisposalTable(result);
385
490
  }
386
491
  }
387
492
  export function printJson(result) {
@@ -16,7 +16,7 @@
16
16
  */
17
17
  import { round2 } from './types.js';
18
18
  import { validatePositive } from './validate.js';
19
- import { journalStep } from './blueprint.js';
19
+ import { journalStep, fmtAmt } from './blueprint.js';
20
20
  export function calculateFxReval(inputs) {
21
21
  const { amount, bookRate, closingRate, currency = 'USD', baseCurrency = 'SGD', } = inputs;
22
22
  validatePositive(amount, 'Foreign currency amount');
@@ -58,9 +58,19 @@ export function calculateFxReval(inputs) {
58
58
  })),
59
59
  };
60
60
  // Blueprint: revaluation + reversal
61
+ const workings = [
62
+ `FX Revaluation Workings (IAS 21)`,
63
+ `Foreign currency: ${currency} ${amount.toLocaleString()} | Base currency: ${baseCurrency}`,
64
+ `Book rate: ${bookRate} → Book value: ${fmtAmt(bookValue, baseCurrency)}`,
65
+ `Closing rate: ${closingRate} → Closing value: ${fmtAmt(closingValue, baseCurrency)}`,
66
+ `${isGain ? 'Unrealized gain' : 'Unrealized loss'}: ${fmtAmt(Math.abs(gainOrLoss), baseCurrency)}`,
67
+ `Method: IAS 21.23 — monetary items translated at closing rate`,
68
+ `Reversal: Day 1 next period (standard reval/reverse approach)`,
69
+ ].join('\n');
61
70
  const blueprint = {
62
71
  capsuleType: 'FX Revaluation',
63
72
  capsuleName: `FX Reval — ${currency} ${amount.toLocaleString()} — ${bookRate} → ${closingRate}`,
73
+ capsuleDescription: workings,
64
74
  tags: ['FX Revaluation', currency],
65
75
  customFields: { 'Source Account': null, 'Period End Date': null },
66
76
  steps: [
@@ -11,20 +11,29 @@
11
11
  */
12
12
  import { pv } from 'financial';
13
13
  import { round2, addMonths } from './types.js';
14
- import { validatePositive, validatePositiveInteger, validateDateFormat, validateRate } from './validate.js';
15
- import { journalStep, noteStep, fmtCapsuleAmount } from './blueprint.js';
14
+ import { CalcValidationError, validatePositive, validatePositiveInteger, validateDateFormat, validateRate } from './validate.js';
15
+ import { journalStep, fixedAssetStep, fmtCapsuleAmount, fmtAmt } from './blueprint.js';
16
16
  export function calculateLease(inputs) {
17
- const { monthlyPayment, termMonths, annualRate, startDate, currency } = inputs;
17
+ const { monthlyPayment, termMonths, annualRate, usefulLifeMonths, startDate, currency } = inputs;
18
18
  validatePositive(monthlyPayment, 'Monthly payment');
19
19
  validateRate(annualRate, 'Annual rate (IBR)');
20
20
  validatePositiveInteger(termMonths, 'Term (months)');
21
+ if (usefulLifeMonths !== undefined) {
22
+ validatePositiveInteger(usefulLifeMonths, 'Useful life (months)');
23
+ if (usefulLifeMonths < termMonths) {
24
+ throw new CalcValidationError(`Useful life (${usefulLifeMonths} months) must be >= lease term (${termMonths} months) for hire purchase.`);
25
+ }
26
+ }
21
27
  validateDateFormat(startDate);
28
+ const isHirePurchase = usefulLifeMonths !== undefined;
22
29
  const monthlyRate = annualRate / 100 / 12;
23
30
  // PV of an ordinary annuity (payments at end of period)
24
31
  // pv() returns negative, negate for positive value
25
32
  const presentValue = round2(-pv(monthlyRate, termMonths, monthlyPayment));
26
33
  // ROU depreciation: straight-line over lease term (IFRS 16.31-32)
27
- const monthlyRouDepreciation = round2(presentValue / termMonths);
34
+ // For hire purchase (ownership transfers): depreciate over useful life, not lease term
35
+ const depreciationMonths = isHirePurchase ? usefulLifeMonths : termMonths;
36
+ const monthlyRouDepreciation = round2(presentValue / depreciationMonths);
28
37
  const initialJournal = {
29
38
  description: 'Initial recognition — IFRS 16 lease',
30
39
  lines: [
@@ -83,16 +92,37 @@ export function calculateLease(inputs) {
83
92
  // Build blueprint for agent execution
84
93
  let blueprint = null;
85
94
  if (startDate) {
95
+ const depNote = isHirePurchase
96
+ ? `Register Right-of-Use Asset in Fixed Asset register: cost ${presentValue}, salvage 0, life ${depreciationMonths} months (straight-line over useful life, not lease term). Jaz will auto-post monthly depreciation of ${monthlyRouDepreciation}.`
97
+ : `Register Right-of-Use Asset in Fixed Asset register: cost ${presentValue}, salvage 0, life ${termMonths} months (straight-line). Jaz will auto-post monthly depreciation of ${monthlyRouDepreciation}.`;
86
98
  const steps = [
87
99
  journalStep(1, initialJournal.description, startDate, initialJournal.lines),
88
- noteStep(2, `Register Right-of-Use Asset in Fixed Asset register: cost ${presentValue}, salvage 0, life ${termMonths} months (straight-line). Jaz will auto-post monthly depreciation of ${monthlyRouDepreciation}.`, startDate),
100
+ fixedAssetStep(2, depNote, startDate),
89
101
  ...schedule.map((row, idx) => journalStep(idx + 3, row.journal.description, row.date, row.journal.lines)),
90
102
  ];
103
+ const capsuleType = isHirePurchase ? 'Hire Purchase' : 'Lease Accounting';
104
+ const capsuleName = isHirePurchase
105
+ ? `Hire Purchase — ${fmtCapsuleAmount(monthlyPayment, currency)}/month — ${termMonths} months — useful life ${depreciationMonths} months`
106
+ : `IFRS 16 Lease — ${fmtCapsuleAmount(monthlyPayment, currency)}/month — ${termMonths} months`;
107
+ const c = currency ?? undefined;
108
+ const workingsLines = [
109
+ `${isHirePurchase ? 'Hire Purchase' : 'IFRS 16 Lease'} Workings`,
110
+ `Monthly payment: ${fmtAmt(monthlyPayment, c)} | IBR: ${annualRate}% p.a. (${round2(monthlyRate * 100)}% monthly)`,
111
+ `Lease term: ${termMonths} months | PV of payments: ${fmtAmt(presentValue, c)} (IFRS 16.26)`,
112
+ ];
113
+ if (isHirePurchase) {
114
+ workingsLines.push(`Useful life: ${usefulLifeMonths} months | ROU depreciation: ${fmtAmt(monthlyRouDepreciation, c)}/month over ${depreciationMonths} months`);
115
+ }
116
+ else {
117
+ workingsLines.push(`ROU depreciation: ${fmtAmt(monthlyRouDepreciation, c)}/month (SL over ${termMonths} months)`);
118
+ }
119
+ workingsLines.push(`Total cash payments: ${fmtAmt(round2(totalInterest + totalPrincipal), c)} | Total interest: ${fmtAmt(totalInterest, c)}`, `Method: Effective interest (IFRS 16.36-37), ROU straight-line (IFRS 16.31-32)`, `Rounding: 2dp per period, final period closes liability to $0.00`);
91
120
  blueprint = {
92
- capsuleType: 'Lease Accounting',
93
- capsuleName: `IFRS 16 Lease — ${fmtCapsuleAmount(monthlyPayment, currency)}/month — ${termMonths} months`,
94
- tags: ['Lease Accounting'],
95
- customFields: { 'Lease Contract #': null },
121
+ capsuleType,
122
+ capsuleName,
123
+ capsuleDescription: workingsLines.join('\n'),
124
+ tags: [capsuleType],
125
+ customFields: isHirePurchase ? { 'HP Agreement #': null } : { 'Lease Contract #': null },
96
126
  steps,
97
127
  };
98
128
  }
@@ -103,10 +133,13 @@ export function calculateLease(inputs) {
103
133
  monthlyPayment,
104
134
  termMonths,
105
135
  annualRate,
136
+ usefulLifeMonths: usefulLifeMonths ?? null,
106
137
  startDate: startDate ?? null,
107
138
  },
108
139
  presentValue,
109
140
  monthlyRouDepreciation,
141
+ depreciationMonths,
142
+ isHirePurchase,
110
143
  totalCashPayments: round2(totalInterest + totalPrincipal),
111
144
  totalInterest,
112
145
  totalDepreciation,
package/dist/calc/loan.js CHANGED
@@ -6,7 +6,7 @@
6
6
  import { pmt } from 'financial';
7
7
  import { round2, addMonths } from './types.js';
8
8
  import { validatePositive, validatePositiveInteger, validateDateFormat, validateRate } from './validate.js';
9
- import { journalStep, fmtCapsuleAmount } from './blueprint.js';
9
+ import { journalStep, cashInStep, fmtCapsuleAmount, fmtAmt } from './blueprint.js';
10
10
  export function calculateLoan(inputs) {
11
11
  const { principal, annualRate, termMonths, startDate, currency } = inputs;
12
12
  validatePositive(principal, 'Principal');
@@ -64,15 +64,25 @@ export function calculateLoan(inputs) {
64
64
  let blueprint = null;
65
65
  if (startDate) {
66
66
  const steps = [
67
- journalStep(1, 'Loan disbursement', startDate, [
67
+ cashInStep(1, 'Record loan proceeds received from bank', startDate, [
68
68
  { account: 'Cash / Bank Account', debit: principal, credit: 0 },
69
69
  { account: 'Loan Payable', debit: 0, credit: principal },
70
70
  ]),
71
71
  ...schedule.map((row, idx) => journalStep(idx + 2, row.journal.description, row.date, row.journal.lines)),
72
72
  ];
73
+ const c = currency ?? undefined;
74
+ const workings = [
75
+ `Loan Amortization Workings`,
76
+ `Principal: ${fmtAmt(principal, c)} | Rate: ${annualRate}% p.a. (${round2(monthlyRate * 100)}% monthly)`,
77
+ `Term: ${termMonths} months | Monthly payment: ${fmtAmt(payment, c)}`,
78
+ `Total payments: ${fmtAmt(round2(totalInterest + totalPrincipal), c)} | Total interest: ${fmtAmt(totalInterest, c)}`,
79
+ `Method: PMT formula, constant payment amortization`,
80
+ `Rounding: 2dp per period, final period closes balance to $0.00`,
81
+ ].join('\n');
73
82
  blueprint = {
74
83
  capsuleType: 'Loan Repayment',
75
84
  capsuleName: `Bank Loan — ${fmtCapsuleAmount(principal, currency)} — ${annualRate}% — ${termMonths} months`,
85
+ capsuleDescription: workings,
76
86
  tags: ['Bank Loan'],
77
87
  customFields: { 'Loan Reference': null },
78
88
  steps,
@@ -22,7 +22,7 @@
22
22
  import { pv } from 'financial';
23
23
  import { round2, addMonths } from './types.js';
24
24
  import { validatePositive, validatePositiveInteger, validateDateFormat, validateRate } from './validate.js';
25
- import { journalStep, fmtCapsuleAmount } from './blueprint.js';
25
+ import { journalStep, cashOutStep, fmtCapsuleAmount, fmtAmt } from './blueprint.js';
26
26
  export function calculateProvision(inputs) {
27
27
  const { amount, annualRate, termMonths, startDate, currency } = inputs;
28
28
  validatePositive(amount, 'Estimated future outflow');
@@ -81,13 +81,29 @@ export function calculateProvision(inputs) {
81
81
  // Build blueprint for agent execution
82
82
  let blueprint = null;
83
83
  if (startDate) {
84
+ const settlementDate = addMonths(startDate, termMonths);
84
85
  const steps = [
85
86
  journalStep(1, initialJournal.description, startDate, initialJournal.lines),
86
87
  ...schedule.map((row, idx) => journalStep(idx + 2, row.journal.description, row.date, row.journal.lines)),
88
+ cashOutStep(termMonths + 2, 'Settlement — pay the obligation', settlementDate, [
89
+ { account: 'Provision for Obligations', debit: amount, credit: 0 },
90
+ { account: 'Cash / Bank Account', debit: 0, credit: amount },
91
+ ]),
87
92
  ];
93
+ const c = currency ?? undefined;
94
+ const workings = [
95
+ `IAS 37 Provision Workings`,
96
+ `Nominal obligation: ${fmtAmt(amount, c)} | Discount rate: ${annualRate}% p.a. (${round2(monthlyRate * 100)}% monthly)`,
97
+ `Term to settlement: ${termMonths} months | PV at recognition: ${fmtAmt(presentValue, c)}`,
98
+ `Total unwinding (finance cost): ${fmtAmt(totalUnwinding, c)}`,
99
+ `Method: PV of single future outflow (IAS 37.45), unwinding via effective interest (IAS 37.60)`,
100
+ `Settlement: ${fmtAmt(amount, c)} cash out on ${settlementDate}`,
101
+ `Rounding: 2dp per period, final period closes to nominal amount`,
102
+ ].join('\n');
88
103
  blueprint = {
89
104
  capsuleType: 'Provisions',
90
105
  capsuleName: `Provision — ${fmtCapsuleAmount(amount, currency)} — ${annualRate}% — ${termMonths} months`,
106
+ capsuleDescription: workings,
91
107
  tags: ['Provision', 'IAS 37'],
92
108
  customFields: { 'Obligation Type': null, 'Expected Settlement Date': null },
93
109
  steps,
@@ -6,6 +6,8 @@ import { calculatePrepaidExpense, calculateDeferredRevenue } from '../calc/amort
6
6
  import { calculateFxReval } from '../calc/fx-reval.js';
7
7
  import { calculateEcl } from '../calc/ecl.js';
8
8
  import { calculateProvision } from '../calc/provision.js';
9
+ import { calculateFixedDeposit } from '../calc/fixed-deposit.js';
10
+ import { calculateAssetDisposal } from '../calc/asset-disposal.js';
9
11
  import { printResult, printJson } from '../calc/format.js';
10
12
  import { CalcValidationError } from '../calc/validate.js';
11
13
  /** Wrap calc action with validation error handling. */
@@ -26,7 +28,7 @@ function calcAction(fn) {
26
28
  export function registerCalcCommand(program) {
27
29
  const calc = program
28
30
  .command('calc')
29
- .description('Financial calculators — loan, lease, depreciation, prepaid-expense, deferred-revenue, fx-reval, ecl, provision');
31
+ .description('Financial calculators — loan, lease, depreciation, prepaid-expense, deferred-revenue, fx-reval, ecl, provision, fixed-deposit, asset-disposal');
30
32
  // ── jaz calc loan ──────────────────────────────────────────────
31
33
  calc
32
34
  .command('loan')
@@ -50,10 +52,11 @@ export function registerCalcCommand(program) {
50
52
  // ── jaz calc lease ─────────────────────────────────────────────
51
53
  calc
52
54
  .command('lease')
53
- .description('IFRS 16 lease schedule (liability unwinding + ROU depreciation)')
55
+ .description('IFRS 16 lease schedule (liability unwinding + ROU depreciation). Add --useful-life for hire purchase.')
54
56
  .requiredOption('--payment <amount>', 'Monthly lease payment', parseFloat)
55
57
  .requiredOption('--term <months>', 'Lease term in months', parseInt)
56
58
  .requiredOption('--rate <percent>', 'Incremental borrowing rate (%)', parseFloat)
59
+ .option('--useful-life <months>', 'Asset useful life in months (hire purchase: depreciate over useful life, not term)', parseInt)
57
60
  .option('--start-date <date>', 'Lease commencement date (YYYY-MM-DD)')
58
61
  .option('--currency <code>', 'Currency code (e.g. SGD, USD)')
59
62
  .option('--json', 'Output as JSON')
@@ -62,6 +65,7 @@ export function registerCalcCommand(program) {
62
65
  monthlyPayment: opts.payment,
63
66
  termMonths: opts.term,
64
67
  annualRate: opts.rate,
68
+ usefulLifeMonths: opts.usefulLife,
65
69
  startDate: opts.startDate,
66
70
  currency: opts.currency,
67
71
  });
@@ -197,4 +201,52 @@ export function registerCalcCommand(program) {
197
201
  });
198
202
  opts.json ? printJson(result) : printResult(result);
199
203
  }));
204
+ // ── jaz calc fixed-deposit ────────────────────────────────────
205
+ calc
206
+ .command('fixed-deposit')
207
+ .description('Fixed deposit interest accrual schedule (IFRS 9 amortized cost)')
208
+ .requiredOption('--principal <amount>', 'Deposit principal', parseFloat)
209
+ .requiredOption('--rate <percent>', 'Annual interest rate (%)', parseFloat)
210
+ .requiredOption('--term <months>', 'Term in months', parseInt)
211
+ .option('--compound <method>', 'Compounding: none (default), monthly, quarterly, annually', 'none')
212
+ .option('--start-date <date>', 'Placement date (YYYY-MM-DD)')
213
+ .option('--currency <code>', 'Currency code (e.g. SGD, USD)')
214
+ .option('--json', 'Output as JSON')
215
+ .action(calcAction((opts) => {
216
+ const result = calculateFixedDeposit({
217
+ principal: opts.principal,
218
+ annualRate: opts.rate,
219
+ termMonths: opts.term,
220
+ compounding: opts.compound,
221
+ startDate: opts.startDate,
222
+ currency: opts.currency,
223
+ });
224
+ opts.json ? printJson(result) : printResult(result);
225
+ }));
226
+ // ── jaz calc asset-disposal ───────────────────────────────────
227
+ calc
228
+ .command('asset-disposal')
229
+ .description('Fixed asset disposal — gain/loss calculation (IAS 16)')
230
+ .requiredOption('--cost <amount>', 'Original asset cost', parseFloat)
231
+ .requiredOption('--salvage <amount>', 'Salvage value', parseFloat)
232
+ .requiredOption('--life <years>', 'Useful life in years', parseInt)
233
+ .requiredOption('--acquired <date>', 'Acquisition date (YYYY-MM-DD)')
234
+ .requiredOption('--disposed <date>', 'Disposal date (YYYY-MM-DD)')
235
+ .requiredOption('--proceeds <amount>', 'Disposal proceeds (0 for scrap)', parseFloat)
236
+ .option('--method <method>', 'Depreciation method: sl (default), ddb, 150db', 'sl')
237
+ .option('--currency <code>', 'Currency code (e.g. SGD, USD)')
238
+ .option('--json', 'Output as JSON')
239
+ .action(calcAction((opts) => {
240
+ const result = calculateAssetDisposal({
241
+ cost: opts.cost,
242
+ salvageValue: opts.salvage,
243
+ usefulLifeYears: opts.life,
244
+ acquisitionDate: opts.acquired,
245
+ disposalDate: opts.disposed,
246
+ proceeds: opts.proceeds,
247
+ method: opts.method,
248
+ currency: opts.currency,
249
+ });
250
+ opts.json ? printJson(result) : printResult(result);
251
+ }));
200
252
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jaz-cli",
3
- "version": "2.5.0",
3
+ "version": "2.7.0",
4
4
  "description": "CLI to install Jaz AI skills for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,6 +13,8 @@
13
13
  "scripts": {
14
14
  "build": "tsc && node --input-type=module -e \"import{readFileSync,writeFileSync}from'fs';const c=readFileSync('dist/index.js','utf8');if(!c.startsWith('#!')){writeFileSync('dist/index.js','#!/usr/bin/env node\\n'+c)}\"",
15
15
  "dev": "node --loader ts-node/esm src/index.ts",
16
+ "test": "vitest run",
17
+ "test:watch": "vitest",
16
18
  "prepublishOnly": "npm run build"
17
19
  },
18
20
  "keywords": [
@@ -45,6 +47,7 @@
45
47
  "devDependencies": {
46
48
  "@types/node": "^22.10.1",
47
49
  "@types/prompts": "^2.4.9",
48
- "typescript": "^5.7.2"
50
+ "typescript": "^5.7.2",
51
+ "vitest": "^4.0.18"
49
52
  }
50
53
  }