@classytic/ledger 0.10.2 → 0.11.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/constants/index.d.mts +1 -1
- package/dist/constants/index.mjs +2 -2
- package/dist/{core-DwjkrRkJ.d.mts → core-B7uVjqGS.d.mts} +25 -0
- package/dist/country/index.d.mts +1 -1
- package/dist/events/index.d.mts +1 -1
- package/dist/exports/index.d.mts +1 -1
- package/dist/exports/index.mjs +1 -1
- package/dist/{fx-realization.plugin-Dzqzi3u0.mjs → fx-realization.plugin-DY3pPxIi.mjs} +70 -1
- package/dist/{index-ClLwzNRF.d.mts → index-BFPFihTF.d.mts} +8 -0
- package/dist/{index-08IpHhrU.d.mts → index-Dd7HknPP.d.mts} +1 -1
- package/dist/index.d.mts +120 -24
- package/dist/index.mjs +375 -165
- package/dist/{journals-DUpWwFt1.d.mts → journals-CTrAuzdk.d.mts} +1 -1
- package/dist/{partner-ledger-CR0geilx.mjs → partner-ledger-B0eym6Ss.mjs} +951 -213
- package/dist/plugins/index.d.mts +1 -1
- package/dist/plugins/index.mjs +1 -1
- package/dist/reports/index.d.mts +2 -2
- package/dist/reports/index.mjs +2 -2
- package/dist/{trial-balance-DyNm5bFu.d.mts → trial-balance-UXV2PN6x.d.mts} +280 -75
- package/package.json +8 -20
- package/dist/opening-balance-1cixYh6Y.mjs +0 -60
- package/dist/sync/index.d.mts +0 -324
- package/dist/sync/index.mjs +0 -530
- package/dist/sync-JvchM3FO.d.mts +0 -152
- /package/dist/{categories-FJlrvzcl.mjs → categories-CclX7Q94.mjs} +0 -0
- /package/dist/{currencies-Jo5oaM_4.mjs → currencies-OuPHPyS2.mjs} +0 -0
- /package/dist/{exports-C30yRapf.mjs → exports-B3whucXe.mjs} +0 -0
- /package/dist/{index-Bl0gP9lD.d.mts → index-DygMrab0.d.mts} +0 -0
- /package/dist/{index-J-XIbXH-.d.mts → index-pRW5cZhF.d.mts} +0 -0
- /package/dist/{outbox-store-BcCiHMPw.d.mts → outbox-store-CPLeocPg.d.mts} +0 -0
package/dist/index.mjs
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
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
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
|
-
import { a as
|
|
3
|
+
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, u as isReverseMarkClaim } from "./fx-realization.plugin-DY3pPxIi.mjs";
|
|
4
|
+
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-OuPHPyS2.mjs";
|
|
4
5
|
import { Money, add, allocate, format, formatPlain, fromDecimal, multiply, parseCents, percentage, splitTaxExclusive, splitTaxInclusive, subtract, toDecimal } from "./money.mjs";
|
|
5
|
-
import { C as
|
|
6
|
-
import { c as getNormalBalance, d as isValidCategory, l as isBalanceSheet, n as CATEGORY_KEYS, t as CATEGORIES, u as isIncomeStatement } from "./categories-
|
|
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
|
-
import { t as buildOpeningBalanceEntry } from "./opening-balance-1cixYh6Y.mjs";
|
|
6
|
+
import { C as computeEndingBalance, D as requireOrgScope, E as generateAgedBalance, S as calculateTotal, T as DEFAULT_BUCKETS, _ as generateBalanceSheet, a as finalizeSession, b as getFiscalYearStart, c as generateRevaluation, d as generateIncomeStatement, f as generateGeneralLedger, g as generateBudgetVsActual, h as generateCashFlow, i as acquireSession, l as buildRevaluationEntry, m as generateDaybook, n as closeFiscalPeriod, o as defaultLogger, p as generateDimensionBreakdown, r as reopenFiscalPeriod, s as generateTrialBalance, t as generatePartnerLedger, u as computeRevaluation, v as buildItemFilters, w as isVirtualTaxAccount, x as buildAccountTypeMap, y as getDateRange } from "./partner-ledger-B0eym6Ss.mjs";
|
|
7
|
+
import { c as getNormalBalance, d as isValidCategory, l as isBalanceSheet, n as CATEGORY_KEYS, t as CATEGORIES, u as isIncomeStatement } from "./categories-CclX7Q94.mjs";
|
|
9
8
|
import { defineCountryPack } from "./country/index.mjs";
|
|
10
|
-
import { a as exportToCsv, i as quickbooksFieldMap, r as universalFieldMap, t as flattenJournalEntries } from "./exports-
|
|
9
|
+
import { a as exportToCsv, i as quickbooksFieldMap, r as universalFieldMap, t as flattenJournalEntries } from "./exports-B3whucXe.mjs";
|
|
11
10
|
import { QueryParser, Repository, getNextSequence, multiTenantPlugin, withTransaction } from "@classytic/mongokit";
|
|
12
11
|
import mongoose, { Schema } from "mongoose";
|
|
13
|
-
import { resolveTenantConfig } from "@classytic/
|
|
12
|
+
import { resolveTenantConfig } from "@classytic/repo-core/tenant";
|
|
14
13
|
//#region src/plugins/immutable-guard.plugin.ts
|
|
15
14
|
/**
|
|
16
15
|
* Returns a mongokit plugin function. Install only when
|
|
@@ -35,13 +34,24 @@ function immutableGuardPlugin(options) {
|
|
|
35
34
|
if (orgField && ctx.query && orgField in ctx.query) query[orgField] = ctx.query[orgField];
|
|
36
35
|
if ((await JournalEntryModel.findOne(query).select({ state: 1 }).lean())?.state === "posted") throw new ImmutableViolationError(id);
|
|
37
36
|
});
|
|
37
|
+
repo.on("before:claim", async (rawCtx) => {
|
|
38
|
+
const ctx = rawCtx;
|
|
39
|
+
if (ctx._ledgerInternal) return;
|
|
40
|
+
if (isReverseMarkClaim(ctx)) return;
|
|
41
|
+
const transition = ctx.transition;
|
|
42
|
+
if (!transition) return;
|
|
43
|
+
if ((transition.field ?? "state") !== "state") return;
|
|
44
|
+
const fromSpec = transition.from;
|
|
45
|
+
if (!(Array.isArray(fromSpec) ? fromSpec.includes("posted") : fromSpec === "posted")) return;
|
|
46
|
+
throw new ImmutableViolationError(ctx.id);
|
|
47
|
+
});
|
|
38
48
|
};
|
|
39
49
|
}
|
|
40
50
|
//#endregion
|
|
41
51
|
//#region src/models/inject-tenant.ts
|
|
42
52
|
/**
|
|
43
53
|
* Mongoose-specific adapter around `resolveTenantConfig()` from
|
|
44
|
-
* `@classytic/
|
|
54
|
+
* `@classytic/repo-core/tenant`. The pure resolution lives in primitives
|
|
45
55
|
* (zero runtime deps) — this file only handles the Mongoose schema
|
|
46
56
|
* mutations (add field, prepend tenant onto compound indexes) that
|
|
47
57
|
* primitives can't own without a mongoose dependency.
|
|
@@ -134,31 +144,28 @@ function buildCurrencyField(config) {
|
|
|
134
144
|
*
|
|
135
145
|
* Creates a Mongoose schema for Chart of Accounts that is:
|
|
136
146
|
* - Multi-tenant aware (adds org field + compound indexes when configured)
|
|
137
|
-
* - Validates accountTypeCode against the country pack
|
|
138
147
|
* - Supports accountNumber (unique per org) and name (user-facing display)
|
|
139
148
|
* - Lean: no cached balances — always computed from journal entries
|
|
149
|
+
*
|
|
150
|
+
* Country-specific validation (accountTypeCode against the country pack) is
|
|
151
|
+
* intentionally NOT done at the schema layer — schema validators are baked in
|
|
152
|
+
* at model-registration time and would bleed across country engines that share
|
|
153
|
+
* the same connection. Validation lives in wireAccountMethods.before:create.
|
|
140
154
|
*/
|
|
141
155
|
function createAccountSchema(config, options = {}) {
|
|
142
|
-
const { country } = config;
|
|
143
156
|
const scope = resolveLedgerTenant(config);
|
|
144
157
|
const { indexes = true, extraFields = {}, extraIndexes = [] } = options;
|
|
145
158
|
const fields = {
|
|
146
159
|
accountTypeCode: {
|
|
147
160
|
type: String,
|
|
148
|
-
required: true
|
|
149
|
-
validate: {
|
|
150
|
-
validator: (code) => country.isValidAccountType(code),
|
|
151
|
-
message: (props) => `"${props.value}" is not a valid account type code for ${country.name}.`
|
|
152
|
-
}
|
|
161
|
+
required: true
|
|
153
162
|
},
|
|
154
163
|
accountNumber: {
|
|
155
164
|
type: String,
|
|
156
|
-
required: true,
|
|
157
165
|
trim: true
|
|
158
166
|
},
|
|
159
167
|
name: {
|
|
160
168
|
type: String,
|
|
161
|
-
required: true,
|
|
162
169
|
trim: true
|
|
163
170
|
},
|
|
164
171
|
active: {
|
|
@@ -168,6 +175,25 @@ function createAccountSchema(config, options = {}) {
|
|
|
168
175
|
isCashAccount: {
|
|
169
176
|
type: Boolean,
|
|
170
177
|
default: false
|
|
178
|
+
},
|
|
179
|
+
/**
|
|
180
|
+
* Optional per-account override for Cash Flow Statement classification.
|
|
181
|
+
* Wins over the country-pack `account_type` taxonomy. Use case: a
|
|
182
|
+
* "Long-term deferred revenue" account whose type would default to
|
|
183
|
+
* Financing but the business intent is Operating. Mirrors Xero's
|
|
184
|
+
* per-account Cash Flow category override; one of:
|
|
185
|
+
* 'operating' | 'investing' | 'financing' | 'excluded'
|
|
186
|
+
* `null` (default) → fall back to country-pack inference.
|
|
187
|
+
*/
|
|
188
|
+
cashflowSection: {
|
|
189
|
+
type: String,
|
|
190
|
+
enum: [
|
|
191
|
+
"operating",
|
|
192
|
+
"investing",
|
|
193
|
+
"financing",
|
|
194
|
+
"excluded"
|
|
195
|
+
],
|
|
196
|
+
default: null
|
|
171
197
|
}
|
|
172
198
|
};
|
|
173
199
|
const currencyField = buildCurrencyField(config);
|
|
@@ -176,10 +202,6 @@ function createAccountSchema(config, options = {}) {
|
|
|
176
202
|
const schema = new mongoose.Schema(fields, { timestamps: true });
|
|
177
203
|
schema.pre("validate", function() {
|
|
178
204
|
if (!this.accountNumber && this.accountTypeCode) this.accountNumber = this.accountTypeCode;
|
|
179
|
-
if (!this.name && this.accountTypeCode) {
|
|
180
|
-
const at = country.getAccountType(this.accountTypeCode);
|
|
181
|
-
this.name = at?.name ?? this.accountTypeCode;
|
|
182
|
-
}
|
|
183
205
|
});
|
|
184
206
|
if (indexes) {
|
|
185
207
|
schema.index({ active: 1 });
|
|
@@ -192,13 +214,6 @@ function createAccountSchema(config, options = {}) {
|
|
|
192
214
|
}
|
|
193
215
|
//#endregion
|
|
194
216
|
//#region src/schemas/budget.schema.ts
|
|
195
|
-
/**
|
|
196
|
-
* Budget Schema Factory
|
|
197
|
-
*
|
|
198
|
-
* Creates a Mongoose schema for budget records.
|
|
199
|
-
* Each record represents a budgeted amount for an account over a specific period.
|
|
200
|
-
* All monetary amounts are in integer cents.
|
|
201
|
-
*/
|
|
202
217
|
function createBudgetSchema(config, options = {}) {
|
|
203
218
|
const scope = resolveLedgerTenant(config);
|
|
204
219
|
const { indexes = true, extraFields = {}, extraIndexes = [] } = options;
|
|
@@ -228,6 +243,10 @@ function createBudgetSchema(config, options = {}) {
|
|
|
228
243
|
type: String,
|
|
229
244
|
default: null
|
|
230
245
|
},
|
|
246
|
+
approvals: {
|
|
247
|
+
type: mongoose.Schema.Types.Mixed,
|
|
248
|
+
default: null
|
|
249
|
+
},
|
|
231
250
|
...extraFields
|
|
232
251
|
};
|
|
233
252
|
const schema = new mongoose.Schema(fields, { timestamps: true });
|
|
@@ -457,6 +476,47 @@ function createJournalSchema(config, accountModelName, options = {}) {
|
|
|
457
476
|
* - Double-entry validation on post
|
|
458
477
|
* - Optimized indexes for high-load reporting
|
|
459
478
|
*/
|
|
479
|
+
/**
|
|
480
|
+
* Recommended opt-in indexes for the line-level provenance fields
|
|
481
|
+
* (`journalItems.sourceRef.*` + `journalItems.linkedRefs.*`).
|
|
482
|
+
*
|
|
483
|
+
* Schema fields ship in the core schema unconditionally. Index creation
|
|
484
|
+
* costs writes on every JE insert, so the package ships them as opt-in:
|
|
485
|
+
* spread this into `schemaOptions.journalEntry.extraIndexes` to enable
|
|
486
|
+
* fast `/by-source` lookups against the line-level slots.
|
|
487
|
+
*
|
|
488
|
+
* Both indexes are sparse + partial — only lines that actually carry a
|
|
489
|
+
* sourceModel are indexed. Hosts that never write line-level provenance
|
|
490
|
+
* pay zero index storage / no insert overhead.
|
|
491
|
+
*
|
|
492
|
+
* @example
|
|
493
|
+
* import { createAccountingEngine, LINE_SOURCE_INDEXES } from '@classytic/ledger';
|
|
494
|
+
* createAccountingEngine({
|
|
495
|
+
* schemaOptions: {
|
|
496
|
+
* journalEntry: { extraIndexes: [...LINE_SOURCE_INDEXES] },
|
|
497
|
+
* },
|
|
498
|
+
* });
|
|
499
|
+
*/
|
|
500
|
+
const LINE_SOURCE_INDEXES = [{
|
|
501
|
+
fields: {
|
|
502
|
+
"journalItems.sourceRef.sourceModel": 1,
|
|
503
|
+
"journalItems.sourceRef.sourceId": 1
|
|
504
|
+
},
|
|
505
|
+
options: {
|
|
506
|
+
sparse: true,
|
|
507
|
+
partialFilterExpression: { "journalItems.sourceRef.sourceModel": { $type: "string" } },
|
|
508
|
+
name: "journalItems_sourceRef_idx"
|
|
509
|
+
}
|
|
510
|
+
}, {
|
|
511
|
+
fields: {
|
|
512
|
+
"journalItems.linkedRefs.sourceModel": 1,
|
|
513
|
+
"journalItems.linkedRefs.sourceId": 1
|
|
514
|
+
},
|
|
515
|
+
options: {
|
|
516
|
+
sparse: true,
|
|
517
|
+
name: "journalItems_linkedRefs_idx"
|
|
518
|
+
}
|
|
519
|
+
}];
|
|
460
520
|
function createJournalEntrySchema(config, accountModelName, options = {}) {
|
|
461
521
|
const { multiTenant } = config;
|
|
462
522
|
const scope = resolveLedgerTenant(config);
|
|
@@ -465,6 +525,16 @@ function createJournalEntrySchema(config, accountModelName, options = {}) {
|
|
|
465
525
|
taxCode: { type: String },
|
|
466
526
|
taxName: { type: String }
|
|
467
527
|
}, { _id: false });
|
|
528
|
+
const ItemSourceRefSchema = new mongoose.Schema({
|
|
529
|
+
sourceModel: {
|
|
530
|
+
type: String,
|
|
531
|
+
default: null
|
|
532
|
+
},
|
|
533
|
+
sourceId: {
|
|
534
|
+
type: String,
|
|
535
|
+
default: null
|
|
536
|
+
}
|
|
537
|
+
}, { _id: false });
|
|
468
538
|
const amountValidator = {
|
|
469
539
|
validator: (v) => Number.isInteger(v) && v >= 0,
|
|
470
540
|
message: "{PATH} must be a non-negative integer (cents), got {VALUE}"
|
|
@@ -528,6 +598,18 @@ function createJournalEntrySchema(config, accountModelName, options = {}) {
|
|
|
528
598
|
type: Date,
|
|
529
599
|
default: null
|
|
530
600
|
},
|
|
601
|
+
meta: {
|
|
602
|
+
type: mongoose.Schema.Types.Mixed,
|
|
603
|
+
default: null
|
|
604
|
+
},
|
|
605
|
+
sourceRef: {
|
|
606
|
+
type: ItemSourceRefSchema,
|
|
607
|
+
default: () => ({})
|
|
608
|
+
},
|
|
609
|
+
linkedRefs: {
|
|
610
|
+
type: [ItemSourceRefSchema],
|
|
611
|
+
default: void 0
|
|
612
|
+
},
|
|
531
613
|
...currencyItemFields,
|
|
532
614
|
...extraItemFields
|
|
533
615
|
}, { _id: false });
|
|
@@ -602,6 +684,10 @@ function createJournalEntrySchema(config, accountModelName, options = {}) {
|
|
|
602
684
|
ref: "JournalEntry",
|
|
603
685
|
default: null
|
|
604
686
|
},
|
|
687
|
+
approvals: {
|
|
688
|
+
type: mongoose.Schema.Types.Mixed,
|
|
689
|
+
default: null
|
|
690
|
+
},
|
|
605
691
|
...extraFields
|
|
606
692
|
};
|
|
607
693
|
if (config.audit?.trackActor) {
|
|
@@ -704,14 +790,6 @@ function createJournalEntrySchema(config, accountModelName, options = {}) {
|
|
|
704
790
|
state: 1
|
|
705
791
|
});
|
|
706
792
|
schema.index({ reversed: 1 });
|
|
707
|
-
if (config.idempotency) {
|
|
708
|
-
const ttlSeconds = typeof config.idempotencyTtlSeconds === "number" && config.idempotencyTtlSeconds > 0 ? config.idempotencyTtlSeconds : 86400;
|
|
709
|
-
schema.index({ createdAt: 1 }, {
|
|
710
|
-
name: "idempotency_ttl_idx",
|
|
711
|
-
expireAfterSeconds: ttlSeconds,
|
|
712
|
-
partialFilterExpression: { idempotencyKey: { $type: "string" } }
|
|
713
|
-
});
|
|
714
|
-
}
|
|
715
793
|
}
|
|
716
794
|
if (textSearch) schema.index({
|
|
717
795
|
referenceNumber: "text",
|
|
@@ -899,9 +977,14 @@ function createModels(connection, config) {
|
|
|
899
977
|
};
|
|
900
978
|
}
|
|
901
979
|
//#endregion
|
|
902
|
-
//#region src/
|
|
903
|
-
|
|
904
|
-
|
|
980
|
+
//#region src/utils/safe-publish.ts
|
|
981
|
+
/**
|
|
982
|
+
* Persist and publish a ledger domain event without letting delivery failures
|
|
983
|
+
* break the write path. When an outbox is configured, the outbox row is saved
|
|
984
|
+
* first and participates in the caller's mongoose session.
|
|
985
|
+
*/
|
|
986
|
+
async function safePublish(events, outboxStore, type, payload, ctx, meta) {
|
|
987
|
+
const event = createEvent(type, payload, ctx, meta);
|
|
905
988
|
if (outboxStore) try {
|
|
906
989
|
await outboxStore.save(event, { session: ctx?.session ?? void 0 });
|
|
907
990
|
} catch {}
|
|
@@ -909,6 +992,8 @@ async function safePublish$3(events, outboxStore, type, payload, ctx) {
|
|
|
909
992
|
await events.publish(event);
|
|
910
993
|
} catch {}
|
|
911
994
|
}
|
|
995
|
+
//#endregion
|
|
996
|
+
//#region src/repositories/account.repository.ts
|
|
912
997
|
function isDuplicateKeyBulkError(err) {
|
|
913
998
|
if (!err || typeof err !== "object") return false;
|
|
914
999
|
const e = err;
|
|
@@ -928,9 +1013,32 @@ function isDuplicateKeyBulkError(err) {
|
|
|
928
1013
|
function wireAccountMethods(repository, country, orgField, integrations = {}) {
|
|
929
1014
|
const events = integrations.events;
|
|
930
1015
|
const outboxStore = integrations.outboxStore;
|
|
931
|
-
|
|
932
|
-
|
|
1016
|
+
const journalEntryModel = integrations.journalEntryModel;
|
|
1017
|
+
repository.on("before:create", async (ctx) => {
|
|
1018
|
+
const data = ctx.data;
|
|
1019
|
+
const code = data?.accountTypeCode;
|
|
933
1020
|
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.`);
|
|
1021
|
+
if (data && !data.name && code) {
|
|
1022
|
+
const at = country.getAccountType(code);
|
|
1023
|
+
if (at) data.name = at.name ?? code;
|
|
1024
|
+
}
|
|
1025
|
+
if (!data) return;
|
|
1026
|
+
const accountNumber = data.accountNumber ?? code;
|
|
1027
|
+
if (!accountNumber) return;
|
|
1028
|
+
const filter = { accountNumber };
|
|
1029
|
+
if (orgField) {
|
|
1030
|
+
const orgId = data[orgField] ?? ctx.organizationId;
|
|
1031
|
+
if (orgId != null) filter[orgField] = orgId;
|
|
1032
|
+
}
|
|
1033
|
+
if (await repository.getByQuery(filter, {
|
|
1034
|
+
throwOnNotFound: false,
|
|
1035
|
+
lean: true
|
|
1036
|
+
})) throw Errors.validation(`Account number "${accountNumber}" already exists. Provide a custom accountNumber (e.g. "${accountNumber}-NORTH") to create a sub-account.`);
|
|
1037
|
+
});
|
|
1038
|
+
if (journalEntryModel) repository.on("before:delete", async (ctx) => {
|
|
1039
|
+
const id = ctx.id;
|
|
1040
|
+
if (id == null) return;
|
|
1041
|
+
if (await journalEntryModel.exists({ "journalItems.account": id })) throw Errors.validation(`Cannot delete account ${String(id)} — it is referenced by one or more journal entries. Set active: false to retire it instead.`);
|
|
934
1042
|
});
|
|
935
1043
|
/**
|
|
936
1044
|
* Seed standard posting accounts for an organization.
|
|
@@ -977,7 +1085,7 @@ function wireAccountMethods(repository, country, orgField, integrations = {}) {
|
|
|
977
1085
|
};
|
|
978
1086
|
} else throw err;
|
|
979
1087
|
}
|
|
980
|
-
await safePublish
|
|
1088
|
+
await safePublish(events, outboxStore, LEDGER_EVENTS.ACCOUNT_SEEDED, {
|
|
981
1089
|
created: result.created,
|
|
982
1090
|
skipped: result.skipped,
|
|
983
1091
|
organizationId: orgId
|
|
@@ -1127,7 +1235,7 @@ function wireAccountMethods(repository, country, orgField, integrations = {}) {
|
|
|
1127
1235
|
skipped: results.skipped.length,
|
|
1128
1236
|
errors: results.errors.length
|
|
1129
1237
|
};
|
|
1130
|
-
await safePublish
|
|
1238
|
+
await safePublish(events, outboxStore, LEDGER_EVENTS.ACCOUNT_BULK_CREATED, {
|
|
1131
1239
|
created: summary.created,
|
|
1132
1240
|
skipped: summary.skipped,
|
|
1133
1241
|
errors: summary.errors,
|
|
@@ -1151,15 +1259,6 @@ function wireAccountMethods(repository, country, orgField, integrations = {}) {
|
|
|
1151
1259
|
}
|
|
1152
1260
|
//#endregion
|
|
1153
1261
|
//#region src/repositories/journal.repository.ts
|
|
1154
|
-
async function safePublish$2(events, outboxStore, type, payload, ctx) {
|
|
1155
|
-
const event = createEvent(type, payload, ctx);
|
|
1156
|
-
if (outboxStore) try {
|
|
1157
|
-
await outboxStore.save(event);
|
|
1158
|
-
} catch {}
|
|
1159
|
-
if (events) try {
|
|
1160
|
-
await events.publish(event);
|
|
1161
|
-
} catch {}
|
|
1162
|
-
}
|
|
1163
1262
|
/**
|
|
1164
1263
|
* Lean default set used when a country pack doesn't provide
|
|
1165
1264
|
* `journalTemplates`. Covers the Stripe/QuickBooks/Xero baseline.
|
|
@@ -1231,7 +1330,7 @@ function wireJournalMethods(repository, country, orgField, integrations = {}) {
|
|
|
1231
1330
|
await create(data);
|
|
1232
1331
|
created += 1;
|
|
1233
1332
|
}
|
|
1234
|
-
await safePublish
|
|
1333
|
+
await safePublish(events, outboxStore, LEDGER_EVENTS.JOURNAL_SEEDED, {
|
|
1235
1334
|
created,
|
|
1236
1335
|
skipped,
|
|
1237
1336
|
organizationId: orgId
|
|
@@ -1265,25 +1364,6 @@ function wireJournalMethods(repository, country, orgField, integrations = {}) {
|
|
|
1265
1364
|
}
|
|
1266
1365
|
//#endregion
|
|
1267
1366
|
//#region src/repositories/journal-entry.repository.ts
|
|
1268
|
-
/**
|
|
1269
|
-
* Publish a domain event. When an outbox store is provided, first persist
|
|
1270
|
-
* the event inside the caller's session (so outbox + ledger write commit
|
|
1271
|
-
* atomically), then fire-and-forget publish to the transport. Without an
|
|
1272
|
-
* outbox, publish-only, still fire-and-forget — transport errors never
|
|
1273
|
-
* propagate into ledger mutations.
|
|
1274
|
-
*
|
|
1275
|
-
* Tracks PACKAGE_RULES §16 (host-composed transactional outbox) and §14
|
|
1276
|
-
* (domain verbs publish via injected transport).
|
|
1277
|
-
*/
|
|
1278
|
-
async function safePublish$1(events, outboxStore, type, payload, ctx, meta) {
|
|
1279
|
-
const event = createEvent(type, payload, ctx, meta);
|
|
1280
|
-
if (outboxStore) try {
|
|
1281
|
-
await outboxStore.save(event, { session: ctx?.session ?? void 0 });
|
|
1282
|
-
} catch {}
|
|
1283
|
-
if (events) try {
|
|
1284
|
-
await events.publish(event);
|
|
1285
|
-
} catch {}
|
|
1286
|
-
}
|
|
1287
1367
|
/** Keys that are either handled explicitly or must not be copied */
|
|
1288
1368
|
const ITEM_CORE_KEYS = new Set([
|
|
1289
1369
|
"account",
|
|
@@ -1311,7 +1391,8 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
|
|
|
1311
1391
|
const outboxStore = integrations.outboxStore;
|
|
1312
1392
|
const getByQuery = repository.getByQuery.bind(repository);
|
|
1313
1393
|
const baseCreate = repository.create.bind(repository);
|
|
1314
|
-
|
|
1394
|
+
repository.update.bind(repository);
|
|
1395
|
+
const claim = repository.claim.bind(repository);
|
|
1315
1396
|
const withTransaction$1 = (fn, opts) => withTransaction(repository.Model.db, fn, opts);
|
|
1316
1397
|
const raceSafeCreate = async (data, options) => {
|
|
1317
1398
|
const input = data;
|
|
@@ -1416,13 +1497,37 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
|
|
|
1416
1497
|
return await getByQuery(query, opts);
|
|
1417
1498
|
}
|
|
1418
1499
|
/**
|
|
1500
|
+
* Build the options bag passed to `repo.claim()` from the call's
|
|
1501
|
+
* org/actor/session context. mongokit's `multiTenantPlugin` reads
|
|
1502
|
+
* `options.organizationId`, audit plugins read `options.userId`, and
|
|
1503
|
+
* the transaction layer reads `options.session` — same shape we'd
|
|
1504
|
+
* forward via `repoOptionsFromCtx(ctx)` from a host route.
|
|
1505
|
+
*/
|
|
1506
|
+
function buildClaimOptions(orgId, actorId, session) {
|
|
1507
|
+
const opts = {};
|
|
1508
|
+
if (session) opts.session = session;
|
|
1509
|
+
if (orgField && orgId != null) opts.organizationId = orgId;
|
|
1510
|
+
if (actorId !== void 0 && actorId !== null) opts.userId = actorId;
|
|
1511
|
+
return opts;
|
|
1512
|
+
}
|
|
1513
|
+
/**
|
|
1514
|
+
* Build the `where` predicate for a state-transition claim. Encodes the
|
|
1515
|
+
* tenant-scope guard so the CAS only matches docs in the caller's org.
|
|
1516
|
+
*/
|
|
1517
|
+
function buildClaimWhere(orgId, extra) {
|
|
1518
|
+
const where = { ...extra ?? {} };
|
|
1519
|
+
if (orgField && orgId != null) where[orgField] = orgId;
|
|
1520
|
+
return where;
|
|
1521
|
+
}
|
|
1522
|
+
/**
|
|
1419
1523
|
* Post an entry (draft → posted).
|
|
1420
1524
|
* Validates items, balance, and accounts before changing state.
|
|
1421
1525
|
*/
|
|
1422
1526
|
repository.post = async (id, orgId, options = {}) => {
|
|
1423
1527
|
if (strictness?.requireActor && !options.actorId) throw Errors.validation("actorId is required for post operations.");
|
|
1424
1528
|
requireOrgScope(orgField, orgId);
|
|
1425
|
-
const
|
|
1529
|
+
const query = buildQuery(id, orgId);
|
|
1530
|
+
const entry = await findEntry(query, {
|
|
1426
1531
|
session: options.session,
|
|
1427
1532
|
populate: "journalItems.account"
|
|
1428
1533
|
});
|
|
@@ -1458,17 +1563,22 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
|
|
|
1458
1563
|
const totalDebit = entry.journalItems.reduce((s, i) => s + (i.debit || 0), 0);
|
|
1459
1564
|
const totalCredit = entry.journalItems.reduce((s, i) => s + (i.credit || 0), 0);
|
|
1460
1565
|
if (totalDebit !== totalCredit) throw Errors.validation(`Entry is not balanced. Debit: ${totalDebit}, Credit: ${totalCredit}`);
|
|
1461
|
-
const
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1566
|
+
const $set = { stateChangedAt: /* @__PURE__ */ new Date() };
|
|
1567
|
+
if (options.actorId) $set.postedBy = options.actorId;
|
|
1568
|
+
const claimed = await claim(entry._id, {
|
|
1569
|
+
field: "state",
|
|
1570
|
+
from: "draft",
|
|
1571
|
+
to: "posted",
|
|
1572
|
+
where: buildClaimWhere(orgId)
|
|
1573
|
+
}, { $set }, buildClaimOptions(orgId, options.actorId, options.session ?? null));
|
|
1574
|
+
let final;
|
|
1575
|
+
if (!claimed) {
|
|
1576
|
+
const reread = await findEntry(query, { session: options.session });
|
|
1577
|
+
if (reread && reread.state === "posted") final = reread;
|
|
1578
|
+
else if (reread) throw new ConcurrencyError("JournalEntry", String(entry._id));
|
|
1579
|
+
else throw Errors.notFound("Entry not found");
|
|
1580
|
+
} else final = claimed;
|
|
1581
|
+
await safePublish(events, outboxStore, LEDGER_EVENTS.ENTRY_POSTED, {
|
|
1472
1582
|
entryId: final._id,
|
|
1473
1583
|
referenceNumber: final.referenceNumber,
|
|
1474
1584
|
postedBy: options.actorId,
|
|
@@ -1494,19 +1604,25 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
|
|
|
1494
1604
|
if (strictness?.immutable) throw Errors.immutable("Unpost is disabled in strict mode. Use reverse() to correct posted entries.");
|
|
1495
1605
|
if (strictness?.requireActor && !options.actorId) throw Errors.validation("actorId is required for unpost operations.");
|
|
1496
1606
|
requireOrgScope(orgField, orgId);
|
|
1497
|
-
const
|
|
1607
|
+
const query = buildQuery(id, orgId);
|
|
1608
|
+
const entry = await findEntry(query, { session: options.session });
|
|
1498
1609
|
if (!entry) throw Errors.notFound("Entry not found");
|
|
1499
1610
|
if (entry.state !== "posted") throw Errors.validation("Only posted entries can be unposted");
|
|
1500
1611
|
if (entry.reversed) throw Errors.validation("Cannot unpost a reversed entry. The reversal entry is still posted and linked to this entry. Reverse the reversal entry first, or create a new correcting entry instead.");
|
|
1501
|
-
const
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1612
|
+
const claimed = await claim(entry._id, {
|
|
1613
|
+
field: "state",
|
|
1614
|
+
from: "posted",
|
|
1615
|
+
to: "draft",
|
|
1616
|
+
where: buildClaimWhere(orgId, { reversed: { $ne: true } })
|
|
1617
|
+
}, { $set: { stateChangedAt: /* @__PURE__ */ new Date() } }, buildClaimOptions(orgId, options.actorId, options.session ?? null));
|
|
1618
|
+
let final;
|
|
1619
|
+
if (!claimed) {
|
|
1620
|
+
const reread = await findEntry(query, { session: options.session });
|
|
1621
|
+
if (reread && reread.state === "draft") final = reread;
|
|
1622
|
+
else if (reread) throw new ConcurrencyError("JournalEntry", String(entry._id));
|
|
1623
|
+
else throw Errors.notFound("Entry not found");
|
|
1624
|
+
} else final = claimed;
|
|
1625
|
+
await safePublish(events, outboxStore, LEDGER_EVENTS.ENTRY_UNPOSTED, {
|
|
1510
1626
|
entryId: final._id,
|
|
1511
1627
|
unpostedBy: options.actorId,
|
|
1512
1628
|
organizationId: orgId
|
|
@@ -1528,18 +1644,24 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
|
|
|
1528
1644
|
repository.archive = async (id, orgId, options = {}) => {
|
|
1529
1645
|
if (strictness?.requireActor && !options.actorId) throw Errors.validation("actorId is required for archive operations.");
|
|
1530
1646
|
requireOrgScope(orgField, orgId);
|
|
1531
|
-
const
|
|
1647
|
+
const query = buildQuery(id, orgId);
|
|
1648
|
+
const entry = await findEntry(query, { session: options.session });
|
|
1532
1649
|
if (!entry) throw Errors.notFound("Entry not found");
|
|
1533
1650
|
if (entry.state !== "draft") throw Errors.validation("Only draft entries can be archived");
|
|
1534
|
-
const
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1651
|
+
const claimed = await claim(entry._id, {
|
|
1652
|
+
field: "state",
|
|
1653
|
+
from: "draft",
|
|
1654
|
+
to: "archived",
|
|
1655
|
+
where: buildClaimWhere(orgId)
|
|
1656
|
+
}, { $set: { stateChangedAt: /* @__PURE__ */ new Date() } }, buildClaimOptions(orgId, options.actorId, options.session ?? null));
|
|
1657
|
+
let final;
|
|
1658
|
+
if (!claimed) {
|
|
1659
|
+
const reread = await findEntry(query, { session: options.session });
|
|
1660
|
+
if (reread && reread.state === "archived") final = reread;
|
|
1661
|
+
else if (reread) throw new ConcurrencyError("JournalEntry", String(entry._id));
|
|
1662
|
+
else throw Errors.notFound("Entry not found");
|
|
1663
|
+
} else final = claimed;
|
|
1664
|
+
await safePublish(events, outboxStore, LEDGER_EVENTS.ENTRY_ARCHIVED, {
|
|
1543
1665
|
entryId: final._id,
|
|
1544
1666
|
archivedBy: options.actorId,
|
|
1545
1667
|
organizationId: orgId
|
|
@@ -1584,7 +1706,7 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
|
|
|
1584
1706
|
copyExtraTopLevel(entry, duplicateData);
|
|
1585
1707
|
const duplicated = await create(duplicateData, options.session ? { session: options.session } : {});
|
|
1586
1708
|
const dup = duplicated;
|
|
1587
|
-
await safePublish
|
|
1709
|
+
await safePublish(events, outboxStore, LEDGER_EVENTS.ENTRY_DUPLICATED, {
|
|
1588
1710
|
sourceEntryId: entry._id,
|
|
1589
1711
|
duplicateEntryId: dup._id,
|
|
1590
1712
|
organizationId: orgId
|
|
@@ -1638,29 +1760,39 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
|
|
|
1638
1760
|
const totalCredit = reversalItems.reduce((s, i) => s + i.credit, 0);
|
|
1639
1761
|
const reversalData = {
|
|
1640
1762
|
journalType: entry.journalType ?? "MISC",
|
|
1641
|
-
state: "posted",
|
|
1642
1763
|
date: options.reversalDate ?? /* @__PURE__ */ new Date(),
|
|
1643
1764
|
label: `Reversal of ${entry.referenceNumber ?? entry._id}`,
|
|
1644
1765
|
journalItems: reversalItems,
|
|
1645
1766
|
totalDebit,
|
|
1646
1767
|
totalCredit,
|
|
1647
|
-
reversalOf: entry._id
|
|
1648
|
-
stateChangedAt: /* @__PURE__ */ new Date()
|
|
1768
|
+
reversalOf: entry._id
|
|
1649
1769
|
};
|
|
1650
1770
|
copyExtraTopLevel(entry, reversalData);
|
|
1651
|
-
|
|
1652
|
-
const
|
|
1653
|
-
|
|
1771
|
+
let reversalEntry = await create(reversalData, session ? { session } : {});
|
|
1772
|
+
const postFn = repository.post;
|
|
1773
|
+
if (options.autoPost && postFn) {
|
|
1774
|
+
const posted = await postFn(reversalEntry._id, orgId, {
|
|
1775
|
+
actorId: options.actorId,
|
|
1776
|
+
...session ? { session } : {}
|
|
1777
|
+
});
|
|
1778
|
+
if (posted) reversalEntry = posted;
|
|
1779
|
+
}
|
|
1780
|
+
const $set = {
|
|
1654
1781
|
reversed: true,
|
|
1655
1782
|
reversedBy: reversalEntry._id
|
|
1656
1783
|
};
|
|
1657
|
-
if (options.actorId)
|
|
1658
|
-
const
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
const original = await
|
|
1663
|
-
|
|
1784
|
+
if (options.actorId) $set.reversedByUser = options.actorId;
|
|
1785
|
+
const claimOpts = {};
|
|
1786
|
+
if (session) claimOpts.session = session;
|
|
1787
|
+
if (orgField && orgId != null) claimOpts.organizationId = orgId;
|
|
1788
|
+
if (options.actorId) claimOpts.userId = options.actorId;
|
|
1789
|
+
const original = await claim(entry._id, {
|
|
1790
|
+
field: "state",
|
|
1791
|
+
from: "posted",
|
|
1792
|
+
to: "posted",
|
|
1793
|
+
where: { reversed: { $ne: true } }
|
|
1794
|
+
}, { $set }, claimOpts) ?? entry;
|
|
1795
|
+
await safePublish(events, outboxStore, LEDGER_EVENTS.ENTRY_REVERSED, {
|
|
1664
1796
|
originalEntryId: original._id,
|
|
1665
1797
|
reversalEntryId: reversalEntry._id,
|
|
1666
1798
|
reversalDate: reversalData.date ?? /* @__PURE__ */ new Date(),
|
|
@@ -1703,44 +1835,54 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
|
|
|
1703
1835
|
}
|
|
1704
1836
|
//#endregion
|
|
1705
1837
|
//#region src/repositories/reconciliation.repository.ts
|
|
1706
|
-
async function safePublish(events, outboxStore, type, payload, ctx) {
|
|
1707
|
-
const event = createEvent(type, payload, ctx);
|
|
1708
|
-
if (outboxStore) try {
|
|
1709
|
-
await outboxStore.save(event, { session: ctx?.session ?? void 0 });
|
|
1710
|
-
} catch {}
|
|
1711
|
-
if (events) try {
|
|
1712
|
-
await events.publish(event);
|
|
1713
|
-
} catch {}
|
|
1714
|
-
}
|
|
1715
1838
|
/**
|
|
1716
|
-
*
|
|
1717
|
-
*
|
|
1718
|
-
*
|
|
1719
|
-
*
|
|
1839
|
+
* Reconciliation Repository Factory (0.6.0 — item-level open-item matching)
|
|
1840
|
+
*
|
|
1841
|
+
* Implements the three new primitives:
|
|
1842
|
+
*
|
|
1843
|
+
* - `match({ account, items, ... })` stamps a shared matchingNumber onto
|
|
1844
|
+
* every referenced item and creates a reconciliation document.
|
|
1845
|
+
* Triggers `after:match` hook for downstream plugins (fxRealization,
|
|
1846
|
+
* cash-basis exigibility).
|
|
1847
|
+
*
|
|
1848
|
+
* - `unmatch({ matchingNumber })` clears the matching number from every
|
|
1849
|
+
* referenced item and removes the reconciliation. If an FX realization
|
|
1850
|
+
* entry was booked, it is reversed via journalEntries.reverse.
|
|
1851
|
+
*
|
|
1852
|
+
* - `getOpenItems({ accountId })` returns posted journal items against
|
|
1853
|
+
* the account that have no matchingNumber yet. Backed by the sparse
|
|
1854
|
+
* index on `journalItems.matchingNumber`.
|
|
1855
|
+
*
|
|
1856
|
+
* Matching numbers auto-generate as `RECN-{n}` if the caller doesn't
|
|
1857
|
+
* supply one. Uniqueness is enforced by the org-scoped unique index on
|
|
1858
|
+
* the reconciliation collection.
|
|
1720
1859
|
*/
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1860
|
+
/**
|
|
1861
|
+
* Default matching-number generator — delegates to mongokit's
|
|
1862
|
+
* `getNextSequence(counterKey, 1, connection, session)` which atomically
|
|
1863
|
+
* bumps a counter row in the shared `_mongokit_counters` collection. Same
|
|
1864
|
+
* primitive that backs `journalEntries.referenceNumber` allocation in this
|
|
1865
|
+
* package (see `journal-entry.schema.ts:431`) and the per-package id
|
|
1866
|
+
* generators in `@classytic/invoice`, `order`, `cart`, `revenue`.
|
|
1867
|
+
*
|
|
1868
|
+
* Why this replaced the in-collection sentinel pattern (pre-0.10.4): the
|
|
1869
|
+
* sentinel approach stored the counter as a synthetic `matchingNumber:
|
|
1870
|
+
* '__counter__'` doc in the reconciliation collection itself, with the
|
|
1871
|
+
* counter slot as `seq`. After the 0.10.x refactor that routed the bump
|
|
1872
|
+
* through `repository.findOneAndUpdate(...)` (to flow through the plugin
|
|
1873
|
+
* pipeline), mongoose strict mode silently dropped `$inc: { seq: 1 }`
|
|
1874
|
+
* because `seq` was never declared in the schema — every call returned
|
|
1875
|
+
* `undefined`, fell back to `1`, and every match resolved to `RECN-000001`.
|
|
1876
|
+
* The first match per engine succeeded; the second collided on the unique
|
|
1877
|
+
* index. `getNextSequence` sidesteps this entirely: dedicated counter
|
|
1878
|
+
* collection, no schema pollution, session-aware (counter rolls back if
|
|
1879
|
+
* the calling transaction aborts), multi-tenant via key prefix.
|
|
1880
|
+
*/
|
|
1881
|
+
async function nextMatchingNumber(connection, orgField, orgId, session) {
|
|
1882
|
+
let orgScope = "global";
|
|
1883
|
+
if (orgField && orgId != null) orgScope = typeof orgId.toHexString === "function" ? orgId.toHexString() : String(orgId);
|
|
1884
|
+
else if (orgField) orgScope = "unscoped";
|
|
1885
|
+
const seq = await getNextSequence(`ledger:${orgScope}:matchingNumber`, 1, connection, session ?? void 0);
|
|
1744
1886
|
return `RECN-${String(seq).padStart(6, "0")}`;
|
|
1745
1887
|
}
|
|
1746
1888
|
function wireReconciliationMethods(repository, ReconciliationModel, JournalEntryModel, orgField, integrations = {}) {
|
|
@@ -1794,7 +1936,7 @@ function wireReconciliationMethods(repository, ReconciliationModel, JournalEntry
|
|
|
1794
1936
|
const difference = debitTotal - creditTotal;
|
|
1795
1937
|
const isFullReconcile = difference === 0;
|
|
1796
1938
|
const sharedCurrency = currencies.size === 1 ? Array.from(currencies)[0] : null;
|
|
1797
|
-
if (!matchingNumber) matchingNumber = await nextMatchingNumber(ReconciliationModel, orgField, organizationId, session);
|
|
1939
|
+
if (!matchingNumber) matchingNumber = await nextMatchingNumber(ReconciliationModel.db, orgField, organizationId, session);
|
|
1798
1940
|
const hookCtx = {
|
|
1799
1941
|
input,
|
|
1800
1942
|
items: itemSnapshots,
|
|
@@ -1884,7 +2026,7 @@ function wireReconciliationMethods(repository, ReconciliationModel, JournalEntry
|
|
|
1884
2026
|
} }));
|
|
1885
2027
|
if (bulkOps.length > 0) await JournalEntryModel.bulkWrite(bulkOps, { session: session ?? void 0 });
|
|
1886
2028
|
const result = await deleteById(String(existing._id));
|
|
1887
|
-
if (!result
|
|
2029
|
+
if (!result) throw Errors.notFound("Failed to delete reconciliation record");
|
|
1888
2030
|
await emitHook("after:unmatch", unmatchCtx);
|
|
1889
2031
|
await safePublish(events, outboxStore, LEDGER_EVENTS.RECONCILIATION_UNMATCHED, {
|
|
1890
2032
|
matchingNumber,
|
|
@@ -1979,13 +2121,17 @@ function createRepositories(models, config, plugins = {}, pagination = {}, integ
|
|
|
1979
2121
|
const strictness = config.strictness;
|
|
1980
2122
|
const country = config.country;
|
|
1981
2123
|
const { events, bridges, outboxStore } = integrations;
|
|
2124
|
+
const tenantFieldType = config.tenantFieldType ?? "objectId";
|
|
1982
2125
|
const tenantPlugins = [];
|
|
1983
|
-
if (orgField && config.multiTenant?.plugin)
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
2126
|
+
if (orgField && config.multiTenant?.plugin) {
|
|
2127
|
+
const contextKey = config.multiTenant.contextKey ?? "organizationId";
|
|
2128
|
+
tenantPlugins.push(multiTenantPlugin({
|
|
2129
|
+
tenantField: orgField,
|
|
2130
|
+
contextKey,
|
|
2131
|
+
required: config.multiTenant.required ?? false,
|
|
2132
|
+
fieldType: tenantFieldType
|
|
2133
|
+
}));
|
|
2134
|
+
}
|
|
1989
2135
|
const accountPagination = pagination.account ?? {};
|
|
1990
2136
|
const jePagination = pagination.journalEntry ?? {};
|
|
1991
2137
|
const fpPagination = pagination.fiscalPeriod ?? {};
|
|
@@ -1994,7 +2140,8 @@ function createRepositories(models, config, plugins = {}, pagination = {}, integ
|
|
|
1994
2140
|
const accounts = wireAccountMethods(new Repository(models.Account, [...tenantPlugins, ...plugins.account ?? []], accountPagination), country, orgField, {
|
|
1995
2141
|
events,
|
|
1996
2142
|
bridges,
|
|
1997
|
-
outboxStore
|
|
2143
|
+
outboxStore,
|
|
2144
|
+
journalEntryModel: models.JournalEntry
|
|
1998
2145
|
});
|
|
1999
2146
|
const jePlugins = [
|
|
2000
2147
|
...tenantPlugins,
|
|
@@ -2368,6 +2515,65 @@ function buildIntrospectAPI({ models, country, config }) {
|
|
|
2368
2515
|
};
|
|
2369
2516
|
}
|
|
2370
2517
|
//#endregion
|
|
2518
|
+
//#region src/builders/opening-balance.ts
|
|
2519
|
+
function buildOpeningBalanceEntry(input) {
|
|
2520
|
+
const { cutoverDate, balances, equityAccountCode } = input;
|
|
2521
|
+
const dateStr = cutoverDate.toISOString().split("T")[0];
|
|
2522
|
+
const label = input.label ?? `Opening Balance — Cutover ${dateStr}`;
|
|
2523
|
+
const items = [];
|
|
2524
|
+
let totalDebit = 0;
|
|
2525
|
+
let totalCredit = 0;
|
|
2526
|
+
for (const { accountCode, balance } of balances) {
|
|
2527
|
+
if (balance === 0) continue;
|
|
2528
|
+
if (balance > 0) {
|
|
2529
|
+
items.push({
|
|
2530
|
+
account: accountCode,
|
|
2531
|
+
debit: balance,
|
|
2532
|
+
credit: 0,
|
|
2533
|
+
label: "Opening balance"
|
|
2534
|
+
});
|
|
2535
|
+
totalDebit += balance;
|
|
2536
|
+
} else {
|
|
2537
|
+
const absBalance = Math.abs(balance);
|
|
2538
|
+
items.push({
|
|
2539
|
+
account: accountCode,
|
|
2540
|
+
debit: 0,
|
|
2541
|
+
credit: absBalance,
|
|
2542
|
+
label: "Opening balance"
|
|
2543
|
+
});
|
|
2544
|
+
totalCredit += absBalance;
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
const residual = totalDebit - totalCredit;
|
|
2548
|
+
const lineCount = items.length;
|
|
2549
|
+
if (residual > 0) items.push({
|
|
2550
|
+
account: equityAccountCode,
|
|
2551
|
+
debit: 0,
|
|
2552
|
+
credit: residual,
|
|
2553
|
+
label: "Opening balance equity (contra)"
|
|
2554
|
+
});
|
|
2555
|
+
else if (residual < 0) items.push({
|
|
2556
|
+
account: equityAccountCode,
|
|
2557
|
+
debit: Math.abs(residual),
|
|
2558
|
+
credit: 0,
|
|
2559
|
+
label: "Opening balance equity (contra)"
|
|
2560
|
+
});
|
|
2561
|
+
return {
|
|
2562
|
+
entry: {
|
|
2563
|
+
date: cutoverDate,
|
|
2564
|
+
label,
|
|
2565
|
+
journalType: "GENERAL",
|
|
2566
|
+
journalItems: items,
|
|
2567
|
+
extra: {
|
|
2568
|
+
_externalId: `opening-balance:${dateStr}`,
|
|
2569
|
+
_importSource: "opening-balance"
|
|
2570
|
+
}
|
|
2571
|
+
},
|
|
2572
|
+
residual,
|
|
2573
|
+
lineCount
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2576
|
+
//#endregion
|
|
2371
2577
|
//#region src/semantic/record.ts
|
|
2372
2578
|
function buildRecordAPI({ models, repositories, config }) {
|
|
2373
2579
|
const AccountModel = models.Account;
|
|
@@ -2813,6 +3019,10 @@ var AccountingEngine = class {
|
|
|
2813
3019
|
country,
|
|
2814
3020
|
orgField
|
|
2815
3021
|
}, params),
|
|
3022
|
+
daybook: (params) => generateDaybook({
|
|
3023
|
+
JournalEntryModel,
|
|
3024
|
+
orgField
|
|
3025
|
+
}, params),
|
|
2816
3026
|
agedBalance: (params) => generateAgedBalance({
|
|
2817
3027
|
AccountModel,
|
|
2818
3028
|
JournalEntryModel,
|
|
@@ -2905,4 +3115,4 @@ function buildDimensionIndexes(dimensions, orgField) {
|
|
|
2905
3115
|
});
|
|
2906
3116
|
}
|
|
2907
3117
|
//#endregion
|
|
2908
|
-
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 };
|
|
3118
|
+
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, LINE_SOURCE_INDEXES, Money, OutboxOwnershipError, ReconciliationMatched, ReconciliationUnmatched, acquireSession, add, allocate, buildAccountTypeMap, buildDimensionFields, buildDimensionIndexes, buildItemFilters, buildOpeningBalanceEntry, 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 };
|