@classytic/ledger 0.1.3

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 (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +108 -0
  3. package/dist/account.repository-1C2sZvB2.d.mts +29 -0
  4. package/dist/account.repository-1C2sZvB2.d.mts.map +1 -0
  5. package/dist/account.repository-Crf5DGO4.mjs +393 -0
  6. package/dist/account.repository-Crf5DGO4.mjs.map +1 -0
  7. package/dist/categories-BNJBd4ze.mjs +70 -0
  8. package/dist/categories-BNJBd4ze.mjs.map +1 -0
  9. package/dist/constants/index.d.mts +2 -0
  10. package/dist/constants/index.mjs +5 -0
  11. package/dist/core-Cx0baosR.d.mts +104 -0
  12. package/dist/core-Cx0baosR.d.mts.map +1 -0
  13. package/dist/country/index.d.mts +105 -0
  14. package/dist/country/index.d.mts.map +1 -0
  15. package/dist/country/index.mjs +27 -0
  16. package/dist/country/index.mjs.map +1 -0
  17. package/dist/currencies-BBk3NwXn.mjs +82 -0
  18. package/dist/currencies-BBk3NwXn.mjs.map +1 -0
  19. package/dist/currencies-Bkn3FNkC.d.mts +38 -0
  20. package/dist/currencies-Bkn3FNkC.d.mts.map +1 -0
  21. package/dist/engine-Cd73EOT6.d.mts +72 -0
  22. package/dist/engine-Cd73EOT6.d.mts.map +1 -0
  23. package/dist/errors-CeqRahE-.mjs +28 -0
  24. package/dist/errors-CeqRahE-.mjs.map +1 -0
  25. package/dist/exports/index.d.mts +2 -0
  26. package/dist/exports/index.mjs +3 -0
  27. package/dist/fiscal-close-CNOwv_ud.mjs +934 -0
  28. package/dist/fiscal-close-CNOwv_ud.mjs.map +1 -0
  29. package/dist/fiscal-close-CzUzpnMg.d.mts +270 -0
  30. package/dist/fiscal-close-CzUzpnMg.d.mts.map +1 -0
  31. package/dist/fiscal-period.schema-CbALaaKl.mjs +477 -0
  32. package/dist/fiscal-period.schema-CbALaaKl.mjs.map +1 -0
  33. package/dist/fiscal-period.schema-DI2scngu.d.mts +38 -0
  34. package/dist/fiscal-period.schema-DI2scngu.d.mts.map +1 -0
  35. package/dist/idempotency.plugin-BESs9YPD.d.mts +58 -0
  36. package/dist/idempotency.plugin-BESs9YPD.d.mts.map +1 -0
  37. package/dist/idempotency.plugin-C6r8RI8d.mjs +165 -0
  38. package/dist/idempotency.plugin-C6r8RI8d.mjs.map +1 -0
  39. package/dist/index.d.mts +308 -0
  40. package/dist/index.d.mts.map +1 -0
  41. package/dist/index.mjs +171 -0
  42. package/dist/index.mjs.map +1 -0
  43. package/dist/journals-CI3Wb4EF.mjs +92 -0
  44. package/dist/journals-CI3Wb4EF.mjs.map +1 -0
  45. package/dist/logger-Cv6VVc4r.d.mts +15 -0
  46. package/dist/logger-Cv6VVc4r.d.mts.map +1 -0
  47. package/dist/money.d.mts +129 -0
  48. package/dist/money.d.mts.map +1 -0
  49. package/dist/money.mjs +197 -0
  50. package/dist/money.mjs.map +1 -0
  51. package/dist/plugins/index.d.mts +2 -0
  52. package/dist/plugins/index.mjs +3 -0
  53. package/dist/reports/index.d.mts +2 -0
  54. package/dist/reports/index.mjs +3 -0
  55. package/dist/repositories/index.d.mts +2 -0
  56. package/dist/repositories/index.mjs +3 -0
  57. package/dist/schemas/index.d.mts +2 -0
  58. package/dist/schemas/index.mjs +3 -0
  59. package/dist/session-Dh0s6zG4.mjs +87 -0
  60. package/dist/session-Dh0s6zG4.mjs.map +1 -0
  61. package/dist/universal-CMfrZ2hG.mjs +257 -0
  62. package/dist/universal-CMfrZ2hG.mjs.map +1 -0
  63. package/dist/universal-x33ZJODp.d.mts +137 -0
  64. package/dist/universal-x33ZJODp.d.mts.map +1 -0
  65. package/docs/country-packs.md +117 -0
  66. package/docs/engine.md +147 -0
  67. package/docs/exports.md +81 -0
  68. package/docs/money.md +81 -0
  69. package/docs/plugins.md +136 -0
  70. package/docs/reports.md +154 -0
  71. package/docs/repositories.md +239 -0
  72. package/docs/schemas.md +146 -0
  73. package/docs/subledger-integration.md +287 -0
  74. package/package.json +116 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Classytic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # @classytic/ledger
2
+
3
+ Production-grade double-entry accounting engine for MongoDB. Built on [@classytic/mongokit](../mongokit). Designed for multi-tenant SaaS, AI-powered finance, and global tax compliance.
4
+
5
+ ## Features
6
+
7
+ - **Double-entry bookkeeping** with balance validation and posted-entry protection (optionally immutable via strictness config)
8
+ - **Multi-tenant** isolation via configurable org field
9
+ - **Country packs** for localized chart of accounts and tax codes
10
+ - **Financial reports** — trial balance, balance sheet, income statement, general ledger, cash flow
11
+ - **Fiscal period management** — close and reopen with automatic year-end entries, overlap protection
12
+ - **CSV export** — QuickBooks-compatible and universal field maps
13
+ - **Cents-based Money** arithmetic for precision
14
+ - **Plugin system** — fiscal lock, double-entry validation, idempotency (via mongokit hooks)
15
+ - **Dimension fields** — custom fields on journal items (departmentId, projectId, etc.) preserved through all workflows
16
+ - **Dimension filters** — filter all reports by custom journal item fields
17
+ - **Strictness controls** — configurable immutability, actor tracking, and approval requirements
18
+ - **Subledger contracts** — typed interfaces for integrating billing, inventory, payroll, and other subledgers
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ npm install @classytic/ledger @classytic/mongokit mongoose
24
+ ```
25
+
26
+ ## Quick Start
27
+
28
+ ```typescript
29
+ import { createAccountingEngine } from '@classytic/ledger';
30
+ import { canadaPack } from '@classytic/ledger-ca';
31
+ import mongoose from 'mongoose';
32
+
33
+ // 1. Create engine
34
+ const accounting = createAccountingEngine({
35
+ country: canadaPack,
36
+ currency: 'CAD',
37
+ multiTenant: { orgField: 'business', orgRef: 'Business' },
38
+ audit: { trackActor: true },
39
+ idempotency: true,
40
+ strictness: { immutable: true, requireActor: true },
41
+ });
42
+
43
+ // 2. Create schemas & models
44
+ const Account = mongoose.model('Account', accounting.createAccountSchema());
45
+ const JournalEntry = mongoose.model('JournalEntry', accounting.createJournalEntrySchema('Account', {
46
+ extraItemFields: {
47
+ departmentId: { type: mongoose.Schema.Types.ObjectId, ref: 'Department' },
48
+ },
49
+ }));
50
+ const FiscalPeriod = mongoose.model('FiscalPeriod', accounting.createFiscalPeriodSchema());
51
+
52
+ // 3. Wire repositories (adds post, reverse, duplicate, unpost, seedAccounts, bulkCreate)
53
+ import { createRepository } from '@classytic/mongokit';
54
+
55
+ const journalRepo = accounting.createJournalEntryRepository(
56
+ createRepository,
57
+ { JournalEntryModel: JournalEntry, AccountModel: Account, FiscalPeriodModel: FiscalPeriod },
58
+ );
59
+
60
+ const accountRepo = createRepository(Account, []);
61
+ accounting.wireAccountRepository(accountRepo, Account);
62
+
63
+ // 4. Generate reports (with optional dimension filters)
64
+ const reports = accounting.createReports({ Account, JournalEntry });
65
+ const bs = await reports.balanceSheet({
66
+ organizationId: orgId,
67
+ dateOption: 'year',
68
+ dateValue: 2025,
69
+ filters: { 'journalItems.departmentId': deptId },
70
+ });
71
+ ```
72
+
73
+ ## Subpath Exports
74
+
75
+ | Import path | Contents |
76
+ |---|---|
77
+ | `@classytic/ledger` | Engine, Money, schemas, plugins, reports, repositories, constants, types |
78
+ | `@classytic/ledger/money` | `Money` class (cents-based arithmetic) |
79
+ | `@classytic/ledger/schemas` | `createAccountSchema`, `createJournalEntrySchema`, `createFiscalPeriodSchema` |
80
+ | `@classytic/ledger/reports` | Report generators (trial balance, balance sheet, etc.) |
81
+ | `@classytic/ledger/plugins` | `doubleEntryPlugin`, `fiscalLockPlugin`, `idempotencyPlugin` |
82
+ | `@classytic/ledger/repositories` | `wireJournalEntryMethods`, `wireAccountMethods` |
83
+ | `@classytic/ledger/exports` | CSV export: `exportToCsv`, `flattenJournalEntries`, field maps |
84
+ | `@classytic/ledger/constants` | Categories, journal types, currencies |
85
+ | `@classytic/ledger/country` | `defineCountryPack`, `CountryPack` interface |
86
+
87
+ ## Documentation
88
+
89
+ - [Engine & Configuration](docs/engine.md)
90
+ - [Schemas](docs/schemas.md)
91
+ - [Repositories](docs/repositories.md)
92
+ - [Reports](docs/reports.md)
93
+ - [Plugins](docs/plugins.md)
94
+ - [Exports](docs/exports.md)
95
+ - [Country Packs](docs/country-packs.md)
96
+ - [Money](docs/money.md)
97
+ - [Subledger Integration](docs/subledger-integration.md)
98
+
99
+ ## Requirements
100
+
101
+ - Node.js >= 22
102
+ - MongoDB (replica set recommended for transactions)
103
+ - Mongoose >= 9
104
+ - @classytic/mongokit >= 3.3.2
105
+
106
+ ## License
107
+
108
+ MIT
@@ -0,0 +1,29 @@
1
+ import { CountryPack } from "./country/index.mjs";
2
+ import { o as StrictnessConfig } from "./engine-Cd73EOT6.mjs";
3
+ import { Model } from "mongoose";
4
+
5
+ //#region src/repositories/journal-entry.repository.d.ts
6
+ /**
7
+ * Wire post/reverse onto an existing mongokit Repository.
8
+ *
9
+ * @param repository - A mongokit Repository instance (already created)
10
+ * @param JournalEntryModel - The Mongoose model for journal entries
11
+ * @param orgField - The multi-tenant field name (e.g. 'business')
12
+ * @param strictness - Strictness rules (immutable, requireActor, requireApproval)
13
+ */
14
+ declare function wireJournalEntryMethods(repository: any, JournalEntryModel: Model<unknown>, orgField?: string, strictness?: StrictnessConfig): void;
15
+ //#endregion
16
+ //#region src/repositories/account.repository.d.ts
17
+ /**
18
+ * Wire seedAccounts, bulkCreate and posting-account validation
19
+ * onto an existing mongokit Repository.
20
+ *
21
+ * @param repository - A mongokit Repository instance (already created)
22
+ * @param AccountModel - The Mongoose model for accounts
23
+ * @param country - The CountryPack for account type lookups
24
+ * @param orgField - The multi-tenant field name (e.g. 'business')
25
+ */
26
+ declare function wireAccountMethods(repository: any, AccountModel: Model<unknown>, country: CountryPack, orgField?: string): void;
27
+ //#endregion
28
+ export { wireJournalEntryMethods as n, wireAccountMethods as t };
29
+ //# sourceMappingURL=account.repository-1C2sZvB2.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"account.repository-1C2sZvB2.d.mts","names":[],"sources":["../src/repositories/journal-entry.repository.ts","../src/repositories/account.repository.ts"],"mappings":";;;;;;;;;;;;;iBAgEgB,uBAAA,CAEd,UAAA,OACA,iBAAA,EAAmB,KAAA,WACnB,QAAA,WACA,UAAA,GAAa,gBAAA;;;;;;;;;;;;iBC3CC,kBAAA,CAEd,UAAA,OACA,YAAA,EAAc,KAAA,WACd,OAAA,EAAS,WAAA,EACT,QAAA"}
@@ -0,0 +1,393 @@
1
+ import { n as Errors } from "./errors-CeqRahE-.mjs";
2
+ import { i as requireOrgScope, n as finalizeSession, t as acquireSession } from "./session-Dh0s6zG4.mjs";
3
+
4
+ //#region src/repositories/journal-entry.repository.ts
5
+ /** Keys that are either handled explicitly or must not be copied */
6
+ const ITEM_CORE_KEYS = new Set([
7
+ "account",
8
+ "debit",
9
+ "credit",
10
+ "label",
11
+ "date",
12
+ "taxDetails",
13
+ "_id",
14
+ "id"
15
+ ]);
16
+ /**
17
+ * Wire post/reverse onto an existing mongokit Repository.
18
+ *
19
+ * @param repository - A mongokit Repository instance (already created)
20
+ * @param JournalEntryModel - The Mongoose model for journal entries
21
+ * @param orgField - The multi-tenant field name (e.g. 'business')
22
+ * @param strictness - Strictness rules (immutable, requireActor, requireApproval)
23
+ */
24
+ function wireJournalEntryMethods(repository, JournalEntryModel, orgField, strictness) {
25
+ /**
26
+ * Post an entry (draft → posted).
27
+ * Validates items, balance, and accounts before changing state.
28
+ */
29
+ repository.post = async function(id, orgId, options = {}) {
30
+ if (strictness?.requireActor && !options.actorId) throw Errors.validation("actorId is required for post operations.");
31
+ requireOrgScope(orgField, orgId);
32
+ const query = { _id: id };
33
+ if (orgField && orgId != null) query[orgField] = orgId;
34
+ const entry = await JournalEntryModel.findOne(query).populate("journalItems.account").session(options.session || null);
35
+ if (!entry) throw Errors.notFound("Entry not found");
36
+ if (entry.idempotencyKey && entry.state === "posted") return entry;
37
+ if (entry.state !== "draft") throw Errors.validation("Only draft entries can be posted");
38
+ if (strictness?.requireApproval) {
39
+ if (!entry.approvedBy || !entry.approvedAt) throw Errors.validation("Entry must be approved before posting. Both approvedBy and approvedAt are required.");
40
+ }
41
+ if (!entry.journalItems || entry.journalItems.length < 2) throw Errors.validation("Journal entry must have at least 2 items to post");
42
+ const missing = entry.journalItems.filter((i) => !i.account || i.account === "");
43
+ if (missing.length > 0) throw Errors.validation(`${missing.length} item(s) missing an account`);
44
+ if (orgField && orgId != null) {
45
+ const crossTenant = entry.journalItems.filter((i) => {
46
+ const acct = i.account;
47
+ if (!acct || typeof acct !== "object") return false;
48
+ return String(acct[orgField]) !== String(orgId);
49
+ });
50
+ if (crossTenant.length > 0) throw Errors.validation(`${crossTenant.length} item(s) reference accounts from another organization`);
51
+ }
52
+ const zeroed = entry.journalItems.filter((i) => (i.debit || 0) === 0 && (i.credit || 0) === 0);
53
+ if (zeroed.length > 0) throw Errors.validation(`${zeroed.length} item(s) have both debit and credit as zero`);
54
+ const bothSet = entry.journalItems.filter((i) => (i.debit || 0) > 0 && (i.credit || 0) > 0);
55
+ if (bothSet.length > 0) throw Errors.validation(`${bothSet.length} item(s) have both debit and credit set — each line must be debit OR credit, not both`);
56
+ const totalDebit = entry.journalItems.reduce((s, i) => s + (i.debit || 0), 0);
57
+ const totalCredit = entry.journalItems.reduce((s, i) => s + (i.credit || 0), 0);
58
+ if (totalDebit !== totalCredit) throw Errors.validation(`Entry is not balanced. Debit: ${totalDebit}, Credit: ${totalCredit}`);
59
+ entry.state = "posted";
60
+ entry.stateChangedAt = /* @__PURE__ */ new Date();
61
+ if (options.actorId) entry.postedBy = options.actorId;
62
+ await entry.save({ session: options.session });
63
+ return entry;
64
+ };
65
+ /**
66
+ * Unpost an entry (posted → draft).
67
+ * Resets state to draft so the entry can be edited and re-posted.
68
+ * Also clears the reversed flag if set, allowing full re-editing.
69
+ */
70
+ repository.unpost = async function(id, orgId, options = {}) {
71
+ if (strictness?.immutable) throw Errors.immutable("Unpost is disabled in strict mode. Use reverse() to correct posted entries.");
72
+ if (strictness?.requireActor && !options.actorId) throw Errors.validation("actorId is required for unpost operations.");
73
+ requireOrgScope(orgField, orgId);
74
+ const query = { _id: id };
75
+ if (orgField && orgId != null) query[orgField] = orgId;
76
+ const entry = await JournalEntryModel.findOne(query).session(options.session || null);
77
+ if (!entry) throw Errors.notFound("Entry not found");
78
+ if (entry.state !== "posted") throw Errors.validation("Only posted entries can be unposted");
79
+ entry.state = "draft";
80
+ entry.stateChangedAt = /* @__PURE__ */ new Date();
81
+ if (entry.reversed) {
82
+ entry.reversed = false;
83
+ entry.reversedBy = void 0;
84
+ }
85
+ await entry.save({ session: options.session });
86
+ return entry;
87
+ };
88
+ /**
89
+ * Archive a draft entry (draft → archived).
90
+ * Used to discard unneeded drafts without deleting them, preserving audit trail.
91
+ * Only draft entries can be archived. Posted entries must be reversed instead.
92
+ */
93
+ repository.archive = async function(id, orgId, options = {}) {
94
+ if (strictness?.requireActor && !options.actorId) throw Errors.validation("actorId is required for archive operations.");
95
+ requireOrgScope(orgField, orgId);
96
+ const query = { _id: id };
97
+ if (orgField && orgId != null) query[orgField] = orgId;
98
+ const entry = await JournalEntryModel.findOne(query).session(options.session || null);
99
+ if (!entry) throw Errors.notFound("Entry not found");
100
+ if (entry.state !== "draft") throw Errors.validation("Only draft entries can be archived");
101
+ entry.state = "archived";
102
+ entry.stateChangedAt = /* @__PURE__ */ new Date();
103
+ await entry.save({ session: options.session });
104
+ return entry;
105
+ };
106
+ /**
107
+ * Duplicate an entry as a new draft.
108
+ * Copies journal items, journal type, and label. Assigns today's date.
109
+ */
110
+ repository.duplicate = async function(id, orgId, options = {}) {
111
+ requireOrgScope(orgField, orgId);
112
+ const query = { _id: id };
113
+ if (orgField && orgId != null) query[orgField] = orgId;
114
+ const entry = await JournalEntryModel.findOne(query).session(options.session || null);
115
+ if (!entry) throw Errors.notFound("Entry not found");
116
+ const duplicateData = {
117
+ journalType: entry.journalType,
118
+ state: "draft",
119
+ date: /* @__PURE__ */ new Date(),
120
+ label: entry.label ? `Copy of ${entry.label}` : "Duplicated entry",
121
+ journalItems: entry.journalItems.map((item) => {
122
+ const accountId = typeof item.account === "object" && item.account !== null ? item.account._id : item.account;
123
+ const extra = {};
124
+ for (const key of Object.keys(item)) if (!ITEM_CORE_KEYS.has(key)) extra[key] = item[key];
125
+ return {
126
+ ...extra,
127
+ account: accountId,
128
+ debit: item.debit ?? 0,
129
+ credit: item.credit ?? 0,
130
+ label: item.label,
131
+ date: /* @__PURE__ */ new Date(),
132
+ taxDetails: item.taxDetails ?? []
133
+ };
134
+ })
135
+ };
136
+ if (orgField && entry[orgField] != null) duplicateData[orgField] = entry[orgField];
137
+ return await repository.create(duplicateData, options.session ? { session: options.session } : {});
138
+ };
139
+ /**
140
+ * Reverse a posted entry by creating a mirror entry with flipped debits/credits.
141
+ * Marks the original as reversed and links both entries bidirectionally.
142
+ *
143
+ * Atomic: creates an internal transaction by default. Pass an external session
144
+ * to join a caller-managed transaction instead. On standalone MongoDB (no
145
+ * replica set), falls back to non-atomic execution with a warning.
146
+ *
147
+ * Routes the reversal through repository.create() so all plugins (fiscal-lock,
148
+ * double-entry) enforce policy on the reversal entry.
149
+ */
150
+ repository.reverse = async function(id, orgId, options = {}) {
151
+ if (strictness?.requireActor && !options.actorId) throw Errors.validation("actorId is required for reverse operations.");
152
+ requireOrgScope(orgField, orgId);
153
+ const { session, ownSession } = await acquireSession(JournalEntryModel.db, options.session);
154
+ let success = false;
155
+ try {
156
+ const query = { _id: id };
157
+ if (orgField && orgId != null) query[orgField] = orgId;
158
+ const entry = await JournalEntryModel.findOne(query).populate("journalItems.account").session(session || null);
159
+ if (!entry) throw Errors.notFound("Entry not found");
160
+ if (entry.state !== "posted") throw Errors.validation("Only posted entries can be reversed");
161
+ if (entry.reversed) throw Errors.validation("Entry has already been reversed");
162
+ const reversalItems = entry.journalItems.map((item) => {
163
+ const accountId = typeof item.account === "object" && item.account !== null ? item.account._id : item.account;
164
+ const extra = {};
165
+ for (const key of Object.keys(item)) if (!ITEM_CORE_KEYS.has(key)) extra[key] = item[key];
166
+ return {
167
+ ...extra,
168
+ account: accountId,
169
+ debit: item.credit ?? 0,
170
+ credit: item.debit ?? 0,
171
+ label: item.label ? `Reversal: ${item.label}` : void 0,
172
+ date: item.date,
173
+ taxDetails: item.taxDetails ?? []
174
+ };
175
+ });
176
+ const totalDebit = reversalItems.reduce((s, i) => s + i.debit, 0);
177
+ const totalCredit = reversalItems.reduce((s, i) => s + i.credit, 0);
178
+ const reversalData = {
179
+ journalType: entry.journalType ?? "MISC",
180
+ state: "posted",
181
+ date: options.reversalDate ?? /* @__PURE__ */ new Date(),
182
+ label: `Reversal of ${entry.referenceNumber ?? entry._id}`,
183
+ journalItems: reversalItems,
184
+ totalDebit,
185
+ totalCredit,
186
+ reversalOf: entry._id,
187
+ stateChangedAt: /* @__PURE__ */ new Date()
188
+ };
189
+ if (orgField && entry[orgField] != null) reversalData[orgField] = entry[orgField];
190
+ if (options.actorId) reversalData.postedBy = options.actorId;
191
+ const reversalEntry = await repository.create(reversalData, session ? { session } : {});
192
+ entry.reversed = true;
193
+ entry.reversedBy = reversalEntry._id;
194
+ if (options.actorId) entry.reversedByUser = options.actorId;
195
+ await entry.save({ session });
196
+ success = true;
197
+ return {
198
+ original: entry,
199
+ reversal: reversalEntry
200
+ };
201
+ } finally {
202
+ await finalizeSession(session, ownSession, success);
203
+ }
204
+ };
205
+ }
206
+
207
+ //#endregion
208
+ //#region src/repositories/account.repository.ts
209
+ /**
210
+ * Wire seedAccounts, bulkCreate and posting-account validation
211
+ * onto an existing mongokit Repository.
212
+ *
213
+ * @param repository - A mongokit Repository instance (already created)
214
+ * @param AccountModel - The Mongoose model for accounts
215
+ * @param country - The CountryPack for account type lookups
216
+ * @param orgField - The multi-tenant field name (e.g. 'business')
217
+ */
218
+ function wireAccountMethods(repository, AccountModel, country, orgField) {
219
+ repository.on("before:create", (ctx) => {
220
+ const code = ctx.data?.accountTypeCode;
221
+ if (code && !country.isPostingAccount(code)) throw Errors.validation(`Cannot create account with type "${code}" — it is a structural group or calculated total, not a posting account.`);
222
+ });
223
+ /**
224
+ * Seed standard posting accounts for an organization.
225
+ */
226
+ repository.seedAccounts = async function(orgId, options = {}) {
227
+ requireOrgScope(orgField, orgId);
228
+ const postingTypes = country.getPostingAccountTypes();
229
+ const filter = {};
230
+ if (orgField && orgId != null) filter[orgField] = orgId;
231
+ const existing = await AccountModel.find(filter).select("accountNumber").lean();
232
+ const existingNumbers = new Set(existing.map((a) => a.accountNumber));
233
+ const toCreate = postingTypes.filter((at) => !existingNumbers.has(at.code)).map((at) => {
234
+ const doc = {
235
+ accountTypeCode: at.code,
236
+ accountNumber: at.code,
237
+ name: at.name
238
+ };
239
+ if (orgField && orgId != null) doc[orgField] = orgId;
240
+ return doc;
241
+ });
242
+ if (toCreate.length === 0) return {
243
+ created: 0,
244
+ skipped: existingNumbers.size
245
+ };
246
+ try {
247
+ return {
248
+ created: (await AccountModel.insertMany(toCreate, {
249
+ session: options.session ?? void 0,
250
+ ordered: false
251
+ })).length,
252
+ skipped: existingNumbers.size
253
+ };
254
+ } catch (err) {
255
+ const bulkError = err;
256
+ if (bulkError.code === 11e3 || bulkError.writeErrors) {
257
+ const insertedDocs = bulkError.insertedDocs ?? [];
258
+ return {
259
+ created: insertedDocs.length,
260
+ skipped: existingNumbers.size + (toCreate.length - insertedDocs.length)
261
+ };
262
+ }
263
+ throw err;
264
+ }
265
+ };
266
+ /**
267
+ * Bulk create accounts with validation and skip-if-exists logic.
268
+ *
269
+ * Uses a single batch query to check existing accounts (instead of N+1),
270
+ * and ordered: false on insertMany to handle concurrent race conditions
271
+ * gracefully (duplicate key errors on individual docs don't abort the batch).
272
+ */
273
+ repository.bulkCreate = async function(accounts, orgId) {
274
+ requireOrgScope(orgField, orgId);
275
+ const results = {
276
+ created: [],
277
+ skipped: [],
278
+ errors: []
279
+ };
280
+ const validAccounts = [];
281
+ for (let i = 0; i < accounts.length; i++) {
282
+ const { accountTypeCode, accountNumber, name, active = true, isCashAccount = false } = accounts[i];
283
+ if (!accountTypeCode) {
284
+ results.errors.push({
285
+ index: i,
286
+ reason: "accountTypeCode is required"
287
+ });
288
+ continue;
289
+ }
290
+ const at = country.getAccountType(accountTypeCode);
291
+ if (!at) {
292
+ results.errors.push({
293
+ index: i,
294
+ accountTypeCode,
295
+ reason: "Invalid account type code"
296
+ });
297
+ continue;
298
+ }
299
+ if (!country.isPostingAccount(accountTypeCode)) {
300
+ results.errors.push({
301
+ index: i,
302
+ accountTypeCode,
303
+ reason: `Not a posting account (${at.isGroup ? "group" : "total"})`
304
+ });
305
+ continue;
306
+ }
307
+ const resolvedNumber = accountNumber ?? accountTypeCode;
308
+ const resolvedName = name ?? at.name ?? accountTypeCode;
309
+ validAccounts.push({
310
+ index: i,
311
+ accountTypeCode,
312
+ accountNumber: resolvedNumber,
313
+ name: resolvedName,
314
+ active: Boolean(active),
315
+ isCashAccount: Boolean(isCashAccount)
316
+ });
317
+ }
318
+ if (validAccounts.length === 0) return {
319
+ summary: {
320
+ total: accounts.length,
321
+ created: 0,
322
+ skipped: results.skipped.length,
323
+ errors: results.errors.length
324
+ },
325
+ ...results
326
+ };
327
+ const existsFilter = { accountNumber: { $in: validAccounts.map((a) => a.accountNumber) } };
328
+ if (orgField && orgId != null) existsFilter[orgField] = orgId;
329
+ const existingDocs = await AccountModel.find(existsFilter).select("accountNumber").lean();
330
+ const existingNumbers = new Set(existingDocs.map((d) => d.accountNumber));
331
+ const toCreate = [];
332
+ for (const item of validAccounts) if (existingNumbers.has(item.accountNumber)) results.skipped.push({
333
+ index: item.index,
334
+ accountTypeCode: item.accountTypeCode,
335
+ reason: "Already exists"
336
+ });
337
+ else toCreate.push(item);
338
+ if (toCreate.length > 0) {
339
+ const docs = toCreate.map((item) => {
340
+ const doc = {
341
+ accountTypeCode: item.accountTypeCode,
342
+ accountNumber: item.accountNumber,
343
+ name: item.name,
344
+ active: item.active,
345
+ isCashAccount: item.isCashAccount
346
+ };
347
+ if (orgField && orgId != null) doc[orgField] = orgId;
348
+ return doc;
349
+ });
350
+ try {
351
+ const inserted = await AccountModel.insertMany(docs, { ordered: false });
352
+ results.created = toCreate.map((item, idx) => ({
353
+ accountTypeCode: item.accountTypeCode,
354
+ active: item.active,
355
+ isCashAccount: item.isCashAccount,
356
+ _id: inserted[idx]._id
357
+ }));
358
+ } catch (err) {
359
+ const bulkError = err;
360
+ if (bulkError.code === 11e3 || bulkError.writeErrors) {
361
+ const insertedDocs = bulkError.insertedDocs ?? [];
362
+ const insertedNumbers = new Set(insertedDocs.map((d) => d.accountNumber));
363
+ for (const item of toCreate) if (insertedNumbers.has(item.accountNumber)) {
364
+ const iDoc = insertedDocs.find((d) => d.accountNumber === item.accountNumber);
365
+ results.created.push({
366
+ accountTypeCode: item.accountTypeCode,
367
+ active: item.active,
368
+ isCashAccount: item.isCashAccount,
369
+ _id: iDoc?._id
370
+ });
371
+ } else results.skipped.push({
372
+ index: item.index,
373
+ accountTypeCode: item.accountTypeCode,
374
+ reason: "Already exists (concurrent insert)"
375
+ });
376
+ } else throw err;
377
+ }
378
+ }
379
+ return {
380
+ summary: {
381
+ total: accounts.length,
382
+ created: results.created.length,
383
+ skipped: results.skipped.length,
384
+ errors: results.errors.length
385
+ },
386
+ ...results
387
+ };
388
+ };
389
+ }
390
+
391
+ //#endregion
392
+ export { wireJournalEntryMethods as n, wireAccountMethods as t };
393
+ //# sourceMappingURL=account.repository-Crf5DGO4.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"account.repository-Crf5DGO4.mjs","names":[],"sources":["../src/repositories/journal-entry.repository.ts","../src/repositories/account.repository.ts"],"sourcesContent":["/**\r\n * Journal Entry Repository Factory\r\n *\r\n * Creates a mongokit Repository with post/reverse domain logic baked in.\r\n * Used by AccountingEngine.createJournalEntryRepository().\r\n */\r\n\r\nimport type { Model, ClientSession } from 'mongoose';\r\nimport type { StrictnessConfig } from '../types/engine.js';\r\nimport { Errors } from '../utils/errors.js';\r\nimport { requireOrgScope } from '../utils/tenant-guard.js';\r\nimport { acquireSession, finalizeSession } from '../utils/session.js';\r\n\r\ninterface PostOptions {\r\n session?: ClientSession | null;\r\n /** Actor performing this operation (required when strictness.requireActor is enabled) */\r\n actorId?: unknown;\r\n}\r\n\r\ninterface JournalItem {\r\n account?: unknown;\r\n debit?: number;\r\n credit?: number;\r\n}\r\n\r\ninterface JournalItemWithLabel extends JournalItem {\r\n label?: string;\r\n date?: Date;\r\n taxDetails?: unknown[];\r\n [key: string]: unknown;\r\n}\r\n\r\n/** Keys that are either handled explicitly or must not be copied */\r\nconst ITEM_CORE_KEYS = new Set(['account', 'debit', 'credit', 'label', 'date', 'taxDetails', '_id', 'id']);\r\n\r\ninterface JournalEntryDoc {\r\n _id: unknown;\r\n state: string;\r\n stateChangedAt?: Date;\r\n journalType?: string;\r\n referenceNumber?: string;\r\n label?: string;\r\n date?: Date;\r\n reversed?: boolean;\r\n reversedBy?: unknown;\r\n reversalOf?: unknown;\r\n journalItems: JournalItemWithLabel[];\r\n save(options?: { session?: ClientSession | null }): Promise<this>;\r\n [key: string]: unknown;\r\n}\r\n\r\ninterface ReverseOptions extends PostOptions {\r\n /** Date for the reversal entry (defaults to now) */\r\n reversalDate?: Date;\r\n}\r\n\r\n/**\r\n * Wire post/reverse onto an existing mongokit Repository.\r\n *\r\n * @param repository - A mongokit Repository instance (already created)\r\n * @param JournalEntryModel - The Mongoose model for journal entries\r\n * @param orgField - The multi-tenant field name (e.g. 'business')\r\n * @param strictness - Strictness rules (immutable, requireActor, requireApproval)\r\n */\r\nexport function wireJournalEntryMethods(\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n repository: any,\r\n JournalEntryModel: Model<unknown>,\r\n orgField?: string,\r\n strictness?: StrictnessConfig,\r\n): void {\r\n /**\r\n * Post an entry (draft → posted).\r\n * Validates items, balance, and accounts before changing state.\r\n */\r\n repository.post = async function (id: unknown, orgId?: unknown, options: PostOptions = {}) {\r\n if (strictness?.requireActor && !options.actorId) {\r\n throw Errors.validation('actorId is required for post operations.');\r\n }\r\n requireOrgScope(orgField, orgId);\r\n const query: Record<string, unknown> = { _id: id };\r\n if (orgField && orgId != null) query[orgField] = orgId;\r\n\r\n const entry = (await JournalEntryModel.findOne(query)\r\n .populate('journalItems.account')\r\n .session(options.session || null)) as JournalEntryDoc | null;\r\n\r\n if (!entry) {\r\n throw Errors.notFound('Entry not found');\r\n }\r\n\r\n // Idempotency: if already posted with same idempotency key, return as-is\r\n if (entry.idempotencyKey && entry.state === 'posted') {\r\n return entry;\r\n }\r\n\r\n if (entry.state !== 'draft') {\r\n throw Errors.validation('Only draft entries can be posted');\r\n }\r\n\r\n // Approval requirement — both approvedBy and approvedAt must be set\r\n if (strictness?.requireApproval) {\r\n if (!entry.approvedBy || !entry.approvedAt) {\r\n throw Errors.validation('Entry must be approved before posting. Both approvedBy and approvedAt are required.');\r\n }\r\n }\r\n\r\n // Must have >= 2 items\r\n if (!entry.journalItems || entry.journalItems.length < 2) {\r\n throw Errors.validation('Journal entry must have at least 2 items to post');\r\n }\r\n\r\n // Every item must have a valid account\r\n const missing = entry.journalItems.filter((i: JournalItem) => !i.account || i.account === '');\r\n if (missing.length > 0) {\r\n throw Errors.validation(`${missing.length} item(s) missing an account`);\r\n }\r\n\r\n // Verify all populated accounts belong to the same org (multi-tenant integrity)\r\n if (orgField && orgId != null) {\r\n const crossTenant = entry.journalItems.filter((i: JournalItem) => {\r\n const acct = i.account as Record<string, unknown> | null;\r\n if (!acct || typeof acct !== 'object') return false;\r\n return String(acct[orgField]) !== String(orgId);\r\n });\r\n if (crossTenant.length > 0) {\r\n throw Errors.validation(\r\n `${crossTenant.length} item(s) reference accounts from another organization`,\r\n );\r\n }\r\n }\r\n\r\n // Every item must have debit or credit > 0\r\n const zeroed = entry.journalItems.filter((i: JournalItem) => (i.debit || 0) === 0 && (i.credit || 0) === 0);\r\n if (zeroed.length > 0) {\r\n throw Errors.validation(`${zeroed.length} item(s) have both debit and credit as zero`);\r\n }\r\n\r\n // Each line must be debit OR credit, not both\r\n const bothSet = entry.journalItems.filter((i: JournalItem) => (i.debit || 0) > 0 && (i.credit || 0) > 0);\r\n if (bothSet.length > 0) {\r\n throw Errors.validation(\r\n `${bothSet.length} item(s) have both debit and credit set — each line must be debit OR credit, not both`,\r\n );\r\n }\r\n\r\n // Must be balanced — integer cents, exact comparison\r\n const totalDebit = entry.journalItems.reduce((s: number, i: JournalItem) => s + (i.debit || 0), 0);\r\n const totalCredit = entry.journalItems.reduce((s: number, i: JournalItem) => s + (i.credit || 0), 0);\r\n if (totalDebit !== totalCredit) {\r\n throw Errors.validation(\r\n `Entry is not balanced. Debit: ${totalDebit}, Credit: ${totalCredit}`,\r\n );\r\n }\r\n\r\n entry.state = 'posted';\r\n entry.stateChangedAt = new Date();\r\n if (options.actorId) {\r\n entry.postedBy = options.actorId;\r\n }\r\n await entry.save({ session: options.session });\r\n\r\n return entry;\r\n };\r\n\r\n /**\r\n * Unpost an entry (posted → draft).\r\n * Resets state to draft so the entry can be edited and re-posted.\r\n * Also clears the reversed flag if set, allowing full re-editing.\r\n */\r\n repository.unpost = async function (id: unknown, orgId?: unknown, options: PostOptions = {}) {\r\n if (strictness?.immutable) {\r\n throw Errors.immutable('Unpost is disabled in strict mode. Use reverse() to correct posted entries.');\r\n }\r\n if (strictness?.requireActor && !options.actorId) {\r\n throw Errors.validation('actorId is required for unpost operations.');\r\n }\r\n requireOrgScope(orgField, orgId);\r\n const query: Record<string, unknown> = { _id: id };\r\n if (orgField && orgId != null) query[orgField] = orgId;\r\n\r\n const entry = (await JournalEntryModel.findOne(query)\r\n .session(options.session || null)) as JournalEntryDoc | null;\r\n\r\n if (!entry) {\r\n throw Errors.notFound('Entry not found');\r\n }\r\n if (entry.state !== 'posted') {\r\n throw Errors.validation('Only posted entries can be unposted');\r\n }\r\n\r\n entry.state = 'draft';\r\n entry.stateChangedAt = new Date();\r\n // Clear reversal flags so the entry is fully editable as a draft\r\n if (entry.reversed) {\r\n entry.reversed = false;\r\n entry.reversedBy = undefined;\r\n }\r\n await entry.save({ session: options.session });\r\n\r\n return entry;\r\n };\r\n\r\n /**\r\n * Archive a draft entry (draft → archived).\r\n * Used to discard unneeded drafts without deleting them, preserving audit trail.\r\n * Only draft entries can be archived. Posted entries must be reversed instead.\r\n */\r\n repository.archive = async function (id: unknown, orgId?: unknown, options: PostOptions = {}) {\r\n if (strictness?.requireActor && !options.actorId) {\r\n throw Errors.validation('actorId is required for archive operations.');\r\n }\r\n requireOrgScope(orgField, orgId);\r\n const query: Record<string, unknown> = { _id: id };\r\n if (orgField && orgId != null) query[orgField] = orgId;\r\n\r\n const entry = (await JournalEntryModel.findOne(query)\r\n .session(options.session || null)) as JournalEntryDoc | null;\r\n\r\n if (!entry) {\r\n throw Errors.notFound('Entry not found');\r\n }\r\n if (entry.state !== 'draft') {\r\n throw Errors.validation('Only draft entries can be archived');\r\n }\r\n\r\n entry.state = 'archived';\r\n entry.stateChangedAt = new Date();\r\n await entry.save({ session: options.session });\r\n\r\n return entry;\r\n };\r\n\r\n /**\r\n * Duplicate an entry as a new draft.\r\n * Copies journal items, journal type, and label. Assigns today's date.\r\n */\r\n repository.duplicate = async function (id: unknown, orgId?: unknown, options: PostOptions = {}) {\r\n requireOrgScope(orgField, orgId);\r\n const query: Record<string, unknown> = { _id: id };\r\n if (orgField && orgId != null) query[orgField] = orgId;\r\n\r\n const entry = (await JournalEntryModel.findOne(query)\r\n .session(options.session || null)) as JournalEntryDoc | null;\r\n\r\n if (!entry) {\r\n throw Errors.notFound('Entry not found');\r\n }\r\n\r\n const duplicateData: Record<string, unknown> = {\r\n journalType: entry.journalType,\r\n state: 'draft',\r\n date: new Date(),\r\n label: entry.label ? `Copy of ${entry.label}` : 'Duplicated entry',\r\n journalItems: entry.journalItems.map((item: JournalItemWithLabel) => {\r\n const accountId = typeof item.account === 'object' && item.account !== null\r\n ? (item.account as Record<string, unknown>)._id\r\n : item.account;\r\n\r\n // Preserve dimension/extra fields (departmentId, projectId, locationId, etc.)\r\n const extra: Record<string, unknown> = {};\r\n for (const key of Object.keys(item)) {\r\n if (!ITEM_CORE_KEYS.has(key)) extra[key] = item[key];\r\n }\r\n\r\n return {\r\n ...extra,\r\n account: accountId,\r\n debit: item.debit ?? 0,\r\n credit: item.credit ?? 0,\r\n label: item.label,\r\n date: new Date(),\r\n taxDetails: item.taxDetails ?? [],\r\n };\r\n }),\r\n };\r\n\r\n // Carry over org field\r\n if (orgField && entry[orgField] != null) {\r\n duplicateData[orgField] = entry[orgField];\r\n }\r\n\r\n const duplicated = await repository.create(duplicateData, options.session ? { session: options.session } : {});\r\n return duplicated;\r\n };\r\n\r\n /**\r\n * Reverse a posted entry by creating a mirror entry with flipped debits/credits.\r\n * Marks the original as reversed and links both entries bidirectionally.\r\n *\r\n * Atomic: creates an internal transaction by default. Pass an external session\r\n * to join a caller-managed transaction instead. On standalone MongoDB (no\r\n * replica set), falls back to non-atomic execution with a warning.\r\n *\r\n * Routes the reversal through repository.create() so all plugins (fiscal-lock,\r\n * double-entry) enforce policy on the reversal entry.\r\n */\r\n repository.reverse = async function (id: unknown, orgId?: unknown, options: ReverseOptions = {}) {\r\n if (strictness?.requireActor && !options.actorId) {\r\n throw Errors.validation('actorId is required for reverse operations.');\r\n }\r\n requireOrgScope(orgField, orgId);\r\n const { session, ownSession } = await acquireSession(\r\n JournalEntryModel.db,\r\n options.session,\r\n );\r\n let success = false;\r\n\r\n try {\r\n const query: Record<string, unknown> = { _id: id };\r\n if (orgField && orgId != null) query[orgField] = orgId;\r\n\r\n const entry = (await JournalEntryModel.findOne(query)\r\n .populate('journalItems.account')\r\n .session(session || null)) as JournalEntryDoc | null;\r\n\r\n if (!entry) {\r\n throw Errors.notFound('Entry not found');\r\n }\r\n if (entry.state !== 'posted') {\r\n throw Errors.validation('Only posted entries can be reversed');\r\n }\r\n if (entry.reversed) {\r\n throw Errors.validation('Entry has already been reversed');\r\n }\r\n\r\n // Build reversal items — swap debit ↔ credit for each line\r\n const reversalItems = entry.journalItems.map((item: JournalItemWithLabel) => {\r\n const accountId = typeof item.account === 'object' && item.account !== null\r\n ? (item.account as Record<string, unknown>)._id\r\n : item.account;\r\n\r\n // Preserve dimension/extra fields (departmentId, projectId, locationId, etc.)\r\n const extra: Record<string, unknown> = {};\r\n for (const key of Object.keys(item)) {\r\n if (!ITEM_CORE_KEYS.has(key)) extra[key] = item[key];\r\n }\r\n\r\n return {\r\n ...extra,\r\n account: accountId,\r\n debit: item.credit ?? 0,\r\n credit: item.debit ?? 0,\r\n label: item.label ? `Reversal: ${item.label}` : undefined,\r\n date: item.date,\r\n taxDetails: item.taxDetails ?? [],\r\n };\r\n });\r\n\r\n const totalDebit = reversalItems.reduce((s: number, i: { debit: number }) => s + i.debit, 0);\r\n const totalCredit = reversalItems.reduce((s: number, i: { credit: number }) => s + i.credit, 0);\r\n\r\n // Build reversal entry data\r\n const reversalData: Record<string, unknown> = {\r\n journalType: entry.journalType ?? 'MISC',\r\n state: 'posted',\r\n date: options.reversalDate ?? new Date(),\r\n label: `Reversal of ${entry.referenceNumber ?? entry._id}`,\r\n journalItems: reversalItems,\r\n totalDebit,\r\n totalCredit,\r\n reversalOf: entry._id,\r\n stateChangedAt: new Date(),\r\n };\r\n\r\n // Carry over org field\r\n if (orgField && entry[orgField] != null) {\r\n reversalData[orgField] = entry[orgField];\r\n }\r\n\r\n // Stamp actor on reversal entry\r\n if (options.actorId) {\r\n reversalData.postedBy = options.actorId;\r\n }\r\n\r\n // Create reversal via repository so plugins (fiscal-lock, double-entry) run\r\n const reversalEntry = await repository.create(\r\n reversalData,\r\n session ? { session } : {},\r\n );\r\n\r\n // Mark original as reversed (bidirectional link)\r\n entry.reversed = true;\r\n entry.reversedBy = reversalEntry._id;\r\n if (options.actorId) {\r\n entry.reversedByUser = options.actorId;\r\n }\r\n await entry.save({ session });\r\n\r\n success = true;\r\n return { original: entry, reversal: reversalEntry };\r\n } finally {\r\n await finalizeSession(session, ownSession, success);\r\n }\r\n };\r\n}\r\n","/**\r\n * Account Repository Factory\r\n *\r\n * Creates a mongokit Repository with seedAccounts/bulkCreate and\r\n * posting-account validation baked in.\r\n * Used by AccountingEngine.createAccountRepository().\r\n */\r\n\r\nimport type { Model, ClientSession } from 'mongoose';\r\nimport type { CountryPack } from '../country/index.js';\r\nimport { Errors } from '../utils/errors.js';\r\nimport { requireOrgScope } from '../utils/tenant-guard.js';\r\n\r\ninterface SeedOptions {\r\n session?: ClientSession | null;\r\n}\r\n\r\n/**\r\n * Wire seedAccounts, bulkCreate and posting-account validation\r\n * onto an existing mongokit Repository.\r\n *\r\n * @param repository - A mongokit Repository instance (already created)\r\n * @param AccountModel - The Mongoose model for accounts\r\n * @param country - The CountryPack for account type lookups\r\n * @param orgField - The multi-tenant field name (e.g. 'business')\r\n */\r\nexport function wireAccountMethods(\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n repository: any,\r\n AccountModel: Model<unknown>,\r\n country: CountryPack,\r\n orgField?: string,\r\n): void {\r\n // Validate posting accounts on create\r\n repository.on('before:create', (ctx: Record<string, unknown>) => {\r\n const data = ctx.data as Record<string, unknown> | undefined;\r\n const code = data?.accountTypeCode as string | undefined;\r\n if (code && !country.isPostingAccount(code)) {\r\n throw Errors.validation(\r\n `Cannot create account with type \"${code}\" — it is a structural group or calculated total, not a posting account.`,\r\n );\r\n }\r\n });\r\n\r\n /**\r\n * Seed standard posting accounts for an organization.\r\n */\r\n repository.seedAccounts = async function (orgId: unknown, options: SeedOptions = {}) {\r\n requireOrgScope(orgField, orgId);\r\n const postingTypes = country.getPostingAccountTypes();\r\n const filter: Record<string, unknown> = {};\r\n if (orgField && orgId != null) filter[orgField] = orgId;\r\n\r\n const existing = await AccountModel.find(filter).select('accountNumber').lean() as unknown as Array<{ accountNumber: string }>;\r\n const existingNumbers = new Set(existing.map(a => a.accountNumber));\r\n\r\n const toCreate = postingTypes\r\n .filter(at => !existingNumbers.has(at.code))\r\n .map(at => {\r\n const doc: Record<string, unknown> = {\r\n accountTypeCode: at.code,\r\n accountNumber: at.code,\r\n name: at.name,\r\n };\r\n if (orgField && orgId != null) doc[orgField] = orgId;\r\n return doc;\r\n });\r\n\r\n if (toCreate.length === 0) return { created: 0, skipped: existingNumbers.size };\r\n\r\n try {\r\n // ordered: false ensures a dup-key on one doc doesn't abort the rest\r\n // (handles concurrent seed calls hitting the unique accountNumber index)\r\n const inserted = await AccountModel.insertMany(toCreate, {\r\n session: options.session ?? undefined,\r\n ordered: false,\r\n });\r\n return { created: inserted.length, skipped: existingNumbers.size };\r\n } catch (err: unknown) {\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n const bulkError = err as any;\r\n if (bulkError.code === 11000 || bulkError.writeErrors) {\r\n // Partial success: some docs inserted, some hit dup-key from concurrent caller\r\n const insertedDocs = bulkError.insertedDocs ?? [];\r\n return {\r\n created: insertedDocs.length,\r\n skipped: existingNumbers.size + (toCreate.length - insertedDocs.length),\r\n };\r\n }\r\n throw err;\r\n }\r\n };\r\n\r\n /**\r\n * Bulk create accounts with validation and skip-if-exists logic.\r\n *\r\n * Uses a single batch query to check existing accounts (instead of N+1),\r\n * and ordered: false on insertMany to handle concurrent race conditions\r\n * gracefully (duplicate key errors on individual docs don't abort the batch).\r\n */\r\n repository.bulkCreate = async function (\r\n accounts: Array<{ accountTypeCode?: string; accountNumber?: string; name?: string; active?: boolean; isCashAccount?: boolean }>,\r\n orgId: unknown,\r\n ) {\r\n requireOrgScope(orgField, orgId);\r\n const results: {\r\n created: Array<Record<string, unknown>>;\r\n skipped: Array<Record<string, unknown>>;\r\n errors: Array<Record<string, unknown>>;\r\n } = { created: [], skipped: [], errors: [] };\r\n\r\n // Validate all accounts first (no DB calls)\r\n const validAccounts: Array<{ index: number; accountTypeCode: string; accountNumber: string; name: string; active: boolean; isCashAccount: boolean }> = [];\r\n\r\n for (let i = 0; i < accounts.length; i++) {\r\n const { accountTypeCode, accountNumber, name, active = true, isCashAccount = false } = accounts[i];\r\n\r\n if (!accountTypeCode) {\r\n results.errors.push({ index: i, reason: 'accountTypeCode is required' });\r\n continue;\r\n }\r\n\r\n const at = country.getAccountType(accountTypeCode);\r\n if (!at) {\r\n results.errors.push({ index: i, accountTypeCode, reason: 'Invalid account type code' });\r\n continue;\r\n }\r\n\r\n if (!country.isPostingAccount(accountTypeCode)) {\r\n results.errors.push({\r\n index: i,\r\n accountTypeCode,\r\n reason: `Not a posting account (${(at as unknown as Record<string, unknown>).isGroup ? 'group' : 'total'})`,\r\n });\r\n continue;\r\n }\r\n\r\n const resolvedNumber = accountNumber ?? accountTypeCode;\r\n const resolvedName = name ?? (at as unknown as Record<string, unknown>).name as string ?? accountTypeCode;\r\n validAccounts.push({ index: i, accountTypeCode, accountNumber: resolvedNumber, name: resolvedName, active: Boolean(active), isCashAccount: Boolean(isCashAccount) });\r\n }\r\n\r\n if (validAccounts.length === 0) {\r\n return {\r\n summary: { total: accounts.length, created: 0, skipped: results.skipped.length, errors: results.errors.length },\r\n ...results,\r\n };\r\n }\r\n\r\n // Single batch query to find all existing accounts by accountNumber for this org\r\n const numbersToCheck = validAccounts.map(a => a.accountNumber);\r\n const existsFilter: Record<string, unknown> = { accountNumber: { $in: numbersToCheck } };\r\n if (orgField && orgId != null) existsFilter[orgField] = orgId;\r\n\r\n const existingDocs = await AccountModel.find(existsFilter)\r\n .select('accountNumber')\r\n .lean() as Array<Record<string, unknown>>;\r\n const existingNumbers = new Set(existingDocs.map(d => d.accountNumber as string));\r\n\r\n // Partition into create vs skip\r\n const toCreate: Array<{ index: number; accountTypeCode: string; accountNumber: string; name: string; active: boolean; isCashAccount: boolean }> = [];\r\n for (const item of validAccounts) {\r\n if (existingNumbers.has(item.accountNumber)) {\r\n results.skipped.push({ index: item.index, accountTypeCode: item.accountTypeCode, reason: 'Already exists' });\r\n } else {\r\n toCreate.push(item);\r\n }\r\n }\r\n\r\n if (toCreate.length > 0) {\r\n const docs = toCreate.map(item => {\r\n const doc: Record<string, unknown> = {\r\n accountTypeCode: item.accountTypeCode,\r\n accountNumber: item.accountNumber,\r\n name: item.name,\r\n active: item.active,\r\n isCashAccount: item.isCashAccount,\r\n };\r\n if (orgField && orgId != null) doc[orgField] = orgId;\r\n return doc;\r\n });\r\n\r\n try {\r\n // ordered: false ensures a dup-key on one doc doesn't abort the rest\r\n const inserted = await AccountModel.insertMany(docs, { ordered: false });\r\n results.created = toCreate.map((item, idx) => ({\r\n accountTypeCode: item.accountTypeCode,\r\n active: item.active,\r\n isCashAccount: item.isCashAccount,\r\n _id: (inserted[idx] as unknown as Record<string, unknown>)._id,\r\n }));\r\n } catch (err: unknown) {\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n const bulkError = err as any;\r\n if (bulkError.code === 11000 || bulkError.writeErrors) {\r\n // Partial success: some docs inserted, some hit dup-key from concurrent caller\r\n const insertedDocs = bulkError.insertedDocs ?? [];\r\n const insertedNumbers = new Set(\r\n insertedDocs.map((d: Record<string, unknown>) => d.accountNumber as string),\r\n );\r\n for (const item of toCreate) {\r\n if (insertedNumbers.has(item.accountNumber)) {\r\n const iDoc = insertedDocs.find(\r\n (d: Record<string, unknown>) => d.accountNumber === item.accountNumber,\r\n );\r\n results.created.push({\r\n accountTypeCode: item.accountTypeCode,\r\n active: item.active,\r\n isCashAccount: item.isCashAccount,\r\n _id: iDoc?._id,\r\n });\r\n } else {\r\n results.skipped.push({\r\n index: item.index,\r\n accountTypeCode: item.accountTypeCode,\r\n reason: 'Already exists (concurrent insert)',\r\n });\r\n }\r\n }\r\n } else {\r\n throw err;\r\n }\r\n }\r\n }\r\n\r\n return {\r\n summary: {\r\n total: accounts.length,\r\n created: results.created.length,\r\n skipped: results.skipped.length,\r\n errors: results.errors.length,\r\n },\r\n ...results,\r\n };\r\n };\r\n}\r\n"],"mappings":"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"}