jaz-cli 2.3.0 → 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.
@@ -0,0 +1,150 @@
1
+ # Recipe: Intercompany Transactions
2
+
3
+ ## Scenario
4
+
5
+ Your Singapore holding company (Entity A) provides management services to its Malaysian subsidiary (Entity B) at $15,000 per month. Both entities are on Jaz. At month-end, Entity A invoices Entity B, and each entity records mirrored journal entries. A capsule in each entity tracks the full intercompany lifecycle — charges, settlements, and reconciliation.
6
+
7
+ **Pattern:** Manual journals mirrored in two entities + capsule in each entity
8
+
9
+ ---
10
+
11
+ ## Accounts Involved
12
+
13
+ ### Entity A (Service Provider)
14
+
15
+ | Account | Type | Subtype | Role |
16
+ |---|---|---|---|
17
+ | Intercompany Receivable — Entity B | Asset | Current Asset | Amount owed by Entity B |
18
+ | Management Fee Income | Revenue | Revenue | Service charge recognized |
19
+ | Cash / Bank Account | Asset | Bank | Receives settlement payment |
20
+
21
+ ### Entity B (Service Recipient)
22
+
23
+ | Account | Type | Subtype | Role |
24
+ |---|---|---|---|
25
+ | Intercompany Payable — Entity A | Liability | Current Liability | Amount owed to Entity A |
26
+ | Management Fee Expense | Expense | Expense | Service charge incurred |
27
+ | Cash / Bank Account | Asset | Bank | Sends settlement payment |
28
+
29
+ ---
30
+
31
+ ## Journal Entries
32
+
33
+ ### Step 1: Service Charge (Entity A — Provider)
34
+
35
+ **Option A — Invoice:** Create an invoice from Entity A to Entity B (preferred if you want AR aging and payment tracking):
36
+ - Invoice: $15,000 to "Entity B" contact
37
+ - Coded to Management Fee Income
38
+ - Assign to capsule
39
+
40
+ **Option B — Manual journal:** If you want to bypass AR/AP:
41
+
42
+ | Line | Account | Debit | Credit |
43
+ |---|---|---|---|
44
+ | 1 | Intercompany Receivable — Entity B | $15,000 | |
45
+ | 2 | Management Fee Income | | $15,000 |
46
+
47
+ ### Step 2: Corresponding Entry (Entity B — Recipient)
48
+
49
+ **Option A — Bill:** Create a bill from Entity A in Entity B's books:
50
+ - Bill: $15,000 from "Entity A" contact
51
+ - Coded to Management Fee Expense
52
+ - Assign to capsule
53
+
54
+ **Option B — Manual journal:**
55
+
56
+ | Line | Account | Debit | Credit |
57
+ |---|---|---|---|
58
+ | 1 | Management Fee Expense | $15,000 | |
59
+ | 2 | Intercompany Payable — Entity A | | $15,000 |
60
+
61
+ ### Step 3: Settlement (cash transfer)
62
+
63
+ **Entity A (receives cash):**
64
+ - Record receipt against the invoice, or:
65
+ - Dr Cash / Cr Intercompany Receivable — Entity B
66
+
67
+ **Entity B (sends cash):**
68
+ - Record payment against the bill, or:
69
+ - Dr Intercompany Payable — Entity A / Cr Cash
70
+
71
+ ---
72
+
73
+ ## Capsule Structure
74
+
75
+ **Entity A Capsule:** "Intercompany — Q1 2025 — Entity A → Entity B"
76
+ **Entity B Capsule:** "Intercompany — Q1 2025 — Entity A → Entity B"
77
+ **Capsule Type:** "Intercompany"
78
+
79
+ Contents per entity:
80
+ - 3 monthly charge entries (invoices/bills or journals)
81
+ - Settlement entries
82
+ - **Total entries per entity:** 4-6 per quarter
83
+
84
+ ---
85
+
86
+ ## Worked Example
87
+
88
+ **Setup:**
89
+ - Management fee: $15,000/month
90
+ - Entities: Entity A (Singapore) and Entity B (Malaysia)
91
+ - Quarter: Q1 2025 (Jan, Feb, Mar)
92
+
93
+ **Jan 31 — Entity A:**
94
+ - Create invoice: $15,000 to Entity B
95
+ - Code to Management Fee Income
96
+ - Capsule: "Intercompany — Q1 2025 — Entity A → Entity B"
97
+ - Custom field: "Intercompany Ref" → "IC-2025-001"
98
+
99
+ **Jan 31 — Entity B:**
100
+ - Create bill: $15,000 from Entity A
101
+ - Code to Management Fee Expense
102
+ - Capsule: "Intercompany — Q1 2025 — Entity A → Entity B"
103
+ - Custom field: "Intercompany Ref" → "IC-2025-001"
104
+
105
+ *Repeat for Feb and Mar with matching Intercompany Ref.*
106
+
107
+ **Mar 31 — Quarterly settlement:**
108
+ - Entity B transfers $45,000 to Entity A
109
+ - Entity A: record receipt against 3 invoices
110
+ - Entity B: record payment against 3 bills
111
+ - Both capsules now show matching charge + settlement entries
112
+
113
+ **Consolidation verification:**
114
+ - Entity A: Intercompany Receivable $0, Management Fee Income $45,000
115
+ - Entity B: Intercompany Payable $0, Management Fee Expense $45,000
116
+ - On consolidation: both entries eliminate (income vs. expense, receivable vs. payable)
117
+
118
+ ---
119
+
120
+ ## Enrichment Suggestions
121
+
122
+ | Enrichment | Value | Why |
123
+ |---|---|---|
124
+ | Tracking Tag | "Intercompany" | Filter all IC entries across entities |
125
+ | Custom Field | "Intercompany Ref" → "IC-2025-001" | **Critical** — matching reference in both entities for reconciliation |
126
+ | Custom Field | "Counterparty Entity" → "Entity B" | Identify the other party |
127
+ | Nano Classifier | Service Type → "Management Fee" | Categorize the type of IC charge |
128
+
129
+ ---
130
+
131
+ ## Verification
132
+
133
+ 1. **Intercompany Reconciliation** → At any point, Entity A's Intercompany Receivable balance should equal Entity B's Intercompany Payable balance. Any mismatch indicates a missing or incorrect entry.
134
+ 2. **Trial Balance** → After settlement, both Intercompany Receivable and Intercompany Payable should be $0.
135
+ 3. **P&L match** → Entity A's Management Fee Income = Entity B's Management Fee Expense (same amounts, opposite directions).
136
+ 4. **Custom Field search** → Search both entities for matching "Intercompany Ref" values to verify every charge has a counterpart.
137
+
138
+ ---
139
+
140
+ ## Variations
141
+
142
+ **Cross-currency intercompany:** If Entity A is SGD and Entity B is MYR, use the FX invoice/bill format: `currency: { sourceCurrency: "SGD" }` on Entity B's bill. FX differences are auto-handled. For manual journals, apply the spot rate and record any FX difference.
143
+
144
+ **Cost allocation (not a service fee):** If Entity A allocates shared costs (rent, IT, insurance) to Entity B, the pattern is the same but the income account may be "Intercompany Cost Recovery" (contra-expense) instead of revenue.
145
+
146
+ **Netting:** If both entities charge each other (e.g., A provides management, B provides warehousing), net the amounts and settle only the difference. Each entity still records the full gross charges — netting applies only to the cash settlement.
147
+
148
+ **Transfer pricing:** Ensure intercompany prices are at arm's length for tax compliance. Document the pricing methodology in the capsule custom field (e.g., "TP Method" → "Cost Plus 10%").
149
+
150
+ **Loan vs. trade:** If the intercompany transaction is a loan (not a service), use Intercompany Loan Receivable/Payable instead of trade accounts. Interest may apply — use the loan recipe for interest calculations.
@@ -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
+ }