@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.
Files changed (31) hide show
  1. package/dist/bridges/index.d.mts +1 -1
  2. package/dist/constants/index.d.mts +1 -1
  3. package/dist/constants/index.mjs +2 -2
  4. package/dist/{core-DwjkrRkJ.d.mts → core-B7uVjqGS.d.mts} +25 -0
  5. package/dist/country/index.d.mts +1 -1
  6. package/dist/events/index.d.mts +1 -1
  7. package/dist/exports/index.d.mts +1 -1
  8. package/dist/exports/index.mjs +1 -1
  9. package/dist/{fx-realization.plugin-Dzqzi3u0.mjs → fx-realization.plugin-DY3pPxIi.mjs} +70 -1
  10. package/dist/{index-ClLwzNRF.d.mts → index-BFPFihTF.d.mts} +8 -0
  11. package/dist/{index-08IpHhrU.d.mts → index-Dd7HknPP.d.mts} +1 -1
  12. package/dist/index.d.mts +115 -19
  13. package/dist/index.mjs +340 -156
  14. package/dist/{journals-DUpWwFt1.d.mts → journals-CTrAuzdk.d.mts} +1 -1
  15. package/dist/{partner-ledger-BIkmQsAc.mjs → partner-ledger-B0eym6Ss.mjs} +868 -212
  16. package/dist/plugins/index.d.mts +1 -1
  17. package/dist/plugins/index.mjs +1 -1
  18. package/dist/reports/index.d.mts +1 -1
  19. package/dist/reports/index.mjs +1 -1
  20. package/dist/{trial-balance-DCG5lOoC.d.mts → trial-balance-UXV2PN6x.d.mts} +215 -75
  21. package/package.json +8 -20
  22. package/dist/opening-balance-1cixYh6Y.mjs +0 -60
  23. package/dist/sync/index.d.mts +0 -324
  24. package/dist/sync/index.mjs +0 -530
  25. package/dist/sync-JvchM3FO.d.mts +0 -152
  26. /package/dist/{categories-FJlrvzcl.mjs → categories-CclX7Q94.mjs} +0 -0
  27. /package/dist/{currencies-Jo5oaM_4.mjs → currencies-OuPHPyS2.mjs} +0 -0
  28. /package/dist/{exports-C30yRapf.mjs → exports-B3whucXe.mjs} +0 -0
  29. /package/dist/{index-Bl0gP9lD.d.mts → index-DygMrab0.d.mts} +0 -0
  30. /package/dist/{index-J-XIbXH-.d.mts → index-pRW5cZhF.d.mts} +0 -0
  31. /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 JOURNAL_CODES, c as getCustomJournalTypes, d as isValidJournalType, f as registerJournalType, i as isValidCurrency, l as getJournalType, n as getCurrency, o as JOURNAL_TYPES, r as getMinorUnit, s as _freezeJournalTypes, t as CURRENCIES, u as getJournalTypeCodes } from "./currencies-Jo5oaM_4.mjs";
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-BIkmQsAc.mjs";
6
- import { c as getNormalBalance, d as isValidCategory, l as isBalanceSheet, n as CATEGORY_KEYS, t as CATEGORIES, u as isIncomeStatement } from "./categories-FJlrvzcl.mjs";
7
- import { a as watermarkResolver, c as idempotencyPlugin, i as fiscalLockPlugin, l as doubleEntryPlugin, n as creditLimitPlugin, o as periodResolver, r as dailyLockPlugin, s as createLockPlugin, t as fxRealizationPlugin } from "./fx-realization.plugin-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-C30yRapf.mjs";
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/primitives/tenant";
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/primitives/tenant`. The pure resolution lives in primitives
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/repositories/account.repository.ts
901
- async function safePublish$3(events, outboxStore, type, payload, ctx) {
902
- const event = createEvent(type, payload, ctx);
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$3(events, outboxStore, LEDGER_EVENTS.ACCOUNT_SEEDED, {
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$3(events, outboxStore, LEDGER_EVENTS.ACCOUNT_BULK_CREATED, {
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$2(events, outboxStore, LEDGER_EVENTS.JOURNAL_SEEDED, {
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
- const update = repository.update.bind(repository);
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 entry = await findEntry(buildQuery(id, orgId), {
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 patch = {
1479
- state: "posted",
1480
- stateChangedAt: /* @__PURE__ */ new Date()
1481
- };
1482
- if (options.actorId) patch.postedBy = options.actorId;
1483
- const updateOptions = {
1484
- _ledgerInternal: "post",
1485
- ...options.session ? { session: options.session } : {}
1486
- };
1487
- const final = await update(entry._id, patch, updateOptions) ?? entry;
1488
- await safePublish$1(events, outboxStore, LEDGER_EVENTS.ENTRY_POSTED, {
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 entry = await findEntry(buildQuery(id, orgId), { session: options.session });
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 updateOptions = {
1519
- _ledgerInternal: "unpost",
1520
- ...options.session ? { session: options.session } : {}
1521
- };
1522
- const final = await update(entry._id, {
1523
- state: "draft",
1524
- stateChangedAt: /* @__PURE__ */ new Date()
1525
- }, updateOptions) ?? entry;
1526
- await safePublish$1(events, outboxStore, LEDGER_EVENTS.ENTRY_UNPOSTED, {
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 entry = await findEntry(buildQuery(id, orgId), { session: options.session });
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 updateOptions = {
1552
- _ledgerInternal: "archive",
1553
- ...options.session ? { session: options.session } : {}
1554
- };
1555
- const final = await update(entry._id, {
1556
- state: "archived",
1557
- stateChangedAt: /* @__PURE__ */ new Date()
1558
- }, updateOptions) ?? entry;
1559
- await safePublish$1(events, outboxStore, LEDGER_EVENTS.ENTRY_ARCHIVED, {
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$1(events, outboxStore, LEDGER_EVENTS.ENTRY_DUPLICATED, {
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
- if (options.actorId) reversalData.postedBy = options.actorId;
1669
- const reversalEntry = await create(reversalData, session ? { session } : {});
1670
- const markPatch = {
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) markPatch.reversedByUser = options.actorId;
1675
- const markOptions = {
1676
- _ledgerInternal: "reverseMark",
1677
- ...session ? { session } : {}
1678
- };
1679
- const original = await update(entry._id, markPatch, markOptions) ?? entry;
1680
- await safePublish$1(events, outboxStore, LEDGER_EVENTS.ENTRY_REVERSED, {
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
- * Default matching-number generatoratomic counter stored in the
1734
- * reconciliation collection. Uses a dedicated sentinel document keyed
1735
- * `{ matchingNumber: '__counter__', [org]: orgId }` so each org has its
1736
- * own counter, safe under concurrent match calls.
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
- async function nextMatchingNumber(ReconciliationModel, orgField, orgId, session) {
1739
- const counterQuery = { matchingNumber: "__counter__" };
1740
- if (orgField && orgId != null) counterQuery[orgField] = orgId;
1741
- const seq = (await ReconciliationModel.findOneAndUpdate(counterQuery, {
1742
- $inc: { seq: 1 },
1743
- $setOnInsert: {
1744
- account: null,
1745
- items: [{
1746
- entry: null,
1747
- itemIndex: 0
1748
- }, {
1749
- entry: null,
1750
- itemIndex: 1
1751
- }],
1752
- debitTotal: 0,
1753
- creditTotal: 0
1754
- }
1755
- }, {
1756
- new: true,
1757
- upsert: true,
1758
- session,
1759
- strict: false
1760
- }).lean())?.seq ?? 1;
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.success) throw Errors.notFound("Failed to delete reconciliation record");
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 };