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
package/dist/calc/ecl.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expected Credit Loss (ECL) calculator — IFRS 9 simplified approach.
|
|
3
|
+
*
|
|
4
|
+
* Builds a provision matrix from aged receivables buckets and historical
|
|
5
|
+
* loss rates, then calculates the required provision adjustment.
|
|
6
|
+
*
|
|
7
|
+
* Compliance references:
|
|
8
|
+
* - IFRS 9.5.5.15: Simplified approach for trade receivables
|
|
9
|
+
* - IFRS 9.B5.5.35: Provision matrix method
|
|
10
|
+
* - IFRS 9.5.5.17: Always recognize lifetime ECL (no staging)
|
|
11
|
+
*
|
|
12
|
+
* Standard aging buckets: Current, 1-30d, 31-60d, 61-90d, 91+d
|
|
13
|
+
* Each bucket gets a historical loss rate (%) applied.
|
|
14
|
+
* The delta between total ECL and existing provision = period charge.
|
|
15
|
+
*/
|
|
16
|
+
import { round2 } from './types.js';
|
|
17
|
+
import { validateNonNegative } from './validate.js';
|
|
18
|
+
import { journalStep, fmtCapsuleAmount, fmtAmt } from './blueprint.js';
|
|
19
|
+
export function calculateEcl(inputs) {
|
|
20
|
+
const { buckets, existingProvision = 0, currency } = inputs;
|
|
21
|
+
// Validate each bucket
|
|
22
|
+
for (const b of buckets) {
|
|
23
|
+
validateNonNegative(b.balance, `${b.name} balance`);
|
|
24
|
+
validateNonNegative(b.rate, `${b.name} loss rate`);
|
|
25
|
+
}
|
|
26
|
+
validateNonNegative(existingProvision, 'Existing provision');
|
|
27
|
+
// Calculate ECL per bucket
|
|
28
|
+
const bucketDetails = buckets.map(b => ({
|
|
29
|
+
bucket: b.name,
|
|
30
|
+
balance: b.balance,
|
|
31
|
+
lossRate: b.rate,
|
|
32
|
+
ecl: round2(b.balance * b.rate / 100),
|
|
33
|
+
}));
|
|
34
|
+
const totalReceivables = round2(buckets.reduce((sum, b) => sum + b.balance, 0));
|
|
35
|
+
const totalEcl = round2(bucketDetails.reduce((sum, b) => sum + b.ecl, 0));
|
|
36
|
+
const adjustmentRequired = round2(totalEcl - existingProvision);
|
|
37
|
+
const isIncrease = adjustmentRequired >= 0;
|
|
38
|
+
const absAdj = Math.abs(adjustmentRequired);
|
|
39
|
+
// Weighted average loss rate for summary
|
|
40
|
+
const weightedRate = totalReceivables > 0
|
|
41
|
+
? round2(totalEcl / totalReceivables * 100 * 100) / 100 // round to 2dp %
|
|
42
|
+
: 0;
|
|
43
|
+
// Journal entry for the provision adjustment
|
|
44
|
+
let journal;
|
|
45
|
+
if (isIncrease) {
|
|
46
|
+
journal = {
|
|
47
|
+
description: `ECL provision increase — IFRS 9 simplified approach`,
|
|
48
|
+
lines: [
|
|
49
|
+
{ account: 'Bad Debt Expense', debit: absAdj, credit: 0 },
|
|
50
|
+
{ account: 'Allowance for Doubtful Debts', debit: 0, credit: absAdj },
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
journal = {
|
|
56
|
+
description: `ECL provision release — IFRS 9 simplified approach`,
|
|
57
|
+
lines: [
|
|
58
|
+
{ account: 'Allowance for Doubtful Debts', debit: absAdj, credit: 0 },
|
|
59
|
+
{ account: 'Bad Debt Expense', debit: 0, credit: absAdj },
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
// Blueprint
|
|
64
|
+
const c = currency ?? undefined;
|
|
65
|
+
const bucketWorkings = bucketDetails.map(b => ` ${b.bucket}: ${fmtAmt(b.balance, c)} × ${b.lossRate}% = ${fmtAmt(b.ecl, c)}`).join('\n');
|
|
66
|
+
const workings = [
|
|
67
|
+
`ECL Provision Matrix Workings (IFRS 9)`,
|
|
68
|
+
`Total receivables: ${fmtAmt(totalReceivables, c)} | Weighted avg loss rate: ${weightedRate}%`,
|
|
69
|
+
`Provision matrix:`,
|
|
70
|
+
bucketWorkings,
|
|
71
|
+
`Total ECL required: ${fmtAmt(totalEcl, c)} | Existing provision: ${fmtAmt(existingProvision, c)}`,
|
|
72
|
+
`Adjustment: ${fmtAmt(Math.abs(adjustmentRequired), c)} (${isIncrease ? 'increase' : 'release'})`,
|
|
73
|
+
`Method: IFRS 9.5.5.15 simplified approach — lifetime ECL, provision matrix`,
|
|
74
|
+
].join('\n');
|
|
75
|
+
const blueprint = {
|
|
76
|
+
capsuleType: 'ECL Provision',
|
|
77
|
+
capsuleName: `ECL Provision — ${fmtCapsuleAmount(totalEcl, currency)} — ${buckets.length} buckets`,
|
|
78
|
+
capsuleDescription: workings,
|
|
79
|
+
tags: ['ECL', 'Bad Debt'],
|
|
80
|
+
customFields: { 'Reporting Period': null, 'Aged Receivables Report Date': null },
|
|
81
|
+
steps: [
|
|
82
|
+
journalStep(1, journal.description, null, journal.lines),
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
return {
|
|
86
|
+
type: 'ecl',
|
|
87
|
+
currency: currency ?? null,
|
|
88
|
+
inputs: {
|
|
89
|
+
buckets: buckets.map(b => ({ name: b.name, balance: b.balance, rate: b.rate })),
|
|
90
|
+
existingProvision,
|
|
91
|
+
},
|
|
92
|
+
totalReceivables,
|
|
93
|
+
totalEcl,
|
|
94
|
+
weightedRate,
|
|
95
|
+
adjustmentRequired,
|
|
96
|
+
isIncrease,
|
|
97
|
+
bucketDetails,
|
|
98
|
+
journal,
|
|
99
|
+
blueprint,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -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
|
+
}
|