jaz-cli 2.5.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.
@@ -0,0 +1,190 @@
1
+ # Recipe: Hire Purchase
2
+
3
+ ## Scenario
4
+
5
+ Your company acquires a motor vehicle under a hire purchase (HP) agreement: $5,000/month for 36 months at 5% incremental borrowing rate. Ownership transfers to you at the end of the term. Under IFRS 16, this is accounted for the same way as any other lease — right-of-use asset, lease liability, effective interest unwinding — with **one critical difference**: the ROU asset is depreciated over its **useful life** (60 months), not the lease term (36 months), because ownership transfers at the end.
6
+
7
+ **Pattern:** Hybrid — native fixed asset (straight-line ROU depreciation over useful life) + manual journals (liability unwinding) + capsule
8
+
9
+ **Cross-reference:** This recipe is a variant of the [IFRS 16 Lease recipe](./ifrs16-lease.md). Everything is identical except the depreciation period. Read the IFRS 16 recipe first for the full foundational treatment, then return here for the hire purchase-specific difference.
10
+
11
+ **Why the difference:** IFRS 16.32 requires that when ownership transfers (or a purchase option is reasonably certain to be exercised), the ROU asset is depreciated over the asset's useful life rather than the lease term. Hire purchase agreements always transfer ownership, so this rule always applies.
12
+
13
+ ---
14
+
15
+ ## Accounts Involved
16
+
17
+ | Account | Type | Subtype | Role |
18
+ |---|---|---|---|
19
+ | Right-of-Use Asset | Asset | Non-Current Asset | The HP'd asset (e.g., motor vehicle) |
20
+ | Accumulated Depreciation — ROU | Asset | Non-Current Asset | Contra-asset reducing ROU net book value |
21
+ | Lease Liability | Liability | Non-Current Liability | PV of future HP payments |
22
+ | Interest Expense — Leases | Expense | Finance Cost | Effective interest on lease liability |
23
+ | Depreciation Expense — ROU | Expense | Depreciation | Straight-line over useful life (NOT lease term) |
24
+ | Cash / Bank Account | Asset | Bank | Makes monthly HP payments |
25
+
26
+ > **Same accounts as IFRS 16 lease.** The only change is the depreciation calculation — the accounts, journal structure, and liability unwinding are all identical.
27
+
28
+ ---
29
+
30
+ ## Journal Entries
31
+
32
+ ### Step 1: Initial Recognition (same as IFRS 16)
33
+
34
+ | Line | Account | Debit | Credit |
35
+ |---|---|---|---|
36
+ | 1 | Right-of-Use Asset | $166,828.51 | |
37
+ | 2 | Lease Liability | | $166,828.51 |
38
+
39
+ PV of 36 payments of $5,000 at 5% annual (0.4167% monthly).
40
+
41
+ ### Step 2: Monthly HP Payment (same as IFRS 16)
42
+
43
+ Each $5,000 payment splits between interest (expense) and principal (liability reduction):
44
+
45
+ | Line | Account | Debit | Credit |
46
+ |---|---|---|---|
47
+ | 1 | Lease Liability | *principal portion* | |
48
+ | 2 | Interest Expense — Leases | *interest portion* | |
49
+ | 3 | Cash / Bank Account | | $5,000 |
50
+
51
+ **Calculation per month:**
52
+ - Interest = Outstanding liability x 0.4167%
53
+ - Principal = $5,000 - Interest
54
+ - New liability = Outstanding liability - Principal
55
+
56
+ ### Step 3: Monthly Depreciation — THE KEY DIFFERENCE
57
+
58
+ | Line | Account | Debit | Credit |
59
+ |---|---|---|---|
60
+ | 1 | Depreciation Expense — ROU | $2,780.48 | |
61
+ | 2 | Accumulated Depreciation — ROU | | $2,780.48 |
62
+
63
+ **Standard IFRS 16 lease:** $166,828.51 / 36 months = **$4,634.13/month**
64
+ **Hire purchase:** $166,828.51 / 60 months = **$2,780.48/month**
65
+
66
+ > The hire purchase depreciation rate is **$1,853.65/month LESS** than a standard lease. This is because the asset's economic life (60 months) extends well beyond the payment term (36 months). Ownership transfers, so the asset continues generating value after the last payment.
67
+
68
+ ---
69
+
70
+ ## Capsule Structure
71
+
72
+ **Capsule:** "Hire Purchase — Motor Vehicle — 2025"
73
+ **Capsule Type:** "Hire Purchase"
74
+
75
+ Contents:
76
+ - 1 initial recognition journal (ROU asset + lease liability)
77
+ - 36 monthly payment journals (manual — interest changes each month)
78
+ - 60 monthly depreciation entries (auto-generated by fixed asset register — these appear in the ledger but aren't manually created)
79
+ - **Manually created entries:** 37
80
+
81
+ > The fixed asset depreciation entries are not assigned to the capsule automatically. Group the General Ledger by the ROU asset account alongside the capsule view for a complete picture. Depreciation continues for 24 months after the last HP payment.
82
+
83
+ ---
84
+
85
+ ## Worked Example
86
+
87
+ **HP terms:**
88
+ - Monthly payment: $5,000
89
+ - Lease term: 36 months
90
+ - Incremental borrowing rate: 5% annual (0.4167% monthly)
91
+ - Asset useful life: 60 months
92
+ - PV of lease payments: $166,828.51
93
+
94
+ **CLI command:**
95
+ ```
96
+ jaz calc lease --payment 5000 --term 36 --rate 5 --useful-life 60 --start-date 2025-01-01
97
+ ```
98
+
99
+ The `--useful-life 60` flag tells the calculator this is a hire purchase — depreciation will use 60 months instead of the 36-month lease term. The CLI generates a `capsuleDescription` with full workings so the capsule is self-documenting.
100
+
101
+ ### Liability Unwinding Table (first 3 months + month 36)
102
+
103
+ | Month | Opening Liability | Interest (0.4167%) | Principal | Closing Liability |
104
+ |---|---|---|---|---|
105
+ | 1 | $166,828.51 | $695.12 | $4,304.88 | $162,523.63 |
106
+ | 2 | $162,523.63 | $677.18 | $4,322.82 | $158,200.81 |
107
+ | 3 | $158,200.81 | $659.17 | $4,340.83 | $153,859.98 |
108
+ | ... | ... | ... | ... | ... |
109
+ | 36 | $4,979.24 | $20.75 | $4,979.24 | $0.00* |
110
+
111
+ > *Final payment adjusted to close the liability exactly to zero (payment = $4,999.99).
112
+
113
+ **Month 1 journal entry (manual — same as IFRS 16):**
114
+ - Dr Lease Liability $4,304.88
115
+ - Dr Interest Expense — Leases $695.12
116
+ - Cr Cash $5,000.00
117
+ - Description: "HP payment — Month 1 of 36 (Motor Vehicle)"
118
+ - Assign to capsule
119
+
120
+ **Month 1 depreciation (automatic — posted by Jaz FA register):**
121
+ - Dr Depreciation Expense — ROU $2,780.48
122
+ - Cr Accumulated Depreciation — ROU $2,780.48
123
+
124
+ ### Depreciation Comparison: Hire Purchase vs Standard Lease
125
+
126
+ | Item | Standard IFRS 16 Lease | Hire Purchase |
127
+ |---|---|---|
128
+ | Depreciation period | 36 months (lease term) | 60 months (useful life) |
129
+ | Monthly depreciation | $4,634.13 | $2,780.48 |
130
+ | **Difference** | — | **$1,853.65/month LESS** |
131
+
132
+ ### What Happens After Month 36 (Lease Payments End)
133
+
134
+ | Item | Value |
135
+ |---|---|
136
+ | Lease Liability | $0 (fully unwound) |
137
+ | ROU Asset (gross) | $166,828.51 |
138
+ | Accumulated Depreciation (36 months) | $100,097.28 |
139
+ | **ROU Net Book Value** | **$66,731.23** |
140
+ | Remaining depreciation period | 24 more months |
141
+
142
+ After the last HP payment, the lease liability is zero and no more cash leaves the account. But the ROU asset still has a book value of $66,731.23 because it was depreciated over 60 months, not 36. Depreciation of $2,780.48/month continues for another 24 months until the asset is fully depreciated.
143
+
144
+ ### Summary Over Full Useful Life (60 Months)
145
+
146
+ | Item | Total |
147
+ |---|---|
148
+ | Total cash payments | $180,000 (36 x $5,000) |
149
+ | Total interest expense | $13,171.48 |
150
+ | Total depreciation expense | $166,828.51 |
151
+ | **Total P&L impact** | **$180,000** |
152
+ | Lease liability at month 36 | $0 |
153
+ | ROU asset net book value at month 36 | $66,731.23 |
154
+ | ROU asset net book value at month 60 | $0 |
155
+
156
+ > **P&L timing differs from a standard lease.** With hire purchase, the P&L charge is spread over 60 months instead of 36. In months 1-36, both interest and depreciation hit the P&L. In months 37-60, only depreciation remains — at a lower rate than the original cash payment.
157
+
158
+ ---
159
+
160
+ ## Enrichment Suggestions
161
+
162
+ | Enrichment | Value | Why |
163
+ |---|---|---|
164
+ | Tracking Tag | "Hire Purchase" | Filter all HP-related transactions |
165
+ | Nano Classifier | Asset Type → "Motor Vehicle" | Break down by asset class if multiple HPs |
166
+ | Custom Field | "HP Agreement #" → "HP-2025-0089" | Record the hire purchase agreement reference |
167
+
168
+ ---
169
+
170
+ ## Verification
171
+
172
+ 1. **Lease Liability should reduce to $0 at end of lease term (month 36)** → Group General Ledger by Capsule. Liability starts at $166,828.51 and closes at $0 after 36 payments.
173
+ 2. **Interest Expense total = total cash payments - PV** → $180,000 − $166,828.51 = $13,171.48 (rounding may cause ±$0.01 variance).
174
+ 3. **ROU Asset continues depreciating after lease payments end** → At month 36, ROU net book value = $66,731.23. Depreciation continues at $2,780.48/month for 24 more months.
175
+ 4. **At end of useful life (month 60), ROU asset is fully depreciated** → ROU Asset $166,828.51 − Accumulated Depreciation $166,828.51 = $0 net book value.
176
+ 5. **Balance Sheet check** → Between months 37-60, no lease liability exists but the ROU asset (net of accumulated depreciation) continues appearing on the balance sheet. This is the distinguishing feature of hire purchase vs operating lease.
177
+
178
+ ---
179
+
180
+ ## Variations
181
+
182
+ **Standard operating lease (no ownership transfer):** Use `jaz calc lease --payment 5000 --term 36 --rate 5` without the `--useful-life` flag. The ROU asset is depreciated over the lease term (36 months), not useful life. See the [IFRS 16 Lease recipe](./ifrs16-lease.md).
183
+
184
+ **Guaranteed residual value:** If the HP agreement includes a guaranteed residual value (e.g., $10,000 balloon payment at end of term), add the PV of the residual to the initial ROU asset and lease liability. The liability schedule must include the balloon as the final period payment.
185
+
186
+ **Variable payments:** Only the fixed portion of HP payments is included in the PV calculation. Any variable component (e.g., mileage-based charges) is recognized as expense when incurred, not capitalized in the ROU asset.
187
+
188
+ **SG motor vehicle specifics:** The COE (Certificate of Entitlement) is typically treated as a separate intangible asset with its own amortization schedule (10 years for Category A/B), not bundled into the HP's ROU asset. Create a separate capsule for the COE if applicable.
189
+
190
+ **SG tax depreciation (IRAS):** Capital allowances for motor vehicles under Section 19/19A may differ from IFRS depreciation. The S-car cap ($35,000 for private cars) and the 3-year write-off period for commercial vehicles don't align with IFRS useful life estimates. Businesses may need to track both IFRS depreciation (for financial reporting) and tax depreciation (for S68 deductions) separately. This recipe covers IFRS only — tax depreciation adjustments are out of scope.
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { round2, addMonths } from './types.js';
6
6
  import { validatePositive, validatePositiveInteger, validateDateFormat } from './validate.js';
7
- import { journalStep, fmtCapsuleAmount } from './blueprint.js';
7
+ import { journalStep, billStep, invoiceStep, fmtCapsuleAmount, fmtAmt } from './blueprint.js';
8
8
  export function calculatePrepaidExpense(inputs) {
9
9
  const { amount, periods, frequency = 'monthly', startDate, currency } = inputs;
10
10
  validatePositive(amount, 'Amount');
@@ -14,15 +14,24 @@ export function calculatePrepaidExpense(inputs) {
14
14
  let blueprint = null;
15
15
  if (startDate) {
16
16
  const steps = [
17
- journalStep(1, 'Initial prepaid payment (bill or cash-out entry)', startDate, [
17
+ billStep(1, 'Create bill from supplier coded to Prepaid Asset, then pay from Cash / Bank Account', startDate, [
18
18
  { account: 'Prepaid Asset', debit: amount, credit: 0 },
19
19
  { account: 'Cash / Bank Account', debit: 0, credit: amount },
20
20
  ]),
21
21
  ...schedule.map((row, idx) => journalStep(idx + 2, row.journal.description, row.date, row.journal.lines)),
22
22
  ];
23
+ const c = currency ?? undefined;
24
+ const workings = [
25
+ `Prepaid Expense Recognition Workings`,
26
+ `Total prepaid: ${fmtAmt(amount, c)} | Periods: ${periods} (${frequency})`,
27
+ `Per period: ${fmtAmt(round2(amount / periods), c)}`,
28
+ `Method: Straight-line recognition over ${periods} ${frequency} periods`,
29
+ `Rounding: 2dp per period, final period absorbs remainder`,
30
+ ].join('\n');
23
31
  blueprint = {
24
32
  capsuleType: 'Prepaid Expenses',
25
33
  capsuleName: `Prepaid Expense — ${fmtCapsuleAmount(amount, currency)} — ${periods} periods`,
34
+ capsuleDescription: workings,
26
35
  tags: ['Prepaid Expense'],
27
36
  customFields: { 'Policy / Contract #': null },
28
37
  steps,
@@ -46,15 +55,24 @@ export function calculateDeferredRevenue(inputs) {
46
55
  let blueprint = null;
47
56
  if (startDate) {
48
57
  const steps = [
49
- journalStep(1, 'Initial deferred receipt (invoice or cash-in entry)', startDate, [
58
+ invoiceStep(1, 'Create invoice to customer coded to Deferred Revenue, record payment to Cash / Bank Account', startDate, [
50
59
  { account: 'Cash / Bank Account', debit: amount, credit: 0 },
51
60
  { account: 'Deferred Revenue', debit: 0, credit: amount },
52
61
  ]),
53
62
  ...schedule.map((row, idx) => journalStep(idx + 2, row.journal.description, row.date, row.journal.lines)),
54
63
  ];
64
+ const c2 = currency ?? undefined;
65
+ const workings2 = [
66
+ `Deferred Revenue Recognition Workings`,
67
+ `Total deferred: ${fmtAmt(amount, c2)} | Periods: ${periods} (${frequency})`,
68
+ `Per period: ${fmtAmt(round2(amount / periods), c2)}`,
69
+ `Method: Straight-line recognition over ${periods} ${frequency} periods`,
70
+ `Rounding: 2dp per period, final period absorbs remainder`,
71
+ ].join('\n');
55
72
  blueprint = {
56
73
  capsuleType: 'Deferred Revenue',
57
74
  capsuleName: `Deferred Revenue — ${fmtCapsuleAmount(amount, currency)} — ${periods} periods`,
75
+ capsuleDescription: workings2,
58
76
  tags: ['Deferred Revenue'],
59
77
  customFields: { 'Contract #': null },
60
78
  steps,
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Asset disposal gain/loss calculator.
3
+ *
4
+ * Compliance references:
5
+ * - IAS 16.67-72: Derecognition on disposal or when no future economic benefits expected
6
+ * - IAS 16.68: Gain/loss = Net disposal proceeds - Carrying amount
7
+ * - IAS 16.71: Gain ≠ revenue (classified separately in P&L)
8
+ *
9
+ * Computes depreciation to the disposal date (pro-rated partial period),
10
+ * then calculates NBV and gain/loss. Supports SL, DDB, and 150DB methods.
11
+ */
12
+ import { round2 } from './types.js';
13
+ import { CalcValidationError, validatePositive, validateNonNegative, validatePositiveInteger, validateSalvageLessThanCost, validateDateFormat, } from './validate.js';
14
+ import { journalStep, noteStep, fmtCapsuleAmount, fmtAmt } from './blueprint.js';
15
+ /**
16
+ * Count full months between two YYYY-MM-DD dates.
17
+ * Partial months are rounded up (any day in a month counts as a full month).
18
+ */
19
+ function monthsBetween(from, to) {
20
+ const d1 = new Date(from + 'T00:00:00');
21
+ const d2 = new Date(to + 'T00:00:00');
22
+ const months = (d2.getFullYear() - d1.getFullYear()) * 12 + (d2.getMonth() - d1.getMonth());
23
+ // If disposal day > acquisition day, it's a partial month — round up
24
+ return d2.getDate() >= d1.getDate() ? months + 1 : Math.max(months, 0);
25
+ }
26
+ /**
27
+ * Compute accumulated depreciation from acquisition to disposal date.
28
+ */
29
+ function computeAccumDepreciation(cost, salvageValue, usefulLifeYears, method, monthsHeld) {
30
+ const totalMonths = usefulLifeYears * 12;
31
+ const depreciableBase = round2(cost - salvageValue);
32
+ // Cap at useful life — asset is fully depreciated
33
+ const effectiveMonths = Math.min(monthsHeld, totalMonths);
34
+ if (method === 'sl') {
35
+ // Straight-line: pro-rate by months
36
+ const monthlyDep = depreciableBase / totalMonths;
37
+ return round2(Math.min(monthlyDep * effectiveMonths, depreciableBase));
38
+ }
39
+ // DDB / 150DB: compute year-by-year then pro-rate final partial year
40
+ const multiplier = method === 'ddb' ? 2 : 1.5;
41
+ const annualRate = multiplier / usefulLifeYears;
42
+ let bookValue = cost;
43
+ let totalDep = 0;
44
+ let monthsRemaining = effectiveMonths;
45
+ for (let year = 1; year <= usefulLifeYears && monthsRemaining > 0; year++) {
46
+ const remainingYears = usefulLifeYears - year + 1;
47
+ const ddbAmount = round2(bookValue * annualRate);
48
+ const slAmount = round2((bookValue - salvageValue) / remainingYears);
49
+ const useSL = slAmount >= ddbAmount || round2(bookValue - ddbAmount) < salvageValue;
50
+ let annualDep = useSL ? slAmount : ddbAmount;
51
+ // Don't breach salvage floor
52
+ if (round2(bookValue - annualDep) < salvageValue) {
53
+ annualDep = round2(bookValue - salvageValue);
54
+ }
55
+ if (monthsRemaining >= 12) {
56
+ // Full year
57
+ totalDep = round2(totalDep + annualDep);
58
+ bookValue = round2(bookValue - annualDep);
59
+ monthsRemaining -= 12;
60
+ }
61
+ else {
62
+ // Partial year: pro-rate
63
+ const partialDep = round2(annualDep * monthsRemaining / 12);
64
+ totalDep = round2(totalDep + partialDep);
65
+ monthsRemaining = 0;
66
+ }
67
+ }
68
+ return Math.min(totalDep, depreciableBase);
69
+ }
70
+ export function calculateAssetDisposal(inputs) {
71
+ const { cost, salvageValue, usefulLifeYears, acquisitionDate, disposalDate, proceeds, method = 'sl', currency, } = inputs;
72
+ validatePositive(cost, 'Asset cost');
73
+ validateNonNegative(salvageValue, 'Salvage value');
74
+ validatePositiveInteger(usefulLifeYears, 'Useful life (years)');
75
+ if (salvageValue > 0)
76
+ validateSalvageLessThanCost(salvageValue, cost);
77
+ validateNonNegative(proceeds, 'Disposal proceeds');
78
+ validateDateFormat(acquisitionDate);
79
+ validateDateFormat(disposalDate);
80
+ // Validate disposal is after acquisition
81
+ if (disposalDate <= acquisitionDate) {
82
+ throw new CalcValidationError('Disposal date must be after acquisition date.');
83
+ }
84
+ const monthsHeld = monthsBetween(acquisitionDate, disposalDate);
85
+ const accumulatedDepreciation = computeAccumDepreciation(cost, salvageValue, usefulLifeYears, method, monthsHeld);
86
+ const netBookValue = round2(cost - accumulatedDepreciation);
87
+ const gainOrLoss = round2(proceeds - netBookValue);
88
+ const isGain = gainOrLoss >= 0;
89
+ // Build disposal journal entry
90
+ const lines = [];
91
+ if (proceeds > 0) {
92
+ lines.push({ account: 'Cash / Bank Account', debit: proceeds, credit: 0 });
93
+ }
94
+ if (accumulatedDepreciation > 0) {
95
+ lines.push({ account: 'Accumulated Depreciation', debit: accumulatedDepreciation, credit: 0 });
96
+ }
97
+ if (!isGain) {
98
+ lines.push({ account: 'Loss on Disposal', debit: Math.abs(gainOrLoss), credit: 0 });
99
+ }
100
+ lines.push({ account: 'Fixed Asset (at cost)', debit: 0, credit: cost });
101
+ if (isGain && gainOrLoss > 0) {
102
+ lines.push({ account: 'Gain on Disposal', debit: 0, credit: gainOrLoss });
103
+ }
104
+ const disposalJournal = {
105
+ description: `Asset disposal — ${isGain ? (gainOrLoss > 0 ? 'gain' : 'at book value') : 'loss'}`,
106
+ lines,
107
+ };
108
+ // Build blueprint
109
+ const c = currency ?? undefined;
110
+ const methodLabel = method === 'sl' ? 'Straight-line' : method === 'ddb' ? 'Double declining' : '150% declining';
111
+ const workings = [
112
+ `Asset Disposal Workings (IAS 16)`,
113
+ `Cost: ${fmtAmt(cost, c)} | Salvage: ${fmtAmt(salvageValue, c)} | Life: ${usefulLifeYears} years (${methodLabel})`,
114
+ `Acquired: ${acquisitionDate} | Disposed: ${disposalDate} | Held: ${monthsHeld} months`,
115
+ `Accumulated depreciation: ${fmtAmt(accumulatedDepreciation, c)} | NBV: ${fmtAmt(netBookValue, c)}`,
116
+ `Proceeds: ${fmtAmt(proceeds, c)} | ${isGain ? (gainOrLoss > 0 ? `Gain: ${fmtAmt(gainOrLoss, c)}` : 'At book value (no gain/loss)') : `Loss: ${fmtAmt(Math.abs(gainOrLoss), c)}`}`,
117
+ `Method: IAS 16.68 — Gain/Loss = Proceeds − NBV = ${fmtAmt(proceeds, c)} − ${fmtAmt(netBookValue, c)} = ${fmtAmt(gainOrLoss, c)}`,
118
+ ].join('\n');
119
+ let blueprint = null;
120
+ blueprint = {
121
+ capsuleType: 'Asset Disposal',
122
+ capsuleName: `Asset Disposal — ${fmtCapsuleAmount(cost, currency)} asset — ${disposalDate}`,
123
+ capsuleDescription: workings,
124
+ tags: ['Asset Disposal'],
125
+ customFields: { 'Asset Description': null },
126
+ steps: [
127
+ journalStep(1, disposalJournal.description, disposalDate, disposalJournal.lines),
128
+ noteStep(2, `Update Jaz FA register: use POST /mark-as-sold/fixed-assets (if sold) or POST /discard-fixed-assets/:id (if scrapped).`, disposalDate),
129
+ ],
130
+ };
131
+ return {
132
+ type: 'asset-disposal',
133
+ currency: currency ?? null,
134
+ inputs: {
135
+ cost,
136
+ salvageValue,
137
+ usefulLifeYears,
138
+ acquisitionDate,
139
+ disposalDate,
140
+ proceeds,
141
+ method,
142
+ },
143
+ monthsHeld,
144
+ accumulatedDepreciation,
145
+ netBookValue,
146
+ gainOrLoss,
147
+ isGain,
148
+ disposalJournal,
149
+ blueprint,
150
+ };
151
+ }
@@ -10,7 +10,27 @@
10
10
  export function journalStep(stepNum, description, date, lines) {
11
11
  return { step: stepNum, action: 'journal', description, date, lines };
12
12
  }
13
- /** Build a note step (instruction, not a journal — e.g. "register fixed asset"). */
13
+ /** Build a bill step (create supplier bill, e.g. prepaid expense). Lines show net accounting effect. */
14
+ export function billStep(stepNum, description, date, lines) {
15
+ return { step: stepNum, action: 'bill', description, date, lines };
16
+ }
17
+ /** Build an invoice step (create customer invoice, e.g. deferred revenue). Lines show net accounting effect. */
18
+ export function invoiceStep(stepNum, description, date, lines) {
19
+ return { step: stepNum, action: 'invoice', description, date, lines };
20
+ }
21
+ /** Build a cash-out step (cash disbursement, e.g. deposit placement). */
22
+ export function cashOutStep(stepNum, description, date, lines) {
23
+ return { step: stepNum, action: 'cash-out', description, date, lines };
24
+ }
25
+ /** Build a cash-in step (cash receipt, e.g. deposit maturity). */
26
+ export function cashInStep(stepNum, description, date, lines) {
27
+ return { step: stepNum, action: 'cash-in', description, date, lines };
28
+ }
29
+ /** Build a fixed-asset registration step (e.g. register ROU asset in FA module). */
30
+ export function fixedAssetStep(stepNum, description, date = null) {
31
+ return { step: stepNum, action: 'fixed-asset', description, date, lines: [] };
32
+ }
33
+ /** Build a note step (instruction, not a journal — e.g. "update FA register"). */
14
34
  export function noteStep(stepNum, description, date = null) {
15
35
  return { step: stepNum, action: 'note', description, date, lines: [] };
16
36
  }
@@ -19,3 +39,8 @@ export function fmtCapsuleAmount(amount, currency) {
19
39
  const formatted = amount.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 0 });
20
40
  return currency ? `${currency} ${formatted}` : formatted;
21
41
  }
42
+ /** Format a currency amount for workings text (e.g. "SGD 100,000.00"). */
43
+ export function fmtAmt(amount, currency) {
44
+ const formatted = amount.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
45
+ return currency ? `${currency} ${formatted}` : formatted;
46
+ }
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import { round2 } from './types.js';
13
13
  import { validatePositive, validateNonNegative, validatePositiveInteger, validateSalvageLessThanCost } from './validate.js';
14
- import { journalStep } from './blueprint.js';
14
+ import { journalStep, fmtAmt } from './blueprint.js';
15
15
  export function calculateDepreciation(inputs) {
16
16
  const { cost, salvageValue, usefulLifeYears, method = 'ddb', frequency = 'annual', currency, } = inputs;
17
17
  validatePositive(cost, 'Asset cost');
@@ -105,6 +105,16 @@ function buildStraightLineResult(cost, salvageValue, usefulLifeYears, depreciabl
105
105
  });
106
106
  }
107
107
  const blueprintSteps = schedule.map((row, idx) => journalStep(idx + 1, row.journal.description, row.date, row.journal.lines));
108
+ const c = currency ?? undefined;
109
+ const workings = [
110
+ `Straight-Line Depreciation Workings`,
111
+ `Cost: ${fmtAmt(cost, c)} | Salvage: ${fmtAmt(salvageValue, c)} | Depreciable base: ${fmtAmt(depreciableBase, c)}`,
112
+ `Useful life: ${usefulLifeYears} years | Frequency: ${frequency}`,
113
+ `Per ${frequency === 'monthly' ? 'month' : 'year'}: ${fmtAmt(perPeriod, c)}`,
114
+ `Total depreciation: ${fmtAmt(depreciableBase, c)}`,
115
+ `Method: (Cost − Salvage) ÷ Life = ${fmtAmt(depreciableBase, c)} ÷ ${totalPeriods} = ${fmtAmt(perPeriod, c)}`,
116
+ `Rounding: 2dp per period, final period closes to salvage value`,
117
+ ].join('\n');
108
118
  return {
109
119
  type: 'depreciation',
110
120
  currency: currency ?? null,
@@ -114,6 +124,7 @@ function buildStraightLineResult(cost, salvageValue, usefulLifeYears, depreciabl
114
124
  blueprint: {
115
125
  capsuleType: 'Depreciation',
116
126
  capsuleName: `SL Depreciation — ${usefulLifeYears} years`,
127
+ capsuleDescription: workings,
117
128
  tags: ['Depreciation'],
118
129
  customFields: { 'Asset ID': null },
119
130
  steps: blueprintSteps,
@@ -160,6 +171,17 @@ function buildDecliningSchedule(cost, salvageValue, usefulLifeYears, annualRate,
160
171
  });
161
172
  }
162
173
  const blueprintSteps = schedule.map((row, idx) => journalStep(idx + 1, row.journal.description, row.date, row.journal.lines));
174
+ const c = currency ?? undefined;
175
+ const methodLabel = method === 'ddb' ? 'Double Declining Balance' : '150% Declining Balance';
176
+ const ratePercent = round2(annualRate * 100);
177
+ const workings = [
178
+ `${methodLabel} Depreciation Workings`,
179
+ `Cost: ${fmtAmt(cost, c)} | Salvage: ${fmtAmt(salvageValue, c)} | Depreciable base: ${fmtAmt(depreciableBase, c)}`,
180
+ `Useful life: ${usefulLifeYears} years | Rate: ${ratePercent}% (${method === 'ddb' ? '2' : '1.5'} ÷ ${usefulLifeYears})`,
181
+ `Total depreciation: ${fmtAmt(depreciableBase, c)}`,
182
+ `Method: ${method.toUpperCase()} with auto switch to SL when SL ≥ declining or floor hit`,
183
+ `Rounding: 2dp per period, book value never falls below salvage`,
184
+ ].join('\n');
163
185
  return {
164
186
  type: 'depreciation',
165
187
  currency: currency ?? null,
@@ -169,6 +191,7 @@ function buildDecliningSchedule(cost, salvageValue, usefulLifeYears, annualRate,
169
191
  blueprint: {
170
192
  capsuleType: 'Depreciation',
171
193
  capsuleName: `${method.toUpperCase()} Depreciation — ${usefulLifeYears} years`,
194
+ capsuleDescription: workings,
172
195
  tags: ['Depreciation'],
173
196
  customFields: { 'Asset ID': null },
174
197
  steps: blueprintSteps,
package/dist/calc/ecl.js CHANGED
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import { round2 } from './types.js';
17
17
  import { validateNonNegative } from './validate.js';
18
- import { journalStep, fmtCapsuleAmount } from './blueprint.js';
18
+ import { journalStep, fmtCapsuleAmount, fmtAmt } from './blueprint.js';
19
19
  export function calculateEcl(inputs) {
20
20
  const { buckets, existingProvision = 0, currency } = inputs;
21
21
  // Validate each bucket
@@ -61,9 +61,21 @@ export function calculateEcl(inputs) {
61
61
  };
62
62
  }
63
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');
64
75
  const blueprint = {
65
76
  capsuleType: 'ECL Provision',
66
77
  capsuleName: `ECL Provision — ${fmtCapsuleAmount(totalEcl, currency)} — ${buckets.length} buckets`,
78
+ capsuleDescription: workings,
67
79
  tags: ['ECL', 'Bad Debt'],
68
80
  customFields: { 'Reporting Period': null, 'Aged Receivables Report Date': null },
69
81
  steps: [