@classytic/ledger 0.2.0 → 0.3.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 (66) hide show
  1. package/README.md +161 -64
  2. package/dist/{account.repository-kDKwDt0I.mjs → account.repository-BpkSd6q3.mjs} +189 -38
  3. package/dist/categories-CclX7Q94.mjs +0 -2
  4. package/dist/core-8Xfnpn6g.d.mts +1 -2
  5. package/dist/country/index.d.mts +1 -1
  6. package/dist/country/index.mjs +0 -2
  7. package/dist/currencies-4WAbFRlw.d.mts +1 -2
  8. package/dist/currencies-W8kQAkm0.mjs +0 -2
  9. package/dist/{idempotency.plugin-v9NQ_ta-.mjs → date-lock.plugin-eYAJ9h_u.mjs} +49 -9
  10. package/dist/{engine-BzBMpWuy.d.mts → engine-Cn-9yerQ.d.mts} +11 -7
  11. package/dist/errors-B7yC-Jfw.mjs +0 -2
  12. package/dist/exports-I5Xkq-9_.mjs +0 -2
  13. package/dist/{fiscal-close-L631E3De.mjs → fiscal-close-B6LhQ10f.mjs} +737 -20
  14. package/dist/fiscal-period.schema-BMnlI9H5.d.mts +103 -0
  15. package/dist/{idempotency.plugin-CPxPt4vX.d.mts → idempotency.plugin-B_CNsInz.d.mts} +19 -17
  16. package/dist/index-BPukb3L8.d.mts +1 -2
  17. package/dist/{index-ZnSiqHYV.d.mts → index-CxZqRaOU.d.mts} +20 -6
  18. package/dist/index.d.mts +248 -26
  19. package/dist/index.mjs +119 -21
  20. package/dist/journals-oH-FK3g8.mjs +0 -2
  21. package/dist/{logger-UbTdBb1x.d.mts → logger-CbHWZl7v.d.mts} +1 -2
  22. package/dist/money.d.mts +1 -2
  23. package/dist/money.mjs +3 -3
  24. package/dist/plugins/index.d.mts +38 -2
  25. package/dist/plugins/index.mjs +57 -2
  26. package/dist/reconciliation.repository-CW4-8q90.d.mts +135 -0
  27. package/dist/{fiscal-period.schema-BQ5wsAq3.mjs → reconciliation.schema-BuetvZTd.mjs} +168 -24
  28. package/dist/reports/index.d.mts +2 -2
  29. package/dist/reports/index.mjs +2 -2
  30. package/dist/repositories/index.d.mts +2 -2
  31. package/dist/repositories/index.mjs +2 -2
  32. package/dist/revaluation-D9x0NE8w.d.mts +530 -0
  33. package/dist/schemas/index.d.mts +71 -2
  34. package/dist/schemas/index.mjs +2 -2
  35. package/dist/tenant-guard-Fm6AID_6.mjs +13 -0
  36. package/docs/reports.md +1 -1
  37. package/package.json +2 -2
  38. package/dist/account.repository-C7gwFLfM.d.mts +0 -29
  39. package/dist/account.repository-C7gwFLfM.d.mts.map +0 -1
  40. package/dist/account.repository-kDKwDt0I.mjs.map +0 -1
  41. package/dist/categories-CclX7Q94.mjs.map +0 -1
  42. package/dist/core-8Xfnpn6g.d.mts.map +0 -1
  43. package/dist/country/index.mjs.map +0 -1
  44. package/dist/currencies-4WAbFRlw.d.mts.map +0 -1
  45. package/dist/currencies-W8kQAkm0.mjs.map +0 -1
  46. package/dist/engine-BzBMpWuy.d.mts.map +0 -1
  47. package/dist/errors-B7yC-Jfw.mjs.map +0 -1
  48. package/dist/exports-I5Xkq-9_.mjs.map +0 -1
  49. package/dist/fiscal-close-L631E3De.mjs.map +0 -1
  50. package/dist/fiscal-close-dNlzB37y.d.mts +0 -270
  51. package/dist/fiscal-close-dNlzB37y.d.mts.map +0 -1
  52. package/dist/fiscal-period.schema-BQ5wsAq3.mjs.map +0 -1
  53. package/dist/fiscal-period.schema-BRdKAjrr.d.mts +0 -38
  54. package/dist/fiscal-period.schema-BRdKAjrr.d.mts.map +0 -1
  55. package/dist/idempotency.plugin-CPxPt4vX.d.mts.map +0 -1
  56. package/dist/idempotency.plugin-v9NQ_ta-.mjs.map +0 -1
  57. package/dist/index-BPukb3L8.d.mts.map +0 -1
  58. package/dist/index-ZnSiqHYV.d.mts.map +0 -1
  59. package/dist/index.d.mts.map +0 -1
  60. package/dist/index.mjs.map +0 -1
  61. package/dist/journals-oH-FK3g8.mjs.map +0 -1
  62. package/dist/logger-UbTdBb1x.d.mts.map +0 -1
  63. package/dist/money.d.mts.map +0 -1
  64. package/dist/money.mjs.map +0 -1
  65. package/dist/session-Ba8E3Ufa.mjs +0 -84
  66. package/dist/session-Ba8E3Ufa.mjs.map +0 -1
@@ -1,10 +1,13 @@
1
1
  import { n as Errors } from "./errors-B7yC-Jfw.mjs";
2
- import { i as requireOrgScope, n as finalizeSession, r as defaultLogger, t as acquireSession } from "./session-Ba8E3Ufa.mjs";
2
+ import { t as requireOrgScope } from "./tenant-guard-Fm6AID_6.mjs";
3
3
  import { i as extractMainType } from "./categories-CclX7Q94.mjs";
4
+ import mongoose from "mongoose";
4
5
  //#region src/utils/date-range.ts
5
6
  /**
6
7
  * Compute start/end dates from a date option + value.
7
8
  *
9
+ * @throws {Error} If value is null/undefined/invalid for the given option
10
+ *
8
11
  * Examples:
9
12
  * getDateRange('month', '2025-03') → Mar 1 – Mar 31
10
13
  * getDateRange('quarter', { quarter: 2, year: 2025 }) → Apr 1 – Jun 30
@@ -12,6 +15,7 @@ import { i as extractMainType } from "./categories-CclX7Q94.mjs";
12
15
  * getDateRange('custom', { startDate, endDate })
13
16
  */
14
17
  function getDateRange(option, value) {
18
+ if (value == null && (option === "month" || option === "quarter" || option === "year" || option === "custom")) throw new Error(`dateValue is required for dateOption "${option}"`);
15
19
  switch (option) {
16
20
  case "month": {
17
21
  let year;
@@ -22,16 +26,21 @@ function getDateRange(option, value) {
22
26
  month = parseInt(match[2], 10) - 1;
23
27
  } else {
24
28
  const date = new Date(value);
29
+ if (isNaN(date.getTime())) throw new Error(`Invalid month value: ${String(value)}`);
25
30
  year = date.getFullYear();
26
31
  month = date.getMonth();
27
32
  }
33
+ if (year < 1900 || year > 9999) throw new Error(`Year ${year} is out of valid range (1900–9999)`);
28
34
  return {
29
35
  startDate: new Date(year, month, 1),
30
36
  endDate: new Date(year, month + 1, 0, 23, 59, 59, 999)
31
37
  };
32
38
  }
33
39
  case "quarter": {
40
+ if (typeof value !== "object" || value === null) throw new Error("Quarter dateValue must be an object with { quarter, year }");
34
41
  const { quarter, year } = value;
42
+ if (!Number.isInteger(quarter) || quarter < 1 || quarter > 4) throw new Error(`Invalid quarter: ${quarter}. Must be 1–4.`);
43
+ if (!Number.isInteger(year) || year < 1900 || year > 9999) throw new Error(`Invalid year: ${year}. Must be 1900–9999.`);
35
44
  const startMonth = (quarter - 1) * 3;
36
45
  return {
37
46
  startDate: new Date(year, startMonth, 1),
@@ -40,17 +49,23 @@ function getDateRange(option, value) {
40
49
  }
41
50
  case "year": {
42
51
  const year = typeof value === "number" ? value : parseInt(String(value), 10);
52
+ if (isNaN(year) || year < 1900 || year > 9999) throw new Error(`Invalid year: ${String(value)}. Must be a number between 1900–9999.`);
43
53
  return {
44
54
  startDate: new Date(year, 0, 1),
45
55
  endDate: new Date(year, 11, 31, 23, 59, 59, 999)
46
56
  };
47
57
  }
48
58
  case "custom": {
49
- const { startDate, endDate } = value;
50
- const end = new Date(endDate);
59
+ if (typeof value !== "object" || value === null) throw new Error("Custom dateValue must be an object with { startDate, endDate }");
60
+ const { startDate: rawStart, endDate: rawEnd } = value;
61
+ if (!rawStart || !rawEnd) throw new Error("Custom date range requires both startDate and endDate");
62
+ const start = new Date(rawStart);
63
+ const end = new Date(rawEnd);
64
+ if (isNaN(start.getTime()) || isNaN(end.getTime())) throw new Error("Custom date range contains invalid dates");
65
+ if (start > end) throw new Error("startDate must be before endDate");
51
66
  if (end.getHours() === 0 && end.getMinutes() === 0 && end.getSeconds() === 0 && end.getMilliseconds() === 0) end.setHours(23, 59, 59, 999);
52
67
  return {
53
- startDate: new Date(startDate),
68
+ startDate: start,
54
69
  endDate: end
55
70
  };
56
71
  }
@@ -199,7 +214,31 @@ async function generateTrialBalance(opts, params) {
199
214
  }
200
215
  });
201
216
  }
217
+ rows.sort((a, b) => {
218
+ const codeA = a.account?.accountNumber ?? a.account?.accountTypeCode ?? "";
219
+ const codeB = b.account?.accountNumber ?? b.account?.accountTypeCode ?? "";
220
+ return codeA.localeCompare(codeB, void 0, { numeric: true });
221
+ });
222
+ const periodDisplay = params.dateOption === "year" ? `For the year ended ${endDate.toLocaleDateString("en-US", {
223
+ year: "numeric",
224
+ month: "long",
225
+ day: "numeric"
226
+ })}` : `${startDate.toLocaleDateString("en-US", {
227
+ month: "short",
228
+ day: "numeric"
229
+ })} – ${endDate.toLocaleDateString("en-US", {
230
+ year: "numeric",
231
+ month: "short",
232
+ day: "numeric"
233
+ })}`;
202
234
  return {
235
+ metadata: {
236
+ businessName: params.businessName,
237
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
238
+ periodStart: startDate.toISOString().split("T")[0],
239
+ periodEnd: endDate.toISOString().split("T")[0],
240
+ displayPeriod: periodDisplay
241
+ },
203
242
  rows,
204
243
  period: {
205
244
  startDate,
@@ -254,7 +293,7 @@ function buildAccountTypeMap(accountTypes) {
254
293
  //#endregion
255
294
  //#region src/reports/balance-sheet.ts
256
295
  async function generateBalanceSheet(opts, params) {
257
- const { AccountModel, JournalEntryModel, country, orgField, fiscalYearStartMonth = 1, retainedEarningsCode = country.retainedEarningsCode ?? "3660", currentYearEarningsCode = country.currentYearEarningsCode ?? "3680" } = opts;
296
+ const { AccountModel, JournalEntryModel, country, orgField, fiscalYearStartMonth = 1, retainedEarningsAccountCode = country.retainedEarningsAccountCode, retainedEarningsDisplayCode = country.retainedEarningsDisplayCode ?? retainedEarningsAccountCode, currentYearEarningsCode = country.currentYearEarningsCode ?? "3680" } = opts;
258
297
  requireOrgScope(orgField, params.organizationId);
259
298
  const { endDate } = getDateRange(params.dateOption, params.dateValue);
260
299
  const fiscalYearStart = getFiscalYearStart(endDate, fiscalYearStartMonth);
@@ -266,13 +305,15 @@ async function generateBalanceSheet(opts, params) {
266
305
  const at = country.getAccountType(a.accountTypeCode);
267
306
  return at && !at.isGroup && at.category.startsWith("Balance Sheet");
268
307
  }).map((a) => a._id);
308
+ const reAccountIds = retainedEarningsAccountCode ? allAccounts.filter((a) => a.accountTypeCode === retainedEarningsAccountCode).map((a) => a._id) : [];
309
+ const reAccountIdSet = new Set(reAccountIds.map(String));
269
310
  const isIds = allAccounts.filter((a) => {
270
311
  const at = country.getAccountType(a.accountTypeCode);
271
312
  return at && !at.isGroup && !at.isTotal && at.category.startsWith("Income Statement");
272
313
  }).map((a) => a._id);
273
314
  const baseMatch = { state: "posted" };
274
315
  if (orgField && params.organizationId) baseMatch[orgField] = params.organizationId;
275
- const [bsResults, netIncomeResults, priorRetainedResults] = await Promise.all([
316
+ const [bsResults, netIncomeResults, priorRetainedResults, reAccountResults] = await Promise.all([
276
317
  JournalEntryModel.aggregate([
277
318
  { $match: {
278
319
  ...baseMatch,
@@ -323,10 +364,27 @@ async function generateBalanceSheet(opts, params) {
323
364
  d: { $sum: "$journalItems.debit" },
324
365
  c: { $sum: "$journalItems.credit" }
325
366
  } }
326
- ])
367
+ ]),
368
+ ...reAccountIds.length > 0 ? [JournalEntryModel.aggregate([
369
+ { $match: {
370
+ ...baseMatch,
371
+ date: { $lte: endDate }
372
+ } },
373
+ { $unwind: "$journalItems" },
374
+ { $match: {
375
+ "journalItems.account": { $in: reAccountIds },
376
+ ...itemFilters
377
+ } },
378
+ { $group: {
379
+ _id: null,
380
+ d: { $sum: "$journalItems.debit" },
381
+ c: { $sum: "$journalItems.credit" }
382
+ } }
383
+ ])] : [Promise.resolve([])]
327
384
  ]);
328
385
  const netIncome = netIncomeResults.length > 0 ? netIncomeResults[0].c - netIncomeResults[0].d : 0;
329
- const priorRetained = priorRetainedResults.length > 0 ? priorRetainedResults[0].c - priorRetainedResults[0].d : 0;
386
+ const priorUnclosedPL = priorRetainedResults.length > 0 ? priorRetainedResults[0].c - priorRetainedResults[0].d : 0;
387
+ const priorRetained = (reAccountResults.length > 0 ? reAccountResults[0].c - reAccountResults[0].d : 0) + priorUnclosedPL;
330
388
  const accountMap = new Map(allAccounts.map((a) => [String(a._id), a]));
331
389
  const accountTypeMap = buildAccountTypeMap(country.accountTypes);
332
390
  const balanceMap = /* @__PURE__ */ new Map();
@@ -354,6 +412,7 @@ async function generateBalanceSheet(opts, params) {
354
412
  for (const r of bsResults) {
355
413
  const acc = accountMap.get(String(r._id));
356
414
  if (!acc) continue;
415
+ if (reAccountIdSet.has(String(r._id))) continue;
357
416
  const at = country.getAccountType(acc.accountTypeCode);
358
417
  if (!at) continue;
359
418
  const mainType = extractMainType(at.category) ?? "Asset";
@@ -382,7 +441,7 @@ async function generateBalanceSheet(opts, params) {
382
441
  accounts: [{
383
442
  id: "prior-retained",
384
443
  name: "Previous Years Retained Earnings",
385
- code: retainedEarningsCode,
444
+ code: retainedEarningsDisplayCode ?? retainedEarningsAccountCode ?? "",
386
445
  balance: priorRetained
387
446
  }, {
388
447
  id: "current-year",
@@ -397,13 +456,24 @@ async function generateBalanceSheet(opts, params) {
397
456
  groupsMap.Equity[reGroup.name].accounts.push(...reGroup.accounts);
398
457
  groupsMap.Equity[reGroup.name].total += reGroup.total;
399
458
  }
459
+ const sortAccountsInGroups = (groups) => {
460
+ for (const g of Object.values(groups)) g.accounts.sort((a, b) => (a.code ?? "").localeCompare(b.code ?? "", void 0, { numeric: true }));
461
+ };
462
+ sortAccountsInGroups(groupsMap.Asset);
463
+ sortAccountsInGroups(groupsMap.Liability);
464
+ sortAccountsInGroups(groupsMap.Equity);
465
+ const sortGroupsByCode = (groups) => groups.sort((a, b) => {
466
+ const codeA = a.accounts[0]?.code ?? "";
467
+ const codeB = b.accounts[0]?.code ?? "";
468
+ return codeA.localeCompare(codeB, void 0, { numeric: true });
469
+ });
400
470
  const pruneGroups = (groups) => Object.values(groups).map((g) => ({
401
471
  ...g,
402
472
  accounts: g.accounts.filter((a) => a.balance !== 0 || a.isTotal || a.isCalculated)
403
473
  })).filter((g) => g.accounts.length > 0 || g.total !== 0);
404
- assets.groups = pruneGroups(groupsMap.Asset);
405
- liabilities.groups = pruneGroups(groupsMap.Liability);
406
- equity.groups = Object.values(groupsMap.Equity);
474
+ assets.groups = sortGroupsByCode(pruneGroups(groupsMap.Asset));
475
+ liabilities.groups = sortGroupsByCode(pruneGroups(groupsMap.Liability));
476
+ equity.groups = sortGroupsByCode(Object.values(groupsMap.Equity));
407
477
  assets.total = assets.groups.reduce((s, g) => s + g.total, 0);
408
478
  liabilities.total = liabilities.groups.reduce((s, g) => s + g.total, 0);
409
479
  equity.total = equity.groups.reduce((s, g) => s + g.total, 0);
@@ -503,16 +573,26 @@ async function generateIncomeStatement(opts, params) {
503
573
  });
504
574
  groups[groupName].total += netAmount;
505
575
  }
576
+ const sortGroups = (groups) => {
577
+ const sorted = Object.values(groups);
578
+ for (const g of sorted) g.accounts.sort((a, b) => (a.code ?? "").localeCompare(b.code ?? "", void 0, { numeric: true }));
579
+ sorted.sort((a, b) => {
580
+ const codeA = a.accounts[0]?.code ?? "";
581
+ const codeB = b.accounts[0]?.code ?? "";
582
+ return codeA.localeCompare(codeB, void 0, { numeric: true });
583
+ });
584
+ return sorted;
585
+ };
506
586
  const labels = country.reportLabels ?? {};
507
587
  const revenue = {
508
588
  name: labels.revenue ?? "Revenue",
509
589
  total: Object.values(revenueGroups).reduce((s, g) => s + g.total, 0),
510
- groups: Object.values(revenueGroups)
590
+ groups: sortGroups(revenueGroups)
511
591
  };
512
592
  const expenses = {
513
593
  name: labels.expenses ?? "Expenses",
514
594
  total: Object.values(expenseGroups).reduce((s, g) => s + g.total, 0),
515
- groups: Object.values(expenseGroups)
595
+ groups: sortGroups(expenseGroups)
516
596
  };
517
597
  const cogsCode = country.cogsGroupCode;
518
598
  const isCogs = (name) => cogsCode ? name === cogsCode : name === "Cost of Sales" || name === "Cost of Goods Sold";
@@ -576,6 +656,11 @@ async function generateGeneralLedger(opts, params) {
576
656
  endDate
577
657
  }
578
658
  };
659
+ filtered.sort((a, b) => {
660
+ const codeA = a.acc.accountNumber ?? a.at.code;
661
+ const codeB = b.acc.accountNumber ?? b.at.code;
662
+ return codeA.localeCompare(codeB, void 0, { numeric: true });
663
+ });
579
664
  const bsAccountIds = [];
580
665
  const isAccountIds = [];
581
666
  const allAccountIds = [];
@@ -674,7 +759,26 @@ async function generateGeneralLedger(opts, params) {
674
759
  closingBalance: runningBalance
675
760
  });
676
761
  }
762
+ const periodDisplay = params.dateOption === "year" ? `For the year ended ${endDate.toLocaleDateString("en-US", {
763
+ year: "numeric",
764
+ month: "long",
765
+ day: "numeric"
766
+ })}` : `${startDate.toLocaleDateString("en-US", {
767
+ month: "short",
768
+ day: "numeric"
769
+ })} – ${endDate.toLocaleDateString("en-US", {
770
+ year: "numeric",
771
+ month: "short",
772
+ day: "numeric"
773
+ })}`;
677
774
  return {
775
+ metadata: {
776
+ businessName: params.businessName,
777
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
778
+ periodStart: startDate.toISOString().split("T")[0],
779
+ periodEnd: endDate.toISOString().split("T")[0],
780
+ displayPeriod: periodDisplay
781
+ },
678
782
  accounts: glAccounts,
679
783
  period: {
680
784
  startDate,
@@ -756,6 +860,7 @@ async function generateCashFlow(opts, params) {
756
860
  });
757
861
  flows[meta.cfCategory].total += amount;
758
862
  }
863
+ for (const section of Object.values(flows)) section.accounts.sort((a, b) => a.code.localeCompare(b.code, void 0, { numeric: true }));
759
864
  const netCashFlow = flows.Operating.total + flows.Investing.total + flows.Financing.total;
760
865
  const periodDisplay = `${startDate.toLocaleDateString("en-US", {
761
866
  month: "short",
@@ -780,9 +885,623 @@ async function generateCashFlow(opts, params) {
780
885
  };
781
886
  }
782
887
  //#endregion
888
+ //#region src/reports/aged-balance.ts
889
+ const DEFAULT_BUCKETS = [
890
+ {
891
+ label: "Current",
892
+ minDays: 0,
893
+ maxDays: 31
894
+ },
895
+ {
896
+ label: "31-60",
897
+ minDays: 31,
898
+ maxDays: 61
899
+ },
900
+ {
901
+ label: "61-90",
902
+ minDays: 61,
903
+ maxDays: 91
904
+ },
905
+ {
906
+ label: "90+",
907
+ minDays: 91,
908
+ maxDays: Infinity
909
+ }
910
+ ];
911
+ async function generateAgedBalance(opts, params) {
912
+ const { AccountModel, JournalEntryModel, country, orgField } = opts;
913
+ requireOrgScope(orgField, params.organizationId);
914
+ const asOfDate = params.asOfDate ?? /* @__PURE__ */ new Date();
915
+ const buckets = params.buckets ?? DEFAULT_BUCKETS;
916
+ const bucketLabels = buckets.map((b) => b.label);
917
+ const dueDateField = params.dueDateField ?? "journalItems.dueDate";
918
+ const contactField = params.contactField;
919
+ const accountQuery = { active: true };
920
+ if (orgField && params.organizationId) accountQuery[orgField] = params.organizationId;
921
+ let targetAccountIds;
922
+ if (params.accountIds && params.accountIds.length > 0) targetAccountIds = params.accountIds;
923
+ else {
924
+ const allAccounts = await AccountModel.find(accountQuery).lean();
925
+ const categoryPrefix = params.type === "receivable" ? "Balance Sheet-Asset" : "Balance Sheet-Liability";
926
+ targetAccountIds = allAccounts.filter((a) => {
927
+ const at = country.getAccountType(a.accountTypeCode);
928
+ return at && !at.isGroup && at.category.startsWith(categoryPrefix);
929
+ }).map((a) => a._id);
930
+ }
931
+ if (targetAccountIds.length === 0) return {
932
+ metadata: {
933
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
934
+ asOfDate: asOfDate.toISOString().split("T")[0],
935
+ type: params.type
936
+ },
937
+ bucketLabels,
938
+ rows: [],
939
+ totals: Object.fromEntries(bucketLabels.map((l) => [l, 0])),
940
+ grandTotal: 0
941
+ };
942
+ const allAccounts = await AccountModel.find(accountQuery).lean();
943
+ const accountLookup = new Map(allAccounts.map((a) => [String(a._id), a]));
944
+ const baseMatch = {
945
+ state: "posted",
946
+ date: { $lte: asOfDate }
947
+ };
948
+ if (orgField && params.organizationId) baseMatch[orgField] = params.organizationId;
949
+ asOfDate.getTime();
950
+ const groupId = { account: "$journalItems.account" };
951
+ if (contactField) groupId.contact = `$${contactField}`;
952
+ const bucketBranches = buckets.map((b) => {
953
+ return {
954
+ case: b.maxDays === Infinity ? { $gte: ["$daysPastDue", b.minDays] } : { $and: [{ $gte: ["$daysPastDue", b.minDays] }, { $lt: ["$daysPastDue", b.maxDays] }] },
955
+ then: b.label
956
+ };
957
+ });
958
+ const pipeline = [
959
+ { $match: baseMatch },
960
+ { $unwind: "$journalItems" },
961
+ { $match: { "journalItems.account": { $in: targetAccountIds } } },
962
+ { $addFields: { daysPastDue: { $floor: { $divide: [{ $subtract: [asOfDate, { $ifNull: [`$${dueDateField}`, asOfDate] }] }, 1e3 * 60 * 60 * 24] } } } },
963
+ { $addFields: { daysPastDue: { $max: ["$daysPastDue", 0] } } },
964
+ { $addFields: { bucketLabel: { $switch: {
965
+ branches: bucketBranches,
966
+ default: bucketLabels[bucketLabels.length - 1]
967
+ } } } },
968
+ { $addFields: { netAmount: params.type === "receivable" ? { $subtract: ["$journalItems.debit", "$journalItems.credit"] } : { $subtract: ["$journalItems.credit", "$journalItems.debit"] } } },
969
+ { $group: {
970
+ _id: {
971
+ ...groupId,
972
+ bucket: "$bucketLabel"
973
+ },
974
+ amount: { $sum: "$netAmount" }
975
+ } }
976
+ ];
977
+ const results = await JournalEntryModel.aggregate(pipeline);
978
+ const rowKey = (accountId, contactId) => contactField ? `${String(accountId)}::${String(contactId ?? "")}` : String(accountId);
979
+ const rowMap = /* @__PURE__ */ new Map();
980
+ for (const r of results) {
981
+ const key = rowKey(r._id.account, r._id.contact);
982
+ if (!rowMap.has(key)) {
983
+ const acc = accountLookup.get(String(r._id.account));
984
+ rowMap.set(key, {
985
+ accountId: r._id.account,
986
+ accountName: acc?.name ?? "",
987
+ accountCode: acc?.accountNumber ?? "",
988
+ ...contactField ? { contactId: r._id.contact } : {},
989
+ total: 0,
990
+ buckets: Object.fromEntries(bucketLabels.map((l) => [l, 0]))
991
+ });
992
+ }
993
+ const row = rowMap.get(key);
994
+ if (row.buckets[r._id.bucket] !== void 0) row.buckets[r._id.bucket] += r.amount;
995
+ row.total += r.amount;
996
+ }
997
+ const rows = Array.from(rowMap.values()).sort((a, b) => a.accountCode.localeCompare(b.accountCode, void 0, { numeric: true }));
998
+ const totals = Object.fromEntries(bucketLabels.map((l) => [l, 0]));
999
+ let grandTotal = 0;
1000
+ for (const row of rows) {
1001
+ for (const label of bucketLabels) totals[label] += row.buckets[label];
1002
+ grandTotal += row.total;
1003
+ }
1004
+ return {
1005
+ metadata: {
1006
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1007
+ asOfDate: asOfDate.toISOString().split("T")[0],
1008
+ type: params.type
1009
+ },
1010
+ bucketLabels,
1011
+ rows,
1012
+ totals,
1013
+ grandTotal
1014
+ };
1015
+ }
1016
+ //#endregion
1017
+ //#region src/reports/dimension-breakdown.ts
1018
+ async function generateDimensionBreakdown(opts, params) {
1019
+ const { AccountModel, JournalEntryModel, country, orgField } = opts;
1020
+ requireOrgScope(orgField, params.organizationId);
1021
+ const { startDate, endDate } = getDateRange(params.dateOption, params.dateValue);
1022
+ const itemFilters = buildItemFilters(params.filters);
1023
+ const accountQuery = { active: true };
1024
+ if (orgField && params.organizationId) accountQuery[orgField] = params.organizationId;
1025
+ const allAccounts = await AccountModel.find(accountQuery).lean();
1026
+ const accountIds = allAccounts.filter((a) => {
1027
+ const at = country.getAccountType(a.accountTypeCode);
1028
+ if (!at || at.isGroup || at.isTotal) return false;
1029
+ if (params.accountCategory && at.category !== params.accountCategory) return false;
1030
+ return true;
1031
+ }).map((a) => a._id);
1032
+ if (accountIds.length === 0) return {
1033
+ metadata: {
1034
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1035
+ dimension: params.dimension,
1036
+ periodStart: startDate.toISOString().split("T")[0],
1037
+ periodEnd: endDate.toISOString().split("T")[0]
1038
+ },
1039
+ rows: [],
1040
+ grandTotal: 0
1041
+ };
1042
+ const accountMap = new Map(allAccounts.map((a) => [String(a._id), a]));
1043
+ const dimensionPath = `journalItems.${params.dimension}`;
1044
+ const baseMatch = {
1045
+ state: "posted",
1046
+ date: {
1047
+ $gte: startDate,
1048
+ $lte: endDate
1049
+ }
1050
+ };
1051
+ if (orgField && params.organizationId) baseMatch[orgField] = params.organizationId;
1052
+ const pipeline = [
1053
+ { $match: baseMatch },
1054
+ { $unwind: "$journalItems" },
1055
+ { $match: {
1056
+ "journalItems.account": { $in: accountIds },
1057
+ ...itemFilters
1058
+ } },
1059
+ { $group: {
1060
+ _id: {
1061
+ dimension: `$${dimensionPath}`,
1062
+ account: "$journalItems.account"
1063
+ },
1064
+ d: { $sum: "$journalItems.debit" },
1065
+ c: { $sum: "$journalItems.credit" }
1066
+ } }
1067
+ ];
1068
+ const results = await JournalEntryModel.aggregate(pipeline);
1069
+ const dimensionMap = /* @__PURE__ */ new Map();
1070
+ for (const r of results) {
1071
+ const dimKey = r._id.dimension == null ? "__null__" : String(r._id.dimension);
1072
+ const accKey = String(r._id.account);
1073
+ if (!dimensionMap.has(dimKey)) dimensionMap.set(dimKey, /* @__PURE__ */ new Map());
1074
+ dimensionMap.get(dimKey).set(accKey, {
1075
+ d: r.d,
1076
+ c: r.c
1077
+ });
1078
+ }
1079
+ const rows = [];
1080
+ const sortedDimKeys = [...dimensionMap.keys()].sort((a, b) => {
1081
+ if (a === "__null__") return 1;
1082
+ if (b === "__null__") return -1;
1083
+ return a.localeCompare(b);
1084
+ });
1085
+ for (const dimKey of sortedDimKeys) {
1086
+ const accountBalances = dimensionMap.get(dimKey);
1087
+ const dimensionValue = dimKey === "__null__" ? null : results.find((r) => String(r._id.dimension) === dimKey)?._id.dimension ?? null;
1088
+ const accounts = [];
1089
+ let total = 0;
1090
+ for (const [accId, bal] of accountBalances) {
1091
+ const acc = accountMap.get(accId);
1092
+ if (!acc) continue;
1093
+ const at = country.getAccountType(acc.accountTypeCode);
1094
+ const balance = at && (at.category === "Income Statement-Income" || at.category === "Balance Sheet-Liability" || at.category === "Balance Sheet-Equity") ? bal.c - bal.d : bal.d - bal.c;
1095
+ accounts.push({
1096
+ id: acc._id,
1097
+ name: acc.name ?? at?.name ?? "",
1098
+ code: acc.accountNumber ?? at?.code ?? "",
1099
+ balance
1100
+ });
1101
+ total += balance;
1102
+ }
1103
+ accounts.sort((a, b) => a.code.localeCompare(b.code, void 0, { numeric: true }));
1104
+ rows.push({
1105
+ dimensionValue,
1106
+ accounts,
1107
+ total
1108
+ });
1109
+ }
1110
+ const grandTotal = rows.reduce((s, r) => s + r.total, 0);
1111
+ return {
1112
+ metadata: {
1113
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1114
+ dimension: params.dimension,
1115
+ periodStart: startDate.toISOString().split("T")[0],
1116
+ periodEnd: endDate.toISOString().split("T")[0]
1117
+ },
1118
+ rows,
1119
+ grandTotal
1120
+ };
1121
+ }
1122
+ //#endregion
1123
+ //#region src/reports/budget-vs-actual.ts
1124
+ /**
1125
+ * Budget vs Actual Report
1126
+ *
1127
+ * Compares budgeted amounts against actual journal entry balances
1128
+ * for a given period. All monetary values are integer cents.
1129
+ */
1130
+ async function generateBudgetVsActual(opts, params) {
1131
+ const { AccountModel, JournalEntryModel, BudgetModel, country, orgField } = opts;
1132
+ requireOrgScope(orgField, params.organizationId);
1133
+ const { startDate, endDate } = getDateRange(params.dateOption, params.dateValue);
1134
+ const budgetQuery = {
1135
+ periodStart: { $lte: endDate },
1136
+ periodEnd: { $gte: startDate }
1137
+ };
1138
+ if (orgField && params.organizationId) budgetQuery[orgField] = params.organizationId;
1139
+ if (params.accountIds && params.accountIds.length > 0) budgetQuery.account = { $in: params.accountIds };
1140
+ const budgets = await BudgetModel.find(budgetQuery).lean();
1141
+ if (budgets.length === 0) return {
1142
+ metadata: {
1143
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1144
+ periodStart: startDate.toISOString(),
1145
+ periodEnd: endDate.toISOString()
1146
+ },
1147
+ rows: [],
1148
+ summary: {
1149
+ totalBudget: 0,
1150
+ totalActual: 0,
1151
+ totalVariance: 0
1152
+ }
1153
+ };
1154
+ const budgetByAccount = /* @__PURE__ */ new Map();
1155
+ for (const b of budgets) {
1156
+ const key = String(b.account);
1157
+ budgetByAccount.set(key, (budgetByAccount.get(key) ?? 0) + b.amount);
1158
+ }
1159
+ const accountIds = [...budgetByAccount.keys()];
1160
+ const accountQuery = { _id: { $in: accountIds } };
1161
+ if (orgField && params.organizationId) accountQuery[orgField] = params.organizationId;
1162
+ const accounts = await AccountModel.find(accountQuery).lean();
1163
+ const accountMap = new Map(accounts.map((a) => [String(a._id), a]));
1164
+ const baseMatch = {
1165
+ state: "posted",
1166
+ date: {
1167
+ $gte: startDate,
1168
+ $lte: endDate
1169
+ }
1170
+ };
1171
+ if (orgField && params.organizationId) baseMatch[orgField] = params.organizationId;
1172
+ const pipeline = [
1173
+ { $match: baseMatch },
1174
+ { $unwind: "$journalItems" },
1175
+ { $match: { "journalItems.account": { $in: accountIds.map((id) => new mongoose.Types.ObjectId(id)) } } },
1176
+ { $group: {
1177
+ _id: "$journalItems.account",
1178
+ totalDebit: { $sum: "$journalItems.debit" },
1179
+ totalCredit: { $sum: "$journalItems.credit" }
1180
+ } }
1181
+ ];
1182
+ const actuals = await JournalEntryModel.aggregate(pipeline);
1183
+ const actualByAccount = /* @__PURE__ */ new Map();
1184
+ for (const a of actuals) actualByAccount.set(String(a._id), {
1185
+ debit: a.totalDebit,
1186
+ credit: a.totalCredit
1187
+ });
1188
+ const rows = [];
1189
+ for (const [accountId, budgetAmount] of budgetByAccount) {
1190
+ const acc = accountMap.get(accountId);
1191
+ if (!acc) continue;
1192
+ const at = country.getAccountType(acc.accountTypeCode);
1193
+ if (!at || at.isGroup) continue;
1194
+ const actual = actualByAccount.get(accountId) ?? {
1195
+ debit: 0,
1196
+ credit: 0
1197
+ };
1198
+ const mainType = extractMainType(at.category);
1199
+ let actualAmount;
1200
+ if (mainType === "Income") actualAmount = actual.credit - actual.debit;
1201
+ else if (mainType === "Expense") actualAmount = actual.debit - actual.credit;
1202
+ else if (mainType === "Asset") actualAmount = actual.debit - actual.credit;
1203
+ else actualAmount = actual.credit - actual.debit;
1204
+ const variance = actualAmount - budgetAmount;
1205
+ const variancePercent = budgetAmount !== 0 ? Math.round(variance / budgetAmount * 1e4) / 100 : 0;
1206
+ rows.push({
1207
+ accountId: acc._id,
1208
+ accountName: acc.name,
1209
+ accountCode: acc.accountNumber,
1210
+ category: at.category,
1211
+ budgetAmount,
1212
+ actualAmount,
1213
+ variance,
1214
+ variancePercent
1215
+ });
1216
+ }
1217
+ rows.sort((a, b) => a.accountCode.localeCompare(b.accountCode));
1218
+ const summary = {
1219
+ totalBudget: rows.reduce((s, r) => s + r.budgetAmount, 0),
1220
+ totalActual: rows.reduce((s, r) => s + r.actualAmount, 0),
1221
+ totalVariance: rows.reduce((s, r) => s + r.variance, 0)
1222
+ };
1223
+ return {
1224
+ metadata: {
1225
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1226
+ periodStart: startDate.toISOString(),
1227
+ periodEnd: endDate.toISOString()
1228
+ },
1229
+ rows,
1230
+ summary
1231
+ };
1232
+ }
1233
+ //#endregion
1234
+ //#region src/utils/revaluation.ts
1235
+ /**
1236
+ * Compute revaluation results for a set of accounts at new exchange rates.
1237
+ *
1238
+ * For each account, finds the matching rate by currency, computes the
1239
+ * revalued base amount, and determines the gain/loss.
1240
+ * Accounts with zero gain/loss are excluded from the results.
1241
+ *
1242
+ * @param accounts - Foreign-currency account balances at historical rates
1243
+ * @param rates - New exchange rates to revalue against
1244
+ * @param baseCurrency - The functional/base currency code (accounts in this currency are skipped)
1245
+ */
1246
+ function computeRevaluation(accounts, rates, baseCurrency) {
1247
+ const rateMap = new Map(rates.map((r) => [r.currency, r.rate]));
1248
+ const results = [];
1249
+ for (const acct of accounts) {
1250
+ if (acct.currency === baseCurrency) continue;
1251
+ const rate = rateMap.get(acct.currency);
1252
+ if (rate === void 0) continue;
1253
+ const revaluedBase = Math.round(acct.foreignBalance * rate);
1254
+ const gainLoss = revaluedBase - acct.baseBalance;
1255
+ if (gainLoss === 0) continue;
1256
+ results.push({
1257
+ accountId: acct.accountId,
1258
+ accountName: acct.accountName,
1259
+ accountCode: acct.accountCode,
1260
+ currency: acct.currency,
1261
+ foreignBalance: acct.foreignBalance,
1262
+ historicalBase: acct.baseBalance,
1263
+ revaluedBase,
1264
+ gainLoss
1265
+ });
1266
+ }
1267
+ return results;
1268
+ }
1269
+ /**
1270
+ * Build a balanced revaluation journal entry from revaluation results.
1271
+ *
1272
+ * For each result with a non-zero gain/loss:
1273
+ * - Gain (positive gainLoss): Debit the account, Credit the unrealized gain/loss account
1274
+ * - Loss (negative gainLoss): Credit the account, Debit the unrealized gain/loss account
1275
+ *
1276
+ * @param results - Revaluation results from computeRevaluation
1277
+ * @param unrealizedGainLossAccountId - The account to book the offsetting entry against
1278
+ * @param date - Date for the revaluation entry
1279
+ */
1280
+ function buildRevaluationEntry(results, unrealizedGainLossAccountId, date) {
1281
+ const journalItems = [];
1282
+ let totalDebit = 0;
1283
+ let totalCredit = 0;
1284
+ for (const r of results) {
1285
+ if (r.gainLoss === 0) continue;
1286
+ const absAmount = Math.abs(r.gainLoss);
1287
+ if (r.gainLoss > 0) {
1288
+ journalItems.push({
1289
+ account: r.accountId,
1290
+ debit: absAmount,
1291
+ credit: 0,
1292
+ originalDebit: 0,
1293
+ originalCredit: 0,
1294
+ label: `FX revaluation ${r.currency} — gain`
1295
+ });
1296
+ journalItems.push({
1297
+ account: unrealizedGainLossAccountId,
1298
+ debit: 0,
1299
+ credit: absAmount,
1300
+ originalDebit: 0,
1301
+ originalCredit: 0,
1302
+ label: `FX revaluation ${r.currency} — gain`
1303
+ });
1304
+ } else {
1305
+ journalItems.push({
1306
+ account: r.accountId,
1307
+ debit: 0,
1308
+ credit: absAmount,
1309
+ originalDebit: 0,
1310
+ originalCredit: 0,
1311
+ label: `FX revaluation ${r.currency} — loss`
1312
+ });
1313
+ journalItems.push({
1314
+ account: unrealizedGainLossAccountId,
1315
+ debit: absAmount,
1316
+ credit: 0,
1317
+ originalDebit: 0,
1318
+ originalCredit: 0,
1319
+ label: `FX revaluation ${r.currency} — loss`
1320
+ });
1321
+ }
1322
+ totalDebit += absAmount;
1323
+ totalCredit += absAmount;
1324
+ }
1325
+ const dateStr = date.toISOString().split("T")[0];
1326
+ return {
1327
+ journalItems,
1328
+ totalDebit,
1329
+ totalCredit,
1330
+ label: `Foreign exchange revaluation — ${dateStr}`
1331
+ };
1332
+ }
1333
+ //#endregion
1334
+ //#region src/reports/revaluation.ts
1335
+ /**
1336
+ * Generate a foreign exchange revaluation report.
1337
+ *
1338
+ * 1. Finds all accounts with a `currency` field (foreign-currency accounts)
1339
+ * 2. Filters to balance sheet accounts only (not P&L)
1340
+ * 3. Aggregates foreign-currency and base-currency balances from posted entries
1341
+ * 4. Computes gain/loss at the new rates
1342
+ * 5. Optionally creates and saves a balanced journal entry
1343
+ */
1344
+ async function generateRevaluation(opts, params) {
1345
+ const { AccountModel, JournalEntryModel, country, orgField, baseCurrency } = opts;
1346
+ requireOrgScope(orgField, params.organizationId);
1347
+ const accountQuery = {
1348
+ active: true,
1349
+ currency: {
1350
+ $exists: true,
1351
+ $ne: null
1352
+ }
1353
+ };
1354
+ if (orgField && params.organizationId) accountQuery[orgField] = params.organizationId;
1355
+ const bsAccounts = (await AccountModel.find(accountQuery).lean()).filter((a) => {
1356
+ const at = country.getAccountType(a.accountTypeCode);
1357
+ return at && !at.isGroup && at.category.startsWith("Balance Sheet");
1358
+ });
1359
+ if (bsAccounts.length === 0) return {
1360
+ metadata: {
1361
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1362
+ asOfDate: params.asOfDate.toISOString().split("T")[0],
1363
+ baseCurrency
1364
+ },
1365
+ results: [],
1366
+ totalGainLoss: 0
1367
+ };
1368
+ const bsAccountIds = bsAccounts.map((a) => a._id);
1369
+ const baseMatch = {
1370
+ state: "posted",
1371
+ date: { $lte: params.asOfDate }
1372
+ };
1373
+ if (orgField && params.organizationId) baseMatch[orgField] = params.organizationId;
1374
+ const balanceResults = await JournalEntryModel.aggregate([
1375
+ { $match: baseMatch },
1376
+ { $unwind: "$journalItems" },
1377
+ { $match: { "journalItems.account": { $in: bsAccountIds } } },
1378
+ { $group: {
1379
+ _id: "$journalItems.account",
1380
+ debit: { $sum: "$journalItems.debit" },
1381
+ credit: { $sum: "$journalItems.credit" },
1382
+ originalDebit: { $sum: { $ifNull: ["$journalItems.originalDebit", 0] } },
1383
+ originalCredit: { $sum: { $ifNull: ["$journalItems.originalCredit", 0] } }
1384
+ } }
1385
+ ]);
1386
+ const accountMap = new Map(bsAccounts.map((a) => [String(a._id), a]));
1387
+ const accountBalances = [];
1388
+ for (const r of balanceResults) {
1389
+ const acct = accountMap.get(String(r._id));
1390
+ if (!acct) continue;
1391
+ const at = country.getAccountType(acct.accountTypeCode);
1392
+ if (!at) continue;
1393
+ accountBalances.push({
1394
+ accountId: r._id,
1395
+ accountName: acct.name ?? at.name,
1396
+ accountCode: acct.accountNumber ?? at.code,
1397
+ currency: acct.currency,
1398
+ foreignBalance: r.originalDebit - r.originalCredit,
1399
+ baseBalance: r.debit - r.credit,
1400
+ category: at.category
1401
+ });
1402
+ }
1403
+ const results = computeRevaluation(accountBalances, params.rates, baseCurrency);
1404
+ const totalGainLoss = results.reduce((sum, r) => sum + r.gainLoss, 0);
1405
+ let entryId;
1406
+ if (params.generateEntry && results.length > 0) {
1407
+ const entryData = buildRevaluationEntry(results, params.unrealizedGainLossAccountId, params.asOfDate);
1408
+ const doc = {
1409
+ journalType: "GENERAL",
1410
+ state: "posted",
1411
+ date: params.asOfDate,
1412
+ label: entryData.label,
1413
+ journalItems: entryData.journalItems,
1414
+ totalDebit: entryData.totalDebit,
1415
+ totalCredit: entryData.totalCredit
1416
+ };
1417
+ if (orgField && params.organizationId) doc[orgField] = params.organizationId;
1418
+ entryId = (await JournalEntryModel.create(doc))._id;
1419
+ }
1420
+ return {
1421
+ metadata: {
1422
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1423
+ asOfDate: params.asOfDate.toISOString().split("T")[0],
1424
+ baseCurrency
1425
+ },
1426
+ results,
1427
+ totalGainLoss,
1428
+ ...entryId !== void 0 ? { entryId } : {}
1429
+ };
1430
+ }
1431
+ //#endregion
1432
+ //#region src/utils/logger.ts
1433
+ /** Default console-based implementation */
1434
+ const defaultLogger = {
1435
+ warn: (msg, meta) => console.warn(`[accounting] ${msg}`, meta ?? ""),
1436
+ error: (msg, meta) => console.error(`[accounting] ${msg}`, meta ?? ""),
1437
+ info: (msg, meta) => console.info(`[accounting] ${msg}`, meta ?? "")
1438
+ };
1439
+ //#endregion
1440
+ //#region src/utils/session.ts
1441
+ /**
1442
+ * Acquire a session: uses external if provided, otherwise creates an internal one.
1443
+ * Returns { session, ownSession } so callers can commit/abort/end appropriately.
1444
+ *
1445
+ * When transactions are unavailable (no replica set / standalone), returns
1446
+ * session=null and the function runs without transactional safety.
1447
+ */
1448
+ async function acquireSession(db, externalSession, logger = defaultLogger) {
1449
+ if (externalSession) return {
1450
+ session: externalSession,
1451
+ ownSession: false
1452
+ };
1453
+ try {
1454
+ const session = await db.startSession();
1455
+ try {
1456
+ const conn = db;
1457
+ if ((conn.getClient?.() ?? conn.client)?.topology?.description?.type === "Single") {
1458
+ session.endSession();
1459
+ logger.warn("Transactions unavailable (standalone MongoDB). Operation is not atomic.");
1460
+ return {
1461
+ session: null,
1462
+ ownSession: false
1463
+ };
1464
+ }
1465
+ } catch {}
1466
+ try {
1467
+ session.startTransaction();
1468
+ return {
1469
+ session,
1470
+ ownSession: true
1471
+ };
1472
+ } catch (err) {
1473
+ session.endSession();
1474
+ logger.warn("Transactions unavailable (no replica set). Operation is not atomic.", { error: err.message });
1475
+ return {
1476
+ session: null,
1477
+ ownSession: false
1478
+ };
1479
+ }
1480
+ } catch {
1481
+ return {
1482
+ session: null,
1483
+ ownSession: false
1484
+ };
1485
+ }
1486
+ }
1487
+ /**
1488
+ * Finalize an owned session: commit or abort, then always end.
1489
+ */
1490
+ async function finalizeSession(session, ownSession, success) {
1491
+ if (!ownSession || !session) return;
1492
+ try {
1493
+ if (success && session.inTransaction()) await session.commitTransaction();
1494
+ else if (!success && session.inTransaction()) try {
1495
+ await session.abortTransaction();
1496
+ } catch {}
1497
+ } finally {
1498
+ session.endSession();
1499
+ }
1500
+ }
1501
+ //#endregion
783
1502
  //#region src/reports/fiscal-close.ts
784
1503
  async function closeFiscalPeriod(opts, params) {
785
- const { AccountModel, JournalEntryModel, FiscalPeriodModel, country, orgField, retainedEarningsCode = country.retainedEarningsCode ?? "3660", logger = defaultLogger } = opts;
1504
+ const { AccountModel, JournalEntryModel, FiscalPeriodModel, country, orgField, retainedEarningsAccountCode = country.retainedEarningsAccountCode ?? "3600", logger = defaultLogger } = opts;
786
1505
  const { periodId, organizationId, closedBy } = params;
787
1506
  requireOrgScope(orgField, organizationId);
788
1507
  const { session, ownSession } = await acquireSession(AccountModel.db, params.session, logger);
@@ -804,7 +1523,7 @@ async function closeFiscalPeriod(opts, params) {
804
1523
  for (const acc of allAccounts) {
805
1524
  const at = country.getAccountType(acc.accountTypeCode);
806
1525
  if (!at) continue;
807
- if (acc.accountTypeCode === retainedEarningsCode) retainedEarningsId = acc._id;
1526
+ if (acc.accountTypeCode === retainedEarningsAccountCode) retainedEarningsId = acc._id;
808
1527
  if (at.isGroup || at.isTotal) continue;
809
1528
  if (at.category.startsWith("Income Statement")) isAccounts.push({
810
1529
  id: acc._id,
@@ -812,7 +1531,7 @@ async function closeFiscalPeriod(opts, params) {
812
1531
  isIncome: at.category === "Income Statement-Income"
813
1532
  });
814
1533
  }
815
- if (!retainedEarningsId) throw Errors.fiscal(`Retained earnings account (code: ${retainedEarningsCode}) not found. Create this account before closing the fiscal period.`);
1534
+ if (!retainedEarningsId) throw Errors.fiscal(`Retained earnings account (code: ${retainedEarningsAccountCode}) not found. Create this account before closing the fiscal period.`);
816
1535
  const baseMatch = {
817
1536
  state: "posted",
818
1537
  date: {
@@ -934,6 +1653,4 @@ async function reopenFiscalPeriod(opts, params) {
934
1653
  }
935
1654
  }
936
1655
  //#endregion
937
- export { generateIncomeStatement as a, calculateTotal as c, generateTrialBalance as d, buildItemFilters as f, generateGeneralLedger as i, computeEndingBalance as l, getFiscalYearStart as m, reopenFiscalPeriod as n, generateBalanceSheet as o, getDateRange as p, generateCashFlow as r, buildAccountTypeMap as s, closeFiscalPeriod as t, isVirtualTaxAccount as u };
938
-
939
- //# sourceMappingURL=fiscal-close-L631E3De.mjs.map
1656
+ export { getDateRange as C, buildItemFilters as S, buildAccountTypeMap as _, defaultLogger as a, isVirtualTaxAccount as b, computeRevaluation as c, DEFAULT_BUCKETS as d, generateAgedBalance as f, generateBalanceSheet as g, generateIncomeStatement as h, finalizeSession as i, generateBudgetVsActual as l, generateGeneralLedger as m, reopenFiscalPeriod as n, generateRevaluation as o, generateCashFlow as p, acquireSession as r, buildRevaluationEntry as s, closeFiscalPeriod as t, generateDimensionBreakdown as u, calculateTotal as v, getFiscalYearStart as w, generateTrialBalance as x, computeEndingBalance as y };