jaz-cli 2.3.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.
- package/assets/skills/api/SKILL.md +35 -34
- package/assets/skills/api/references/errors.md +15 -7
- package/assets/skills/api/references/feature-glossary.md +2 -0
- package/assets/skills/api/references/field-map.md +3 -3
- package/assets/skills/conversion/SKILL.md +1 -1
- package/assets/skills/transaction-recipes/SKILL.md +158 -14
- package/assets/skills/transaction-recipes/references/asset-disposal.md +174 -0
- package/assets/skills/transaction-recipes/references/bad-debt-provision.md +145 -0
- package/assets/skills/transaction-recipes/references/building-blocks.md +25 -2
- package/assets/skills/transaction-recipes/references/capital-wip.md +167 -0
- package/assets/skills/transaction-recipes/references/dividend.md +111 -0
- package/assets/skills/transaction-recipes/references/employee-accruals.md +154 -0
- package/assets/skills/transaction-recipes/references/fixed-deposit.md +164 -0
- package/assets/skills/transaction-recipes/references/fx-revaluation.md +135 -0
- package/assets/skills/transaction-recipes/references/hire-purchase.md +190 -0
- package/assets/skills/transaction-recipes/references/intercompany.md +150 -0
- package/assets/skills/transaction-recipes/references/provisions.md +142 -0
- package/dist/calc/amortization.js +122 -0
- package/dist/calc/asset-disposal.js +151 -0
- package/dist/calc/blueprint.js +46 -0
- package/dist/calc/depreciation.js +200 -0
- package/dist/calc/ecl.js +101 -0
- package/dist/calc/fixed-deposit.js +169 -0
- package/dist/calc/format.js +494 -0
- package/dist/calc/fx-reval.js +93 -0
- package/dist/calc/lease.js +146 -0
- package/dist/calc/loan.js +107 -0
- package/dist/calc/provision.js +128 -0
- package/dist/calc/types.js +21 -0
- package/dist/calc/validate.js +48 -0
- package/dist/commands/calc.js +252 -0
- package/dist/index.js +2 -0
- package/package.json +3 -2
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IFRS 16 lease calculator.
|
|
3
|
+
*
|
|
4
|
+
* Compliance references:
|
|
5
|
+
* - IFRS 16.26: Initial measurement — PV of lease payments
|
|
6
|
+
* - IFRS 16.36-37: Subsequent measurement — effective interest method
|
|
7
|
+
* - IFRS 16.31-32: ROU depreciation — straight-line over lease term
|
|
8
|
+
*
|
|
9
|
+
* Uses `financial` package for PV calculation.
|
|
10
|
+
* Final period penny adjustment closes lease liability to exactly zero.
|
|
11
|
+
*/
|
|
12
|
+
import { pv } from 'financial';
|
|
13
|
+
import { round2, addMonths } from './types.js';
|
|
14
|
+
import { validatePositive, validatePositiveInteger, validateDateFormat, validateRate } from './validate.js';
|
|
15
|
+
import { journalStep, fixedAssetStep, fmtCapsuleAmount, fmtAmt } from './blueprint.js';
|
|
16
|
+
export function calculateLease(inputs) {
|
|
17
|
+
const { monthlyPayment, termMonths, annualRate, usefulLifeMonths, startDate, currency } = inputs;
|
|
18
|
+
validatePositive(monthlyPayment, 'Monthly payment');
|
|
19
|
+
validateRate(annualRate, 'Annual rate (IBR)');
|
|
20
|
+
validatePositiveInteger(termMonths, 'Term (months)');
|
|
21
|
+
if (usefulLifeMonths !== undefined)
|
|
22
|
+
validatePositiveInteger(usefulLifeMonths, 'Useful life (months)');
|
|
23
|
+
validateDateFormat(startDate);
|
|
24
|
+
const isHirePurchase = usefulLifeMonths !== undefined && usefulLifeMonths > termMonths;
|
|
25
|
+
const monthlyRate = annualRate / 100 / 12;
|
|
26
|
+
// PV of an ordinary annuity (payments at end of period)
|
|
27
|
+
// pv() returns negative, negate for positive value
|
|
28
|
+
const presentValue = round2(-pv(monthlyRate, termMonths, monthlyPayment));
|
|
29
|
+
// ROU depreciation: straight-line over lease term (IFRS 16.31-32)
|
|
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);
|
|
33
|
+
const initialJournal = {
|
|
34
|
+
description: 'Initial recognition — IFRS 16 lease',
|
|
35
|
+
lines: [
|
|
36
|
+
{ account: 'Right-of-Use Asset', debit: presentValue, credit: 0 },
|
|
37
|
+
{ account: 'Lease Liability', debit: 0, credit: presentValue },
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
// Liability unwinding schedule (effective interest method, IFRS 16.36-37)
|
|
41
|
+
const schedule = [];
|
|
42
|
+
let liability = presentValue;
|
|
43
|
+
let totalInterest = 0;
|
|
44
|
+
let totalPrincipal = 0;
|
|
45
|
+
for (let i = 1; i <= termMonths; i++) {
|
|
46
|
+
const openingBalance = round2(liability);
|
|
47
|
+
const isFinal = i === termMonths;
|
|
48
|
+
let interest;
|
|
49
|
+
let principalPortion;
|
|
50
|
+
let periodPayment;
|
|
51
|
+
if (isFinal) {
|
|
52
|
+
// Final period: close liability to exactly zero
|
|
53
|
+
interest = round2(openingBalance * monthlyRate);
|
|
54
|
+
principalPortion = openingBalance;
|
|
55
|
+
periodPayment = round2(principalPortion + interest);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
interest = round2(openingBalance * monthlyRate);
|
|
59
|
+
principalPortion = round2(monthlyPayment - interest);
|
|
60
|
+
periodPayment = monthlyPayment;
|
|
61
|
+
}
|
|
62
|
+
liability = round2(openingBalance - principalPortion);
|
|
63
|
+
totalInterest = round2(totalInterest + interest);
|
|
64
|
+
totalPrincipal = round2(totalPrincipal + principalPortion);
|
|
65
|
+
const date = startDate ? addMonths(startDate, i) : null;
|
|
66
|
+
const journal = {
|
|
67
|
+
description: `Lease payment — Month ${i} of ${termMonths}`,
|
|
68
|
+
lines: [
|
|
69
|
+
{ account: 'Lease Liability', debit: principalPortion, credit: 0 },
|
|
70
|
+
{ account: 'Interest Expense — Leases', debit: interest, credit: 0 },
|
|
71
|
+
{ account: 'Cash / Bank Account', debit: 0, credit: periodPayment },
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
schedule.push({
|
|
75
|
+
period: i,
|
|
76
|
+
date,
|
|
77
|
+
openingBalance,
|
|
78
|
+
payment: periodPayment,
|
|
79
|
+
interest,
|
|
80
|
+
principal: principalPortion,
|
|
81
|
+
closingBalance: liability,
|
|
82
|
+
journal,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
// ROU depreciation total (final month absorbs rounding)
|
|
86
|
+
const totalDepreciation = round2(monthlyRouDepreciation * (termMonths - 1) +
|
|
87
|
+
(presentValue - monthlyRouDepreciation * (termMonths - 1)));
|
|
88
|
+
// Build blueprint for agent execution
|
|
89
|
+
let blueprint = null;
|
|
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}.`;
|
|
94
|
+
const steps = [
|
|
95
|
+
journalStep(1, initialJournal.description, startDate, initialJournal.lines),
|
|
96
|
+
fixedAssetStep(2, depNote, startDate),
|
|
97
|
+
...schedule.map((row, idx) => journalStep(idx + 3, row.journal.description, row.date, row.journal.lines)),
|
|
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`);
|
|
116
|
+
blueprint = {
|
|
117
|
+
capsuleType,
|
|
118
|
+
capsuleName,
|
|
119
|
+
capsuleDescription: workingsLines.join('\n'),
|
|
120
|
+
tags: [capsuleType],
|
|
121
|
+
customFields: isHirePurchase ? { 'HP Agreement #': null } : { 'Lease Contract #': null },
|
|
122
|
+
steps,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
type: 'lease',
|
|
127
|
+
currency: currency ?? null,
|
|
128
|
+
inputs: {
|
|
129
|
+
monthlyPayment,
|
|
130
|
+
termMonths,
|
|
131
|
+
annualRate,
|
|
132
|
+
usefulLifeMonths: usefulLifeMonths ?? null,
|
|
133
|
+
startDate: startDate ?? null,
|
|
134
|
+
},
|
|
135
|
+
presentValue,
|
|
136
|
+
monthlyRouDepreciation,
|
|
137
|
+
depreciationMonths,
|
|
138
|
+
isHirePurchase,
|
|
139
|
+
totalCashPayments: round2(totalInterest + totalPrincipal),
|
|
140
|
+
totalInterest,
|
|
141
|
+
totalDepreciation,
|
|
142
|
+
initialJournal,
|
|
143
|
+
schedule,
|
|
144
|
+
blueprint,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loan amortization calculator.
|
|
3
|
+
* Uses the `financial` package for PMT calculation.
|
|
4
|
+
* IFRS-compliant rounding: 2 decimals per period, final period closes to zero.
|
|
5
|
+
*/
|
|
6
|
+
import { pmt } from 'financial';
|
|
7
|
+
import { round2, addMonths } from './types.js';
|
|
8
|
+
import { validatePositive, validatePositiveInteger, validateDateFormat, validateRate } from './validate.js';
|
|
9
|
+
import { journalStep, cashInStep, fmtCapsuleAmount, fmtAmt } from './blueprint.js';
|
|
10
|
+
export function calculateLoan(inputs) {
|
|
11
|
+
const { principal, annualRate, termMonths, startDate, currency } = inputs;
|
|
12
|
+
validatePositive(principal, 'Principal');
|
|
13
|
+
validateRate(annualRate, 'Annual rate');
|
|
14
|
+
validatePositiveInteger(termMonths, 'Term (months)');
|
|
15
|
+
validateDateFormat(startDate);
|
|
16
|
+
const monthlyRate = annualRate / 100 / 12;
|
|
17
|
+
// PMT returns negative (cash outflow convention), negate for positive value
|
|
18
|
+
const payment = round2(-pmt(monthlyRate, termMonths, principal));
|
|
19
|
+
const schedule = [];
|
|
20
|
+
let balance = principal;
|
|
21
|
+
let totalInterest = 0;
|
|
22
|
+
let totalPrincipal = 0;
|
|
23
|
+
for (let i = 1; i <= termMonths; i++) {
|
|
24
|
+
const openingBalance = round2(balance);
|
|
25
|
+
const isFinal = i === termMonths;
|
|
26
|
+
let interest;
|
|
27
|
+
let principalPortion;
|
|
28
|
+
let periodPayment;
|
|
29
|
+
if (isFinal) {
|
|
30
|
+
// Final period: close balance to exactly zero
|
|
31
|
+
interest = round2(openingBalance * monthlyRate);
|
|
32
|
+
principalPortion = openingBalance;
|
|
33
|
+
periodPayment = round2(principalPortion + interest);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
interest = round2(openingBalance * monthlyRate);
|
|
37
|
+
principalPortion = round2(payment - interest);
|
|
38
|
+
periodPayment = payment;
|
|
39
|
+
}
|
|
40
|
+
balance = round2(openingBalance - principalPortion);
|
|
41
|
+
totalInterest = round2(totalInterest + interest);
|
|
42
|
+
totalPrincipal = round2(totalPrincipal + principalPortion);
|
|
43
|
+
const date = startDate ? addMonths(startDate, i) : null;
|
|
44
|
+
const journal = {
|
|
45
|
+
description: `Loan payment — Month ${i} of ${termMonths}`,
|
|
46
|
+
lines: [
|
|
47
|
+
{ account: 'Loan Payable', debit: principalPortion, credit: 0 },
|
|
48
|
+
{ account: 'Interest Expense', debit: interest, credit: 0 },
|
|
49
|
+
{ account: 'Cash / Bank Account', debit: 0, credit: periodPayment },
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
schedule.push({
|
|
53
|
+
period: i,
|
|
54
|
+
date,
|
|
55
|
+
openingBalance,
|
|
56
|
+
payment: periodPayment,
|
|
57
|
+
interest,
|
|
58
|
+
principal: principalPortion,
|
|
59
|
+
closingBalance: balance,
|
|
60
|
+
journal,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
// Build blueprint for agent execution
|
|
64
|
+
let blueprint = null;
|
|
65
|
+
if (startDate) {
|
|
66
|
+
const steps = [
|
|
67
|
+
cashInStep(1, 'Record loan proceeds received from bank', startDate, [
|
|
68
|
+
{ account: 'Cash / Bank Account', debit: principal, credit: 0 },
|
|
69
|
+
{ account: 'Loan Payable', debit: 0, credit: principal },
|
|
70
|
+
]),
|
|
71
|
+
...schedule.map((row, idx) => journalStep(idx + 2, row.journal.description, row.date, row.journal.lines)),
|
|
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');
|
|
82
|
+
blueprint = {
|
|
83
|
+
capsuleType: 'Loan Repayment',
|
|
84
|
+
capsuleName: `Bank Loan — ${fmtCapsuleAmount(principal, currency)} — ${annualRate}% — ${termMonths} months`,
|
|
85
|
+
capsuleDescription: workings,
|
|
86
|
+
tags: ['Bank Loan'],
|
|
87
|
+
customFields: { 'Loan Reference': null },
|
|
88
|
+
steps,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
type: 'loan',
|
|
93
|
+
currency: currency ?? null,
|
|
94
|
+
inputs: {
|
|
95
|
+
principal,
|
|
96
|
+
annualRate,
|
|
97
|
+
termMonths,
|
|
98
|
+
startDate: startDate ?? null,
|
|
99
|
+
},
|
|
100
|
+
monthlyPayment: payment,
|
|
101
|
+
totalPayments: round2(totalInterest + totalPrincipal),
|
|
102
|
+
totalInterest,
|
|
103
|
+
totalPrincipal,
|
|
104
|
+
schedule,
|
|
105
|
+
blueprint,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
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, cashOutStep, fmtCapsuleAmount, fmtAmt } 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 settlementDate = addMonths(startDate, termMonths);
|
|
85
|
+
const steps = [
|
|
86
|
+
journalStep(1, initialJournal.description, startDate, initialJournal.lines),
|
|
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
|
+
]),
|
|
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');
|
|
103
|
+
blueprint = {
|
|
104
|
+
capsuleType: 'Provisions',
|
|
105
|
+
capsuleName: `Provision — ${fmtCapsuleAmount(amount, currency)} — ${annualRate}% — ${termMonths} months`,
|
|
106
|
+
capsuleDescription: workings,
|
|
107
|
+
tags: ['Provision', 'IAS 37'],
|
|
108
|
+
customFields: { 'Obligation Type': null, 'Expected Settlement Date': null },
|
|
109
|
+
steps,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
type: 'provision',
|
|
114
|
+
currency: currency ?? null,
|
|
115
|
+
inputs: {
|
|
116
|
+
amount,
|
|
117
|
+
annualRate,
|
|
118
|
+
termMonths,
|
|
119
|
+
startDate: startDate ?? null,
|
|
120
|
+
},
|
|
121
|
+
presentValue,
|
|
122
|
+
nominalAmount: amount,
|
|
123
|
+
totalUnwinding,
|
|
124
|
+
initialJournal,
|
|
125
|
+
schedule,
|
|
126
|
+
blueprint,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -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
|
+
}
|