@classytic/ledger-bd 0.3.1 → 0.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,790 @@
1
+ //#region src/reporting/nbr-schedule/schedule-it11gha.ts
2
+ const IT11GHA_SCHEDULE = [
3
+ {
4
+ box: "47",
5
+ label: "Sales / Turnover / Receipts",
6
+ isTotal: false
7
+ },
8
+ {
9
+ box: "48",
10
+ label: "VAT",
11
+ isTotal: false
12
+ },
13
+ {
14
+ box: "49",
15
+ label: "Net Sales",
16
+ isTotal: true,
17
+ sumsOf: ["47", "48"]
18
+ },
19
+ {
20
+ box: "50",
21
+ label: "Cost of Sales",
22
+ isTotal: false
23
+ },
24
+ {
25
+ box: "51",
26
+ label: "Gross Profit",
27
+ isTotal: true,
28
+ sumsOf: ["49", "50"]
29
+ },
30
+ {
31
+ box: "52",
32
+ label: "Other operating income",
33
+ isTotal: false
34
+ },
35
+ {
36
+ box: "53",
37
+ label: "General & Administrative expenses",
38
+ isTotal: false
39
+ },
40
+ {
41
+ box: "53A",
42
+ label: "G&A expenses — cash paid",
43
+ isTotal: false
44
+ },
45
+ {
46
+ box: "54",
47
+ label: "Selling & Marketing expenses",
48
+ isTotal: false
49
+ },
50
+ {
51
+ box: "54A",
52
+ label: "Selling & Marketing — cash paid",
53
+ isTotal: false
54
+ },
55
+ {
56
+ box: "55",
57
+ label: "Other operating expenses",
58
+ isTotal: false
59
+ },
60
+ {
61
+ box: "55A",
62
+ label: "Other operating expenses — cash paid",
63
+ isTotal: false
64
+ },
65
+ {
66
+ box: "56",
67
+ label: "Profit from Operation",
68
+ isTotal: true,
69
+ sumsOf: [
70
+ "51",
71
+ "52",
72
+ "53",
73
+ "54",
74
+ "55"
75
+ ]
76
+ },
77
+ {
78
+ box: "57",
79
+ label: "Financial expenses",
80
+ isTotal: false
81
+ },
82
+ {
83
+ box: "57A",
84
+ label: "Financial expenses — cash paid",
85
+ isTotal: false
86
+ },
87
+ {
88
+ box: "58",
89
+ label: "Income from associates / subsidiaries",
90
+ isTotal: false
91
+ },
92
+ {
93
+ box: "59",
94
+ label: "Other business income",
95
+ isTotal: false
96
+ },
97
+ {
98
+ box: "60",
99
+ label: "Profit Before Tax",
100
+ isTotal: true,
101
+ sumsOf: [
102
+ "56",
103
+ "57",
104
+ "58",
105
+ "59"
106
+ ]
107
+ },
108
+ {
109
+ box: "61A",
110
+ label: "Property, Plant & Equipment",
111
+ isTotal: false
112
+ },
113
+ {
114
+ box: "61B",
115
+ label: "Intangible assets",
116
+ isTotal: false
117
+ },
118
+ {
119
+ box: "61C",
120
+ label: "Financial assets",
121
+ isTotal: false
122
+ },
123
+ {
124
+ box: "61D",
125
+ label: "Investments / affiliate loans",
126
+ isTotal: false
127
+ },
128
+ {
129
+ box: "61E",
130
+ label: "Other non-current assets",
131
+ isTotal: false
132
+ },
133
+ {
134
+ box: "61",
135
+ label: "Non-current assets",
136
+ isTotal: true,
137
+ sumsOf: [
138
+ "61A",
139
+ "61B",
140
+ "61C",
141
+ "61D",
142
+ "61E"
143
+ ]
144
+ },
145
+ {
146
+ box: "62A",
147
+ label: "Inventories",
148
+ isTotal: false
149
+ },
150
+ {
151
+ box: "62B",
152
+ label: "Receivables",
153
+ isTotal: false
154
+ },
155
+ {
156
+ box: "62C",
157
+ label: "Advances / prepayments",
158
+ isTotal: false
159
+ },
160
+ {
161
+ box: "62D",
162
+ label: "Cash / bank",
163
+ isTotal: false
164
+ },
165
+ {
166
+ box: "62E",
167
+ label: "Other current assets",
168
+ isTotal: false
169
+ },
170
+ {
171
+ box: "62",
172
+ label: "Current assets",
173
+ isTotal: true,
174
+ sumsOf: [
175
+ "62A",
176
+ "62B",
177
+ "62C",
178
+ "62D",
179
+ "62E"
180
+ ]
181
+ },
182
+ {
183
+ box: "63",
184
+ label: "Total Assets",
185
+ isTotal: true,
186
+ sumsOf: ["61", "62"]
187
+ },
188
+ {
189
+ box: "64A",
190
+ label: "Paid-up capital",
191
+ isTotal: false
192
+ },
193
+ {
194
+ box: "64B",
195
+ label: "Reserves & Surplus",
196
+ isTotal: false
197
+ },
198
+ {
199
+ box: "64C",
200
+ label: "Retained earnings",
201
+ isTotal: false
202
+ },
203
+ {
204
+ box: "64",
205
+ label: "Equity",
206
+ isTotal: true,
207
+ sumsOf: [
208
+ "64A",
209
+ "64B",
210
+ "64C"
211
+ ]
212
+ },
213
+ {
214
+ box: "65A",
215
+ label: "Bonds / Debentures",
216
+ isTotal: false
217
+ },
218
+ {
219
+ box: "65B",
220
+ label: "Long-term borrowings",
221
+ isTotal: false
222
+ },
223
+ {
224
+ box: "65C",
225
+ label: "Affiliate loans",
226
+ isTotal: false
227
+ },
228
+ {
229
+ box: "65D",
230
+ label: "Other non-current liabilities",
231
+ isTotal: false
232
+ },
233
+ {
234
+ box: "65",
235
+ label: "Non-current liabilities",
236
+ isTotal: true,
237
+ sumsOf: [
238
+ "65A",
239
+ "65B",
240
+ "65C",
241
+ "65D"
242
+ ]
243
+ },
244
+ {
245
+ box: "66A",
246
+ label: "Short-term borrowings",
247
+ isTotal: false
248
+ },
249
+ {
250
+ box: "66B",
251
+ label: "Payables",
252
+ isTotal: false
253
+ },
254
+ {
255
+ box: "66C",
256
+ label: "Provisions",
257
+ isTotal: false
258
+ },
259
+ {
260
+ box: "66D",
261
+ label: "Other current liabilities",
262
+ isTotal: false
263
+ },
264
+ {
265
+ box: "66",
266
+ label: "Current liabilities",
267
+ isTotal: true,
268
+ sumsOf: [
269
+ "66A",
270
+ "66B",
271
+ "66C",
272
+ "66D"
273
+ ]
274
+ },
275
+ {
276
+ box: "67",
277
+ label: "Total Equity & Liabilities",
278
+ isTotal: true,
279
+ sumsOf: [
280
+ "64",
281
+ "65",
282
+ "66"
283
+ ]
284
+ }
285
+ ];
286
+ function getScheduleLineItem(box) {
287
+ return IT11GHA_SCHEDULE.find((item) => item.box === box);
288
+ }
289
+ //#endregion
290
+ //#region src/reporting/nbr-schedule/mapping.ts
291
+ /** Strip a tax sub-account's dotted suffix (e.g. '2132.VAT15.COLLECTED' → 2132) to get the base numeric code. */
292
+ function baseCode(code) {
293
+ const numeric = code.split(".")[0];
294
+ const parsed = Number.parseInt(numeric, 10);
295
+ return Number.isNaN(parsed) ? null : parsed;
296
+ }
297
+ const EXPLICIT = {
298
+ "1150": "62E",
299
+ "1151": "62E",
300
+ "1152": "62E",
301
+ "1153": "62E",
302
+ "1157": "62E",
303
+ "1160": "62E",
304
+ "1200": "62C",
305
+ "1444": "61D",
306
+ "1527": "62B",
307
+ "3310": "64C",
308
+ "3311": "64C",
309
+ "3312": "64B",
310
+ "2322": "65A",
311
+ "2323": "65A",
312
+ "2200": "66A",
313
+ "2205": "66D",
314
+ "6901": null,
315
+ "6902": null,
316
+ "6903": null
317
+ };
318
+ const ASSET_RANGES = [
319
+ {
320
+ min: 1100,
321
+ max: 1129,
322
+ box: "62D"
323
+ },
324
+ {
325
+ min: 1130,
326
+ max: 1139,
327
+ box: "62E"
328
+ },
329
+ {
330
+ min: 1140,
331
+ max: 1149,
332
+ box: "62B"
333
+ },
334
+ {
335
+ min: 1154,
336
+ max: 1156,
337
+ box: "62B"
338
+ },
339
+ {
340
+ min: 1158,
341
+ max: 1159,
342
+ box: "62B"
343
+ },
344
+ {
345
+ min: 1161,
346
+ max: 1189,
347
+ box: "62A"
348
+ },
349
+ {
350
+ min: 1191,
351
+ max: 1203,
352
+ box: "62C"
353
+ },
354
+ {
355
+ min: 1211,
356
+ max: 1217,
357
+ box: "62A"
358
+ },
359
+ {
360
+ min: 1220,
361
+ max: 1229,
362
+ box: "62E"
363
+ },
364
+ {
365
+ min: 1230,
366
+ max: 1299,
367
+ box: "62E"
368
+ },
369
+ {
370
+ min: 1311,
371
+ max: 1359,
372
+ box: "61A"
373
+ },
374
+ {
375
+ min: 1371,
376
+ max: 1389,
377
+ box: "61E"
378
+ },
379
+ {
380
+ min: 1391,
381
+ max: 1403,
382
+ box: "61B"
383
+ },
384
+ {
385
+ min: 1411,
386
+ max: 1422,
387
+ box: "61C"
388
+ },
389
+ {
390
+ min: 1431,
391
+ max: 1439,
392
+ box: "61E"
393
+ },
394
+ {
395
+ min: 1440,
396
+ max: 1449,
397
+ box: "61E"
398
+ },
399
+ {
400
+ min: 1520,
401
+ max: 1528,
402
+ box: "62A"
403
+ },
404
+ {
405
+ min: 1540,
406
+ max: 1540,
407
+ box: "62B"
408
+ },
409
+ {
410
+ min: 1541,
411
+ max: 1541,
412
+ box: "62D"
413
+ },
414
+ {
415
+ min: 1550,
416
+ max: 1550,
417
+ box: "62A"
418
+ },
419
+ {
420
+ min: 1560,
421
+ max: 1563,
422
+ box: "62B"
423
+ },
424
+ {
425
+ min: 1564,
426
+ max: 1565,
427
+ box: "62B"
428
+ },
429
+ {
430
+ min: 1566,
431
+ max: 1566,
432
+ box: "62D"
433
+ },
434
+ {
435
+ min: 1580,
436
+ max: 1581,
437
+ box: "62B"
438
+ }
439
+ ];
440
+ const LIABILITY_RANGES = [
441
+ {
442
+ min: 2111,
443
+ max: 2126,
444
+ box: "66B"
445
+ },
446
+ {
447
+ min: 2127,
448
+ max: 2130,
449
+ box: "66D"
450
+ },
451
+ {
452
+ min: 2131,
453
+ max: 2142,
454
+ box: "66D"
455
+ },
456
+ {
457
+ min: 2143,
458
+ max: 2144,
459
+ box: "66B"
460
+ },
461
+ {
462
+ min: 2151,
463
+ max: 2162,
464
+ box: "66D"
465
+ },
466
+ {
467
+ min: 2171,
468
+ max: 2182,
469
+ box: "66A"
470
+ },
471
+ {
472
+ min: 2191,
473
+ max: 2201,
474
+ box: "66D"
475
+ },
476
+ {
477
+ min: 2202,
478
+ max: 2205,
479
+ box: "66C"
480
+ },
481
+ {
482
+ min: 2206,
483
+ max: 2206,
484
+ box: "66D"
485
+ },
486
+ {
487
+ min: 2311,
488
+ max: 2327,
489
+ box: "65B"
490
+ },
491
+ {
492
+ min: 2341,
493
+ max: 2349,
494
+ box: "65D"
495
+ },
496
+ {
497
+ min: 2361,
498
+ max: 2363,
499
+ box: "65D"
500
+ },
501
+ {
502
+ min: 2510,
503
+ max: 2513,
504
+ box: "66D"
505
+ },
506
+ {
507
+ min: 2520,
508
+ max: 2521,
509
+ box: "66A"
510
+ },
511
+ {
512
+ min: 2522,
513
+ max: 2522,
514
+ box: "66D"
515
+ },
516
+ {
517
+ min: 2540,
518
+ max: 2541,
519
+ box: "66D"
520
+ },
521
+ {
522
+ min: 2560,
523
+ max: 2560,
524
+ box: "66A"
525
+ },
526
+ {
527
+ min: 2561,
528
+ max: 2564,
529
+ box: "66D"
530
+ },
531
+ {
532
+ min: 2580,
533
+ max: 2581,
534
+ box: "66D"
535
+ },
536
+ {
537
+ min: 2590,
538
+ max: 2590,
539
+ box: "66A"
540
+ },
541
+ {
542
+ min: 2600,
543
+ max: 2601,
544
+ box: "66D"
545
+ }
546
+ ];
547
+ const EQUITY_RANGES = [
548
+ {
549
+ min: 3101,
550
+ max: 3105,
551
+ box: "64A"
552
+ },
553
+ {
554
+ min: 3201,
555
+ max: 3207,
556
+ box: "64A"
557
+ },
558
+ {
559
+ min: 3301,
560
+ max: 3309,
561
+ box: "64B"
562
+ },
563
+ {
564
+ min: 3401,
565
+ max: 3408,
566
+ box: "64B"
567
+ },
568
+ {
569
+ min: 3510,
570
+ max: 3519,
571
+ box: "64B"
572
+ }
573
+ ];
574
+ const EXPENSE_RANGES = [
575
+ {
576
+ min: 6101,
577
+ max: 6212,
578
+ box: "53"
579
+ },
580
+ {
581
+ min: 6301,
582
+ max: 6332,
583
+ box: "53"
584
+ },
585
+ {
586
+ min: 6401,
587
+ max: 6430,
588
+ box: "54"
589
+ },
590
+ {
591
+ min: 6501,
592
+ max: 6513,
593
+ box: "57"
594
+ },
595
+ {
596
+ min: 6601,
597
+ max: 6613,
598
+ box: "53"
599
+ },
600
+ {
601
+ min: 6701,
602
+ max: 6713,
603
+ box: "55"
604
+ },
605
+ {
606
+ min: 6801,
607
+ max: 6825,
608
+ box: "55"
609
+ },
610
+ {
611
+ min: 6910,
612
+ max: 6919,
613
+ box: "53"
614
+ },
615
+ {
616
+ min: 6920,
617
+ max: 6929,
618
+ box: "53"
619
+ },
620
+ {
621
+ min: 6930,
622
+ max: 6939,
623
+ box: "53"
624
+ },
625
+ {
626
+ min: 6940,
627
+ max: 6949,
628
+ box: "53"
629
+ },
630
+ {
631
+ min: 6950,
632
+ max: 6959,
633
+ box: "53"
634
+ },
635
+ {
636
+ min: 6960,
637
+ max: 6969,
638
+ box: "57"
639
+ },
640
+ {
641
+ min: 6970,
642
+ max: 6979,
643
+ box: "53"
644
+ }
645
+ ];
646
+ function matchRange(ranges, code) {
647
+ for (const r of ranges) if (code >= r.min && code <= r.max) return r.box;
648
+ return null;
649
+ }
650
+ function resolveScheduleBox(account) {
651
+ if (account.isGroup) return null;
652
+ const baseCodeStr = account.code.split(".")[0];
653
+ for (const key of [account.code, baseCodeStr]) if (Object.prototype.hasOwnProperty.call(EXPLICIT, key)) {
654
+ const box = EXPLICIT[key];
655
+ return box === null ? null : {
656
+ box,
657
+ confidence: "explicit"
658
+ };
659
+ }
660
+ const code = baseCode(account.code);
661
+ if (code === null) return null;
662
+ switch (account.category) {
663
+ case "Balance Sheet-Asset": {
664
+ const box = matchRange(ASSET_RANGES, code);
665
+ return box ? {
666
+ box,
667
+ confidence: "range-default"
668
+ } : fallbackAsset(account);
669
+ }
670
+ case "Balance Sheet-Liability": {
671
+ const box = matchRange(LIABILITY_RANGES, code);
672
+ return box ? {
673
+ box,
674
+ confidence: "range-default"
675
+ } : fallbackLiability(account);
676
+ }
677
+ case "Balance Sheet-Equity": {
678
+ const box = matchRange(EQUITY_RANGES, code);
679
+ return box ? {
680
+ box,
681
+ confidence: "range-default"
682
+ } : {
683
+ box: "64B",
684
+ confidence: "group-fallback"
685
+ };
686
+ }
687
+ case "Income Statement-Income":
688
+ if (account.parentCode === "Other Income") return {
689
+ box: "59",
690
+ confidence: "group-fallback"
691
+ };
692
+ return {
693
+ box: "47",
694
+ confidence: "group-fallback"
695
+ };
696
+ case "Income Statement-Expense": {
697
+ if (account.parentCode === "Cost of Sales") return {
698
+ box: "50",
699
+ confidence: "group-fallback"
700
+ };
701
+ const box = matchRange(EXPENSE_RANGES, code);
702
+ return box ? {
703
+ box,
704
+ confidence: "range-default"
705
+ } : {
706
+ box: "55",
707
+ confidence: "group-fallback"
708
+ };
709
+ }
710
+ default: return null;
711
+ }
712
+ }
713
+ function fallbackAsset(account) {
714
+ if (account.parentCode === "Current Assets") return {
715
+ box: "62E",
716
+ confidence: "group-fallback"
717
+ };
718
+ if (account.parentCode === "Non-current Assets") return {
719
+ box: "61E",
720
+ confidence: "group-fallback"
721
+ };
722
+ return null;
723
+ }
724
+ function fallbackLiability(account) {
725
+ if (account.parentCode === "Current Liabilities") return {
726
+ box: "66D",
727
+ confidence: "group-fallback"
728
+ };
729
+ if (account.parentCode === "Non-current Liabilities") return {
730
+ box: "65D",
731
+ confidence: "group-fallback"
732
+ };
733
+ return null;
734
+ }
735
+ //#endregion
736
+ //#region src/reporting/nbr-schedule/report.ts
737
+ /**
738
+ * Aggregates a trial balance into the IT-11GHA schedule. Rows for account
739
+ * codes not found in `accountTypes`, or whose account maps to no box (e.g.
740
+ * tax expense — computed after Box 60, see mapping.ts), are excluded from
741
+ * totals; use `getUnmappedAccounts` to audit for gaps.
742
+ */
743
+ function buildNbrScheduleReport(trialBalance, accountTypes) {
744
+ const accountByCode = new Map(accountTypes.map((a) => [a.code, a]));
745
+ const totals = /* @__PURE__ */ new Map();
746
+ for (const row of trialBalance) {
747
+ const account = accountByCode.get(row.accountCode);
748
+ if (!account) continue;
749
+ const result = resolveScheduleBox(account);
750
+ if (!result) continue;
751
+ const entry = totals.get(result.box) ?? {
752
+ total: 0,
753
+ count: 0
754
+ };
755
+ entry.total += row.balance;
756
+ entry.count += 1;
757
+ totals.set(result.box, entry);
758
+ }
759
+ return IT11GHA_SCHEDULE.filter((item) => !item.isTotal).map((item) => {
760
+ const entry = totals.get(item.box) ?? {
761
+ total: 0,
762
+ count: 0
763
+ };
764
+ return {
765
+ box: item.box,
766
+ label: item.label,
767
+ total: entry.total,
768
+ accountCount: entry.count
769
+ };
770
+ });
771
+ }
772
+ /**
773
+ * Coverage audit: which accounts in a chart resolve to NO schedule box.
774
+ * Deliberately unmapped codes (tax expense, per mapping.ts) are excluded —
775
+ * pass `includeIntentional: true` to see them anyway.
776
+ */
777
+ function getUnmappedAccounts(accountTypes, options = {}) {
778
+ const intentionallyUnmapped = /* @__PURE__ */ new Set([
779
+ "6901",
780
+ "6902",
781
+ "6903"
782
+ ]);
783
+ return accountTypes.filter((a) => {
784
+ if (a.isGroup) return false;
785
+ if (!options.includeIntentional && intentionallyUnmapped.has(a.code)) return false;
786
+ return resolveScheduleBox(a) === null;
787
+ });
788
+ }
789
+ //#endregion
790
+ export { IT11GHA_SCHEDULE, buildNbrScheduleReport, getScheduleLineItem, getUnmappedAccounts, resolveScheduleBox };