@classytic/ledger 0.10.3 → 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 +115 -19
- package/dist/index.mjs +340 -156
- package/dist/{journals-DUpWwFt1.d.mts → journals-CTrAuzdk.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-UXV2PN6x.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
|
|
@@ -1144,7 +1235,7 @@ function wireAccountMethods(repository, country, orgField, integrations = {}) {
|
|
|
1144
1235
|
skipped: results.skipped.length,
|
|
1145
1236
|
errors: results.errors.length
|
|
1146
1237
|
};
|
|
1147
|
-
await safePublish
|
|
1238
|
+
await safePublish(events, outboxStore, LEDGER_EVENTS.ACCOUNT_BULK_CREATED, {
|
|
1148
1239
|
created: summary.created,
|
|
1149
1240
|
skipped: summary.skipped,
|
|
1150
1241
|
errors: summary.errors,
|
|
@@ -1168,15 +1259,6 @@ function wireAccountMethods(repository, country, orgField, integrations = {}) {
|
|
|
1168
1259
|
}
|
|
1169
1260
|
//#endregion
|
|
1170
1261
|
//#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
1262
|
/**
|
|
1181
1263
|
* Lean default set used when a country pack doesn't provide
|
|
1182
1264
|
* `journalTemplates`. Covers the Stripe/QuickBooks/Xero baseline.
|
|
@@ -1248,7 +1330,7 @@ function wireJournalMethods(repository, country, orgField, integrations = {}) {
|
|
|
1248
1330
|
await create(data);
|
|
1249
1331
|
created += 1;
|
|
1250
1332
|
}
|
|
1251
|
-
await safePublish
|
|
1333
|
+
await safePublish(events, outboxStore, LEDGER_EVENTS.JOURNAL_SEEDED, {
|
|
1252
1334
|
created,
|
|
1253
1335
|
skipped,
|
|
1254
1336
|
organizationId: orgId
|
|
@@ -1282,25 +1364,6 @@ function wireJournalMethods(repository, country, orgField, integrations = {}) {
|
|
|
1282
1364
|
}
|
|
1283
1365
|
//#endregion
|
|
1284
1366
|
//#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
1367
|
/** Keys that are either handled explicitly or must not be copied */
|
|
1305
1368
|
const ITEM_CORE_KEYS = new Set([
|
|
1306
1369
|
"account",
|
|
@@ -1328,7 +1391,8 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
|
|
|
1328
1391
|
const outboxStore = integrations.outboxStore;
|
|
1329
1392
|
const getByQuery = repository.getByQuery.bind(repository);
|
|
1330
1393
|
const baseCreate = repository.create.bind(repository);
|
|
1331
|
-
|
|
1394
|
+
repository.update.bind(repository);
|
|
1395
|
+
const claim = repository.claim.bind(repository);
|
|
1332
1396
|
const withTransaction$1 = (fn, opts) => withTransaction(repository.Model.db, fn, opts);
|
|
1333
1397
|
const raceSafeCreate = async (data, options) => {
|
|
1334
1398
|
const input = data;
|
|
@@ -1433,13 +1497,37 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
|
|
|
1433
1497
|
return await getByQuery(query, opts);
|
|
1434
1498
|
}
|
|
1435
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
|
+
/**
|
|
1436
1523
|
* Post an entry (draft → posted).
|
|
1437
1524
|
* Validates items, balance, and accounts before changing state.
|
|
1438
1525
|
*/
|
|
1439
1526
|
repository.post = async (id, orgId, options = {}) => {
|
|
1440
1527
|
if (strictness?.requireActor && !options.actorId) throw Errors.validation("actorId is required for post operations.");
|
|
1441
1528
|
requireOrgScope(orgField, orgId);
|
|
1442
|
-
const
|
|
1529
|
+
const query = buildQuery(id, orgId);
|
|
1530
|
+
const entry = await findEntry(query, {
|
|
1443
1531
|
session: options.session,
|
|
1444
1532
|
populate: "journalItems.account"
|
|
1445
1533
|
});
|
|
@@ -1475,17 +1563,22 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
|
|
|
1475
1563
|
const totalDebit = entry.journalItems.reduce((s, i) => s + (i.debit || 0), 0);
|
|
1476
1564
|
const totalCredit = entry.journalItems.reduce((s, i) => s + (i.credit || 0), 0);
|
|
1477
1565
|
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
|
-
|
|
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, {
|
|
1489
1582
|
entryId: final._id,
|
|
1490
1583
|
referenceNumber: final.referenceNumber,
|
|
1491
1584
|
postedBy: options.actorId,
|
|
@@ -1511,19 +1604,25 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
|
|
|
1511
1604
|
if (strictness?.immutable) throw Errors.immutable("Unpost is disabled in strict mode. Use reverse() to correct posted entries.");
|
|
1512
1605
|
if (strictness?.requireActor && !options.actorId) throw Errors.validation("actorId is required for unpost operations.");
|
|
1513
1606
|
requireOrgScope(orgField, orgId);
|
|
1514
|
-
const
|
|
1607
|
+
const query = buildQuery(id, orgId);
|
|
1608
|
+
const entry = await findEntry(query, { session: options.session });
|
|
1515
1609
|
if (!entry) throw Errors.notFound("Entry not found");
|
|
1516
1610
|
if (entry.state !== "posted") throw Errors.validation("Only posted entries can be unposted");
|
|
1517
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.");
|
|
1518
|
-
const
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
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, {
|
|
1527
1626
|
entryId: final._id,
|
|
1528
1627
|
unpostedBy: options.actorId,
|
|
1529
1628
|
organizationId: orgId
|
|
@@ -1545,18 +1644,24 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
|
|
|
1545
1644
|
repository.archive = async (id, orgId, options = {}) => {
|
|
1546
1645
|
if (strictness?.requireActor && !options.actorId) throw Errors.validation("actorId is required for archive operations.");
|
|
1547
1646
|
requireOrgScope(orgField, orgId);
|
|
1548
|
-
const
|
|
1647
|
+
const query = buildQuery(id, orgId);
|
|
1648
|
+
const entry = await findEntry(query, { session: options.session });
|
|
1549
1649
|
if (!entry) throw Errors.notFound("Entry not found");
|
|
1550
1650
|
if (entry.state !== "draft") throw Errors.validation("Only draft entries can be archived");
|
|
1551
|
-
const
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
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, {
|
|
1560
1665
|
entryId: final._id,
|
|
1561
1666
|
archivedBy: options.actorId,
|
|
1562
1667
|
organizationId: orgId
|
|
@@ -1601,7 +1706,7 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
|
|
|
1601
1706
|
copyExtraTopLevel(entry, duplicateData);
|
|
1602
1707
|
const duplicated = await create(duplicateData, options.session ? { session: options.session } : {});
|
|
1603
1708
|
const dup = duplicated;
|
|
1604
|
-
await safePublish
|
|
1709
|
+
await safePublish(events, outboxStore, LEDGER_EVENTS.ENTRY_DUPLICATED, {
|
|
1605
1710
|
sourceEntryId: entry._id,
|
|
1606
1711
|
duplicateEntryId: dup._id,
|
|
1607
1712
|
organizationId: orgId
|
|
@@ -1655,29 +1760,39 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
|
|
|
1655
1760
|
const totalCredit = reversalItems.reduce((s, i) => s + i.credit, 0);
|
|
1656
1761
|
const reversalData = {
|
|
1657
1762
|
journalType: entry.journalType ?? "MISC",
|
|
1658
|
-
state: "posted",
|
|
1659
1763
|
date: options.reversalDate ?? /* @__PURE__ */ new Date(),
|
|
1660
1764
|
label: `Reversal of ${entry.referenceNumber ?? entry._id}`,
|
|
1661
1765
|
journalItems: reversalItems,
|
|
1662
1766
|
totalDebit,
|
|
1663
1767
|
totalCredit,
|
|
1664
|
-
reversalOf: entry._id
|
|
1665
|
-
stateChangedAt: /* @__PURE__ */ new Date()
|
|
1768
|
+
reversalOf: entry._id
|
|
1666
1769
|
};
|
|
1667
1770
|
copyExtraTopLevel(entry, reversalData);
|
|
1668
|
-
|
|
1669
|
-
const
|
|
1670
|
-
|
|
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 = {
|
|
1671
1781
|
reversed: true,
|
|
1672
1782
|
reversedBy: reversalEntry._id
|
|
1673
1783
|
};
|
|
1674
|
-
if (options.actorId)
|
|
1675
|
-
const
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
const original = await
|
|
1680
|
-
|
|
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, {
|
|
1681
1796
|
originalEntryId: original._id,
|
|
1682
1797
|
reversalEntryId: reversalEntry._id,
|
|
1683
1798
|
reversalDate: reversalData.date ?? /* @__PURE__ */ new Date(),
|
|
@@ -1720,44 +1835,54 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
|
|
|
1720
1835
|
}
|
|
1721
1836
|
//#endregion
|
|
1722
1837
|
//#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
1838
|
/**
|
|
1733
|
-
*
|
|
1734
|
-
*
|
|
1735
|
-
*
|
|
1736
|
-
*
|
|
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.
|
|
1737
1859
|
*/
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
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);
|
|
1761
1886
|
return `RECN-${String(seq).padStart(6, "0")}`;
|
|
1762
1887
|
}
|
|
1763
1888
|
function wireReconciliationMethods(repository, ReconciliationModel, JournalEntryModel, orgField, integrations = {}) {
|
|
@@ -1811,7 +1936,7 @@ function wireReconciliationMethods(repository, ReconciliationModel, JournalEntry
|
|
|
1811
1936
|
const difference = debitTotal - creditTotal;
|
|
1812
1937
|
const isFullReconcile = difference === 0;
|
|
1813
1938
|
const sharedCurrency = currencies.size === 1 ? Array.from(currencies)[0] : null;
|
|
1814
|
-
if (!matchingNumber) matchingNumber = await nextMatchingNumber(ReconciliationModel, orgField, organizationId, session);
|
|
1939
|
+
if (!matchingNumber) matchingNumber = await nextMatchingNumber(ReconciliationModel.db, orgField, organizationId, session);
|
|
1815
1940
|
const hookCtx = {
|
|
1816
1941
|
input,
|
|
1817
1942
|
items: itemSnapshots,
|
|
@@ -1901,7 +2026,7 @@ function wireReconciliationMethods(repository, ReconciliationModel, JournalEntry
|
|
|
1901
2026
|
} }));
|
|
1902
2027
|
if (bulkOps.length > 0) await JournalEntryModel.bulkWrite(bulkOps, { session: session ?? void 0 });
|
|
1903
2028
|
const result = await deleteById(String(existing._id));
|
|
1904
|
-
if (!result
|
|
2029
|
+
if (!result) throw Errors.notFound("Failed to delete reconciliation record");
|
|
1905
2030
|
await emitHook("after:unmatch", unmatchCtx);
|
|
1906
2031
|
await safePublish(events, outboxStore, LEDGER_EVENTS.RECONCILIATION_UNMATCHED, {
|
|
1907
2032
|
matchingNumber,
|
|
@@ -2390,6 +2515,65 @@ function buildIntrospectAPI({ models, country, config }) {
|
|
|
2390
2515
|
};
|
|
2391
2516
|
}
|
|
2392
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
|
|
2393
2577
|
//#region src/semantic/record.ts
|
|
2394
2578
|
function buildRecordAPI({ models, repositories, config }) {
|
|
2395
2579
|
const AccountModel = models.Account;
|
|
@@ -2931,4 +3115,4 @@ function buildDimensionIndexes(dimensions, orgField) {
|
|
|
2931
3115
|
});
|
|
2932
3116
|
}
|
|
2933
3117
|
//#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 };
|
|
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 };
|