jaz-cli 2.2.1 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/assets/skills/api/SKILL.md +35 -34
  2. package/assets/skills/api/references/errors.md +15 -7
  3. package/assets/skills/api/references/feature-glossary.md +15 -3
  4. package/assets/skills/api/references/field-map.md +3 -3
  5. package/assets/skills/conversion/SKILL.md +1 -1
  6. package/assets/skills/transaction-recipes/SKILL.md +199 -0
  7. package/assets/skills/transaction-recipes/references/accrued-expenses.md +157 -0
  8. package/assets/skills/transaction-recipes/references/bad-debt-provision.md +145 -0
  9. package/assets/skills/transaction-recipes/references/bank-loan.md +145 -0
  10. package/assets/skills/transaction-recipes/references/building-blocks.md +135 -0
  11. package/assets/skills/transaction-recipes/references/capital-wip.md +167 -0
  12. package/assets/skills/transaction-recipes/references/declining-balance.md +190 -0
  13. package/assets/skills/transaction-recipes/references/deferred-revenue.md +125 -0
  14. package/assets/skills/transaction-recipes/references/dividend.md +111 -0
  15. package/assets/skills/transaction-recipes/references/employee-accruals.md +154 -0
  16. package/assets/skills/transaction-recipes/references/fx-revaluation.md +135 -0
  17. package/assets/skills/transaction-recipes/references/ifrs16-lease.md +188 -0
  18. package/assets/skills/transaction-recipes/references/intercompany.md +150 -0
  19. package/assets/skills/transaction-recipes/references/prepaid-amortization.md +123 -0
  20. package/assets/skills/transaction-recipes/references/provisions.md +142 -0
  21. package/dist/calc/amortization.js +104 -0
  22. package/dist/calc/blueprint.js +21 -0
  23. package/dist/calc/depreciation.js +177 -0
  24. package/dist/calc/ecl.js +89 -0
  25. package/dist/calc/format.js +389 -0
  26. package/dist/calc/fx-reval.js +83 -0
  27. package/dist/calc/lease.js +117 -0
  28. package/dist/calc/loan.js +97 -0
  29. package/dist/calc/provision.js +112 -0
  30. package/dist/calc/types.js +21 -0
  31. package/dist/calc/validate.js +48 -0
  32. package/dist/commands/calc.js +200 -0
  33. package/dist/commands/init.js +8 -3
  34. package/dist/index.js +2 -0
  35. package/dist/types/index.js +2 -1
  36. package/dist/utils/template.js +1 -1
  37. package/package.json +3 -2
@@ -0,0 +1,142 @@
1
+ # Recipe: Provisions with PV Unwinding (IAS 37)
2
+
3
+ ## Scenario
4
+
5
+ Your company has a product warranty obligation. Based on historical claims data, you estimate $500,000 in warranty costs over the next 5 years. Because the time value of money is material (IAS 37.45), you recognize the provision at its present value using a 4% discount rate. Each month, the discount unwinds as a finance cost, gradually increasing the provision balance until it reaches the full $500,000 at settlement.
6
+
7
+ **Pattern:** Manual journals + capsule (interest amounts change each period, same as loan/lease unwinding)
8
+
9
+ ---
10
+
11
+ ## Accounts Involved
12
+
13
+ | Account | Type | Subtype | Role |
14
+ |---|---|---|---|
15
+ | Provision Expense | Expense | Expense | Initial recognition at PV |
16
+ | Provision for Obligations | Liability | Non-Current Liability | Holds the discounted obligation |
17
+ | Finance Cost — Unwinding | Expense | Expense | Monthly discount unwinding charge |
18
+ | Cash / Bank Account | Asset | Bank | Settlement when obligation is paid |
19
+
20
+ ---
21
+
22
+ ## Journal Entries
23
+
24
+ ### Step 1: Initial Recognition (at PV)
25
+
26
+ | Line | Account | Debit | Credit |
27
+ |---|---|---|---|
28
+ | 1 | Provision Expense | *present value* | |
29
+ | 2 | Provision for Obligations | | *present value* |
30
+
31
+ ### Step 2: Monthly Unwinding (each month)
32
+
33
+ ```
34
+ Interest = Opening Provision Balance × Monthly Rate
35
+ Monthly Rate = Annual Rate / 12
36
+ ```
37
+
38
+ | Line | Account | Debit | Credit |
39
+ |---|---|---|---|
40
+ | 1 | Finance Cost — Unwinding | *interest amount* | |
41
+ | 2 | Provision for Obligations | | *interest amount* |
42
+
43
+ The provision balance grows each month until it reaches the nominal amount at settlement.
44
+
45
+ ### Step 3: Settlement (when obligation is paid)
46
+
47
+ | Line | Account | Debit | Credit |
48
+ |---|---|---|---|
49
+ | 1 | Provision for Obligations | *nominal amount* | |
50
+ | 2 | Cash / Bank Account | | *actual amount paid* |
51
+ | 3 | Provision Expense (or Revenue) | difference | (if actual ≠ estimate) |
52
+
53
+ ---
54
+
55
+ ## Capsule Structure
56
+
57
+ **Capsule:** "Warranty Provision — Product X — 2025"
58
+ **Capsule Type:** "Provisions"
59
+
60
+ Contents:
61
+ - 1 initial recognition journal
62
+ - 60 monthly unwinding journals (5 years)
63
+ - Settlement journal(s) when claims are paid
64
+ - Revision journals if estimate changes
65
+ - **Total entries:** 62+
66
+
67
+ ---
68
+
69
+ ## Worked Example
70
+
71
+ **Setup:**
72
+ - Estimated future outflow: $500,000
73
+ - Discount rate: 4% annual (0.333% monthly)
74
+ - Settlement term: 60 months
75
+ - Present value: $409,501.55
76
+
77
+ **Calculation:**
78
+ ```
79
+ PV = 500,000 / (1 + 0.04/12)^60 = $409,501.55
80
+ Total unwinding = $500,000 − $409,501.55 = $90,498.45
81
+ ```
82
+
83
+ **Jan 1, 2025 — Initial recognition:**
84
+ - Dr Provision Expense $409,501.55
85
+ - Cr Provision for Obligations $409,501.55
86
+ - Description: "Initial provision recognition at PV (IAS 37)"
87
+
88
+ **Jan 31, 2025 — Month 1 unwinding:**
89
+ - Opening balance: $409,501.55
90
+ - Interest: $409,501.55 × 0.00333 = $1,365.01
91
+ - Closing balance: $410,866.56
92
+ - Dr Finance Cost — Unwinding $1,365.01
93
+ - Cr Provision for Obligations $1,365.01
94
+
95
+ **Feb 28, 2025 — Month 2 unwinding:**
96
+ - Opening balance: $410,866.56
97
+ - Interest: $410,866.56 × 0.00333 = $1,369.56
98
+ - Closing balance: $412,236.12
99
+
100
+ **Dec 31, 2029 — Month 60 (final):**
101
+ - Closing balance reaches exactly $500,000.00
102
+
103
+ **Settlement:**
104
+ - Dr Provision for Obligations $500,000
105
+ - Cr Cash $500,000
106
+
107
+ **Use the calculator:** `jaz calc provision --amount 500000 --rate 4 --term 60 --start-date 2025-01-01`
108
+
109
+ ---
110
+
111
+ ## Enrichment Suggestions
112
+
113
+ | Enrichment | Value | Why |
114
+ |---|---|---|
115
+ | Tracking Tag | "Provision" | Filter all provision-related entries |
116
+ | Tracking Tag | "IAS 37" | Mark IFRS-specific adjustments |
117
+ | Custom Field | "Obligation Type" → "Warranty" | Identify the type of provision |
118
+ | Custom Field | "Expected Settlement Date" → "2029-12-31" | Record the estimated settlement date |
119
+
120
+ ---
121
+
122
+ ## Verification
123
+
124
+ 1. **Trial Balance at Jan 31** → Provision for Obligations shows $410,866.56 credit. Finance Cost shows $1,365.01.
125
+ 2. **Trial Balance at Dec 31, Year 1** → Provision balance should match the year-end closing balance from the unwinding schedule (~$426,185).
126
+ 3. **Final period** → Provision balance reaches exactly $500,000.00 (the calculator's final-period adjustment ensures this).
127
+ 4. **P&L per year** → Finance Cost — Unwinding increases each year (compounding effect), totaling $90,498.45 over 5 years.
128
+ 5. **Group General Ledger by Capsule** → Complete history from recognition through every unwinding entry.
129
+
130
+ ---
131
+
132
+ ## Variations
133
+
134
+ **Revision of estimate:** If the estimated outflow changes (e.g., from $500,000 to $550,000), adjust the provision and recalculate the unwinding schedule. The change in estimate goes to Provision Expense, not Finance Cost.
135
+
136
+ **Short-term provision (no discounting):** If settlement is within 12 months, PV discounting is not required (IAS 37.45 — "effect of time value not material"). Simply recognize at the nominal amount with no unwinding schedule.
137
+
138
+ **Decommissioning provision:** Same pattern, but the initial recognition is capitalized to the related asset (Dr Asset / Cr Provision) instead of expensed. The asset is then depreciated over its useful life.
139
+
140
+ **Legal claim provision:** If the outcome is uncertain, provision is recognized only when "probable" (>50% likelihood). If only "possible," disclose as a contingent liability but do not recognize. Use the provision calculator once the amount and timing are estimable.
141
+
142
+ **Multiple payment dates:** If the obligation will be settled in tranches (e.g., warranty claims over 5 years), model as multiple provisions or use a weighted average settlement period.
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Prepaid expense & deferred revenue recognition calculators.
3
+ * Equal-period division. Final period absorbs rounding remainder.
4
+ */
5
+ import { round2, addMonths } from './types.js';
6
+ import { validatePositive, validatePositiveInteger, validateDateFormat } from './validate.js';
7
+ import { journalStep, fmtCapsuleAmount } from './blueprint.js';
8
+ export function calculatePrepaidExpense(inputs) {
9
+ const { amount, periods, frequency = 'monthly', startDate, currency } = inputs;
10
+ validatePositive(amount, 'Amount');
11
+ validatePositiveInteger(periods, 'Periods');
12
+ validateDateFormat(startDate);
13
+ const schedule = buildSchedule(amount, periods, frequency, startDate, 'prepaid');
14
+ let blueprint = null;
15
+ if (startDate) {
16
+ const steps = [
17
+ journalStep(1, 'Initial prepaid payment (bill or cash-out entry)', startDate, [
18
+ { account: 'Prepaid Asset', debit: amount, credit: 0 },
19
+ { account: 'Cash / Bank Account', debit: 0, credit: amount },
20
+ ]),
21
+ ...schedule.map((row, idx) => journalStep(idx + 2, row.journal.description, row.date, row.journal.lines)),
22
+ ];
23
+ blueprint = {
24
+ capsuleType: 'Prepaid Expenses',
25
+ capsuleName: `Prepaid Expense — ${fmtCapsuleAmount(amount, currency)} — ${periods} periods`,
26
+ tags: ['Prepaid Expense'],
27
+ customFields: { 'Policy / Contract #': null },
28
+ steps,
29
+ };
30
+ }
31
+ return {
32
+ type: 'prepaid-expense',
33
+ currency: currency ?? null,
34
+ inputs: { amount, periods, frequency, startDate: startDate ?? null },
35
+ perPeriodAmount: round2(amount / periods),
36
+ schedule,
37
+ blueprint,
38
+ };
39
+ }
40
+ export function calculateDeferredRevenue(inputs) {
41
+ const { amount, periods, frequency = 'monthly', startDate, currency } = inputs;
42
+ validatePositive(amount, 'Amount');
43
+ validatePositiveInteger(periods, 'Periods');
44
+ validateDateFormat(startDate);
45
+ const schedule = buildSchedule(amount, periods, frequency, startDate, 'deferred');
46
+ let blueprint = null;
47
+ if (startDate) {
48
+ const steps = [
49
+ journalStep(1, 'Initial deferred receipt (invoice or cash-in entry)', startDate, [
50
+ { account: 'Cash / Bank Account', debit: amount, credit: 0 },
51
+ { account: 'Deferred Revenue', debit: 0, credit: amount },
52
+ ]),
53
+ ...schedule.map((row, idx) => journalStep(idx + 2, row.journal.description, row.date, row.journal.lines)),
54
+ ];
55
+ blueprint = {
56
+ capsuleType: 'Deferred Revenue',
57
+ capsuleName: `Deferred Revenue — ${fmtCapsuleAmount(amount, currency)} — ${periods} periods`,
58
+ tags: ['Deferred Revenue'],
59
+ customFields: { 'Contract #': null },
60
+ steps,
61
+ };
62
+ }
63
+ return {
64
+ type: 'deferred-revenue',
65
+ currency: currency ?? null,
66
+ inputs: { amount, periods, frequency, startDate: startDate ?? null },
67
+ perPeriodAmount: round2(amount / periods),
68
+ schedule,
69
+ blueprint,
70
+ };
71
+ }
72
+ function buildSchedule(amount, periods, frequency, startDate, kind) {
73
+ const perPeriod = round2(amount / periods);
74
+ const schedule = [];
75
+ let remaining = amount;
76
+ const monthsPerPeriod = frequency === 'quarterly' ? 3 : 1;
77
+ for (let i = 1; i <= periods; i++) {
78
+ const isFinal = i === periods;
79
+ const amortized = isFinal ? round2(remaining) : perPeriod;
80
+ remaining = round2(remaining - amortized);
81
+ const date = startDate ? addMonths(startDate, i * monthsPerPeriod) : null;
82
+ let journal;
83
+ if (kind === 'prepaid') {
84
+ journal = {
85
+ description: `Prepaid expense recognition — Period ${i} of ${periods}`,
86
+ lines: [
87
+ { account: 'Expense', debit: amortized, credit: 0 },
88
+ { account: 'Prepaid Asset', debit: 0, credit: amortized },
89
+ ],
90
+ };
91
+ }
92
+ else {
93
+ journal = {
94
+ description: `Deferred revenue recognition — Period ${i} of ${periods}`,
95
+ lines: [
96
+ { account: 'Deferred Revenue', debit: amortized, credit: 0 },
97
+ { account: 'Revenue', debit: 0, credit: amortized },
98
+ ],
99
+ };
100
+ }
101
+ schedule.push({ period: i, date, amortized, remainingBalance: remaining, journal });
102
+ }
103
+ return schedule;
104
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Blueprint builder — translates calculator output into a
3
+ * capsule + transaction execution plan for Jaz.
4
+ *
5
+ * The blueprint gives an AI agent or human everything needed to
6
+ * create the capsule and post all journal entries in Jaz without
7
+ * any manual translation from the schedule.
8
+ */
9
+ /** Build a blueprint step from a journal entry with a date. */
10
+ export function journalStep(stepNum, description, date, lines) {
11
+ return { step: stepNum, action: 'journal', description, date, lines };
12
+ }
13
+ /** Build a note step (instruction, not a journal — e.g. "register fixed asset"). */
14
+ export function noteStep(stepNum, description, date = null) {
15
+ return { step: stepNum, action: 'note', description, date, lines: [] };
16
+ }
17
+ /** Format a currency amount for capsule names (e.g. "SGD 100,000"). */
18
+ export function fmtCapsuleAmount(amount, currency) {
19
+ const formatted = amount.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 0 });
20
+ return currency ? `${currency} ${formatted}` : formatted;
21
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Depreciation schedule calculator.
3
+ *
4
+ * Methods:
5
+ * - sl: Straight-line (cost - salvage) / life
6
+ * - ddb: Double declining balance (2 / life), auto switch-to-SL
7
+ * - 150db: 150% declining balance (1.5 / life), auto switch-to-SL
8
+ *
9
+ * Book value never goes below salvage value.
10
+ * Total depreciation always equals (cost - salvage).
11
+ */
12
+ import { round2 } from './types.js';
13
+ import { validatePositive, validateNonNegative, validatePositiveInteger, validateSalvageLessThanCost } from './validate.js';
14
+ import { journalStep } from './blueprint.js';
15
+ export function calculateDepreciation(inputs) {
16
+ const { cost, salvageValue, usefulLifeYears, method = 'ddb', frequency = 'annual', currency, } = inputs;
17
+ validatePositive(cost, 'Asset cost');
18
+ validateNonNegative(salvageValue, 'Salvage value');
19
+ validatePositiveInteger(usefulLifeYears, 'Useful life (years)');
20
+ if (salvageValue > 0)
21
+ validateSalvageLessThanCost(salvageValue, cost);
22
+ const depreciableBase = round2(cost - salvageValue);
23
+ if (method === 'sl') {
24
+ return buildStraightLineResult(cost, salvageValue, usefulLifeYears, depreciableBase, frequency, currency);
25
+ }
26
+ const multiplier = method === 'ddb' ? 2 : 1.5;
27
+ const annualRate = multiplier / usefulLifeYears;
28
+ if (frequency === 'annual') {
29
+ return buildDecliningSchedule(cost, salvageValue, usefulLifeYears, annualRate, depreciableBase, method, currency);
30
+ }
31
+ // Monthly: compute annual schedule then subdivide each year into 12 equal months
32
+ const annual = buildDecliningSchedule(cost, salvageValue, usefulLifeYears, annualRate, depreciableBase, method, currency);
33
+ const monthlySchedule = [];
34
+ let periodNum = 0;
35
+ for (const yearRow of annual.schedule) {
36
+ const monthlyAmount = round2(yearRow.depreciation / 12);
37
+ let yearRemaining = yearRow.depreciation;
38
+ for (let m = 1; m <= 12; m++) {
39
+ periodNum++;
40
+ const isLastMonth = m === 12;
41
+ const dep = isLastMonth ? round2(yearRemaining) : monthlyAmount;
42
+ yearRemaining = round2(yearRemaining - dep);
43
+ const openingBV = periodNum === 1
44
+ ? cost
45
+ : round2(monthlySchedule[periodNum - 2].closingBookValue);
46
+ const closingBV = round2(openingBV - dep);
47
+ const journal = {
48
+ description: `${yearRow.methodUsed} depreciation — Month ${periodNum} of ${usefulLifeYears * 12}`,
49
+ lines: [
50
+ { account: 'Depreciation Expense', debit: dep, credit: 0 },
51
+ { account: 'Accumulated Depreciation', debit: 0, credit: dep },
52
+ ],
53
+ };
54
+ monthlySchedule.push({
55
+ period: periodNum,
56
+ date: null,
57
+ openingBookValue: openingBV,
58
+ ddbAmount: round2(yearRow.ddbAmount / 12),
59
+ slAmount: round2(yearRow.slAmount / 12),
60
+ methodUsed: yearRow.methodUsed,
61
+ depreciation: dep,
62
+ closingBookValue: closingBV,
63
+ journal,
64
+ });
65
+ }
66
+ }
67
+ return {
68
+ type: 'depreciation',
69
+ currency: currency ?? null,
70
+ inputs: { cost, salvageValue, usefulLifeYears, method, frequency },
71
+ totalDepreciation: depreciableBase,
72
+ schedule: monthlySchedule,
73
+ blueprint: annual.blueprint, // reuse annual blueprint (monthly is just subdivision)
74
+ };
75
+ }
76
+ function buildStraightLineResult(cost, salvageValue, usefulLifeYears, depreciableBase, frequency, currency) {
77
+ const totalPeriods = frequency === 'monthly' ? usefulLifeYears * 12 : usefulLifeYears;
78
+ const perPeriod = round2(depreciableBase / totalPeriods);
79
+ const periodLabel = frequency === 'monthly' ? 'Month' : 'Year';
80
+ const totalLabel = frequency === 'monthly' ? usefulLifeYears * 12 : usefulLifeYears;
81
+ const schedule = [];
82
+ let bookValue = cost;
83
+ for (let i = 1; i <= totalPeriods; i++) {
84
+ const openingBV = round2(bookValue);
85
+ const isFinal = i === totalPeriods;
86
+ const dep = isFinal ? round2(openingBV - salvageValue) : perPeriod;
87
+ bookValue = round2(openingBV - dep);
88
+ const journal = {
89
+ description: `SL depreciation — ${periodLabel} ${i} of ${totalLabel}`,
90
+ lines: [
91
+ { account: 'Depreciation Expense', debit: dep, credit: 0 },
92
+ { account: 'Accumulated Depreciation', debit: 0, credit: dep },
93
+ ],
94
+ };
95
+ schedule.push({
96
+ period: i,
97
+ date: null,
98
+ openingBookValue: openingBV,
99
+ ddbAmount: 0,
100
+ slAmount: dep,
101
+ methodUsed: 'SL',
102
+ depreciation: dep,
103
+ closingBookValue: bookValue,
104
+ journal,
105
+ });
106
+ }
107
+ const blueprintSteps = schedule.map((row, idx) => journalStep(idx + 1, row.journal.description, row.date, row.journal.lines));
108
+ return {
109
+ type: 'depreciation',
110
+ currency: currency ?? null,
111
+ inputs: { cost, salvageValue, usefulLifeYears, method: 'sl', frequency },
112
+ totalDepreciation: depreciableBase,
113
+ schedule,
114
+ blueprint: {
115
+ capsuleType: 'Depreciation',
116
+ capsuleName: `SL Depreciation — ${usefulLifeYears} years`,
117
+ tags: ['Depreciation'],
118
+ customFields: { 'Asset ID': null },
119
+ steps: blueprintSteps,
120
+ },
121
+ };
122
+ }
123
+ function buildDecliningSchedule(cost, salvageValue, usefulLifeYears, annualRate, depreciableBase, method, currency) {
124
+ const schedule = [];
125
+ let bookValue = cost;
126
+ for (let year = 1; year <= usefulLifeYears; year++) {
127
+ const openingBV = round2(bookValue);
128
+ const remainingYears = usefulLifeYears - year + 1;
129
+ // DDB amount (rate × opening book value)
130
+ const ddbAmount = round2(openingBV * annualRate);
131
+ // SL for remaining life: (book value - salvage) / remaining years
132
+ const slAmount = round2((openingBV - salvageValue) / remainingYears);
133
+ // Switch to SL when SL >= DDB, or when DDB would push below salvage
134
+ const ddbCapped = round2(openingBV - ddbAmount) < salvageValue;
135
+ const useSL = slAmount >= ddbAmount || ddbCapped;
136
+ const methodUsed = useSL ? 'SL' : (method === '150db' ? '150DB' : 'DDB');
137
+ // Depreciation cannot push book value below salvage
138
+ let depreciation = useSL ? slAmount : ddbAmount;
139
+ if (round2(openingBV - depreciation) < salvageValue) {
140
+ depreciation = round2(openingBV - salvageValue);
141
+ }
142
+ bookValue = round2(openingBV - depreciation);
143
+ const journal = {
144
+ description: `${methodUsed} depreciation — Year ${year} of ${usefulLifeYears}`,
145
+ lines: [
146
+ { account: 'Depreciation Expense', debit: depreciation, credit: 0 },
147
+ { account: 'Accumulated Depreciation', debit: 0, credit: depreciation },
148
+ ],
149
+ };
150
+ schedule.push({
151
+ period: year,
152
+ date: null,
153
+ openingBookValue: openingBV,
154
+ ddbAmount,
155
+ slAmount,
156
+ methodUsed,
157
+ depreciation,
158
+ closingBookValue: bookValue,
159
+ journal,
160
+ });
161
+ }
162
+ const blueprintSteps = schedule.map((row, idx) => journalStep(idx + 1, row.journal.description, row.date, row.journal.lines));
163
+ return {
164
+ type: 'depreciation',
165
+ currency: currency ?? null,
166
+ inputs: { cost, salvageValue, usefulLifeYears, method, frequency: 'annual' },
167
+ totalDepreciation: depreciableBase,
168
+ schedule,
169
+ blueprint: {
170
+ capsuleType: 'Depreciation',
171
+ capsuleName: `${method.toUpperCase()} Depreciation — ${usefulLifeYears} years`,
172
+ tags: ['Depreciation'],
173
+ customFields: { 'Asset ID': null },
174
+ steps: blueprintSteps,
175
+ },
176
+ };
177
+ }
@@ -0,0 +1,89 @@
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 } 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 blueprint = {
65
+ capsuleType: 'ECL Provision',
66
+ capsuleName: `ECL Provision — ${fmtCapsuleAmount(totalEcl, currency)} — ${buckets.length} buckets`,
67
+ tags: ['ECL', 'Bad Debt'],
68
+ customFields: { 'Reporting Period': null, 'Aged Receivables Report Date': null },
69
+ steps: [
70
+ journalStep(1, journal.description, null, journal.lines),
71
+ ],
72
+ };
73
+ return {
74
+ type: 'ecl',
75
+ currency: currency ?? null,
76
+ inputs: {
77
+ buckets: buckets.map(b => ({ name: b.name, balance: b.balance, rate: b.rate })),
78
+ existingProvision,
79
+ },
80
+ totalReceivables,
81
+ totalEcl,
82
+ weightedRate,
83
+ adjustmentRequired,
84
+ isIncrease,
85
+ bucketDetails,
86
+ journal,
87
+ blueprint,
88
+ };
89
+ }