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.
- package/assets/skills/api/SKILL.md +12 -2
- package/assets/skills/api/references/dependencies.md +3 -2
- package/assets/skills/api/references/endpoints.md +78 -0
- package/assets/skills/api/references/feature-glossary.md +5 -5
- package/assets/skills/api/references/field-map.md +17 -0
- package/assets/skills/api/references/full-api-surface.md +1 -1
- package/assets/skills/conversion/SKILL.md +1 -1
- package/assets/skills/transaction-recipes/SKILL.md +53 -19
- package/assets/skills/transaction-recipes/references/asset-disposal.md +174 -0
- package/assets/skills/transaction-recipes/references/fixed-deposit.md +164 -0
- package/assets/skills/transaction-recipes/references/hire-purchase.md +190 -0
- package/dist/__tests__/amortization.test.js +101 -0
- package/dist/__tests__/asset-disposal.test.js +249 -0
- package/dist/__tests__/blueprint.test.js +72 -0
- package/dist/__tests__/depreciation.test.js +125 -0
- package/dist/__tests__/ecl.test.js +134 -0
- package/dist/__tests__/fixed-deposit.test.js +214 -0
- package/dist/__tests__/fx-reval.test.js +115 -0
- package/dist/__tests__/lease.test.js +96 -0
- package/dist/__tests__/loan.test.js +80 -0
- package/dist/__tests__/provision.test.js +141 -0
- package/dist/__tests__/validate.test.js +81 -0
- package/dist/calc/amortization.js +21 -3
- package/dist/calc/asset-disposal.js +155 -0
- package/dist/calc/blueprint.js +26 -1
- package/dist/calc/depreciation.js +24 -1
- package/dist/calc/ecl.js +13 -1
- package/dist/calc/fixed-deposit.js +178 -0
- package/dist/calc/format.js +107 -2
- package/dist/calc/fx-reval.js +11 -1
- package/dist/calc/lease.js +42 -9
- package/dist/calc/loan.js +12 -2
- package/dist/calc/provision.js +17 -1
- package/dist/commands/calc.js +54 -2
- package/package.json +5 -2
package/dist/calc/format.js
CHANGED
|
@@ -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(
|
|
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) {
|
package/dist/calc/fx-reval.js
CHANGED
|
@@ -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: [
|
package/dist/calc/lease.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
93
|
-
capsuleName
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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,
|
package/dist/calc/provision.js
CHANGED
|
@@ -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,
|
package/dist/commands/calc.js
CHANGED
|
@@ -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.
|
|
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
|
}
|