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.
Files changed (33) 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 +2 -0
  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 +158 -14
  7. package/assets/skills/transaction-recipes/references/asset-disposal.md +174 -0
  8. package/assets/skills/transaction-recipes/references/bad-debt-provision.md +145 -0
  9. package/assets/skills/transaction-recipes/references/building-blocks.md +25 -2
  10. package/assets/skills/transaction-recipes/references/capital-wip.md +167 -0
  11. package/assets/skills/transaction-recipes/references/dividend.md +111 -0
  12. package/assets/skills/transaction-recipes/references/employee-accruals.md +154 -0
  13. package/assets/skills/transaction-recipes/references/fixed-deposit.md +164 -0
  14. package/assets/skills/transaction-recipes/references/fx-revaluation.md +135 -0
  15. package/assets/skills/transaction-recipes/references/hire-purchase.md +190 -0
  16. package/assets/skills/transaction-recipes/references/intercompany.md +150 -0
  17. package/assets/skills/transaction-recipes/references/provisions.md +142 -0
  18. package/dist/calc/amortization.js +122 -0
  19. package/dist/calc/asset-disposal.js +151 -0
  20. package/dist/calc/blueprint.js +46 -0
  21. package/dist/calc/depreciation.js +200 -0
  22. package/dist/calc/ecl.js +101 -0
  23. package/dist/calc/fixed-deposit.js +169 -0
  24. package/dist/calc/format.js +494 -0
  25. package/dist/calc/fx-reval.js +93 -0
  26. package/dist/calc/lease.js +146 -0
  27. package/dist/calc/loan.js +107 -0
  28. package/dist/calc/provision.js +128 -0
  29. package/dist/calc/types.js +21 -0
  30. package/dist/calc/validate.js +48 -0
  31. package/dist/commands/calc.js +252 -0
  32. package/dist/index.js +2 -0
  33. package/package.json +3 -2
@@ -0,0 +1,494 @@
1
+ /**
2
+ * Output formatters for calc results.
3
+ * Table format for human reading, JSON for programmatic use.
4
+ *
5
+ * Currency-aware: when a result has a currency code, it appears in
6
+ * the title and summary lines (e.g. "Loan Amortization Schedule (SGD)").
7
+ */
8
+ import chalk from 'chalk';
9
+ const fmt = (n) => n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
10
+ const fmtR = (n) => fmt(n).padStart(14);
11
+ const fmtPct = (n) => `${n}%`;
12
+ const currTag = (c) => c ? ` (${c})` : '';
13
+ const line = (w) => chalk.dim('─'.repeat(w));
14
+ function printWorkings(result) {
15
+ const bp = result.blueprint;
16
+ if (!bp?.capsuleDescription)
17
+ return;
18
+ console.log(chalk.bold('Workings (capsule description)'));
19
+ console.log(line(60));
20
+ for (const l of bp.capsuleDescription.split('\n')) {
21
+ console.log(chalk.dim(` ${l}`));
22
+ }
23
+ console.log();
24
+ }
25
+ function printJournal(journal) {
26
+ console.log(chalk.dim(` ${journal.description}`));
27
+ for (const l of journal.lines) {
28
+ if (l.debit > 0) {
29
+ console.log(chalk.dim(` Dr ${l.account.padEnd(35)} ${fmt(l.debit)}`));
30
+ }
31
+ if (l.credit > 0) {
32
+ console.log(chalk.dim(` Cr ${l.account.padEnd(35)} ${fmt(l.credit)}`));
33
+ }
34
+ }
35
+ }
36
+ // ── Loan ──────────────────────────────────────────────────────────
37
+ function printLoanTable(result) {
38
+ const W = 90;
39
+ console.log();
40
+ console.log(chalk.bold(`Loan Amortization Schedule${currTag(result.currency)}`));
41
+ console.log(line(W));
42
+ console.log(chalk.bold(` Principal: ${fmt(result.inputs.principal)}`));
43
+ console.log(chalk.bold(` Annual Rate: ${fmtPct(result.inputs.annualRate)}`));
44
+ console.log(chalk.bold(` Term: ${result.inputs.termMonths} months`));
45
+ console.log(chalk.bold(` Monthly PMT: ${fmt(result.monthlyPayment)}`));
46
+ console.log(line(W));
47
+ const header = [
48
+ 'Period'.padStart(6),
49
+ result.inputs.startDate ? 'Date'.padStart(12) : null,
50
+ 'Opening'.padStart(14),
51
+ 'Payment'.padStart(14),
52
+ 'Interest'.padStart(14),
53
+ 'Principal'.padStart(14),
54
+ 'Closing'.padStart(14),
55
+ ].filter(Boolean).join(' ');
56
+ console.log(chalk.dim(header));
57
+ console.log(line(W));
58
+ for (const row of result.schedule) {
59
+ const cols = [
60
+ String(row.period).padStart(6),
61
+ row.date ? row.date.padStart(12) : null,
62
+ fmtR(row.openingBalance),
63
+ fmtR(row.payment),
64
+ fmtR(row.interest),
65
+ fmtR(row.principal),
66
+ fmtR(row.closingBalance),
67
+ ].filter(Boolean).join(' ');
68
+ console.log(cols);
69
+ }
70
+ console.log(line(W));
71
+ const totalCols = [
72
+ 'TOTAL'.padStart(6),
73
+ result.inputs.startDate ? ''.padStart(12) : null,
74
+ ''.padStart(14),
75
+ fmtR(result.totalPayments),
76
+ fmtR(result.totalInterest),
77
+ fmtR(result.totalPrincipal),
78
+ fmtR(0),
79
+ ].filter(Boolean).join(' ');
80
+ console.log(chalk.bold(totalCols));
81
+ console.log();
82
+ console.log(chalk.bold('Journal Entries'));
83
+ console.log(line(60));
84
+ console.log(chalk.dim(' Disbursement:'));
85
+ console.log(chalk.dim(` Dr Cash / Bank Account${' '.repeat(14)}${fmt(result.inputs.principal)}`));
86
+ console.log(chalk.dim(` Cr Loan Payable${' '.repeat(21)} ${fmt(result.inputs.principal)}`));
87
+ console.log();
88
+ console.log(chalk.dim(` Per period (example — Month 1):`));
89
+ printJournal(result.schedule[0].journal);
90
+ console.log();
91
+ printWorkings(result);
92
+ }
93
+ // ── Lease ─────────────────────────────────────────────────────────
94
+ function printLeaseTable(result) {
95
+ const W = 90;
96
+ const hp = result.isHirePurchase;
97
+ const title = hp ? 'Hire Purchase Schedule (IFRS 16)' : 'IFRS 16 Lease Schedule';
98
+ console.log();
99
+ console.log(chalk.bold(`${title}${currTag(result.currency)}`));
100
+ console.log(line(W));
101
+ console.log(chalk.bold(` Monthly Payment: ${fmt(result.inputs.monthlyPayment)}`));
102
+ console.log(chalk.bold(` Lease Term: ${result.inputs.termMonths} months`));
103
+ if (hp) {
104
+ console.log(chalk.bold(` Useful Life: ${result.depreciationMonths} months`));
105
+ }
106
+ console.log(chalk.bold(` Discount Rate (IBR): ${fmtPct(result.inputs.annualRate)}`));
107
+ console.log(chalk.bold(` Present Value: ${fmt(result.presentValue)}`));
108
+ console.log(chalk.bold(` Monthly ROU Dep: ${fmt(result.monthlyRouDepreciation)}${hp ? ` (over ${result.depreciationMonths} months, not ${result.inputs.termMonths})` : ''}`));
109
+ console.log(line(W));
110
+ console.log(chalk.bold('\nInitial Recognition'));
111
+ printJournal(result.initialJournal);
112
+ console.log(chalk.bold('\nLiability Unwinding Schedule'));
113
+ const header = [
114
+ 'Period'.padStart(6),
115
+ result.inputs.startDate ? 'Date'.padStart(12) : null,
116
+ 'Opening'.padStart(14),
117
+ 'Payment'.padStart(14),
118
+ 'Interest'.padStart(14),
119
+ 'Principal'.padStart(14),
120
+ 'Closing'.padStart(14),
121
+ ].filter(Boolean).join(' ');
122
+ console.log(chalk.dim(header));
123
+ console.log(line(W));
124
+ for (const row of result.schedule) {
125
+ const cols = [
126
+ String(row.period).padStart(6),
127
+ row.date ? row.date.padStart(12) : null,
128
+ fmtR(row.openingBalance),
129
+ fmtR(row.payment),
130
+ fmtR(row.interest),
131
+ fmtR(row.principal),
132
+ fmtR(row.closingBalance),
133
+ ].filter(Boolean).join(' ');
134
+ console.log(cols);
135
+ }
136
+ console.log(line(W));
137
+ const totalCols = [
138
+ 'TOTAL'.padStart(6),
139
+ result.inputs.startDate ? ''.padStart(12) : null,
140
+ ''.padStart(14),
141
+ fmtR(result.totalCashPayments),
142
+ fmtR(result.totalInterest),
143
+ fmtR(result.presentValue),
144
+ fmtR(0),
145
+ ].filter(Boolean).join(' ');
146
+ console.log(chalk.bold(totalCols));
147
+ console.log();
148
+ console.log(chalk.bold('Monthly ROU Depreciation'));
149
+ console.log(chalk.dim(` Dr Depreciation Expense — ROU${' '.repeat(8)}${fmt(result.monthlyRouDepreciation)}`));
150
+ console.log(chalk.dim(` Cr Accumulated Depreciation — ROU${' '.repeat(4)} ${fmt(result.monthlyRouDepreciation)}`));
151
+ console.log();
152
+ console.log(chalk.bold('Summary'));
153
+ console.log(` Total cash payments: ${fmt(result.totalCashPayments)}`);
154
+ console.log(` Total interest expense: ${fmt(result.totalInterest)}`);
155
+ console.log(` Total ROU depreciation: ${fmt(result.totalDepreciation)}`);
156
+ console.log(` Total P&L impact: ${fmt(result.totalInterest + result.totalDepreciation)}`);
157
+ console.log();
158
+ printWorkings(result);
159
+ }
160
+ // ── Depreciation ──────────────────────────────────────────────────
161
+ function printDepreciationTable(result) {
162
+ const isSL = result.inputs.method === 'sl';
163
+ const isMonthly = result.inputs.frequency === 'monthly';
164
+ const periodLabel = isMonthly ? 'Month' : 'Year';
165
+ const methodLabel = result.inputs.method.toUpperCase();
166
+ const W = isSL ? 72 : 100;
167
+ console.log();
168
+ console.log(chalk.bold(`${isSL ? 'Straight-Line' : 'Declining Balance'} Depreciation Schedule${isSL ? '' : ` (${methodLabel})`}${currTag(result.currency)}`));
169
+ console.log(line(W));
170
+ console.log(chalk.bold(` Asset Cost: ${fmt(result.inputs.cost)}`));
171
+ console.log(chalk.bold(` Salvage Value: ${fmt(result.inputs.salvageValue)}`));
172
+ console.log(chalk.bold(` Useful Life: ${result.inputs.usefulLifeYears} years`));
173
+ if (!isSL) {
174
+ const mult = result.inputs.method === 'ddb' ? 2 : 1.5;
175
+ console.log(chalk.bold(` Method: ${methodLabel} (${mult} / ${result.inputs.usefulLifeYears} = ${(mult / result.inputs.usefulLifeYears * 100).toFixed(1)}% per year)`));
176
+ }
177
+ console.log(line(W));
178
+ if (isSL) {
179
+ // Straight-line: simpler table — no DDB/SL comparison columns
180
+ const header = [
181
+ periodLabel.padStart(6),
182
+ 'Opening BV'.padStart(14),
183
+ 'Depreciation'.padStart(14),
184
+ 'Closing BV'.padStart(14),
185
+ ].join(' ');
186
+ console.log(chalk.dim(header));
187
+ console.log(line(W));
188
+ for (const row of result.schedule) {
189
+ const cols = [
190
+ String(row.period).padStart(6),
191
+ fmtR(row.openingBookValue),
192
+ fmtR(row.depreciation),
193
+ fmtR(row.closingBookValue),
194
+ ].join(' ');
195
+ console.log(cols);
196
+ }
197
+ }
198
+ else {
199
+ // DDB/150DB: full comparison table
200
+ const header = [
201
+ periodLabel.padStart(6),
202
+ 'Opening BV'.padStart(14),
203
+ 'DDB'.padStart(14),
204
+ 'SL'.padStart(14),
205
+ 'Method'.padStart(8),
206
+ 'Depreciation'.padStart(14),
207
+ 'Closing BV'.padStart(14),
208
+ ].join(' ');
209
+ console.log(chalk.dim(header));
210
+ console.log(line(W));
211
+ for (const row of result.schedule) {
212
+ const cols = [
213
+ String(row.period).padStart(6),
214
+ fmtR(row.openingBookValue),
215
+ fmtR(row.ddbAmount),
216
+ fmtR(row.slAmount),
217
+ row.methodUsed.padStart(8),
218
+ fmtR(row.depreciation),
219
+ fmtR(row.closingBookValue),
220
+ ].join(' ');
221
+ console.log(cols);
222
+ }
223
+ }
224
+ console.log(line(W));
225
+ console.log(chalk.bold(` Total depreciation: ${fmt(result.totalDepreciation)} (cost ${fmt(result.inputs.cost)} − salvage ${fmt(result.inputs.salvageValue)})`));
226
+ console.log();
227
+ console.log(chalk.bold('Journal Entry (per period)'));
228
+ printJournal(result.schedule[0].journal);
229
+ console.log();
230
+ printWorkings(result);
231
+ }
232
+ // ── Prepaid Expense / Deferred Revenue ────────────────────────────
233
+ function printRecognitionTable(result) {
234
+ const isPrepaid = result.type === 'prepaid-expense';
235
+ const title = isPrepaid
236
+ ? 'Prepaid Expense Recognition Schedule'
237
+ : 'Deferred Revenue Recognition Schedule';
238
+ const amountLabel = isPrepaid ? 'Prepaid Amount' : 'Deferred Amount';
239
+ const W = 60;
240
+ console.log();
241
+ console.log(chalk.bold(`${title}${currTag(result.currency)}`));
242
+ console.log(line(W));
243
+ console.log(chalk.bold(` ${amountLabel}: ${fmt(result.inputs.amount)}`));
244
+ console.log(chalk.bold(` Periods: ${result.inputs.periods} (${result.inputs.frequency})`));
245
+ console.log(chalk.bold(` Per Period: ${fmt(result.perPeriodAmount)}`));
246
+ console.log(line(W));
247
+ const recognizedLabel = isPrepaid ? 'Expensed' : 'Recognized';
248
+ const header = [
249
+ 'Period'.padStart(6),
250
+ result.inputs.startDate ? 'Date'.padStart(12) : null,
251
+ recognizedLabel.padStart(14),
252
+ 'Remaining'.padStart(14),
253
+ ].filter(Boolean).join(' ');
254
+ console.log(chalk.dim(header));
255
+ console.log(line(W));
256
+ for (const row of result.schedule) {
257
+ const cols = [
258
+ String(row.period).padStart(6),
259
+ row.date ? row.date.padStart(12) : null,
260
+ fmtR(row.amortized),
261
+ fmtR(row.remainingBalance),
262
+ ].filter(Boolean).join(' ');
263
+ console.log(cols);
264
+ }
265
+ console.log(line(W));
266
+ console.log();
267
+ console.log(chalk.bold('Journal Entry (per period)'));
268
+ printJournal(result.schedule[0].journal);
269
+ console.log();
270
+ printWorkings(result);
271
+ }
272
+ // ── FX Revaluation ────────────────────────────────────────────────
273
+ function printFxRevalTable(result) {
274
+ const W = 70;
275
+ const ccy = result.currency ?? 'FCY';
276
+ const base = result.inputs.baseCurrency;
277
+ console.log();
278
+ console.log(chalk.bold(`FX Revaluation — IAS 21`));
279
+ console.log(line(W));
280
+ console.log(chalk.bold(` Foreign Currency: ${ccy}`));
281
+ console.log(chalk.bold(` Base Currency: ${base}`));
282
+ console.log(chalk.bold(` Amount Outstanding: ${ccy} ${fmt(result.inputs.amount)}`));
283
+ console.log(chalk.bold(` Book Rate: ${result.inputs.bookRate}`));
284
+ console.log(chalk.bold(` Closing Rate: ${result.inputs.closingRate}`));
285
+ console.log(line(W));
286
+ console.log();
287
+ console.log(` Book Value (${base}): ${fmt(result.bookValue)} (${ccy} ${fmt(result.inputs.amount)} × ${result.inputs.bookRate})`);
288
+ console.log(` Closing Value (${base}): ${fmt(result.closingValue)} (${ccy} ${fmt(result.inputs.amount)} × ${result.inputs.closingRate})`);
289
+ console.log(line(W));
290
+ const label = result.isGain ? chalk.green('Unrealized GAIN') : chalk.red('Unrealized LOSS');
291
+ console.log(` ${label}: ${base} ${fmt(Math.abs(result.gainOrLoss))}`);
292
+ console.log();
293
+ console.log(chalk.bold('Revaluation Journal'));
294
+ printJournal(result.journal);
295
+ console.log();
296
+ console.log(chalk.bold('Day 1 Reversal'));
297
+ printJournal(result.reversalJournal);
298
+ console.log();
299
+ printWorkings(result);
300
+ }
301
+ // ── ECL Provision ─────────────────────────────────────────────────
302
+ function printEclTable(result) {
303
+ const W = 75;
304
+ console.log();
305
+ console.log(chalk.bold(`Expected Credit Loss — IFRS 9 Provision Matrix${currTag(result.currency)}`));
306
+ console.log(line(W));
307
+ // Provision matrix table
308
+ const header = [
309
+ 'Aging Bucket'.padEnd(16),
310
+ 'Balance'.padStart(14),
311
+ 'Loss Rate'.padStart(10),
312
+ 'ECL'.padStart(14),
313
+ ].join(' ');
314
+ console.log(chalk.dim(header));
315
+ console.log(line(W));
316
+ for (const row of result.bucketDetails) {
317
+ const cols = [
318
+ row.bucket.padEnd(16),
319
+ fmtR(row.balance),
320
+ `${row.lossRate}%`.padStart(10),
321
+ fmtR(row.ecl),
322
+ ].join(' ');
323
+ console.log(cols);
324
+ }
325
+ console.log(line(W));
326
+ const totalCols = [
327
+ 'TOTAL'.padEnd(16),
328
+ fmtR(result.totalReceivables),
329
+ `${result.weightedRate}%`.padStart(10),
330
+ fmtR(result.totalEcl),
331
+ ].join(' ');
332
+ console.log(chalk.bold(totalCols));
333
+ console.log();
334
+ // Adjustment summary
335
+ console.log(chalk.bold('Provision Adjustment'));
336
+ console.log(` Required provision: ${fmt(result.totalEcl)}`);
337
+ console.log(` Existing provision: ${fmt(result.inputs.existingProvision)}`);
338
+ console.log(line(45));
339
+ const adjLabel = result.isIncrease
340
+ ? chalk.yellow(`Increase needed`)
341
+ : chalk.green(`Release available`);
342
+ console.log(` ${adjLabel}: ${fmt(Math.abs(result.adjustmentRequired))}`);
343
+ console.log();
344
+ console.log(chalk.bold('Journal Entry'));
345
+ printJournal(result.journal);
346
+ console.log();
347
+ printWorkings(result);
348
+ }
349
+ // ── Provision PV Unwinding ────────────────────────────────────────
350
+ function printProvisionTable(result) {
351
+ const W = 80;
352
+ console.log();
353
+ console.log(chalk.bold(`IAS 37 Provision — PV Measurement & Unwinding${currTag(result.currency)}`));
354
+ console.log(line(W));
355
+ console.log(chalk.bold(` Nominal Outflow: ${fmt(result.nominalAmount)}`));
356
+ console.log(chalk.bold(` Discount Rate: ${fmtPct(result.inputs.annualRate)}`));
357
+ console.log(chalk.bold(` Settlement Term: ${result.inputs.termMonths} months`));
358
+ console.log(chalk.bold(` Present Value: ${fmt(result.presentValue)}`));
359
+ console.log(chalk.bold(` Total Unwinding: ${fmt(result.totalUnwinding)}`));
360
+ console.log(line(W));
361
+ console.log(chalk.bold('\nInitial Recognition'));
362
+ printJournal(result.initialJournal);
363
+ console.log(chalk.bold('\nDiscount Unwinding Schedule'));
364
+ const header = [
365
+ 'Period'.padStart(6),
366
+ result.inputs.startDate ? 'Date'.padStart(12) : null,
367
+ 'Opening'.padStart(14),
368
+ 'Unwinding'.padStart(14),
369
+ 'Closing'.padStart(14),
370
+ ].filter(Boolean).join(' ');
371
+ console.log(chalk.dim(header));
372
+ console.log(line(W));
373
+ for (const row of result.schedule) {
374
+ const cols = [
375
+ String(row.period).padStart(6),
376
+ row.date ? row.date.padStart(12) : null,
377
+ fmtR(row.openingBalance),
378
+ fmtR(row.interest),
379
+ fmtR(row.closingBalance),
380
+ ].filter(Boolean).join(' ');
381
+ console.log(cols);
382
+ }
383
+ console.log(line(W));
384
+ const finalRow = result.schedule[result.schedule.length - 1];
385
+ console.log(chalk.bold(` Final provision balance: ${fmt(finalRow.closingBalance)} (= nominal outflow)`));
386
+ console.log();
387
+ console.log(chalk.bold('Summary'));
388
+ console.log(` Initial recognition (PV): ${fmt(result.presentValue)}`);
389
+ console.log(` Total unwinding (P&L): ${fmt(result.totalUnwinding)}`);
390
+ console.log(` Nominal outflow: ${fmt(result.nominalAmount)}`);
391
+ console.log();
392
+ console.log(chalk.bold('Journal Entry (per period)'));
393
+ printJournal(result.schedule[0].journal);
394
+ console.log();
395
+ printWorkings(result);
396
+ }
397
+ // ── Fixed Deposit ────────────────────────────────────────────────
398
+ function printFixedDepositTable(result) {
399
+ const W = 75;
400
+ console.log();
401
+ console.log(chalk.bold(`Fixed Deposit — Interest Accrual Schedule${currTag(result.currency)}`));
402
+ console.log(line(W));
403
+ console.log(chalk.bold(` Principal: ${fmt(result.inputs.principal)}`));
404
+ console.log(chalk.bold(` Annual Rate: ${fmtPct(result.inputs.annualRate)}`));
405
+ console.log(chalk.bold(` Term: ${result.inputs.termMonths} months`));
406
+ console.log(chalk.bold(` Compounding: ${result.inputs.compounding}`));
407
+ console.log(chalk.bold(` Total Interest: ${fmt(result.totalInterest)}`));
408
+ console.log(chalk.bold(` Maturity Value: ${fmt(result.maturityValue)}`));
409
+ if (result.inputs.compounding !== 'none') {
410
+ console.log(chalk.bold(` Effective Rate: ${result.effectiveRate}% p.a.`));
411
+ }
412
+ console.log(line(W));
413
+ const header = [
414
+ 'Period'.padStart(6),
415
+ result.inputs.startDate ? 'Date'.padStart(12) : null,
416
+ 'Carrying Amt'.padStart(14),
417
+ 'Interest'.padStart(14),
418
+ result.inputs.compounding !== 'none' ? 'New Balance'.padStart(14) : null,
419
+ ].filter(Boolean).join(' ');
420
+ console.log(chalk.dim(header));
421
+ console.log(line(W));
422
+ for (const row of result.schedule) {
423
+ const cols = [
424
+ String(row.period).padStart(6),
425
+ row.date ? row.date.padStart(12) : null,
426
+ fmtR(row.openingBalance),
427
+ fmtR(row.interest),
428
+ result.inputs.compounding !== 'none' ? fmtR(row.closingBalance) : null,
429
+ ].filter(Boolean).join(' ');
430
+ console.log(cols);
431
+ }
432
+ console.log(line(W));
433
+ console.log(chalk.bold(` Total interest: ${fmt(result.totalInterest)}`));
434
+ console.log();
435
+ console.log(chalk.bold('Journal Entries'));
436
+ console.log(line(60));
437
+ console.log(chalk.dim(' Placement:'));
438
+ printJournal(result.placementJournal);
439
+ console.log();
440
+ console.log(chalk.dim(' Monthly accrual (example — Month 1):'));
441
+ printJournal(result.schedule[0].journal);
442
+ console.log();
443
+ console.log(chalk.dim(' Maturity:'));
444
+ printJournal(result.maturityJournal);
445
+ console.log();
446
+ printWorkings(result);
447
+ }
448
+ // ── Asset Disposal ───────────────────────────────────────────────
449
+ function printAssetDisposalTable(result) {
450
+ const W = 60;
451
+ console.log();
452
+ console.log(chalk.bold(`Asset Disposal — IAS 16${currTag(result.currency)}`));
453
+ console.log(line(W));
454
+ console.log(chalk.bold(` Asset Cost: ${fmt(result.inputs.cost)}`));
455
+ console.log(chalk.bold(` Salvage Value: ${fmt(result.inputs.salvageValue)}`));
456
+ console.log(chalk.bold(` Useful Life: ${result.inputs.usefulLifeYears} years`));
457
+ console.log(chalk.bold(` Method: ${result.inputs.method.toUpperCase()}`));
458
+ console.log(chalk.bold(` Acquired: ${result.inputs.acquisitionDate}`));
459
+ console.log(chalk.bold(` Disposed: ${result.inputs.disposalDate}`));
460
+ console.log(chalk.bold(` Months Held: ${result.monthsHeld}`));
461
+ console.log(line(W));
462
+ console.log();
463
+ console.log(` Accumulated Depreciation: ${fmt(result.accumulatedDepreciation)}`);
464
+ console.log(` Net Book Value: ${fmt(result.netBookValue)}`);
465
+ console.log(` Disposal Proceeds: ${fmt(result.inputs.proceeds)}`);
466
+ console.log(line(W));
467
+ const label = result.isGain
468
+ ? (result.gainOrLoss > 0 ? chalk.green('GAIN on Disposal') : 'AT BOOK VALUE')
469
+ : chalk.red('LOSS on Disposal');
470
+ console.log(` ${label}: ${fmt(Math.abs(result.gainOrLoss))}`);
471
+ console.log();
472
+ console.log(chalk.bold('Disposal Journal'));
473
+ printJournal(result.disposalJournal);
474
+ console.log();
475
+ printWorkings(result);
476
+ }
477
+ // ── Dispatch ──────────────────────────────────────────────────────
478
+ export function printResult(result) {
479
+ switch (result.type) {
480
+ case 'loan': return printLoanTable(result);
481
+ case 'lease': return printLeaseTable(result);
482
+ case 'depreciation': return printDepreciationTable(result);
483
+ case 'prepaid-expense': return printRecognitionTable(result);
484
+ case 'deferred-revenue': return printRecognitionTable(result);
485
+ case 'fx-reval': return printFxRevalTable(result);
486
+ case 'ecl': return printEclTable(result);
487
+ case 'provision': return printProvisionTable(result);
488
+ case 'fixed-deposit': return printFixedDepositTable(result);
489
+ case 'asset-disposal': return printAssetDisposalTable(result);
490
+ }
491
+ }
492
+ export function printJson(result) {
493
+ console.log(JSON.stringify(result, null, 2));
494
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * FX Revaluation calculator (IAS 21).
3
+ *
4
+ * Calculates unrealized gain/loss on non-AR/AP foreign currency monetary
5
+ * items at period-end. This is for items NOT auto-revalued by the platform
6
+ * (which handles invoices, bills, credit notes, cash, and bank balances).
7
+ *
8
+ * Use cases:
9
+ * - Intercompany loan receivables/payables (booked as manual journals)
10
+ * - Foreign currency term deposits or escrow
11
+ * - FX-denominated provisions
12
+ * - Any manual journal balance in a foreign currency account
13
+ *
14
+ * IAS 21.23: All monetary items translated at closing rate.
15
+ * IAS 21.28: Exchange differences recognized in P&L.
16
+ */
17
+ import { round2 } from './types.js';
18
+ import { validatePositive } from './validate.js';
19
+ import { journalStep, fmtAmt } from './blueprint.js';
20
+ export function calculateFxReval(inputs) {
21
+ const { amount, bookRate, closingRate, currency = 'USD', baseCurrency = 'SGD', } = inputs;
22
+ validatePositive(amount, 'Foreign currency amount');
23
+ validatePositive(bookRate, 'Book rate');
24
+ validatePositive(closingRate, 'Closing rate');
25
+ // Base currency equivalents
26
+ const bookValue = round2(amount * bookRate);
27
+ const closingValue = round2(amount * closingRate);
28
+ const gainOrLoss = round2(closingValue - bookValue);
29
+ const isGain = gainOrLoss >= 0;
30
+ const absAmount = Math.abs(gainOrLoss);
31
+ // Journal entry for the revaluation adjustment
32
+ let journal;
33
+ if (isGain) {
34
+ journal = {
35
+ description: `FX revaluation — ${currency} ${amount.toLocaleString()} @ ${closingRate} (was ${bookRate})`,
36
+ lines: [
37
+ { account: `${currency} Monetary Item`, debit: absAmount, credit: 0 },
38
+ { account: 'FX Unrealized Gain', debit: 0, credit: absAmount },
39
+ ],
40
+ };
41
+ }
42
+ else {
43
+ journal = {
44
+ description: `FX revaluation — ${currency} ${amount.toLocaleString()} @ ${closingRate} (was ${bookRate})`,
45
+ lines: [
46
+ { account: 'FX Unrealized Loss', debit: absAmount, credit: 0 },
47
+ { account: `${currency} Monetary Item`, debit: 0, credit: absAmount },
48
+ ],
49
+ };
50
+ }
51
+ // Reversal journal (Day 1 of next period)
52
+ const reversalJournal = {
53
+ description: `Reversal of FX revaluation — ${currency} ${amount.toLocaleString()}`,
54
+ lines: journal.lines.map(l => ({
55
+ account: l.account,
56
+ debit: l.credit, // swap debit/credit
57
+ credit: l.debit,
58
+ })),
59
+ };
60
+ // Blueprint: revaluation + reversal
61
+ const workings = [
62
+ `FX Revaluation Workings (IAS 21)`,
63
+ `Foreign currency: ${currency} ${amount.toLocaleString()} | Base currency: ${baseCurrency}`,
64
+ `Book rate: ${bookRate} → Book value: ${fmtAmt(bookValue, baseCurrency)}`,
65
+ `Closing rate: ${closingRate} → Closing value: ${fmtAmt(closingValue, baseCurrency)}`,
66
+ `${isGain ? 'Unrealized gain' : 'Unrealized loss'}: ${fmtAmt(Math.abs(gainOrLoss), baseCurrency)}`,
67
+ `Method: IAS 21.23 — monetary items translated at closing rate`,
68
+ `Reversal: Day 1 next period (standard reval/reverse approach)`,
69
+ ].join('\n');
70
+ const blueprint = {
71
+ capsuleType: 'FX Revaluation',
72
+ capsuleName: `FX Reval — ${currency} ${amount.toLocaleString()} — ${bookRate} → ${closingRate}`,
73
+ capsuleDescription: workings,
74
+ tags: ['FX Revaluation', currency],
75
+ customFields: { 'Source Account': null, 'Period End Date': null },
76
+ steps: [
77
+ journalStep(1, journal.description, null, journal.lines),
78
+ journalStep(2, reversalJournal.description, null, reversalJournal.lines),
79
+ ],
80
+ };
81
+ return {
82
+ type: 'fx-reval',
83
+ currency,
84
+ inputs: { amount, bookRate, closingRate, baseCurrency },
85
+ bookValue,
86
+ closingValue,
87
+ gainOrLoss,
88
+ isGain,
89
+ journal,
90
+ reversalJournal,
91
+ blueprint,
92
+ };
93
+ }