@classytic/ledger 0.9.0 → 0.10.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.
package/dist/index.mjs CHANGED
@@ -1,15 +1,16 @@
1
- import { i as LEDGER_EVENTS, n as InProcessLedgerBus, r as createEvent, t as OutboxOwnershipError } from "./outbox-store-DQbL-KYT.mjs";
2
- import { a as IdempotencyConflictError, i as Errors, n as ConcurrencyError, o as ImmutableViolationError, r as DuplicateReferenceError, s as classifyDuplicateKey, t as AccountingError } from "./errors-BI5k4iak.mjs";
1
+ import { _ as LEDGER_EVENTS, a as EntryArchived, c as EntryPosted, d as JournalSeeded, f as ReconciliationMatched, g as createEvent, h as InProcessLedgerBus, i as AccountSeeded, l as EntryReversed, m as ledgerEventDefinitions, n as OutboxOwnershipError, o as EntryCreated, p as ReconciliationUnmatched, r as AccountBulkCreated, s as EntryDuplicated, t as InvalidOutboxEventError, u as EntryUnposted } from "./outbox-store-BbKdQ2eT.mjs";
2
+ import { a as IdempotencyConflictError, i as Errors, n as ConcurrencyError, o as ImmutableViolationError, r as DuplicateReferenceError, s as classifyDuplicateKey, t as AccountingError } from "./errors-vXd932rB.mjs";
3
3
  import { a as JOURNAL_CODES, c as getCustomJournalTypes, d as isValidJournalType, f as registerJournalType, i as isValidCurrency, l as getJournalType, n as getCurrency, o as JOURNAL_TYPES, r as getMinorUnit, s as _freezeJournalTypes, t as CURRENCIES, u as getJournalTypeCodes } from "./currencies-Jo5oaM_4.mjs";
4
4
  import { Money, add, allocate, format, formatPlain, fromDecimal, multiply, parseCents, percentage, splitTaxExclusive, splitTaxInclusive, subtract, toDecimal } from "./money.mjs";
5
- import { C as isVirtualTaxAccount, E as requireOrgScope, S as computeEndingBalance, T as generateAgedBalance, _ as buildItemFilters, a as finalizeSession, b as buildAccountTypeMap, c as generateRevaluation, d as generateIncomeStatement, f as generateGeneralLedger, g as generateBalanceSheet, h as generateBudgetVsActual, i as acquireSession, l as buildRevaluationEntry, m as generateCashFlow, n as closeFiscalPeriod, o as defaultLogger, p as generateDimensionBreakdown, r as reopenFiscalPeriod, s as generateTrialBalance, t as generatePartnerLedger, u as computeRevaluation, v as getDateRange, w as DEFAULT_BUCKETS, x as calculateTotal, y as getFiscalYearStart } from "./partner-ledger-BoebloHk.mjs";
5
+ import { C as isVirtualTaxAccount, E as requireOrgScope, S as computeEndingBalance, T as generateAgedBalance, _ as buildItemFilters, a as finalizeSession, b as buildAccountTypeMap, c as generateRevaluation, d as generateIncomeStatement, f as generateGeneralLedger, g as generateBalanceSheet, h as generateBudgetVsActual, i as acquireSession, l as buildRevaluationEntry, m as generateCashFlow, n as closeFiscalPeriod, o as defaultLogger, p as generateDimensionBreakdown, r as reopenFiscalPeriod, s as generateTrialBalance, t as generatePartnerLedger, u as computeRevaluation, v as getDateRange, w as DEFAULT_BUCKETS, x as calculateTotal, y as getFiscalYearStart } from "./partner-ledger-CR0geilx.mjs";
6
6
  import { c as getNormalBalance, d as isValidCategory, l as isBalanceSheet, n as CATEGORY_KEYS, t as CATEGORIES, u as isIncomeStatement } from "./categories-FJlrvzcl.mjs";
7
- import { a as watermarkResolver, c as idempotencyPlugin, i as fiscalLockPlugin, l as doubleEntryPlugin, n as creditLimitPlugin, o as periodResolver, r as dailyLockPlugin, s as createLockPlugin, t as fxRealizationPlugin } from "./fx-realization.plugin-Bxlb8cIx.mjs";
7
+ import { a as watermarkResolver, c as idempotencyPlugin, i as fiscalLockPlugin, l as doubleEntryPlugin, n as creditLimitPlugin, o as periodResolver, r as dailyLockPlugin, s as createLockPlugin, t as fxRealizationPlugin } from "./fx-realization.plugin-Dzqzi3u0.mjs";
8
8
  import { t as buildOpeningBalanceEntry } from "./opening-balance-1cixYh6Y.mjs";
9
9
  import { defineCountryPack } from "./country/index.mjs";
10
10
  import { a as exportToCsv, i as quickbooksFieldMap, r as universalFieldMap, t as flattenJournalEntries } from "./exports-C30yRapf.mjs";
11
- import { QueryParser, Repository, getNextSequence, multiTenantPlugin } from "@classytic/mongokit";
11
+ import { QueryParser, Repository, getNextSequence, multiTenantPlugin, withTransaction } from "@classytic/mongokit";
12
12
  import mongoose, { Schema } from "mongoose";
13
+ import { resolveTenantConfig } from "@classytic/primitives/tenant";
13
14
  //#region src/plugins/immutable-guard.plugin.ts
14
15
  /**
15
16
  * Returns a mongokit plugin function. Install only when
@@ -37,6 +38,77 @@ function immutableGuardPlugin(options) {
37
38
  };
38
39
  }
39
40
  //#endregion
41
+ //#region src/models/inject-tenant.ts
42
+ /**
43
+ * Mongoose-specific adapter around `resolveTenantConfig()` from
44
+ * `@classytic/primitives/tenant`. The pure resolution lives in primitives
45
+ * (zero runtime deps) — this file only handles the Mongoose schema
46
+ * mutations (add field, prepend tenant onto compound indexes) that
47
+ * primitives can't own without a mongoose dependency.
48
+ *
49
+ * Prior to consolidation, each ledger schema (account, budget,
50
+ * fiscal-period, journal, journal-entry, reconciliation) inlined the same
51
+ * `if (multiTenant) { fields[multiTenant.tenantField] = { type: ObjectId,
52
+ * ref, required: true } }` block. That logic now lives here — schemas call
53
+ * `injectTenantField(schema, scope)` where `scope` is a
54
+ * `ResolvedTenantConfig` produced by `resolveLedgerTenant()`.
55
+ */
56
+ /**
57
+ * Translate ledger's `AccountingEngineConfig` into a `ResolvedTenantConfig`.
58
+ *
59
+ * Ledger's `MultiTenantConfig` tightens primitives' optionals (`tenantField`
60
+ * and `ref` are required in ledger — no default org-collection name) and
61
+ * carries a ledger-specific `plugin` knob for `multiTenantPlugin` adoption
62
+ * that primitives doesn't own. We merge those with `config.tenantFieldType`
63
+ * (engine-level field-type override) and produce a pure
64
+ * `ResolvedTenantConfig` suitable for `injectTenantField()`.
65
+ *
66
+ * When `config.multiTenant` is absent, scoping is disabled (strategy =
67
+ * `'none'`) and no tenant field is added to schemas.
68
+ */
69
+ function resolveLedgerTenant(config) {
70
+ const mt = config.multiTenant;
71
+ if (!mt) return resolveTenantConfig(false);
72
+ return resolveTenantConfig({
73
+ enabled: true,
74
+ strategy: "field",
75
+ tenantField: mt.tenantField,
76
+ ref: mt.ref,
77
+ fieldType: config.tenantFieldType ?? "objectId",
78
+ required: true
79
+ });
80
+ }
81
+ /**
82
+ * Inject the tenant field into a Mongoose schema and (when
83
+ * `strategy === 'field'` is active) prepend the tenant field onto every
84
+ * existing compound index so queries are index-efficient under multi-tenant
85
+ * scoping. Matches the order/people/revenue pattern (PACKAGE_RULES §9.2).
86
+ *
87
+ * Schemas should declare their compound indexes WITHOUT a tenant prefix
88
+ * and then call this helper once — the prefix is added here.
89
+ */
90
+ function injectTenantField(schema, scope) {
91
+ const isFieldStrategy = scope.strategy === "field";
92
+ const isObjectId = scope.fieldType === "objectId";
93
+ if (isFieldStrategy) {
94
+ const fieldDef = {
95
+ type: isObjectId ? mongoose.Schema.Types.ObjectId : String,
96
+ ...scope.enabled && scope.required ? { required: true } : {}
97
+ };
98
+ if (isObjectId && scope.ref) fieldDef.ref = scope.ref;
99
+ schema.add({ [scope.tenantField]: fieldDef });
100
+ }
101
+ if (!scope.enabled || !isFieldStrategy) return;
102
+ const existingIndexes = schema._indexes;
103
+ if (existingIndexes && existingIndexes.length > 0) for (const indexEntry of existingIndexes) {
104
+ const fields = indexEntry[0];
105
+ if (fields[scope.tenantField] !== void 0) continue;
106
+ const newFields = { [scope.tenantField]: 1 };
107
+ for (const [key, val] of Object.entries(fields)) newFields[key] = val;
108
+ indexEntry[0] = newFields;
109
+ }
110
+ }
111
+ //#endregion
40
112
  //#region src/schemas/currency-field.ts
41
113
  /**
42
114
  * Build the Mongoose currency field definition.
@@ -67,7 +139,8 @@ function buildCurrencyField(config) {
67
139
  * - Lean: no cached balances — always computed from journal entries
68
140
  */
69
141
  function createAccountSchema(config, options = {}) {
70
- const { multiTenant, country } = config;
142
+ const { country } = config;
143
+ const scope = resolveLedgerTenant(config);
71
144
  const { indexes = true, extraFields = {}, extraIndexes = [] } = options;
72
145
  const fields = {
73
146
  accountTypeCode: {
@@ -100,36 +173,21 @@ function createAccountSchema(config, options = {}) {
100
173
  const currencyField = buildCurrencyField(config);
101
174
  if (currencyField) fields.currency = currencyField;
102
175
  Object.assign(fields, extraFields);
103
- if (multiTenant) fields[multiTenant.orgField] = {
104
- type: mongoose.Schema.Types.ObjectId,
105
- ref: multiTenant.orgRef,
106
- required: true
107
- };
108
176
  const schema = new mongoose.Schema(fields, { timestamps: true });
109
177
  schema.pre("validate", function() {
110
178
  if (!this.accountNumber && this.accountTypeCode) this.accountNumber = this.accountTypeCode;
111
- if (!this.name && this.accountTypeCode) this.name = country.getAccountType(this.accountTypeCode)?.name ?? this.accountTypeCode;
179
+ if (!this.name && this.accountTypeCode) {
180
+ const at = country.getAccountType(this.accountTypeCode);
181
+ this.name = at?.name ?? this.accountTypeCode;
182
+ }
112
183
  });
113
- if (indexes) if (multiTenant) {
114
- const org = multiTenant.orgField;
115
- schema.index({
116
- [org]: 1,
117
- active: 1
118
- });
119
- schema.index({
120
- [org]: 1,
121
- accountNumber: 1
122
- }, { unique: true });
123
- schema.index({
124
- [org]: 1,
125
- accountTypeCode: 1
126
- });
127
- } else {
184
+ if (indexes) {
128
185
  schema.index({ active: 1 });
129
186
  schema.index({ accountNumber: 1 }, { unique: true });
130
187
  schema.index({ accountTypeCode: 1 });
131
188
  }
132
189
  for (const idx of extraIndexes) schema.index(idx.fields, idx.options);
190
+ injectTenantField(schema, scope);
133
191
  return schema;
134
192
  }
135
193
  //#endregion
@@ -142,7 +200,7 @@ function createAccountSchema(config, options = {}) {
142
200
  * All monetary amounts are in integer cents.
143
201
  */
144
202
  function createBudgetSchema(config, options = {}) {
145
- const { multiTenant } = config;
203
+ const scope = resolveLedgerTenant(config);
146
204
  const { indexes = true, extraFields = {}, extraIndexes = [] } = options;
147
205
  const fields = {
148
206
  account: {
@@ -172,30 +230,12 @@ function createBudgetSchema(config, options = {}) {
172
230
  },
173
231
  ...extraFields
174
232
  };
175
- if (multiTenant) fields[multiTenant.orgField] = {
176
- type: mongoose.Schema.Types.ObjectId,
177
- ref: multiTenant.orgRef,
178
- required: true
179
- };
180
233
  const schema = new mongoose.Schema(fields, { timestamps: true });
181
234
  schema.pre("validate", function() {
182
235
  const doc = this;
183
236
  if (doc.periodStart && doc.periodEnd && doc.periodEnd <= doc.periodStart) doc.invalidate("periodEnd", "periodEnd must be after periodStart.", doc.periodEnd, "periodEnd");
184
237
  });
185
- if (indexes) if (multiTenant) {
186
- const org = multiTenant.orgField;
187
- schema.index({
188
- [org]: 1,
189
- account: 1,
190
- periodStart: 1,
191
- periodEnd: 1
192
- }, { unique: true });
193
- schema.index({
194
- [org]: 1,
195
- periodStart: 1,
196
- periodEnd: 1
197
- });
198
- } else {
238
+ if (indexes) {
199
239
  schema.index({
200
240
  account: 1,
201
241
  periodStart: 1,
@@ -207,6 +247,7 @@ function createBudgetSchema(config, options = {}) {
207
247
  });
208
248
  }
209
249
  for (const idx of extraIndexes) schema.index(idx.fields, idx.options);
250
+ injectTenantField(schema, scope);
210
251
  return schema;
211
252
  }
212
253
  //#endregion
@@ -219,6 +260,7 @@ function createBudgetSchema(config, options = {}) {
219
260
  */
220
261
  function createFiscalPeriodSchema(config, options = {}) {
221
262
  const { multiTenant } = config;
263
+ const scope = resolveLedgerTenant(config);
222
264
  const { indexes = true, extraFields = {}, extraIndexes = [] } = options;
223
265
  const fields = {
224
266
  name: {
@@ -259,24 +301,8 @@ function createFiscalPeriodSchema(config, options = {}) {
259
301
  },
260
302
  ...extraFields
261
303
  };
262
- if (multiTenant) fields[multiTenant.orgField] = {
263
- type: mongoose.Schema.Types.ObjectId,
264
- ref: multiTenant.orgRef,
265
- required: true
266
- };
267
304
  const schema = new mongoose.Schema(fields, { timestamps: true });
268
- if (indexes) if (multiTenant) {
269
- const org = multiTenant.orgField;
270
- schema.index({
271
- [org]: 1,
272
- startDate: 1,
273
- endDate: 1
274
- }, { unique: true });
275
- schema.index({
276
- [org]: 1,
277
- closed: 1
278
- });
279
- } else {
305
+ if (indexes) {
280
306
  schema.index({
281
307
  startDate: 1,
282
308
  endDate: 1
@@ -284,6 +310,7 @@ function createFiscalPeriodSchema(config, options = {}) {
284
310
  schema.index({ closed: 1 });
285
311
  }
286
312
  for (const idx of extraIndexes) schema.index(idx.fields, idx.options);
313
+ injectTenantField(schema, scope);
287
314
  schema.pre("validate", async function() {
288
315
  const doc = this;
289
316
  if (!doc.startDate || !doc.endDate) return;
@@ -292,7 +319,7 @@ function createFiscalPeriodSchema(config, options = {}) {
292
319
  startDate: { $lt: doc.endDate },
293
320
  endDate: { $gt: doc.startDate }
294
321
  };
295
- if (multiTenant) overlapQuery[multiTenant.orgField] = doc[multiTenant.orgField];
322
+ if (multiTenant) overlapQuery[multiTenant.tenantField] = doc[multiTenant.tenantField];
296
323
  const overlap = await doc.collection.findOne(overlapQuery);
297
324
  if (overlap) {
298
325
  const msg = `Fiscal period overlaps with existing period "${overlap.name}" (${new Date(overlap.startDate).toISOString().split("T")[0]} – ${new Date(overlap.endDate).toISOString().split("T")[0]}).`;
@@ -324,23 +351,33 @@ function createFiscalPeriodSchema(config, options = {}) {
324
351
  * `engine.repositories.journals.seedDefaults(orgId)`.
325
352
  */
326
353
  function createJournalSchema(config, accountModelName, options = {}) {
327
- const { multiTenant } = config;
354
+ const scope = resolveLedgerTenant(config);
328
355
  const { indexes = true, extraFields = {}, extraIndexes = [] } = options;
329
356
  const fields = {
357
+ /** Short stable identifier — e.g. `'SALES'`, `'BANK'`. */
330
358
  code: {
331
359
  type: String,
332
360
  required: true,
333
361
  trim: true
334
362
  },
363
+ /** Display name. */
335
364
  name: {
336
365
  type: String,
337
366
  required: true,
338
367
  trim: true
339
368
  },
369
+ /**
370
+ * One of the registered `JOURNAL_TYPES` codes. Connects this journal
371
+ * to the engine's posting contracts and reference-number generator.
372
+ */
340
373
  journalType: {
341
374
  type: String,
342
375
  required: true
343
376
  },
377
+ /**
378
+ * Logical source — drives future lock-date buckets (sale-lock-date,
379
+ * purchase-lock-date) and bank-statement import wiring.
380
+ */
344
381
  kind: {
345
382
  type: String,
346
383
  enum: [
@@ -354,15 +391,18 @@ function createJournalSchema(config, accountModelName, options = {}) {
354
391
  default: "general",
355
392
  required: true
356
393
  },
394
+ /** Reference-number prefix — defaults to `code` when omitted. */
357
395
  sequencePrefix: {
358
396
  type: String,
359
397
  default: null
360
398
  },
399
+ /** Next sequence number (monotonic within this journal). */
361
400
  sequenceNextNum: {
362
401
  type: Number,
363
402
  default: 1,
364
403
  min: 1
365
404
  },
405
+ /** Optional default debit/credit account for quick data entry. */
366
406
  defaultDebitAccount: {
367
407
  type: mongoose.Schema.Types.ObjectId,
368
408
  ref: accountModelName,
@@ -373,10 +413,15 @@ function createJournalSchema(config, accountModelName, options = {}) {
373
413
  ref: accountModelName,
374
414
  default: null
375
415
  },
416
+ /** Free-form payment-method identifiers allowed on entries in this journal. */
376
417
  allowedPaymentMethods: {
377
418
  type: [String],
378
419
  default: []
379
420
  },
421
+ /**
422
+ * Opaque source id — `'manual'`, `'stripe'`, bank connector id, etc.
423
+ * Used by bank-statement and external-integration plugins.
424
+ */
380
425
  source: {
381
426
  type: String,
382
427
  default: "manual"
@@ -387,33 +432,16 @@ function createJournalSchema(config, accountModelName, options = {}) {
387
432
  },
388
433
  ...extraFields
389
434
  };
390
- if (multiTenant) fields[multiTenant.orgField] = {
391
- type: mongoose.Schema.Types.ObjectId,
392
- ref: multiTenant.orgRef,
393
- required: true
394
- };
395
435
  const schema = new mongoose.Schema(fields, { timestamps: true });
396
436
  if (indexes) {
397
- const org = multiTenant?.orgField;
398
- if (org) {
399
- schema.index({
400
- [org]: 1,
401
- code: 1
402
- }, { unique: true });
403
- schema.index({
404
- [org]: 1,
405
- kind: 1,
406
- active: 1
407
- });
408
- } else {
409
- schema.index({ code: 1 }, { unique: true });
410
- schema.index({
411
- kind: 1,
412
- active: 1
413
- });
414
- }
437
+ schema.index({ code: 1 }, { unique: true });
438
+ schema.index({
439
+ kind: 1,
440
+ active: 1
441
+ });
415
442
  }
416
443
  for (const idx of extraIndexes) schema.index(idx.fields, idx.options);
444
+ injectTenantField(schema, scope);
417
445
  return schema;
418
446
  }
419
447
  //#endregion
@@ -431,6 +459,7 @@ function createJournalSchema(config, accountModelName, options = {}) {
431
459
  */
432
460
  function createJournalEntrySchema(config, accountModelName, options = {}) {
433
461
  const { multiTenant } = config;
462
+ const scope = resolveLedgerTenant(config);
434
463
  const { indexes = true, autoReference = true, textSearch = true, extraFields = {}, extraIndexes = [], extraItemFields = {} } = options;
435
464
  const TaxDetailSchema = new mongoose.Schema({
436
465
  taxCode: { type: String },
@@ -603,11 +632,6 @@ function createJournalEntrySchema(config, accountModelName, options = {}) {
603
632
  type: String,
604
633
  default: null
605
634
  };
606
- if (multiTenant) fields[multiTenant.orgField] = {
607
- type: mongoose.Schema.Types.ObjectId,
608
- ref: multiTenant.orgRef,
609
- required: true
610
- };
611
635
  const schema = new mongoose.Schema(fields, {
612
636
  timestamps: true,
613
637
  optimisticConcurrency: true
@@ -640,7 +664,7 @@ function createJournalEntrySchema(config, accountModelName, options = {}) {
640
664
  const month = String(date.getMonth() + 1).padStart(2, "0");
641
665
  let orgScope = "global";
642
666
  if (multiTenant) {
643
- const raw = this.get(multiTenant.orgField);
667
+ const raw = this.get(multiTenant.tenantField);
644
668
  if (raw != null) orgScope = typeof raw.toHexString === "function" ? raw.toHexString() : String(raw);
645
669
  else orgScope = "unscoped";
646
670
  }
@@ -649,72 +673,38 @@ function createJournalEntrySchema(config, accountModelName, options = {}) {
649
673
  }
650
674
  });
651
675
  if (indexes) {
652
- const org = multiTenant?.orgField;
653
- const refPartial = { partialFilterExpression: { referenceNumber: {
654
- $exists: true,
655
- $type: "string"
656
- } } };
657
- if (org) {
658
- schema.index({
659
- [org]: 1,
660
- referenceNumber: 1
661
- }, {
662
- unique: true,
663
- ...refPartial
664
- });
665
- schema.index({
666
- [org]: 1,
667
- state: 1,
668
- date: 1
669
- });
670
- schema.index({
671
- [org]: 1,
672
- date: -1
673
- });
674
- schema.index({
675
- [org]: 1,
676
- journalType: 1
677
- });
678
- schema.index({
679
- "journalItems.account": 1,
680
- state: 1
681
- });
682
- schema.index({
683
- [org]: 1,
684
- "journalItems.account": 1,
685
- date: 1,
686
- state: 1
687
- });
688
- } else {
689
- schema.index({ referenceNumber: 1 }, {
690
- unique: true,
691
- ...refPartial
692
- });
693
- schema.index({
694
- state: 1,
695
- date: 1
696
- });
697
- schema.index({ date: -1 });
698
- schema.index({ journalType: 1 });
699
- schema.index({
700
- "journalItems.account": 1,
701
- state: 1
702
- });
703
- }
704
- schema.index({ reversed: 1 });
705
- if (org) schema.index({
706
- [org]: 1,
707
- "journalItems.matchingNumber": 1
676
+ schema.index({ referenceNumber: 1 }, {
677
+ unique: true,
678
+ partialFilterExpression: { referenceNumber: {
679
+ $exists: true,
680
+ $type: "string"
681
+ } }
708
682
  });
709
- else schema.index({ "journalItems.matchingNumber": 1 });
683
+ schema.index({
684
+ state: 1,
685
+ date: 1
686
+ });
687
+ schema.index({ date: -1 });
688
+ schema.index({ journalType: 1 });
689
+ if (scope.enabled) schema.index({
690
+ "journalItems.account": 1,
691
+ date: 1,
692
+ state: 1
693
+ });
694
+ schema.index({ "journalItems.matchingNumber": 1 });
695
+ if (config.idempotency) schema.index({ idempotencyKey: 1 }, {
696
+ unique: true,
697
+ partialFilterExpression: { idempotencyKey: { $type: "string" } }
698
+ });
699
+ }
700
+ injectTenantField(schema, scope);
701
+ if (indexes) {
702
+ schema.index({
703
+ "journalItems.account": 1,
704
+ state: 1
705
+ });
706
+ schema.index({ reversed: 1 });
710
707
  if (config.idempotency) {
711
- const idempotencyIdx = {};
712
- if (org) idempotencyIdx[org] = 1;
713
- idempotencyIdx.idempotencyKey = 1;
714
- schema.index(idempotencyIdx, {
715
- unique: true,
716
- partialFilterExpression: { idempotencyKey: { $type: "string" } }
717
- });
718
708
  const ttlSeconds = typeof config.idempotencyTtlSeconds === "number" && config.idempotencyTtlSeconds > 0 ? config.idempotencyTtlSeconds : 86400;
719
709
  schema.index({ createdAt: 1 }, {
720
710
  name: "idempotency_ttl_idx",
@@ -758,7 +748,7 @@ function createJournalEntrySchema(config, accountModelName, options = {}) {
758
748
  * `updatedAt` timestamps.
759
749
  */
760
750
  function createReconciliationSchema(config, accountModelName, journalEntryModelName, options = {}) {
761
- const { multiTenant } = config;
751
+ const scope = resolveLedgerTenant(config);
762
752
  const { indexes = true, extraFields = {}, extraIndexes = [] } = options;
763
753
  const MatchedItemRefSchema = new mongoose.Schema({
764
754
  entry: {
@@ -775,16 +765,19 @@ function createReconciliationSchema(config, accountModelName, journalEntryModelN
775
765
  message: "itemIndex must be a non-negative integer"
776
766
  }
777
767
  },
768
+ /** Snapshot of the debit side of the item in cents, for audit. */
778
769
  debit: {
779
770
  type: Number,
780
771
  default: 0,
781
772
  min: 0
782
773
  },
774
+ /** Snapshot of the credit side of the item in cents, for audit. */
783
775
  credit: {
784
776
  type: Number,
785
777
  default: 0,
786
778
  min: 0
787
779
  },
780
+ /** Optional snapshot of the item's foreign amount for FX realization. */
788
781
  amountCurrency: {
789
782
  type: Number,
790
783
  default: null
@@ -795,6 +788,10 @@ function createReconciliationSchema(config, accountModelName, journalEntryModelN
795
788
  }
796
789
  }, { _id: false });
797
790
  const fields = {
791
+ /**
792
+ * Stable identifier shared by every matched item, also stamped onto
793
+ * `journalItems[i].matchingNumber` for cheap open-item lookups.
794
+ */
798
795
  matchingNumber: {
799
796
  type: String,
800
797
  required: true
@@ -822,6 +819,7 @@ function createReconciliationSchema(config, accountModelName, journalEntryModelN
822
819
  required: true,
823
820
  min: 0
824
821
  },
822
+ /** `debitTotal - creditTotal` in cents — zero ⇒ full reconcile. */
825
823
  difference: {
826
824
  type: Number,
827
825
  default: 0
@@ -830,6 +828,11 @@ function createReconciliationSchema(config, accountModelName, journalEntryModelN
830
828
  type: Boolean,
831
829
  default: false
832
830
  },
831
+ /**
832
+ * Optional currency stamp — when all matched items share a single
833
+ * foreign currency, this records it so the FX realization plugin can
834
+ * compute realized gain/loss against the base currency rates.
835
+ */
833
836
  currency: {
834
837
  type: String,
835
838
  default: null
@@ -840,6 +843,7 @@ function createReconciliationSchema(config, accountModelName, journalEntryModelN
840
843
  type: Date,
841
844
  default: Date.now
842
845
  },
846
+ /** Audit ref to the FX realization entry when the plugin fires. */
843
847
  fxRealizationEntry: {
844
848
  type: mongoose.Schema.Types.ObjectId,
845
849
  ref: journalEntryModelName,
@@ -847,40 +851,18 @@ function createReconciliationSchema(config, accountModelName, journalEntryModelN
847
851
  },
848
852
  ...extraFields
849
853
  };
850
- if (multiTenant) fields[multiTenant.orgField] = {
851
- type: mongoose.Schema.Types.ObjectId,
852
- ref: multiTenant.orgRef,
853
- required: true
854
- };
855
854
  const schema = new mongoose.Schema(fields, { timestamps: true });
856
855
  if (indexes) {
857
- const org = multiTenant?.orgField;
858
- if (org) {
859
- schema.index({
860
- [org]: 1,
861
- matchingNumber: 1
862
- }, { unique: true });
863
- schema.index({
864
- [org]: 1,
865
- account: 1,
866
- isFullReconcile: 1,
867
- reconciledAt: 1
868
- });
869
- schema.index({
870
- [org]: 1,
871
- "items.entry": 1
872
- });
873
- } else {
874
- schema.index({ matchingNumber: 1 }, { unique: true });
875
- schema.index({
876
- account: 1,
877
- isFullReconcile: 1,
878
- reconciledAt: 1
879
- });
880
- schema.index({ "items.entry": 1 });
881
- }
856
+ schema.index({ matchingNumber: 1 }, { unique: true });
857
+ schema.index({
858
+ account: 1,
859
+ isFullReconcile: 1,
860
+ reconciledAt: 1
861
+ });
862
+ schema.index({ "items.entry": 1 });
882
863
  }
883
864
  for (const idx of extraIndexes) schema.index(idx.fields, idx.options);
865
+ injectTenantField(schema, scope);
884
866
  return schema;
885
867
  }
886
868
  //#endregion
@@ -1304,7 +1286,7 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
1304
1286
  const getByQuery = repository.getByQuery.bind(repository);
1305
1287
  const baseCreate = repository.create.bind(repository);
1306
1288
  const update = repository.update.bind(repository);
1307
- const withTransaction = repository.withTransaction.bind(repository);
1289
+ const withTransaction$1 = (fn, opts) => withTransaction(repository.Model.db, fn, opts);
1308
1290
  const raceSafeCreate = async (data, options) => {
1309
1291
  const input = data;
1310
1292
  const idempotencyKey = typeof input.idempotencyKey === "string" && input.idempotencyKey.length > 0 ? input.idempotencyKey : void 0;
@@ -1672,7 +1654,7 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
1672
1654
  };
1673
1655
  };
1674
1656
  if (options.session) return await doReverse(options.session);
1675
- if (withTransaction) return await withTransaction((session) => doReverse(session), { allowFallback: true });
1657
+ if (withTransaction$1) return await withTransaction$1((session) => doReverse(session), { allowFallback: true });
1676
1658
  return await doReverse();
1677
1659
  };
1678
1660
  const methodNames = [
@@ -1967,7 +1949,7 @@ function wireReconciliationMethods(repository, ReconciliationModel, JournalEntry
1967
1949
  * Build all ledger repositories with plugins + domain methods pre-wired.
1968
1950
  */
1969
1951
  function createRepositories(models, config, plugins = {}, pagination = {}, integrations = {}) {
1970
- const orgField = config.multiTenant?.orgField;
1952
+ const orgField = config.multiTenant?.tenantField;
1971
1953
  const strictness = config.strictness;
1972
1954
  const country = config.country;
1973
1955
  const { events, bridges, outboxStore } = integrations;
@@ -2301,7 +2283,7 @@ const REPORT_CATALOG = Object.freeze([
2301
2283
  function buildIntrospectAPI({ models, country, config }) {
2302
2284
  const AccountModel = models.Account;
2303
2285
  const FiscalPeriodModel = models.FiscalPeriod;
2304
- const orgField = config.multiTenant?.orgField;
2286
+ const orgField = config.multiTenant?.tenantField;
2305
2287
  const normalBalanceFor = (category) => {
2306
2288
  if (category.endsWith("Asset") || category.endsWith("Expense")) return "debit";
2307
2289
  return "credit";
@@ -2363,7 +2345,7 @@ function buildIntrospectAPI({ models, country, config }) {
2363
2345
  //#region src/semantic/record.ts
2364
2346
  function buildRecordAPI({ models, repositories, config }) {
2365
2347
  const AccountModel = models.Account;
2366
- const orgField = config.multiTenant?.orgField;
2348
+ const orgField = config.multiTenant?.tenantField;
2367
2349
  const resolveAccounts = async (organizationId, codes, path, session) => {
2368
2350
  const unique = Array.from(new Set(codes));
2369
2351
  const filter = { accountTypeCode: { $in: unique } };
@@ -2591,7 +2573,7 @@ function buildRecordAPI({ models, repositories, config }) {
2591
2573
  * mongoose: mongoose.connection,
2592
2574
  * country: canadaPack,
2593
2575
  * currency: 'CAD',
2594
- * multiTenant: { orgField: 'organizationId', orgRef: 'Organization' },
2576
+ * multiTenant: { tenantField: 'organizationId', ref: 'Organization' },
2595
2577
  * });
2596
2578
  *
2597
2579
  * // Models — auto-created Mongoose models
@@ -2669,6 +2651,21 @@ var AccountingEngine = class {
2669
2651
  });
2670
2652
  }
2671
2653
  /**
2654
+ * Explicitly sync indexes on all managed models.
2655
+ * Call this in deploy-time scripts — NOT on every boot.
2656
+ * See PACKAGE_RULES section 32.
2657
+ */
2658
+ async syncIndexes() {
2659
+ await Promise.all([
2660
+ this.models.Account.syncIndexes(),
2661
+ this.models.JournalEntry.syncIndexes(),
2662
+ this.models.FiscalPeriod.syncIndexes(),
2663
+ this.models.Budget.syncIndexes(),
2664
+ this.models.Reconciliation.syncIndexes(),
2665
+ this.models.Journal.syncIndexes()
2666
+ ]);
2667
+ }
2668
+ /**
2672
2669
  * Pre-built reports bound to the engine's owned models.
2673
2670
  * Lazy-initialized on first access.
2674
2671
  */
@@ -2747,7 +2744,7 @@ var AccountingEngine = class {
2747
2744
  const JournalEntryModel = this.models.JournalEntry;
2748
2745
  const BudgetModel = this.models.Budget;
2749
2746
  const { country, config } = this;
2750
- const orgField = config.multiTenant?.orgField;
2747
+ const orgField = config.multiTenant?.tenantField;
2751
2748
  const fiscalYearStartMonth = config.fiscalYearStartMonth ?? 1;
2752
2749
  const retainedEarningsAccountCode = config.retainedEarningsAccountCode;
2753
2750
  const retainedEarningsDisplayCode = config.retainedEarningsDisplayCode;
@@ -2881,4 +2878,4 @@ function buildDimensionIndexes(dimensions, orgField) {
2881
2878
  });
2882
2879
  }
2883
2880
  //#endregion
2884
- export { AccountingEngine, AccountingError, CATEGORIES, CATEGORY_KEYS, CURRENCIES, ConcurrencyError, DEFAULT_BUCKETS, DuplicateReferenceError, Errors, IdempotencyConflictError, ImmutableViolationError, InProcessLedgerBus, JOURNAL_CODES, JOURNAL_TYPES, LEDGER_EVENTS, Money, OutboxOwnershipError, acquireSession, add, allocate, buildAccountTypeMap, buildDimensionFields, buildDimensionIndexes, buildItemFilters, buildRevaluationEntry, calculateTotal, classifyDuplicateKey, closeFiscalPeriod, computeEndingBalance, computeRevaluation, createAccountingEngine, createEvent, createLockPlugin, createModels, createRepositories, creditLimitPlugin, dailyLockPlugin, defaultLogger, defineCountryPack, doubleEntryPlugin, exportToCsv, finalizeSession, fiscalLockPlugin, flattenJournalEntries, format, formatPlain, fromDecimal, fxRealizationPlugin, generateAgedBalance, generateBalanceSheet, generateBudgetVsActual, generateCashFlow, generateDimensionBreakdown, generateGeneralLedger, generateIncomeStatement, generatePartnerLedger, generateRevaluation, generateTrialBalance, getCurrency, getCustomJournalTypes, getDateRange, getFiscalYearStart, getJournalType, getJournalTypeCodes, getMinorUnit, getNormalBalance, idempotencyPlugin, immutableGuardPlugin, isBalanceSheet, isIncomeStatement, isValidCategory, isValidCurrency, isValidJournalType, isVirtualTaxAccount, multiply, parseCents, percentage, periodResolver, quickbooksFieldMap, registerJournalType, reopenFiscalPeriod, resolveModelNames, splitTaxExclusive, splitTaxInclusive, subtract, toDecimal, universalFieldMap, watermarkResolver };
2881
+ export { AccountBulkCreated, AccountSeeded, AccountingEngine, AccountingError, CATEGORIES, CATEGORY_KEYS, CURRENCIES, ConcurrencyError, DEFAULT_BUCKETS, DuplicateReferenceError, EntryArchived, EntryCreated, EntryDuplicated, EntryPosted, EntryReversed, EntryUnposted, Errors, IdempotencyConflictError, ImmutableViolationError, InProcessLedgerBus, InvalidOutboxEventError, JOURNAL_CODES, JOURNAL_TYPES, JournalSeeded, LEDGER_EVENTS, Money, OutboxOwnershipError, ReconciliationMatched, ReconciliationUnmatched, acquireSession, add, allocate, buildAccountTypeMap, buildDimensionFields, buildDimensionIndexes, buildItemFilters, buildRevaluationEntry, calculateTotal, classifyDuplicateKey, closeFiscalPeriod, computeEndingBalance, computeRevaluation, createAccountingEngine, createEvent, createLockPlugin, createModels, createRepositories, creditLimitPlugin, dailyLockPlugin, defaultLogger, defineCountryPack, doubleEntryPlugin, exportToCsv, finalizeSession, fiscalLockPlugin, flattenJournalEntries, format, formatPlain, fromDecimal, fxRealizationPlugin, generateAgedBalance, generateBalanceSheet, generateBudgetVsActual, generateCashFlow, generateDimensionBreakdown, generateGeneralLedger, generateIncomeStatement, generatePartnerLedger, generateRevaluation, generateTrialBalance, getCurrency, getCustomJournalTypes, getDateRange, getFiscalYearStart, getJournalType, getJournalTypeCodes, getMinorUnit, getNormalBalance, idempotencyPlugin, immutableGuardPlugin, isBalanceSheet, isIncomeStatement, isValidCategory, isValidCurrency, isValidJournalType, isVirtualTaxAccount, ledgerEventDefinitions, multiply, parseCents, percentage, periodResolver, quickbooksFieldMap, registerJournalType, reopenFiscalPeriod, resolveModelNames, splitTaxExclusive, splitTaxInclusive, subtract, toDecimal, universalFieldMap, watermarkResolver };