@classytic/ledger 0.9.1 → 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/bridges/index.d.mts +1 -1
- package/dist/{errors-BI5k4iak.mjs → errors-vXd932rB.mjs} +1 -1
- package/dist/events/index.d.mts +3 -2
- package/dist/events/index.mjs +2 -2
- package/dist/{fx-realization.plugin-Bxlb8cIx.mjs → fx-realization.plugin-Dzqzi3u0.mjs} +1 -1
- package/dist/{index-Dih0lM65.d.mts → index-Bl0gP9lD.d.mts} +3 -3
- package/dist/index.d.mts +93 -73
- package/dist/index.mjs +180 -198
- package/dist/outbox-store-BbKdQ2eT.mjs +253 -0
- package/dist/outbox-store-BcCiHMPw.d.mts +305 -0
- package/dist/{partner-ledger-BoebloHk.mjs → partner-ledger-CR0geilx.mjs} +1 -1
- package/dist/plugins/index.mjs +1 -1
- package/dist/reports/index.mjs +1 -1
- package/dist/sync/index.d.mts +13 -2
- package/dist/sync/index.mjs +6 -3
- package/package.json +12 -6
- package/dist/outbox-store-DQbL-KYT.mjs +0 -132
- package/dist/outbox-store-UYC4eZpI.d.mts +0 -249
package/dist/index.mjs
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import {
|
|
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-
|
|
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-
|
|
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-
|
|
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 {
|
|
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)
|
|
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)
|
|
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
|
|
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)
|
|
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)
|
|
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.
|
|
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
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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.
|
|
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
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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
|
+
} }
|
|
682
|
+
});
|
|
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
|
|
708
693
|
});
|
|
709
|
-
|
|
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
|
|
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
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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.
|
|
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?.
|
|
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?.
|
|
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?.
|
|
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: {
|
|
2576
|
+
* multiTenant: { tenantField: 'organizationId', ref: 'Organization' },
|
|
2595
2577
|
* });
|
|
2596
2578
|
*
|
|
2597
2579
|
* // Models — auto-created Mongoose models
|
|
@@ -2762,7 +2744,7 @@ var AccountingEngine = class {
|
|
|
2762
2744
|
const JournalEntryModel = this.models.JournalEntry;
|
|
2763
2745
|
const BudgetModel = this.models.Budget;
|
|
2764
2746
|
const { country, config } = this;
|
|
2765
|
-
const orgField = config.multiTenant?.
|
|
2747
|
+
const orgField = config.multiTenant?.tenantField;
|
|
2766
2748
|
const fiscalYearStartMonth = config.fiscalYearStartMonth ?? 1;
|
|
2767
2749
|
const retainedEarningsAccountCode = config.retainedEarningsAccountCode;
|
|
2768
2750
|
const retainedEarningsDisplayCode = config.retainedEarningsDisplayCode;
|
|
@@ -2896,4 +2878,4 @@ function buildDimensionIndexes(dimensions, orgField) {
|
|
|
2896
2878
|
});
|
|
2897
2879
|
}
|
|
2898
2880
|
//#endregion
|
|
2899
|
-
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 };
|