jaz-cli 2.5.0 → 2.6.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,169 @@
1
+ /**
2
+ * Fixed deposit interest accrual calculator.
3
+ *
4
+ * Compliance references:
5
+ * - IFRS 9.4.1.2: Classification at amortized cost (hold-to-collect, passes SPPI)
6
+ * - IFRS 9.5.4.1: Interest revenue via effective interest rate method
7
+ *
8
+ * Supports simple interest (default) and compound interest (monthly/quarterly/annually).
9
+ * Final period absorbs rounding — maturity value is exact.
10
+ */
11
+ import { fv } from 'financial';
12
+ import { round2, addMonths } from './types.js';
13
+ import { validatePositive, validatePositiveInteger, validateDateFormat, validateRate } from './validate.js';
14
+ import { journalStep, cashOutStep, cashInStep, fmtCapsuleAmount, fmtAmt } from './blueprint.js';
15
+ export function calculateFixedDeposit(inputs) {
16
+ const { principal, annualRate, termMonths, compounding = 'none', startDate, currency, } = inputs;
17
+ validatePositive(principal, 'Principal');
18
+ validateRate(annualRate, 'Annual rate');
19
+ validatePositiveInteger(termMonths, 'Term (months)');
20
+ validateDateFormat(startDate);
21
+ const monthlyRate = annualRate / 100 / 12;
22
+ // Compute total interest based on compounding method
23
+ let maturityValue;
24
+ if (compounding === 'none') {
25
+ // Simple interest: Principal × Rate × Time
26
+ maturityValue = round2(principal + principal * (annualRate / 100) * (termMonths / 12));
27
+ }
28
+ else {
29
+ // Compound interest using fv() from financial package
30
+ // fv(rate, nper, pmt, pv) — returns negative, negate
31
+ const periodsPerYear = compounding === 'monthly' ? 12 : compounding === 'quarterly' ? 4 : 1;
32
+ const compoundRate = annualRate / 100 / periodsPerYear;
33
+ const totalCompoundPeriods = Math.floor(termMonths / (12 / periodsPerYear));
34
+ // fv() with negative pv (outflow) returns positive (inflow at maturity)
35
+ maturityValue = round2(fv(compoundRate, totalCompoundPeriods, 0, -principal));
36
+ }
37
+ const totalInterest = round2(maturityValue - principal);
38
+ // Effective annual rate (for display)
39
+ const effectiveRate = round2((Math.pow(maturityValue / principal, 12 / termMonths) - 1) * 100 * 100) / 100;
40
+ // Build monthly accrual schedule
41
+ const schedule = [];
42
+ let accruedTotal = 0;
43
+ if (compounding === 'none') {
44
+ // Simple interest: equal accrual each month
45
+ const monthlyInterest = round2(totalInterest / termMonths);
46
+ let balance = principal;
47
+ for (let i = 1; i <= termMonths; i++) {
48
+ const isFinal = i === termMonths;
49
+ const interest = isFinal ? round2(totalInterest - accruedTotal) : monthlyInterest;
50
+ accruedTotal = round2(accruedTotal + interest);
51
+ const date = startDate ? addMonths(startDate, i) : null;
52
+ const journal = {
53
+ description: `Interest accrual — Month ${i} of ${termMonths}`,
54
+ lines: [
55
+ { account: 'Accrued Interest Receivable', debit: interest, credit: 0 },
56
+ { account: 'Interest Income', debit: 0, credit: interest },
57
+ ],
58
+ };
59
+ schedule.push({
60
+ period: i,
61
+ date,
62
+ openingBalance: balance,
63
+ interest,
64
+ closingBalance: balance, // simple interest: principal unchanged
65
+ journal,
66
+ });
67
+ }
68
+ }
69
+ else {
70
+ // Compound interest: effective interest on growing carrying amount
71
+ let balance = principal;
72
+ for (let i = 1; i <= termMonths; i++) {
73
+ const openingBalance = round2(balance);
74
+ const isFinal = i === termMonths;
75
+ let interest;
76
+ if (isFinal) {
77
+ // Final period: close to exact maturity value
78
+ interest = round2(maturityValue - openingBalance);
79
+ }
80
+ else {
81
+ interest = round2(openingBalance * monthlyRate);
82
+ }
83
+ balance = round2(openingBalance + interest);
84
+ accruedTotal = round2(accruedTotal + interest);
85
+ const date = startDate ? addMonths(startDate, i) : null;
86
+ const journal = {
87
+ description: `Interest accrual — Month ${i} of ${termMonths}`,
88
+ lines: [
89
+ { account: 'Accrued Interest Receivable', debit: interest, credit: 0 },
90
+ { account: 'Interest Income', debit: 0, credit: interest },
91
+ ],
92
+ };
93
+ schedule.push({
94
+ period: i,
95
+ date,
96
+ openingBalance,
97
+ interest,
98
+ closingBalance: balance,
99
+ journal,
100
+ });
101
+ }
102
+ }
103
+ const placementJournal = {
104
+ description: 'Fixed deposit placement',
105
+ lines: [
106
+ { account: 'Fixed Deposit', debit: principal, credit: 0 },
107
+ { account: 'Cash / Bank Account', debit: 0, credit: principal },
108
+ ],
109
+ };
110
+ const maturityJournal = {
111
+ description: 'Fixed deposit maturity',
112
+ lines: [
113
+ { account: 'Cash / Bank Account', debit: maturityValue, credit: 0 },
114
+ { account: 'Fixed Deposit', debit: 0, credit: principal },
115
+ { account: 'Accrued Interest Receivable', debit: 0, credit: totalInterest },
116
+ ],
117
+ };
118
+ // Build blueprint
119
+ let blueprint = null;
120
+ if (startDate) {
121
+ const steps = [
122
+ cashOutStep(1, 'Transfer funds from operating account to fixed deposit', startDate, placementJournal.lines),
123
+ ...schedule.map((row, idx) => journalStep(idx + 2, row.journal.description, row.date, row.journal.lines)),
124
+ cashInStep(termMonths + 2, 'Fixed deposit maturity — principal + accrued interest returned to Cash', addMonths(startDate, termMonths), maturityJournal.lines),
125
+ ];
126
+ const c = currency ?? undefined;
127
+ const compoundLabel = compounding === 'none' ? 'Simple interest' : `Compound ${compounding}`;
128
+ const monthlyAccrual = compounding === 'none' ? round2(totalInterest / termMonths) : null;
129
+ const workingsLines = [
130
+ `Fixed Deposit Interest Accrual Workings (IFRS 9)`,
131
+ `Principal: ${fmtAmt(principal, c)} | Rate: ${annualRate}% p.a. | Term: ${termMonths} months`,
132
+ `Method: ${compoundLabel}`,
133
+ ];
134
+ if (compounding === 'none') {
135
+ workingsLines.push(`Interest: ${fmtAmt(principal, c)} × ${annualRate}% × ${termMonths}/12 = ${fmtAmt(totalInterest, c)}`, `Monthly accrual: ${fmtAmt(monthlyAccrual, c)}`);
136
+ }
137
+ else {
138
+ const periodsPerYear = compounding === 'monthly' ? 12 : compounding === 'quarterly' ? 4 : 1;
139
+ workingsLines.push(`Compounding: ${periodsPerYear}× per year | Effective annual rate: ${effectiveRate}%`, `Total interest: ${fmtAmt(totalInterest, c)}`);
140
+ }
141
+ workingsLines.push(`Maturity value: ${fmtAmt(maturityValue, c)}`, `Classification: Amortized cost (hold-to-collect, passes SPPI)`, `Rounding: 2dp per period, final period closes to exact maturity value`);
142
+ blueprint = {
143
+ capsuleType: 'Fixed Deposit',
144
+ capsuleName: `Fixed Deposit — ${fmtCapsuleAmount(principal, currency)} — ${annualRate}% — ${termMonths} months`,
145
+ capsuleDescription: workingsLines.join('\n'),
146
+ tags: ['Fixed Deposit'],
147
+ customFields: { 'Deposit Reference': null, 'Bank Name': null },
148
+ steps,
149
+ };
150
+ }
151
+ return {
152
+ type: 'fixed-deposit',
153
+ currency: currency ?? null,
154
+ inputs: {
155
+ principal,
156
+ annualRate,
157
+ termMonths,
158
+ compounding,
159
+ startDate: startDate ?? null,
160
+ },
161
+ maturityValue,
162
+ totalInterest,
163
+ effectiveRate,
164
+ schedule,
165
+ placementJournal,
166
+ maturityJournal,
167
+ blueprint,
168
+ };
169
+ }
@@ -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: [
@@ -12,19 +12,24 @@
12
12
  import { pv } from 'financial';
13
13
  import { round2, addMonths } from './types.js';
14
14
  import { validatePositive, validatePositiveInteger, validateDateFormat, validateRate } from './validate.js';
15
- import { journalStep, noteStep, fmtCapsuleAmount } from './blueprint.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)');
21
23
  validateDateFormat(startDate);
24
+ const isHirePurchase = usefulLifeMonths !== undefined && usefulLifeMonths > termMonths;
22
25
  const monthlyRate = annualRate / 100 / 12;
23
26
  // PV of an ordinary annuity (payments at end of period)
24
27
  // pv() returns negative, negate for positive value
25
28
  const presentValue = round2(-pv(monthlyRate, termMonths, monthlyPayment));
26
29
  // ROU depreciation: straight-line over lease term (IFRS 16.31-32)
27
- const monthlyRouDepreciation = round2(presentValue / termMonths);
30
+ // For hire purchase (ownership transfers): depreciate over useful life, not lease term
31
+ const depreciationMonths = isHirePurchase ? usefulLifeMonths : termMonths;
32
+ const monthlyRouDepreciation = round2(presentValue / depreciationMonths);
28
33
  const initialJournal = {
29
34
  description: 'Initial recognition — IFRS 16 lease',
30
35
  lines: [
@@ -83,16 +88,37 @@ export function calculateLease(inputs) {
83
88
  // Build blueprint for agent execution
84
89
  let blueprint = null;
85
90
  if (startDate) {
91
+ const depNote = isHirePurchase
92
+ ? `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}.`
93
+ : `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
94
  const steps = [
87
95
  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),
96
+ fixedAssetStep(2, depNote, startDate),
89
97
  ...schedule.map((row, idx) => journalStep(idx + 3, row.journal.description, row.date, row.journal.lines)),
90
98
  ];
99
+ const capsuleType = isHirePurchase ? 'Hire Purchase' : 'Lease Accounting';
100
+ const capsuleName = isHirePurchase
101
+ ? `Hire Purchase — ${fmtCapsuleAmount(monthlyPayment, currency)}/month — ${termMonths} months — useful life ${depreciationMonths} months`
102
+ : `IFRS 16 Lease — ${fmtCapsuleAmount(monthlyPayment, currency)}/month — ${termMonths} months`;
103
+ const c = currency ?? undefined;
104
+ const workingsLines = [
105
+ `${isHirePurchase ? 'Hire Purchase' : 'IFRS 16 Lease'} Workings`,
106
+ `Monthly payment: ${fmtAmt(monthlyPayment, c)} | IBR: ${annualRate}% p.a. (${round2(monthlyRate * 100)}% monthly)`,
107
+ `Lease term: ${termMonths} months | PV of payments: ${fmtAmt(presentValue, c)} (IFRS 16.26)`,
108
+ ];
109
+ if (isHirePurchase) {
110
+ workingsLines.push(`Useful life: ${usefulLifeMonths} months | ROU depreciation: ${fmtAmt(monthlyRouDepreciation, c)}/month over ${depreciationMonths} months`);
111
+ }
112
+ else {
113
+ workingsLines.push(`ROU depreciation: ${fmtAmt(monthlyRouDepreciation, c)}/month (SL over ${termMonths} months)`);
114
+ }
115
+ 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
116
  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 },
117
+ capsuleType,
118
+ capsuleName,
119
+ capsuleDescription: workingsLines.join('\n'),
120
+ tags: [capsuleType],
121
+ customFields: isHirePurchase ? { 'HP Agreement #': null } : { 'Lease Contract #': null },
96
122
  steps,
97
123
  };
98
124
  }
@@ -103,10 +129,13 @@ export function calculateLease(inputs) {
103
129
  monthlyPayment,
104
130
  termMonths,
105
131
  annualRate,
132
+ usefulLifeMonths: usefulLifeMonths ?? null,
106
133
  startDate: startDate ?? null,
107
134
  },
108
135
  presentValue,
109
136
  monthlyRouDepreciation,
137
+ depreciationMonths,
138
+ isHirePurchase,
110
139
  totalCashPayments: round2(totalInterest + totalPrincipal),
111
140
  totalInterest,
112
141
  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.6.0",
4
4
  "description": "CLI to install Jaz AI skills for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {