@classytic/ledger 0.10.3 → 0.12.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-B90x0Abq.d.mts} +38 -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-Cr57UKD-.d.mts} +1 -1
- package/dist/index.d.mts +115 -19
- package/dist/index.mjs +343 -158
- package/dist/{journals-DUpWwFt1.d.mts → journals-B1CePayM.d.mts} +1 -1
- package/dist/{partner-ledger-BIkmQsAc.mjs → partner-ledger-B0eym6Ss.mjs} +868 -212
- package/dist/plugins/index.d.mts +1 -1
- package/dist/plugins/index.mjs +1 -1
- package/dist/reports/index.d.mts +1 -1
- package/dist/reports/index.mjs +1 -1
- package/dist/{trial-balance-DCG5lOoC.d.mts → trial-balance-CdslY4pl.d.mts} +215 -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 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-
|
|
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,22 +144,21 @@ 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,
|
|
@@ -166,6 +175,25 @@ function createAccountSchema(config, options = {}) {
|
|
|
166
175
|
isCashAccount: {
|
|
167
176
|
type: Boolean,
|
|
168
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
|
|
169
197
|
}
|
|
170
198
|
};
|
|
171
199
|
const currencyField = buildCurrencyField(config);
|
|
@@ -174,10 +202,6 @@ function createAccountSchema(config, options = {}) {
|
|
|
174
202
|
const schema = new mongoose.Schema(fields, { timestamps: true });
|
|
175
203
|
schema.pre("validate", function() {
|
|
176
204
|
if (!this.accountNumber && this.accountTypeCode) this.accountNumber = this.accountTypeCode;
|
|
177
|
-
if (!this.name && this.accountTypeCode) {
|
|
178
|
-
const at = country.getAccountType(this.accountTypeCode);
|
|
179
|
-
this.name = at?.name ?? this.accountTypeCode;
|
|
180
|
-
}
|
|
181
205
|
});
|
|
182
206
|
if (indexes) {
|
|
183
207
|
schema.index({ active: 1 });
|
|
@@ -190,13 +214,6 @@ function createAccountSchema(config, options = {}) {
|
|
|
190
214
|
}
|
|
191
215
|
//#endregion
|
|
192
216
|
//#region src/schemas/budget.schema.ts
|
|
193
|
-
/**
|
|
194
|
-
* Budget Schema Factory
|
|
195
|
-
*
|
|
196
|
-
* Creates a Mongoose schema for budget records.
|
|
197
|
-
* Each record represents a budgeted amount for an account over a specific period.
|
|
198
|
-
* All monetary amounts are in integer cents.
|
|
199
|
-
*/
|
|
200
217
|
function createBudgetSchema(config, options = {}) {
|
|
201
218
|
const scope = resolveLedgerTenant(config);
|
|
202
219
|
const { indexes = true, extraFields = {}, extraIndexes = [] } = options;
|
|
@@ -226,6 +243,10 @@ function createBudgetSchema(config, options = {}) {
|
|
|
226
243
|
type: String,
|
|
227
244
|
default: null
|
|
228
245
|
},
|
|
246
|
+
approvals: {
|
|
247
|
+
type: mongoose.Schema.Types.Mixed,
|
|
248
|
+
default: null
|
|
249
|
+
},
|
|
229
250
|
...extraFields
|
|
230
251
|
};
|
|
231
252
|
const schema = new mongoose.Schema(fields, { timestamps: true });
|
|
@@ -455,6 +476,47 @@ function createJournalSchema(config, accountModelName, options = {}) {
|
|
|
455
476
|
* - Double-entry validation on post
|
|
456
477
|
* - Optimized indexes for high-load reporting
|
|
457
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
|
+
}];
|
|
458
520
|
function createJournalEntrySchema(config, accountModelName, options = {}) {
|
|
459
521
|
const { multiTenant } = config;
|
|
460
522
|
const scope = resolveLedgerTenant(config);
|
|
@@ -463,6 +525,16 @@ function createJournalEntrySchema(config, accountModelName, options = {}) {
|
|
|
463
525
|
taxCode: { type: String },
|
|
464
526
|
taxName: { type: String }
|
|
465
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 });
|
|
466
538
|
const amountValidator = {
|
|
467
539
|
validator: (v) => Number.isInteger(v) && v >= 0,
|
|
468
540
|
message: "{PATH} must be a non-negative integer (cents), got {VALUE}"
|
|
@@ -526,6 +598,18 @@ function createJournalEntrySchema(config, accountModelName, options = {}) {
|
|
|
526
598
|
type: Date,
|
|
527
599
|
default: null
|
|
528
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
|
+
},
|
|
529
613
|
...currencyItemFields,
|
|
530
614
|
...extraItemFields
|
|
531
615
|
}, { _id: false });
|
|
@@ -600,6 +684,10 @@ function createJournalEntrySchema(config, accountModelName, options = {}) {
|
|
|
600
684
|
ref: "JournalEntry",
|
|
601
685
|
default: null
|
|
602
686
|
},
|
|
687
|
+
approvals: {
|
|
688
|
+
type: mongoose.Schema.Types.Mixed,
|
|
689
|
+
default: null
|
|
690
|
+
},
|
|
603
691
|
...extraFields
|
|
604
692
|
};
|
|
605
693
|
if (config.audit?.trackActor) {
|
|
@@ -702,14 +790,6 @@ function createJournalEntrySchema(config, accountModelName, options = {}) {
|
|
|
702
790
|
state: 1
|
|
703
791
|
});
|
|
704
792
|
schema.index({ reversed: 1 });
|
|
705
|
-
if (config.idempotency) {
|
|
706
|
-
const ttlSeconds = typeof config.idempotencyTtlSeconds === "number" && config.idempotencyTtlSeconds > 0 ? config.idempotencyTtlSeconds : 86400;
|
|
707
|
-
schema.index({ createdAt: 1 }, {
|
|
708
|
-
name: "idempotency_ttl_idx",
|
|
709
|
-
expireAfterSeconds: ttlSeconds,
|
|
710
|
-
partialFilterExpression: { idempotencyKey: { $type: "string" } }
|
|
711
|
-
});
|
|
712
|
-
}
|
|
713
793
|
}
|
|
714
794
|
if (textSearch) schema.index({
|
|
715
795
|
referenceNumber: "text",
|
|
@@ -897,9 +977,14 @@ function createModels(connection, config) {
|
|
|
897
977
|
};
|
|
898
978
|
}
|
|
899
979
|
//#endregion
|
|
900
|
-
//#region src/
|
|
901
|
-
|
|
902
|
-
|
|
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);
|
|
903
988
|
if (outboxStore) try {
|
|
904
989
|
await outboxStore.save(event, { session: ctx?.session ?? void 0 });
|
|
905
990
|
} catch {}
|
|
@@ -907,6 +992,8 @@ async function safePublish$3(events, outboxStore, type, payload, ctx) {
|
|
|
907
992
|
await events.publish(event);
|
|
908
993
|
} catch {}
|
|
909
994
|
}
|
|
995
|
+
//#endregion
|
|
996
|
+
//#region src/repositories/account.repository.ts
|
|
910
997
|
function isDuplicateKeyBulkError(err) {
|
|
911
998
|
if (!err || typeof err !== "object") return false;
|
|
912
999
|
const e = err;
|
|
@@ -928,9 +1015,13 @@ function wireAccountMethods(repository, country, orgField, integrations = {}) {
|
|
|
928
1015
|
const outboxStore = integrations.outboxStore;
|
|
929
1016
|
const journalEntryModel = integrations.journalEntryModel;
|
|
930
1017
|
repository.on("before:create", async (ctx) => {
|
|
931
|
-
const code = ctx.data?.accountTypeCode;
|
|
932
|
-
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.`);
|
|
933
1018
|
const data = ctx.data;
|
|
1019
|
+
const code = data?.accountTypeCode;
|
|
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
|
+
}
|
|
934
1025
|
if (!data) return;
|
|
935
1026
|
const accountNumber = data.accountNumber ?? code;
|
|
936
1027
|
if (!accountNumber) return;
|
|
@@ -994,7 +1085,7 @@ function wireAccountMethods(repository, country, orgField, integrations = {}) {
|
|
|
994
1085
|
};
|
|
995
1086
|
} else throw err;
|
|
996
1087
|
}
|
|
997
|
-
await safePublish
|
|
1088
|
+
await safePublish(events, outboxStore, LEDGER_EVENTS.ACCOUNT_SEEDED, {
|
|
998
1089
|
created: result.created,
|
|
999
1090
|
skipped: result.skipped,
|
|
1000
1091
|
organizationId: orgId
|
|
@@ -1020,7 +1111,7 @@ function wireAccountMethods(repository, country, orgField, integrations = {}) {
|
|
|
1020
1111
|
};
|
|
1021
1112
|
const validAccounts = [];
|
|
1022
1113
|
for (let i = 0; i < accounts.length; i++) {
|
|
1023
|
-
const { accountTypeCode, accountNumber, name, active = true, isCashAccount
|
|
1114
|
+
const { accountTypeCode, accountNumber, name, active = true, isCashAccount } = accounts[i];
|
|
1024
1115
|
if (!accountTypeCode) {
|
|
1025
1116
|
results.errors.push({
|
|
1026
1117
|
index: i,
|
|
@@ -1047,13 +1138,14 @@ function wireAccountMethods(repository, country, orgField, integrations = {}) {
|
|
|
1047
1138
|
}
|
|
1048
1139
|
const resolvedNumber = accountNumber ?? accountTypeCode;
|
|
1049
1140
|
const resolvedName = name ?? at.name ?? accountTypeCode;
|
|
1141
|
+
const resolvedIsCash = isCashAccount === void 0 ? Boolean(at.isCashAccount) : Boolean(isCashAccount);
|
|
1050
1142
|
validAccounts.push({
|
|
1051
1143
|
index: i,
|
|
1052
1144
|
accountTypeCode,
|
|
1053
1145
|
accountNumber: resolvedNumber,
|
|
1054
1146
|
name: resolvedName,
|
|
1055
1147
|
active: Boolean(active),
|
|
1056
|
-
isCashAccount:
|
|
1148
|
+
isCashAccount: resolvedIsCash
|
|
1057
1149
|
});
|
|
1058
1150
|
}
|
|
1059
1151
|
if (validAccounts.length === 0) return {
|
|
@@ -1144,7 +1236,7 @@ function wireAccountMethods(repository, country, orgField, integrations = {}) {
|
|
|
1144
1236
|
skipped: results.skipped.length,
|
|
1145
1237
|
errors: results.errors.length
|
|
1146
1238
|
};
|
|
1147
|
-
await safePublish
|
|
1239
|
+
await safePublish(events, outboxStore, LEDGER_EVENTS.ACCOUNT_BULK_CREATED, {
|
|
1148
1240
|
created: summary.created,
|
|
1149
1241
|
skipped: summary.skipped,
|
|
1150
1242
|
errors: summary.errors,
|
|
@@ -1168,15 +1260,6 @@ function wireAccountMethods(repository, country, orgField, integrations = {}) {
|
|
|
1168
1260
|
}
|
|
1169
1261
|
//#endregion
|
|
1170
1262
|
//#region src/repositories/journal.repository.ts
|
|
1171
|
-
async function safePublish$2(events, outboxStore, type, payload, ctx) {
|
|
1172
|
-
const event = createEvent(type, payload, ctx);
|
|
1173
|
-
if (outboxStore) try {
|
|
1174
|
-
await outboxStore.save(event);
|
|
1175
|
-
} catch {}
|
|
1176
|
-
if (events) try {
|
|
1177
|
-
await events.publish(event);
|
|
1178
|
-
} catch {}
|
|
1179
|
-
}
|
|
1180
1263
|
/**
|
|
1181
1264
|
* Lean default set used when a country pack doesn't provide
|
|
1182
1265
|
* `journalTemplates`. Covers the Stripe/QuickBooks/Xero baseline.
|
|
@@ -1248,7 +1331,7 @@ function wireJournalMethods(repository, country, orgField, integrations = {}) {
|
|
|
1248
1331
|
await create(data);
|
|
1249
1332
|
created += 1;
|
|
1250
1333
|
}
|
|
1251
|
-
await safePublish
|
|
1334
|
+
await safePublish(events, outboxStore, LEDGER_EVENTS.JOURNAL_SEEDED, {
|
|
1252
1335
|
created,
|
|
1253
1336
|
skipped,
|
|
1254
1337
|
organizationId: orgId
|
|
@@ -1282,25 +1365,6 @@ function wireJournalMethods(repository, country, orgField, integrations = {}) {
|
|
|
1282
1365
|
}
|
|
1283
1366
|
//#endregion
|
|
1284
1367
|
//#region src/repositories/journal-entry.repository.ts
|
|
1285
|
-
/**
|
|
1286
|
-
* Publish a domain event. When an outbox store is provided, first persist
|
|
1287
|
-
* the event inside the caller's session (so outbox + ledger write commit
|
|
1288
|
-
* atomically), then fire-and-forget publish to the transport. Without an
|
|
1289
|
-
* outbox, publish-only, still fire-and-forget — transport errors never
|
|
1290
|
-
* propagate into ledger mutations.
|
|
1291
|
-
*
|
|
1292
|
-
* Tracks PACKAGE_RULES §16 (host-composed transactional outbox) and §14
|
|
1293
|
-
* (domain verbs publish via injected transport).
|
|
1294
|
-
*/
|
|
1295
|
-
async function safePublish$1(events, outboxStore, type, payload, ctx, meta) {
|
|
1296
|
-
const event = createEvent(type, payload, ctx, meta);
|
|
1297
|
-
if (outboxStore) try {
|
|
1298
|
-
await outboxStore.save(event, { session: ctx?.session ?? void 0 });
|
|
1299
|
-
} catch {}
|
|
1300
|
-
if (events) try {
|
|
1301
|
-
await events.publish(event);
|
|
1302
|
-
} catch {}
|
|
1303
|
-
}
|
|
1304
1368
|
/** Keys that are either handled explicitly or must not be copied */
|
|
1305
1369
|
const ITEM_CORE_KEYS = new Set([
|
|
1306
1370
|
"account",
|
|
@@ -1328,7 +1392,8 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
|
|
|
1328
1392
|
const outboxStore = integrations.outboxStore;
|
|
1329
1393
|
const getByQuery = repository.getByQuery.bind(repository);
|
|
1330
1394
|
const baseCreate = repository.create.bind(repository);
|
|
1331
|
-
|
|
1395
|
+
repository.update.bind(repository);
|
|
1396
|
+
const claim = repository.claim.bind(repository);
|
|
1332
1397
|
const withTransaction$1 = (fn, opts) => withTransaction(repository.Model.db, fn, opts);
|
|
1333
1398
|
const raceSafeCreate = async (data, options) => {
|
|
1334
1399
|
const input = data;
|
|
@@ -1433,13 +1498,37 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
|
|
|
1433
1498
|
return await getByQuery(query, opts);
|
|
1434
1499
|
}
|
|
1435
1500
|
/**
|
|
1501
|
+
* Build the options bag passed to `repo.claim()` from the call's
|
|
1502
|
+
* org/actor/session context. mongokit's `multiTenantPlugin` reads
|
|
1503
|
+
* `options.organizationId`, audit plugins read `options.userId`, and
|
|
1504
|
+
* the transaction layer reads `options.session` — same shape we'd
|
|
1505
|
+
* forward via `repoOptionsFromCtx(ctx)` from a host route.
|
|
1506
|
+
*/
|
|
1507
|
+
function buildClaimOptions(orgId, actorId, session) {
|
|
1508
|
+
const opts = {};
|
|
1509
|
+
if (session) opts.session = session;
|
|
1510
|
+
if (orgField && orgId != null) opts.organizationId = orgId;
|
|
1511
|
+
if (actorId !== void 0 && actorId !== null) opts.userId = actorId;
|
|
1512
|
+
return opts;
|
|
1513
|
+
}
|
|
1514
|
+
/**
|
|
1515
|
+
* Build the `where` predicate for a state-transition claim. Encodes the
|
|
1516
|
+
* tenant-scope guard so the CAS only matches docs in the caller's org.
|
|
1517
|
+
*/
|
|
1518
|
+
function buildClaimWhere(orgId, extra) {
|
|
1519
|
+
const where = { ...extra ?? {} };
|
|
1520
|
+
if (orgField && orgId != null) where[orgField] = orgId;
|
|
1521
|
+
return where;
|
|
1522
|
+
}
|
|
1523
|
+
/**
|
|
1436
1524
|
* Post an entry (draft → posted).
|
|
1437
1525
|
* Validates items, balance, and accounts before changing state.
|
|
1438
1526
|
*/
|
|
1439
1527
|
repository.post = async (id, orgId, options = {}) => {
|
|
1440
1528
|
if (strictness?.requireActor && !options.actorId) throw Errors.validation("actorId is required for post operations.");
|
|
1441
1529
|
requireOrgScope(orgField, orgId);
|
|
1442
|
-
const
|
|
1530
|
+
const query = buildQuery(id, orgId);
|
|
1531
|
+
const entry = await findEntry(query, {
|
|
1443
1532
|
session: options.session,
|
|
1444
1533
|
populate: "journalItems.account"
|
|
1445
1534
|
});
|
|
@@ -1475,17 +1564,22 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
|
|
|
1475
1564
|
const totalDebit = entry.journalItems.reduce((s, i) => s + (i.debit || 0), 0);
|
|
1476
1565
|
const totalCredit = entry.journalItems.reduce((s, i) => s + (i.credit || 0), 0);
|
|
1477
1566
|
if (totalDebit !== totalCredit) throw Errors.validation(`Entry is not balanced. Debit: ${totalDebit}, Credit: ${totalCredit}`);
|
|
1478
|
-
const
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1567
|
+
const $set = { stateChangedAt: /* @__PURE__ */ new Date() };
|
|
1568
|
+
if (options.actorId) $set.postedBy = options.actorId;
|
|
1569
|
+
const claimed = await claim(entry._id, {
|
|
1570
|
+
field: "state",
|
|
1571
|
+
from: "draft",
|
|
1572
|
+
to: "posted",
|
|
1573
|
+
where: buildClaimWhere(orgId)
|
|
1574
|
+
}, { $set }, buildClaimOptions(orgId, options.actorId, options.session ?? null));
|
|
1575
|
+
let final;
|
|
1576
|
+
if (!claimed) {
|
|
1577
|
+
const reread = await findEntry(query, { session: options.session });
|
|
1578
|
+
if (reread && reread.state === "posted") final = reread;
|
|
1579
|
+
else if (reread) throw new ConcurrencyError("JournalEntry", String(entry._id));
|
|
1580
|
+
else throw Errors.notFound("Entry not found");
|
|
1581
|
+
} else final = claimed;
|
|
1582
|
+
await safePublish(events, outboxStore, LEDGER_EVENTS.ENTRY_POSTED, {
|
|
1489
1583
|
entryId: final._id,
|
|
1490
1584
|
referenceNumber: final.referenceNumber,
|
|
1491
1585
|
postedBy: options.actorId,
|
|
@@ -1511,19 +1605,25 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
|
|
|
1511
1605
|
if (strictness?.immutable) throw Errors.immutable("Unpost is disabled in strict mode. Use reverse() to correct posted entries.");
|
|
1512
1606
|
if (strictness?.requireActor && !options.actorId) throw Errors.validation("actorId is required for unpost operations.");
|
|
1513
1607
|
requireOrgScope(orgField, orgId);
|
|
1514
|
-
const
|
|
1608
|
+
const query = buildQuery(id, orgId);
|
|
1609
|
+
const entry = await findEntry(query, { session: options.session });
|
|
1515
1610
|
if (!entry) throw Errors.notFound("Entry not found");
|
|
1516
1611
|
if (entry.state !== "posted") throw Errors.validation("Only posted entries can be unposted");
|
|
1517
1612
|
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.");
|
|
1518
|
-
const
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1613
|
+
const claimed = await claim(entry._id, {
|
|
1614
|
+
field: "state",
|
|
1615
|
+
from: "posted",
|
|
1616
|
+
to: "draft",
|
|
1617
|
+
where: buildClaimWhere(orgId, { reversed: { $ne: true } })
|
|
1618
|
+
}, { $set: { stateChangedAt: /* @__PURE__ */ new Date() } }, buildClaimOptions(orgId, options.actorId, options.session ?? null));
|
|
1619
|
+
let final;
|
|
1620
|
+
if (!claimed) {
|
|
1621
|
+
const reread = await findEntry(query, { session: options.session });
|
|
1622
|
+
if (reread && reread.state === "draft") final = reread;
|
|
1623
|
+
else if (reread) throw new ConcurrencyError("JournalEntry", String(entry._id));
|
|
1624
|
+
else throw Errors.notFound("Entry not found");
|
|
1625
|
+
} else final = claimed;
|
|
1626
|
+
await safePublish(events, outboxStore, LEDGER_EVENTS.ENTRY_UNPOSTED, {
|
|
1527
1627
|
entryId: final._id,
|
|
1528
1628
|
unpostedBy: options.actorId,
|
|
1529
1629
|
organizationId: orgId
|
|
@@ -1545,18 +1645,24 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
|
|
|
1545
1645
|
repository.archive = async (id, orgId, options = {}) => {
|
|
1546
1646
|
if (strictness?.requireActor && !options.actorId) throw Errors.validation("actorId is required for archive operations.");
|
|
1547
1647
|
requireOrgScope(orgField, orgId);
|
|
1548
|
-
const
|
|
1648
|
+
const query = buildQuery(id, orgId);
|
|
1649
|
+
const entry = await findEntry(query, { session: options.session });
|
|
1549
1650
|
if (!entry) throw Errors.notFound("Entry not found");
|
|
1550
1651
|
if (entry.state !== "draft") throw Errors.validation("Only draft entries can be archived");
|
|
1551
|
-
const
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1652
|
+
const claimed = await claim(entry._id, {
|
|
1653
|
+
field: "state",
|
|
1654
|
+
from: "draft",
|
|
1655
|
+
to: "archived",
|
|
1656
|
+
where: buildClaimWhere(orgId)
|
|
1657
|
+
}, { $set: { stateChangedAt: /* @__PURE__ */ new Date() } }, buildClaimOptions(orgId, options.actorId, options.session ?? null));
|
|
1658
|
+
let final;
|
|
1659
|
+
if (!claimed) {
|
|
1660
|
+
const reread = await findEntry(query, { session: options.session });
|
|
1661
|
+
if (reread && reread.state === "archived") final = reread;
|
|
1662
|
+
else if (reread) throw new ConcurrencyError("JournalEntry", String(entry._id));
|
|
1663
|
+
else throw Errors.notFound("Entry not found");
|
|
1664
|
+
} else final = claimed;
|
|
1665
|
+
await safePublish(events, outboxStore, LEDGER_EVENTS.ENTRY_ARCHIVED, {
|
|
1560
1666
|
entryId: final._id,
|
|
1561
1667
|
archivedBy: options.actorId,
|
|
1562
1668
|
organizationId: orgId
|
|
@@ -1601,7 +1707,7 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
|
|
|
1601
1707
|
copyExtraTopLevel(entry, duplicateData);
|
|
1602
1708
|
const duplicated = await create(duplicateData, options.session ? { session: options.session } : {});
|
|
1603
1709
|
const dup = duplicated;
|
|
1604
|
-
await safePublish
|
|
1710
|
+
await safePublish(events, outboxStore, LEDGER_EVENTS.ENTRY_DUPLICATED, {
|
|
1605
1711
|
sourceEntryId: entry._id,
|
|
1606
1712
|
duplicateEntryId: dup._id,
|
|
1607
1713
|
organizationId: orgId
|
|
@@ -1655,29 +1761,39 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
|
|
|
1655
1761
|
const totalCredit = reversalItems.reduce((s, i) => s + i.credit, 0);
|
|
1656
1762
|
const reversalData = {
|
|
1657
1763
|
journalType: entry.journalType ?? "MISC",
|
|
1658
|
-
state: "posted",
|
|
1659
1764
|
date: options.reversalDate ?? /* @__PURE__ */ new Date(),
|
|
1660
1765
|
label: `Reversal of ${entry.referenceNumber ?? entry._id}`,
|
|
1661
1766
|
journalItems: reversalItems,
|
|
1662
1767
|
totalDebit,
|
|
1663
1768
|
totalCredit,
|
|
1664
|
-
reversalOf: entry._id
|
|
1665
|
-
stateChangedAt: /* @__PURE__ */ new Date()
|
|
1769
|
+
reversalOf: entry._id
|
|
1666
1770
|
};
|
|
1667
1771
|
copyExtraTopLevel(entry, reversalData);
|
|
1668
|
-
|
|
1669
|
-
const
|
|
1670
|
-
|
|
1772
|
+
let reversalEntry = await create(reversalData, session ? { session } : {});
|
|
1773
|
+
const postFn = repository.post;
|
|
1774
|
+
if (options.autoPost && postFn) {
|
|
1775
|
+
const posted = await postFn(reversalEntry._id, orgId, {
|
|
1776
|
+
actorId: options.actorId,
|
|
1777
|
+
...session ? { session } : {}
|
|
1778
|
+
});
|
|
1779
|
+
if (posted) reversalEntry = posted;
|
|
1780
|
+
}
|
|
1781
|
+
const $set = {
|
|
1671
1782
|
reversed: true,
|
|
1672
1783
|
reversedBy: reversalEntry._id
|
|
1673
1784
|
};
|
|
1674
|
-
if (options.actorId)
|
|
1675
|
-
const
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
const original = await
|
|
1680
|
-
|
|
1785
|
+
if (options.actorId) $set.reversedByUser = options.actorId;
|
|
1786
|
+
const claimOpts = {};
|
|
1787
|
+
if (session) claimOpts.session = session;
|
|
1788
|
+
if (orgField && orgId != null) claimOpts.organizationId = orgId;
|
|
1789
|
+
if (options.actorId) claimOpts.userId = options.actorId;
|
|
1790
|
+
const original = await claim(entry._id, {
|
|
1791
|
+
field: "state",
|
|
1792
|
+
from: "posted",
|
|
1793
|
+
to: "posted",
|
|
1794
|
+
where: { reversed: { $ne: true } }
|
|
1795
|
+
}, { $set }, claimOpts) ?? entry;
|
|
1796
|
+
await safePublish(events, outboxStore, LEDGER_EVENTS.ENTRY_REVERSED, {
|
|
1681
1797
|
originalEntryId: original._id,
|
|
1682
1798
|
reversalEntryId: reversalEntry._id,
|
|
1683
1799
|
reversalDate: reversalData.date ?? /* @__PURE__ */ new Date(),
|
|
@@ -1720,44 +1836,54 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
|
|
|
1720
1836
|
}
|
|
1721
1837
|
//#endregion
|
|
1722
1838
|
//#region src/repositories/reconciliation.repository.ts
|
|
1723
|
-
async function safePublish(events, outboxStore, type, payload, ctx) {
|
|
1724
|
-
const event = createEvent(type, payload, ctx);
|
|
1725
|
-
if (outboxStore) try {
|
|
1726
|
-
await outboxStore.save(event, { session: ctx?.session ?? void 0 });
|
|
1727
|
-
} catch {}
|
|
1728
|
-
if (events) try {
|
|
1729
|
-
await events.publish(event);
|
|
1730
|
-
} catch {}
|
|
1731
|
-
}
|
|
1732
1839
|
/**
|
|
1733
|
-
*
|
|
1734
|
-
*
|
|
1735
|
-
*
|
|
1736
|
-
*
|
|
1840
|
+
* Reconciliation Repository Factory (0.6.0 — item-level open-item matching)
|
|
1841
|
+
*
|
|
1842
|
+
* Implements the three new primitives:
|
|
1843
|
+
*
|
|
1844
|
+
* - `match({ account, items, ... })` stamps a shared matchingNumber onto
|
|
1845
|
+
* every referenced item and creates a reconciliation document.
|
|
1846
|
+
* Triggers `after:match` hook for downstream plugins (fxRealization,
|
|
1847
|
+
* cash-basis exigibility).
|
|
1848
|
+
*
|
|
1849
|
+
* - `unmatch({ matchingNumber })` clears the matching number from every
|
|
1850
|
+
* referenced item and removes the reconciliation. If an FX realization
|
|
1851
|
+
* entry was booked, it is reversed via journalEntries.reverse.
|
|
1852
|
+
*
|
|
1853
|
+
* - `getOpenItems({ accountId })` returns posted journal items against
|
|
1854
|
+
* the account that have no matchingNumber yet. Backed by the sparse
|
|
1855
|
+
* index on `journalItems.matchingNumber`.
|
|
1856
|
+
*
|
|
1857
|
+
* Matching numbers auto-generate as `RECN-{n}` if the caller doesn't
|
|
1858
|
+
* supply one. Uniqueness is enforced by the org-scoped unique index on
|
|
1859
|
+
* the reconciliation collection.
|
|
1737
1860
|
*/
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1861
|
+
/**
|
|
1862
|
+
* Default matching-number generator — delegates to mongokit's
|
|
1863
|
+
* `getNextSequence(counterKey, 1, connection, session)` which atomically
|
|
1864
|
+
* bumps a counter row in the shared `_mongokit_counters` collection. Same
|
|
1865
|
+
* primitive that backs `journalEntries.referenceNumber` allocation in this
|
|
1866
|
+
* package (see `journal-entry.schema.ts:431`) and the per-package id
|
|
1867
|
+
* generators in `@classytic/invoice`, `order`, `cart`, `revenue`.
|
|
1868
|
+
*
|
|
1869
|
+
* Why this replaced the in-collection sentinel pattern (pre-0.10.4): the
|
|
1870
|
+
* sentinel approach stored the counter as a synthetic `matchingNumber:
|
|
1871
|
+
* '__counter__'` doc in the reconciliation collection itself, with the
|
|
1872
|
+
* counter slot as `seq`. After the 0.10.x refactor that routed the bump
|
|
1873
|
+
* through `repository.findOneAndUpdate(...)` (to flow through the plugin
|
|
1874
|
+
* pipeline), mongoose strict mode silently dropped `$inc: { seq: 1 }`
|
|
1875
|
+
* because `seq` was never declared in the schema — every call returned
|
|
1876
|
+
* `undefined`, fell back to `1`, and every match resolved to `RECN-000001`.
|
|
1877
|
+
* The first match per engine succeeded; the second collided on the unique
|
|
1878
|
+
* index. `getNextSequence` sidesteps this entirely: dedicated counter
|
|
1879
|
+
* collection, no schema pollution, session-aware (counter rolls back if
|
|
1880
|
+
* the calling transaction aborts), multi-tenant via key prefix.
|
|
1881
|
+
*/
|
|
1882
|
+
async function nextMatchingNumber(connection, orgField, orgId, session) {
|
|
1883
|
+
let orgScope = "global";
|
|
1884
|
+
if (orgField && orgId != null) orgScope = typeof orgId.toHexString === "function" ? orgId.toHexString() : String(orgId);
|
|
1885
|
+
else if (orgField) orgScope = "unscoped";
|
|
1886
|
+
const seq = await getNextSequence(`ledger:${orgScope}:matchingNumber`, 1, connection, session ?? void 0);
|
|
1761
1887
|
return `RECN-${String(seq).padStart(6, "0")}`;
|
|
1762
1888
|
}
|
|
1763
1889
|
function wireReconciliationMethods(repository, ReconciliationModel, JournalEntryModel, orgField, integrations = {}) {
|
|
@@ -1811,7 +1937,7 @@ function wireReconciliationMethods(repository, ReconciliationModel, JournalEntry
|
|
|
1811
1937
|
const difference = debitTotal - creditTotal;
|
|
1812
1938
|
const isFullReconcile = difference === 0;
|
|
1813
1939
|
const sharedCurrency = currencies.size === 1 ? Array.from(currencies)[0] : null;
|
|
1814
|
-
if (!matchingNumber) matchingNumber = await nextMatchingNumber(ReconciliationModel, orgField, organizationId, session);
|
|
1940
|
+
if (!matchingNumber) matchingNumber = await nextMatchingNumber(ReconciliationModel.db, orgField, organizationId, session);
|
|
1815
1941
|
const hookCtx = {
|
|
1816
1942
|
input,
|
|
1817
1943
|
items: itemSnapshots,
|
|
@@ -1901,7 +2027,7 @@ function wireReconciliationMethods(repository, ReconciliationModel, JournalEntry
|
|
|
1901
2027
|
} }));
|
|
1902
2028
|
if (bulkOps.length > 0) await JournalEntryModel.bulkWrite(bulkOps, { session: session ?? void 0 });
|
|
1903
2029
|
const result = await deleteById(String(existing._id));
|
|
1904
|
-
if (!result
|
|
2030
|
+
if (!result) throw Errors.notFound("Failed to delete reconciliation record");
|
|
1905
2031
|
await emitHook("after:unmatch", unmatchCtx);
|
|
1906
2032
|
await safePublish(events, outboxStore, LEDGER_EVENTS.RECONCILIATION_UNMATCHED, {
|
|
1907
2033
|
matchingNumber,
|
|
@@ -2390,6 +2516,65 @@ function buildIntrospectAPI({ models, country, config }) {
|
|
|
2390
2516
|
};
|
|
2391
2517
|
}
|
|
2392
2518
|
//#endregion
|
|
2519
|
+
//#region src/builders/opening-balance.ts
|
|
2520
|
+
function buildOpeningBalanceEntry(input) {
|
|
2521
|
+
const { cutoverDate, balances, equityAccountCode } = input;
|
|
2522
|
+
const dateStr = cutoverDate.toISOString().split("T")[0];
|
|
2523
|
+
const label = input.label ?? `Opening Balance — Cutover ${dateStr}`;
|
|
2524
|
+
const items = [];
|
|
2525
|
+
let totalDebit = 0;
|
|
2526
|
+
let totalCredit = 0;
|
|
2527
|
+
for (const { accountCode, balance } of balances) {
|
|
2528
|
+
if (balance === 0) continue;
|
|
2529
|
+
if (balance > 0) {
|
|
2530
|
+
items.push({
|
|
2531
|
+
account: accountCode,
|
|
2532
|
+
debit: balance,
|
|
2533
|
+
credit: 0,
|
|
2534
|
+
label: "Opening balance"
|
|
2535
|
+
});
|
|
2536
|
+
totalDebit += balance;
|
|
2537
|
+
} else {
|
|
2538
|
+
const absBalance = Math.abs(balance);
|
|
2539
|
+
items.push({
|
|
2540
|
+
account: accountCode,
|
|
2541
|
+
debit: 0,
|
|
2542
|
+
credit: absBalance,
|
|
2543
|
+
label: "Opening balance"
|
|
2544
|
+
});
|
|
2545
|
+
totalCredit += absBalance;
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
const residual = totalDebit - totalCredit;
|
|
2549
|
+
const lineCount = items.length;
|
|
2550
|
+
if (residual > 0) items.push({
|
|
2551
|
+
account: equityAccountCode,
|
|
2552
|
+
debit: 0,
|
|
2553
|
+
credit: residual,
|
|
2554
|
+
label: "Opening balance equity (contra)"
|
|
2555
|
+
});
|
|
2556
|
+
else if (residual < 0) items.push({
|
|
2557
|
+
account: equityAccountCode,
|
|
2558
|
+
debit: Math.abs(residual),
|
|
2559
|
+
credit: 0,
|
|
2560
|
+
label: "Opening balance equity (contra)"
|
|
2561
|
+
});
|
|
2562
|
+
return {
|
|
2563
|
+
entry: {
|
|
2564
|
+
date: cutoverDate,
|
|
2565
|
+
label,
|
|
2566
|
+
journalType: "GENERAL",
|
|
2567
|
+
journalItems: items,
|
|
2568
|
+
extra: {
|
|
2569
|
+
_externalId: `opening-balance:${dateStr}`,
|
|
2570
|
+
_importSource: "opening-balance"
|
|
2571
|
+
}
|
|
2572
|
+
},
|
|
2573
|
+
residual,
|
|
2574
|
+
lineCount
|
|
2575
|
+
};
|
|
2576
|
+
}
|
|
2577
|
+
//#endregion
|
|
2393
2578
|
//#region src/semantic/record.ts
|
|
2394
2579
|
function buildRecordAPI({ models, repositories, config }) {
|
|
2395
2580
|
const AccountModel = models.Account;
|
|
@@ -2931,4 +3116,4 @@ function buildDimensionIndexes(dimensions, orgField) {
|
|
|
2931
3116
|
});
|
|
2932
3117
|
}
|
|
2933
3118
|
//#endregion
|
|
2934
|
-
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 };
|
|
3119
|
+
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 };
|