@classytic/ledger 0.5.0 → 0.6.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/index.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  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-CsuBGfgs.mjs";
2
2
  import { Money, add, allocate, format, formatPlain, fromDecimal, multiply, parseCents, percentage, splitTaxExclusive, splitTaxInclusive, subtract, toDecimal } from "./money.mjs";
3
- import { n as Errors, t as AccountingError } from "./errors-BmRjW38t.mjs";
4
- import { C as DEFAULT_BUCKETS, S as isVirtualTaxAccount, T as requireOrgScope, _ as getDateRange, a as defaultLogger, b as calculateTotal, c as buildRevaluationEntry, d as generateGeneralLedger, f as generateDimensionBreakdown, g as buildItemFilters, h as generateBalanceSheet, i as finalizeSession, l as computeRevaluation, m as generateBudgetVsActual, n as reopenFiscalPeriod, o as generateTrialBalance, p as generateCashFlow, r as acquireSession, s as generateRevaluation, t as closeFiscalPeriod, u as generateIncomeStatement, v as getFiscalYearStart, w as generateAgedBalance, x as computeEndingBalance, y as buildAccountTypeMap } from "./fiscal-close-Dk3yRT9i.mjs";
3
+ import { n as Errors, t as AccountingError } from "./errors-CSDQPNyt.mjs";
4
+ import { C as isVirtualTaxAccount, E as requireOrgScope, S as computeEndingBalance, T as generateAgedBalance, _ as buildItemFilters, a as finalizeSession, b as buildAccountTypeMap, c as generateRevaluation, d as generateIncomeStatement, f as generateGeneralLedger, g as generateBalanceSheet, h as generateBudgetVsActual, i as acquireSession, l as buildRevaluationEntry, m as generateCashFlow, n as closeFiscalPeriod, o as defaultLogger, p as generateDimensionBreakdown, r as reopenFiscalPeriod, s as generateTrialBalance, t as generatePartnerLedger, u as computeRevaluation, v as getDateRange, w as DEFAULT_BUCKETS, x as calculateTotal, y as getFiscalYearStart } from "./partner-ledger-D9H5hegI.mjs";
5
5
  import { c as getNormalBalance, d as isValidCategory, l as isBalanceSheet, n as CATEGORY_KEYS, t as CATEGORIES, u as isIncomeStatement } from "./categories-BkKdv16V.mjs";
6
- import { i as doubleEntryPlugin, n as idempotencyPlugin, r as fiscalLockPlugin, t as dateLockPlugin } from "./date-lock.plugin-B2Jy0ukX.mjs";
6
+ import { a as taxLockPlugin, c as createLockPlugin, i as fiscalLockPlugin, l as idempotencyPlugin, n as creditLimitPlugin, o as watermarkResolver, r as dailyLockPlugin, s as periodResolver, t as fxRealizationPlugin, u as doubleEntryPlugin } from "./fx-realization.plugin-CgQFDGv2.mjs";
7
7
  import { defineCountryPack } from "./country/index.mjs";
8
8
  import { a as exportToCsv, i as quickbooksFieldMap, r as universalFieldMap, t as flattenJournalEntries } from "./exports-BP-0Ni5W.mjs";
9
9
  import mongoose, { Schema } from "mongoose";
@@ -273,6 +273,121 @@ function createFiscalPeriodSchema(config, options = {}) {
273
273
  return schema;
274
274
  }
275
275
  //#endregion
276
+ //#region src/schemas/journal.schema.ts
277
+ /**
278
+ * Journal Schema Factory (0.6.0 — first-class Journal resource)
279
+ *
280
+ * A Journal is an organization-owned posting channel with its own
281
+ * reference-number sequence, permitted payment methods, optional default
282
+ * accounts, and source configuration for bank/statement imports.
283
+ *
284
+ * Journals are **optional** — a consumer that never seeds journals keeps
285
+ * the 0.5.x enum-only flow on journal entries. Consumers that do seed
286
+ * journals gain:
287
+ *
288
+ * - Per-journal reference-number prefix (e.g. `INV/2026/03/0042`)
289
+ * - Per-journal lock wiring (sale-lock on the sales journal, not globally)
290
+ * - Payment method restrictions
291
+ * - Bank-statement source binding for automated imports (0.7+)
292
+ *
293
+ * The schema is additive: existing journal entries without a `journal` ref
294
+ * keep working unchanged, and seed-from-pack is opt-in via
295
+ * `engine.repositories.journals.seedDefaults(orgId)`.
296
+ */
297
+ function createJournalSchema(config, accountModelName, options = {}) {
298
+ const { multiTenant } = config;
299
+ const { indexes = true, extraFields = {}, extraIndexes = [] } = options;
300
+ const fields = {
301
+ code: {
302
+ type: String,
303
+ required: true,
304
+ trim: true
305
+ },
306
+ name: {
307
+ type: String,
308
+ required: true,
309
+ trim: true
310
+ },
311
+ journalType: {
312
+ type: String,
313
+ required: true
314
+ },
315
+ kind: {
316
+ type: String,
317
+ enum: [
318
+ "general",
319
+ "sale",
320
+ "purchase",
321
+ "bank",
322
+ "cash",
323
+ "misc"
324
+ ],
325
+ default: "general",
326
+ required: true
327
+ },
328
+ sequencePrefix: {
329
+ type: String,
330
+ default: null
331
+ },
332
+ sequenceNextNum: {
333
+ type: Number,
334
+ default: 1,
335
+ min: 1
336
+ },
337
+ defaultDebitAccount: {
338
+ type: mongoose.Schema.Types.ObjectId,
339
+ ref: accountModelName,
340
+ default: null
341
+ },
342
+ defaultCreditAccount: {
343
+ type: mongoose.Schema.Types.ObjectId,
344
+ ref: accountModelName,
345
+ default: null
346
+ },
347
+ allowedPaymentMethods: {
348
+ type: [String],
349
+ default: []
350
+ },
351
+ source: {
352
+ type: String,
353
+ default: "manual"
354
+ },
355
+ active: {
356
+ type: Boolean,
357
+ default: true
358
+ },
359
+ ...extraFields
360
+ };
361
+ if (multiTenant) fields[multiTenant.orgField] = {
362
+ type: mongoose.Schema.Types.ObjectId,
363
+ ref: multiTenant.orgRef,
364
+ required: true
365
+ };
366
+ const schema = new mongoose.Schema(fields, { timestamps: true });
367
+ if (indexes) {
368
+ const org = multiTenant?.orgField;
369
+ if (org) {
370
+ schema.index({
371
+ [org]: 1,
372
+ code: 1
373
+ }, { unique: true });
374
+ schema.index({
375
+ [org]: 1,
376
+ kind: 1,
377
+ active: 1
378
+ });
379
+ } else {
380
+ schema.index({ code: 1 }, { unique: true });
381
+ schema.index({
382
+ kind: 1,
383
+ active: 1
384
+ });
385
+ }
386
+ }
387
+ for (const idx of extraIndexes) schema.index(idx.fields, idx.options);
388
+ return schema;
389
+ }
390
+ //#endregion
276
391
  //#region src/schemas/journal-entry.schema.ts
277
392
  /**
278
393
  * Journal Entry Schema Factory
@@ -308,17 +423,19 @@ function createJournalEntrySchema(config, accountModelName, options = {}) {
308
423
  message: "exchangeRate must be greater than zero when set, got {VALUE}"
309
424
  }
310
425
  };
426
+ const originalAmountValidator = {
427
+ validator: (v) => v === null || v === void 0 || Number.isInteger(v) && v >= 0,
428
+ message: "{PATH} must be a non-negative integer (cents), got {VALUE}"
429
+ };
311
430
  currencyItemFields.originalDebit = {
312
431
  type: Number,
313
432
  default: null,
314
- min: 0,
315
- validate: amountValidator
433
+ validate: originalAmountValidator
316
434
  };
317
435
  currencyItemFields.originalCredit = {
318
436
  type: Number,
319
437
  default: null,
320
- min: 0,
321
- validate: amountValidator
438
+ validate: originalAmountValidator
322
439
  };
323
440
  }
324
441
  const JournalItemSchema = new mongoose.Schema({
@@ -345,6 +462,14 @@ function createJournalEntrySchema(config, accountModelName, options = {}) {
345
462
  type: [TaxDetailSchema],
346
463
  default: []
347
464
  },
465
+ matchingNumber: {
466
+ type: String,
467
+ default: null
468
+ },
469
+ maturityDate: {
470
+ type: Date,
471
+ default: null
472
+ },
348
473
  ...currencyItemFields,
349
474
  ...extraItemFields
350
475
  }, { _id: false });
@@ -356,6 +481,10 @@ function createJournalEntrySchema(config, accountModelName, options = {}) {
356
481
  default: JOURNAL_CODES.MISC,
357
482
  required: true
358
483
  },
484
+ journal: {
485
+ type: mongoose.Schema.Types.ObjectId,
486
+ default: null
487
+ },
359
488
  referenceNumber: { type: String },
360
489
  label: { type: String },
361
490
  date: {
@@ -572,6 +701,11 @@ function createJournalEntrySchema(config, accountModelName, options = {}) {
572
701
  });
573
702
  }
574
703
  schema.index({ reversed: 1 });
704
+ if (org) schema.index({
705
+ [org]: 1,
706
+ "journalItems.matchingNumber": 1
707
+ });
708
+ else schema.index({ "journalItems.matchingNumber": 1 });
575
709
  if (config.idempotency) {
576
710
  const idempotencyIdx = {};
577
711
  if (org) idempotencyIdx[org] = 1;
@@ -601,50 +735,112 @@ function createJournalEntrySchema(config, accountModelName, options = {}) {
601
735
  //#endregion
602
736
  //#region src/schemas/reconciliation.schema.ts
603
737
  /**
604
- * Reconciliation Schema Factory
738
+ * Reconciliation Schema Factory (0.6.0 — item-level open-item matching)
739
+ *
740
+ * A reconciliation is a **matching group** of journal items that together
741
+ * settle each other in whole or in part. Each group carries a stable
742
+ * `matchingNumber` string that is stamped onto every referenced journal
743
+ * item. A group is `isFullReconcile = true` iff debit/credit totals
744
+ * balance to zero — partial matches (a cheque paying 2 of 3 invoices) are
745
+ * represented by isFullReconcile=false.
746
+ *
747
+ * This replaces 0.5.x entry-level reconciliation, which could not represent
748
+ * the canonical AR/AP flow where one cheque covers multiple invoices or
749
+ * one invoice is paid by multiple cheques.
605
750
  *
606
- * Creates a Mongoose schema for reconciliation records that link matched
607
- * debit/credit journal items. Used to track which journal entries have been
608
- * reconciled against each other for a given account.
751
+ * A dedicated collection exists so that match/unmatch can atomically stamp
752
+ * `journalItems[i].matchingNumber`, update totals, and trigger the
753
+ * fxRealizationPlugin via mongokit hooks without bumping the entries'
754
+ * `updatedAt` timestamps.
609
755
  */
610
756
  function createReconciliationSchema(config, accountModelName, journalEntryModelName, options = {}) {
611
757
  const { multiTenant } = config;
612
758
  const { indexes = true, extraFields = {}, extraIndexes = [] } = options;
759
+ const MatchedItemRefSchema = new mongoose.Schema({
760
+ entry: {
761
+ type: mongoose.Schema.Types.ObjectId,
762
+ ref: journalEntryModelName,
763
+ required: true
764
+ },
765
+ itemIndex: {
766
+ type: Number,
767
+ required: true,
768
+ min: 0,
769
+ validate: {
770
+ validator: Number.isInteger,
771
+ message: "itemIndex must be a non-negative integer"
772
+ }
773
+ },
774
+ debit: {
775
+ type: Number,
776
+ default: 0,
777
+ min: 0
778
+ },
779
+ credit: {
780
+ type: Number,
781
+ default: 0,
782
+ min: 0
783
+ },
784
+ amountCurrency: {
785
+ type: Number,
786
+ default: null
787
+ },
788
+ exchangeRate: {
789
+ type: Number,
790
+ default: null
791
+ }
792
+ }, { _id: false });
613
793
  const fields = {
794
+ matchingNumber: {
795
+ type: String,
796
+ required: true
797
+ },
614
798
  account: {
615
799
  type: mongoose.Schema.Types.ObjectId,
616
800
  ref: accountModelName,
617
801
  required: true
618
802
  },
619
- journalEntryIds: {
620
- type: [{
621
- type: mongoose.Schema.Types.ObjectId,
622
- ref: journalEntryModelName
623
- }],
803
+ items: {
804
+ type: [MatchedItemRefSchema],
624
805
  required: true,
625
806
  validate: {
626
- validator: (v) => Array.isArray(v) && v.length > 0,
627
- message: "journalEntryIds must contain at least one entry."
807
+ validator: (v) => Array.isArray(v) && v.length >= 2,
808
+ message: "a reconciliation must reference at least two items"
628
809
  }
629
810
  },
630
811
  debitTotal: {
631
812
  type: Number,
632
- required: true
813
+ required: true,
814
+ min: 0
633
815
  },
634
816
  creditTotal: {
635
817
  type: Number,
636
- required: true
818
+ required: true,
819
+ min: 0
637
820
  },
638
821
  difference: {
639
822
  type: Number,
640
823
  default: 0
641
824
  },
825
+ isFullReconcile: {
826
+ type: Boolean,
827
+ default: false
828
+ },
829
+ currency: {
830
+ type: String,
831
+ default: null
832
+ },
642
833
  note: { type: String },
643
834
  reconciledBy: { type: String },
644
835
  reconciledAt: {
645
836
  type: Date,
646
837
  default: Date.now
647
838
  },
839
+ fxRealizationEntry: {
840
+ type: mongoose.Schema.Types.ObjectId,
841
+ ref: journalEntryModelName,
842
+ default: null
843
+ },
648
844
  ...extraFields
649
845
  };
650
846
  if (multiTenant) fields[multiTenant.orgField] = {
@@ -654,18 +850,31 @@ function createReconciliationSchema(config, accountModelName, journalEntryModelN
654
850
  };
655
851
  const schema = new mongoose.Schema(fields, { timestamps: true });
656
852
  if (indexes) {
657
- if (multiTenant) {
658
- const org = multiTenant.orgField;
853
+ const org = multiTenant?.orgField;
854
+ if (org) {
855
+ schema.index({
856
+ [org]: 1,
857
+ matchingNumber: 1
858
+ }, { unique: true });
659
859
  schema.index({
660
860
  [org]: 1,
661
861
  account: 1,
862
+ isFullReconcile: 1,
662
863
  reconciledAt: 1
663
864
  });
664
- } else schema.index({
665
- account: 1,
666
- reconciledAt: 1
667
- });
668
- schema.index({ journalEntryIds: 1 });
865
+ schema.index({
866
+ [org]: 1,
867
+ "items.entry": 1
868
+ });
869
+ } else {
870
+ schema.index({ matchingNumber: 1 }, { unique: true });
871
+ schema.index({
872
+ account: 1,
873
+ isFullReconcile: 1,
874
+ reconciledAt: 1
875
+ });
876
+ schema.index({ "items.entry": 1 });
877
+ }
669
878
  }
670
879
  for (const idx of extraIndexes) schema.index(idx.fields, idx.options);
671
880
  return schema;
@@ -678,33 +887,29 @@ function resolveModelNames(overrides) {
678
887
  journalEntry: overrides?.journalEntry ?? "JournalEntry",
679
888
  fiscalPeriod: overrides?.fiscalPeriod ?? "FiscalPeriod",
680
889
  budget: overrides?.budget ?? "Budget",
681
- reconciliation: overrides?.reconciliation ?? "Reconciliation"
890
+ reconciliation: overrides?.reconciliation ?? "Reconciliation",
891
+ journal: overrides?.journal ?? "Journal"
682
892
  };
683
893
  }
684
- /**
685
- * Create (or reuse) all ledger models on the given connection.
686
- *
687
- * If a model with the same name is already registered on the connection,
688
- * the existing model is reused — this allows multiple engine instances
689
- * to share models and prevents "OverwriteModelError".
690
- */
691
894
  function createModels(connection, config) {
692
895
  const names = resolveModelNames(config.modelNames);
693
896
  const so = config.schemaOptions ?? {};
694
897
  const existing = connection.models;
695
- if (existing[names.account] && existing[names.journalEntry] && existing[names.fiscalPeriod] && existing[names.budget] && existing[names.reconciliation]) return {
898
+ if (existing[names.account] && existing[names.journalEntry] && existing[names.fiscalPeriod] && existing[names.budget] && existing[names.reconciliation] && existing[names.journal]) return {
696
899
  Account: existing[names.account],
697
900
  JournalEntry: existing[names.journalEntry],
698
901
  FiscalPeriod: existing[names.fiscalPeriod],
699
902
  Budget: existing[names.budget],
700
- Reconciliation: existing[names.reconciliation]
903
+ Reconciliation: existing[names.reconciliation],
904
+ Journal: existing[names.journal]
701
905
  };
702
906
  return {
703
907
  Account: existing[names.account] ?? connection.model(names.account, createAccountSchema(config, so.account)),
704
908
  JournalEntry: existing[names.journalEntry] ?? connection.model(names.journalEntry, createJournalEntrySchema(config, names.account, so.journalEntry)),
705
909
  FiscalPeriod: existing[names.fiscalPeriod] ?? connection.model(names.fiscalPeriod, createFiscalPeriodSchema(config, so.fiscalPeriod)),
706
910
  Budget: existing[names.budget] ?? connection.model(names.budget, createBudgetSchema(config, so.budget)),
707
- Reconciliation: existing[names.reconciliation] ?? connection.model(names.reconciliation, createReconciliationSchema(config, names.account, names.journalEntry, so.reconciliation))
911
+ Reconciliation: existing[names.reconciliation] ?? connection.model(names.reconciliation, createReconciliationSchema(config, names.account, names.journalEntry, so.reconciliation)),
912
+ Journal: existing[names.journal] ?? connection.model(names.journal, createJournalSchema(config, names.account, so.journal))
708
913
  };
709
914
  }
710
915
  //#endregion
@@ -901,6 +1106,104 @@ function wireAccountMethods(repository, AccountModel, country, orgField) {
901
1106
  return repository;
902
1107
  }
903
1108
  //#endregion
1109
+ //#region src/repositories/journal.repository.ts
1110
+ /**
1111
+ * Lean default set used when a country pack doesn't provide
1112
+ * `journalTemplates`. Covers the Stripe/QuickBooks/Xero baseline.
1113
+ */
1114
+ const DEFAULT_TEMPLATES = [
1115
+ {
1116
+ code: "SALES",
1117
+ name: "Sales",
1118
+ journalType: "SALES",
1119
+ kind: "sale",
1120
+ sequencePrefix: "INV"
1121
+ },
1122
+ {
1123
+ code: "PURCHASE",
1124
+ name: "Purchases",
1125
+ journalType: "PURCHASES",
1126
+ kind: "purchase",
1127
+ sequencePrefix: "BILL"
1128
+ },
1129
+ {
1130
+ code: "BANK",
1131
+ name: "Bank",
1132
+ journalType: "CASH_RECEIPTS",
1133
+ kind: "bank",
1134
+ sequencePrefix: "BNK"
1135
+ },
1136
+ {
1137
+ code: "CASH",
1138
+ name: "Cash",
1139
+ journalType: "CASH_PAYMENTS",
1140
+ kind: "cash",
1141
+ sequencePrefix: "CSH"
1142
+ },
1143
+ {
1144
+ code: "MISC",
1145
+ name: "Miscellaneous",
1146
+ journalType: "MISC",
1147
+ kind: "general",
1148
+ sequencePrefix: "JE"
1149
+ }
1150
+ ];
1151
+ function wireJournalMethods(repository, country, orgField) {
1152
+ const create = repository.create.bind(repository);
1153
+ const exists = repository.exists.bind(repository);
1154
+ repository.seedDefaults = async (orgId) => {
1155
+ requireOrgScope(orgField, orgId);
1156
+ const templates = country.journalTemplates ?? DEFAULT_TEMPLATES;
1157
+ let created = 0;
1158
+ let skipped = 0;
1159
+ for (const tpl of templates) {
1160
+ const existingQuery = { code: tpl.code };
1161
+ if (orgField && orgId != null) existingQuery[orgField] = orgId;
1162
+ if (await exists(existingQuery)) {
1163
+ skipped += 1;
1164
+ continue;
1165
+ }
1166
+ const data = {
1167
+ code: tpl.code,
1168
+ name: tpl.name,
1169
+ journalType: tpl.journalType,
1170
+ kind: tpl.kind ?? "general",
1171
+ sequencePrefix: tpl.sequencePrefix ?? tpl.code,
1172
+ sequenceNextNum: tpl.sequenceStartNum ?? 1,
1173
+ active: true
1174
+ };
1175
+ if (orgField && orgId != null) data[orgField] = orgId;
1176
+ await create(data);
1177
+ created += 1;
1178
+ }
1179
+ return {
1180
+ created,
1181
+ skipped
1182
+ };
1183
+ };
1184
+ repository.nextSequenceNumber = async (journalId, orgId) => {
1185
+ requireOrgScope(orgField, orgId);
1186
+ const query = { _id: journalId };
1187
+ if (orgField && orgId != null) query[orgField] = orgId;
1188
+ const updated = await repository._executeQuery(async (Model) => Model.findOneAndUpdate(query, { $inc: { sequenceNextNum: 1 } }, { returnDocument: "after" }).lean());
1189
+ if (!updated) throw Errors.notFound(`Journal ${String(journalId)} not found`);
1190
+ const next = (updated.sequenceNextNum ?? 1) - 1;
1191
+ const prefix = updated.sequencePrefix ?? updated.code ?? "JE";
1192
+ const now = /* @__PURE__ */ new Date();
1193
+ return `${prefix}/${now.getFullYear()}/${String(now.getMonth() + 1).padStart(2, "0")}/${String(next).padStart(4, "0")}`;
1194
+ };
1195
+ if (typeof repository.registerMethod === "function") for (const name of ["seedDefaults", "nextSequenceNumber"]) {
1196
+ const fn = repository[name];
1197
+ try {
1198
+ delete repository[name];
1199
+ repository.registerMethod(name, fn);
1200
+ } catch {
1201
+ repository[name] = fn;
1202
+ }
1203
+ }
1204
+ return repository;
1205
+ }
1206
+ //#endregion
904
1207
  //#region src/repositories/journal-entry.repository.ts
905
1208
  /** Keys that are either handled explicitly or must not be copied */
906
1209
  const ITEM_CORE_KEYS = new Set([
@@ -927,7 +1230,43 @@ const ITEM_CORE_KEYS = new Set([
927
1230
  function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, strictness) {
928
1231
  const getByQuery = repository.getByQuery.bind(repository);
929
1232
  const create = repository.create.bind(repository);
1233
+ const update = repository.update.bind(repository);
930
1234
  const withTransaction = repository.withTransaction.bind(repository);
1235
+ const RESERVED_TOPLEVEL = new Set([
1236
+ "_id",
1237
+ "__v",
1238
+ "id",
1239
+ "journalType",
1240
+ "state",
1241
+ "date",
1242
+ "label",
1243
+ "journalItems",
1244
+ "totalDebit",
1245
+ "totalCredit",
1246
+ "reversalOf",
1247
+ "reversedBy",
1248
+ "reversedByUser",
1249
+ "reversed",
1250
+ "stateChangedAt",
1251
+ "createdAt",
1252
+ "updatedAt",
1253
+ "referenceNumber",
1254
+ "idempotencyKey",
1255
+ "postedBy",
1256
+ "approvedBy",
1257
+ "approvedAt"
1258
+ ]);
1259
+ /** Copy non-reserved top-level fields from `source` onto `target`. */
1260
+ function copyExtraTopLevel(source, target) {
1261
+ const obj = typeof source.toObject === "function" ? source.toObject() : source;
1262
+ for (const key of Object.keys(obj)) {
1263
+ if (RESERVED_TOPLEVEL.has(key)) continue;
1264
+ if (key in target) continue;
1265
+ const value = obj[key];
1266
+ if (value === void 0 || value === null) continue;
1267
+ target[key] = value;
1268
+ }
1269
+ }
931
1270
  /** Build a tenant-scoped query for a single entry by ID (injection-safe) */
932
1271
  function buildQuery(id, orgId) {
933
1272
  validateScalarId(id, "entry ID");
@@ -993,11 +1332,16 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
993
1332
  const totalDebit = entry.journalItems.reduce((s, i) => s + (i.debit || 0), 0);
994
1333
  const totalCredit = entry.journalItems.reduce((s, i) => s + (i.credit || 0), 0);
995
1334
  if (totalDebit !== totalCredit) throw Errors.validation(`Entry is not balanced. Debit: ${totalDebit}, Credit: ${totalCredit}`);
996
- entry.state = "posted";
997
- entry.stateChangedAt = /* @__PURE__ */ new Date();
998
- if (options.actorId) entry.postedBy = options.actorId;
999
- await entry.save({ session: options.session });
1000
- return entry;
1335
+ const patch = {
1336
+ state: "posted",
1337
+ stateChangedAt: /* @__PURE__ */ new Date()
1338
+ };
1339
+ if (options.actorId) patch.postedBy = options.actorId;
1340
+ const updateOptions = {
1341
+ _ledgerInternal: "post",
1342
+ ...options.session ? { session: options.session } : {}
1343
+ };
1344
+ return await update(entry._id, patch, updateOptions) ?? entry;
1001
1345
  };
1002
1346
  /**
1003
1347
  * Unpost an entry (posted → draft).
@@ -1012,10 +1356,14 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
1012
1356
  if (!entry) throw Errors.notFound("Entry not found");
1013
1357
  if (entry.state !== "posted") throw Errors.validation("Only posted entries can be unposted");
1014
1358
  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.");
1015
- entry.state = "draft";
1016
- entry.stateChangedAt = /* @__PURE__ */ new Date();
1017
- await entry.save({ session: options.session });
1018
- return entry;
1359
+ const updateOptions = {
1360
+ _ledgerInternal: "unpost",
1361
+ ...options.session ? { session: options.session } : {}
1362
+ };
1363
+ return await update(entry._id, {
1364
+ state: "draft",
1365
+ stateChangedAt: /* @__PURE__ */ new Date()
1366
+ }, updateOptions) ?? entry;
1019
1367
  };
1020
1368
  /**
1021
1369
  * Archive a draft entry (draft → archived).
@@ -1028,10 +1376,14 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
1028
1376
  const entry = await findEntry(buildQuery(id, orgId), { session: options.session });
1029
1377
  if (!entry) throw Errors.notFound("Entry not found");
1030
1378
  if (entry.state !== "draft") throw Errors.validation("Only draft entries can be archived");
1031
- entry.state = "archived";
1032
- entry.stateChangedAt = /* @__PURE__ */ new Date();
1033
- await entry.save({ session: options.session });
1034
- return entry;
1379
+ const updateOptions = {
1380
+ _ledgerInternal: "archive",
1381
+ ...options.session ? { session: options.session } : {}
1382
+ };
1383
+ return await update(entry._id, {
1384
+ state: "archived",
1385
+ stateChangedAt: /* @__PURE__ */ new Date()
1386
+ }, updateOptions) ?? entry;
1035
1387
  };
1036
1388
  /**
1037
1389
  * Duplicate an entry as a new draft.
@@ -1061,7 +1413,7 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
1061
1413
  };
1062
1414
  })
1063
1415
  };
1064
- if (orgField && entry[orgField] != null) duplicateData[orgField] = entry[orgField];
1416
+ copyExtraTopLevel(entry, duplicateData);
1065
1417
  return await create(duplicateData, options.session ? { session: options.session } : {});
1066
1418
  };
1067
1419
  /**
@@ -1113,15 +1465,20 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
1113
1465
  reversalOf: entry._id,
1114
1466
  stateChangedAt: /* @__PURE__ */ new Date()
1115
1467
  };
1116
- if (orgField && entry[orgField] != null) reversalData[orgField] = entry[orgField];
1468
+ copyExtraTopLevel(entry, reversalData);
1117
1469
  if (options.actorId) reversalData.postedBy = options.actorId;
1118
1470
  const reversalEntry = await create(reversalData, session ? { session } : {});
1119
- entry.reversed = true;
1120
- entry.reversedBy = reversalEntry._id;
1121
- if (options.actorId) entry.reversedByUser = options.actorId;
1122
- await entry.save({ session });
1471
+ const markPatch = {
1472
+ reversed: true,
1473
+ reversedBy: reversalEntry._id
1474
+ };
1475
+ if (options.actorId) markPatch.reversedByUser = options.actorId;
1476
+ const markOptions = {
1477
+ _ledgerInternal: "reverseMark",
1478
+ ...session ? { session } : {}
1479
+ };
1123
1480
  return {
1124
- original: entry,
1481
+ original: await update(entry._id, markPatch, markOptions) ?? entry,
1125
1482
  reversal: reversalEntry
1126
1483
  };
1127
1484
  };
@@ -1150,91 +1507,186 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
1150
1507
  //#endregion
1151
1508
  //#region src/repositories/reconciliation.repository.ts
1152
1509
  /**
1153
- * Wire reconciliation methods onto an existing mongokit Repository.
1154
- *
1155
- * - reconcile() uses repository.create() so hooks (multi-tenant, audit) fire
1156
- * - unreconcile() uses repository.delete() so hooks fire
1157
- * - Cross-repo reads (JournalEntryModel) use direct Model access (acceptable)
1510
+ * Default matching-number generator atomic counter stored in the
1511
+ * reconciliation collection. Uses a dedicated sentinel document keyed
1512
+ * `{ matchingNumber: '__counter__', [org]: orgId }` so each org has its
1513
+ * own counter, safe under concurrent match calls.
1158
1514
  */
1159
- function wireReconciliationMethods(repository, _ReconciliationModel, JournalEntryModel, orgField) {
1515
+ async function nextMatchingNumber(ReconciliationModel, orgField, orgId, session) {
1516
+ const counterQuery = { matchingNumber: "__counter__" };
1517
+ if (orgField && orgId != null) counterQuery[orgField] = orgId;
1518
+ const seq = (await ReconciliationModel.findOneAndUpdate(counterQuery, {
1519
+ $inc: { seq: 1 },
1520
+ $setOnInsert: {
1521
+ account: null,
1522
+ items: [{
1523
+ entry: null,
1524
+ itemIndex: 0
1525
+ }, {
1526
+ entry: null,
1527
+ itemIndex: 1
1528
+ }],
1529
+ debitTotal: 0,
1530
+ creditTotal: 0
1531
+ }
1532
+ }, {
1533
+ new: true,
1534
+ upsert: true,
1535
+ session,
1536
+ strict: false
1537
+ }).lean())?.seq ?? 1;
1538
+ return `RECN-${String(seq).padStart(6, "0")}`;
1539
+ }
1540
+ function wireReconciliationMethods(repository, ReconciliationModel, JournalEntryModel, orgField) {
1160
1541
  const create = repository.create.bind(repository);
1161
1542
  const deleteById = repository.delete.bind(repository);
1162
- /**
1163
- * Create a reconciliation record linking matched journal entries.
1164
- * Validates that all entries exist, are posted, and belong to the same account/org.
1165
- */
1166
- repository.reconcile = async (input) => {
1167
- const { account, journalEntryIds, note, reconciledBy, organizationId } = input;
1543
+ const repoInstance = repository;
1544
+ repository.match = async (input) => {
1545
+ const { account, items, note, reconciledBy, organizationId, session = null } = input;
1546
+ let { matchingNumber } = input;
1168
1547
  requireOrgScope(orgField, organizationId);
1169
- if (!journalEntryIds || journalEntryIds.length === 0) throw Errors.validation("journalEntryIds must contain at least one entry.");
1170
- const query = { _id: { $in: journalEntryIds } };
1171
- if (orgField && organizationId != null) query[orgField] = organizationId;
1172
- const entries = await JournalEntryModel.find(query).lean();
1173
- if (entries.length !== journalEntryIds.length) throw Errors.notFound(`Expected ${journalEntryIds.length} entries but found ${entries.length}. Some entries do not exist or belong to a different organization.`);
1174
- const notPosted = entries.filter((e) => e.state !== "posted");
1175
- if (notPosted.length > 0) throw Errors.validation(`${notPosted.length} entry(ies) are not posted. Only posted entries can be reconciled.`);
1548
+ if (!Array.isArray(items) || items.length < 2) throw Errors.validation("match() requires at least two items");
1549
+ const entryIds = Array.from(new Set(items.map((i) => String(i.entry))));
1550
+ const entryQuery = { _id: { $in: entryIds } };
1551
+ if (orgField && organizationId != null) entryQuery[orgField] = organizationId;
1552
+ const entries = await JournalEntryModel.find(entryQuery).session(session).lean();
1553
+ if (entries.length !== entryIds.length) throw Errors.notFound(`Expected ${entryIds.length} entries but found ${entries.length}. Some do not exist or belong to a different organization.`);
1554
+ const entryMap = /* @__PURE__ */ new Map();
1555
+ for (const e of entries) entryMap.set(String(e._id), e);
1176
1556
  const accountStr = String(account);
1177
- for (const entry of entries) if (!entry.journalItems.some((item) => String(item.account) === accountStr)) throw Errors.validation(`Entry ${entry._id} does not contain any items for account ${account}.`);
1178
1557
  let debitTotal = 0;
1179
1558
  let creditTotal = 0;
1180
- for (const entry of entries) for (const item of entry.journalItems) if (String(item.account) === accountStr) {
1181
- debitTotal += item.debit ?? 0;
1182
- creditTotal += item.credit ?? 0;
1559
+ const currencies = /* @__PURE__ */ new Set();
1560
+ const itemSnapshots = [];
1561
+ for (const ref of items) {
1562
+ const entry = entryMap.get(String(ref.entry));
1563
+ if (!entry) throw Errors.notFound(`Entry ${String(ref.entry)} not found in match input`);
1564
+ if (entry.state !== "posted") throw Errors.validation(`Entry ${String(entry._id)} is not posted — only posted entries can be matched`);
1565
+ const item = entry.journalItems[ref.itemIndex];
1566
+ if (!item) throw Errors.validation(`Entry ${String(entry._id)} has no item at index ${ref.itemIndex}`);
1567
+ if (String(item.account) !== accountStr) throw Errors.validation(`Item ${String(entry._id)}[${ref.itemIndex}] is on a different account than the match`);
1568
+ if (item.matchingNumber) throw Errors.conflict(`Item ${String(entry._id)}[${ref.itemIndex}] is already matched (${item.matchingNumber})`);
1569
+ const debit = item.debit ?? 0;
1570
+ const credit = item.credit ?? 0;
1571
+ debitTotal += debit;
1572
+ creditTotal += credit;
1573
+ if (item.currency) currencies.add(item.currency);
1574
+ const amountCurrency = item.originalDebit != null || item.originalCredit != null ? (item.originalDebit ?? 0) - (item.originalCredit ?? 0) : null;
1575
+ itemSnapshots.push({
1576
+ entry: entry._id,
1577
+ itemIndex: ref.itemIndex,
1578
+ debit,
1579
+ credit,
1580
+ amountCurrency,
1581
+ exchangeRate: item.exchangeRate ?? null
1582
+ });
1183
1583
  }
1584
+ const difference = debitTotal - creditTotal;
1585
+ const isFullReconcile = difference === 0;
1586
+ const sharedCurrency = currencies.size === 1 ? Array.from(currencies)[0] : null;
1587
+ if (!matchingNumber) matchingNumber = await nextMatchingNumber(ReconciliationModel, orgField, organizationId, session);
1588
+ const bulkOps = itemSnapshots.map((snap) => ({ updateOne: {
1589
+ filter: { _id: snap.entry },
1590
+ update: { $set: { [`journalItems.${snap.itemIndex}.matchingNumber`]: matchingNumber } }
1591
+ } }));
1592
+ await JournalEntryModel.bulkWrite(bulkOps, { session: session ?? void 0 });
1184
1593
  const reconciliationData = {
1594
+ matchingNumber,
1185
1595
  account,
1186
- journalEntryIds,
1596
+ items: itemSnapshots.map((s) => ({
1597
+ entry: s.entry,
1598
+ itemIndex: s.itemIndex,
1599
+ debit: s.debit,
1600
+ credit: s.credit,
1601
+ amountCurrency: s.amountCurrency,
1602
+ exchangeRate: s.exchangeRate
1603
+ })),
1187
1604
  debitTotal,
1188
1605
  creditTotal,
1189
- difference: debitTotal - creditTotal,
1606
+ difference,
1607
+ isFullReconcile,
1608
+ currency: sharedCurrency,
1190
1609
  note,
1191
1610
  reconciledBy,
1192
1611
  reconciledAt: /* @__PURE__ */ new Date()
1193
1612
  };
1194
1613
  if (orgField && organizationId != null) reconciliationData[orgField] = organizationId;
1195
- return await create(reconciliationData);
1614
+ const record = await create(reconciliationData);
1615
+ const emit = repoInstance._emitHook;
1616
+ if (emit) await emit.call(repoInstance, "after:match", {
1617
+ reconciliation: record,
1618
+ items: itemSnapshots,
1619
+ sharedCurrency,
1620
+ session
1621
+ });
1622
+ return record;
1196
1623
  };
1197
- /**
1198
- * Remove a reconciliation record via repository.delete().
1199
- */
1200
- repository.unreconcile = async (input) => {
1201
- const { reconciliationId, organizationId } = input;
1624
+ repository.unmatch = async (input) => {
1625
+ const { matchingNumber, organizationId, session = null } = input;
1202
1626
  requireOrgScope(orgField, organizationId);
1203
- if (orgField && organizationId != null) {
1204
- if (!await repository._executeQuery(async (Model) => Model.findOne({
1205
- _id: reconciliationId,
1206
- [orgField]: organizationId
1207
- }).select("_id").lean())) throw Errors.notFound("Reconciliation record not found.");
1208
- }
1209
- const result = await deleteById(String(reconciliationId));
1210
- if (!result.success) throw Errors.notFound("Reconciliation record not found.");
1627
+ const query = { matchingNumber };
1628
+ if (orgField && organizationId != null) query[orgField] = organizationId;
1629
+ const existing = await ReconciliationModel.findOne(query).session(session).lean();
1630
+ if (!existing) throw Errors.notFound(`Reconciliation ${matchingNumber} not found`);
1631
+ const bulkOps = (existing.items ?? []).map((it) => ({ updateOne: {
1632
+ filter: { _id: it.entry },
1633
+ update: { $set: { [`journalItems.${it.itemIndex}.matchingNumber`]: null } }
1634
+ } }));
1635
+ if (bulkOps.length > 0) await JournalEntryModel.bulkWrite(bulkOps, { session: session ?? void 0 });
1636
+ const result = await deleteById(String(existing._id));
1637
+ if (!result.success) throw Errors.notFound("Failed to delete reconciliation record");
1211
1638
  return result;
1212
1639
  };
1213
- /**
1214
- * Find journal entries for an account that are NOT in any reconciliation record.
1215
- * Uses repository.getAll() for reconciliation lookups (hooks fire),
1216
- * and direct JournalEntryModel for cross-repo reads (acceptable).
1217
- */
1218
- repository.getUnreconciled = async (input) => {
1219
- const { accountId, organizationId, limit = 100, skip = 0 } = input;
1640
+ repository.getOpenItems = async (params) => {
1641
+ const { accountId, organizationId, filter, asOfDate, limit = 100, skip = 0 } = params;
1220
1642
  requireOrgScope(orgField, organizationId);
1221
- const reconFilter = { account: accountId };
1222
- if (orgField && organizationId != null) reconFilter[orgField] = organizationId;
1223
- const reconciliations = await repository._executeQuery(async (Model) => Model.find(reconFilter).select("journalEntryIds").lean());
1224
- const reconciledIds = /* @__PURE__ */ new Set();
1225
- for (const rec of reconciliations) for (const id of rec.journalEntryIds) reconciledIds.add(String(id));
1226
- const entryFilter = {
1227
- state: "posted",
1228
- "journalItems.account": accountId
1229
- };
1230
- if (orgField && organizationId != null) entryFilter[orgField] = organizationId;
1231
- if (reconciledIds.size > 0) entryFilter._id = { $nin: Array.from(reconciledIds) };
1232
- return await JournalEntryModel.find(entryFilter).sort({ date: -1 }).skip(skip).limit(limit).lean();
1643
+ const match = { state: "posted" };
1644
+ if (orgField && organizationId != null) match[orgField] = organizationId;
1645
+ if (asOfDate) match.date = { $lte: asOfDate };
1646
+ const pipeline = [
1647
+ { $match: match },
1648
+ { $project: {
1649
+ _id: 1,
1650
+ date: 1,
1651
+ referenceNumber: 1,
1652
+ journalItems: 1
1653
+ } },
1654
+ { $addFields: { journalItems: { $map: {
1655
+ input: { $range: [0, { $size: "$journalItems" }] },
1656
+ as: "idx",
1657
+ in: { $mergeObjects: [{ $arrayElemAt: ["$journalItems", "$$idx"] }, { _itemIndex: "$$idx" }] }
1658
+ } } } },
1659
+ { $unwind: "$journalItems" },
1660
+ { $match: {
1661
+ "journalItems.account": accountId,
1662
+ $or: [{ "journalItems.matchingNumber": null }, { "journalItems.matchingNumber": { $exists: false } }],
1663
+ ...filter ? Object.fromEntries(Object.entries(filter).map(([k, v]) => [`journalItems.${k}`, v])) : {}
1664
+ } },
1665
+ { $project: {
1666
+ _id: 0,
1667
+ entry: "$_id",
1668
+ itemIndex: "$journalItems._itemIndex",
1669
+ debit: { $ifNull: ["$journalItems.debit", 0] },
1670
+ credit: { $ifNull: ["$journalItems.credit", 0] },
1671
+ date: { $ifNull: ["$journalItems.date", "$date"] },
1672
+ maturityDate: "$journalItems.maturityDate",
1673
+ account: "$journalItems.account",
1674
+ currency: "$journalItems.currency",
1675
+ exchangeRate: "$journalItems.exchangeRate",
1676
+ label: "$journalItems.label",
1677
+ referenceNumber: 1,
1678
+ item: "$journalItems"
1679
+ } },
1680
+ { $sort: { date: 1 } },
1681
+ { $skip: skip },
1682
+ { $limit: limit }
1683
+ ];
1684
+ return await JournalEntryModel.aggregate(pipeline);
1233
1685
  };
1234
1686
  if (typeof repository.registerMethod === "function") for (const name of [
1235
- "reconcile",
1236
- "unreconcile",
1237
- "getUnreconciled"
1687
+ "match",
1688
+ "unmatch",
1689
+ "getOpenItems"
1238
1690
  ]) {
1239
1691
  const fn = repository[name];
1240
1692
  try {
@@ -1293,12 +1745,18 @@ function createRepositories(models, config, plugins = {}, pagination = {}) {
1293
1745
  JournalEntryModel: models.JournalEntry,
1294
1746
  orgField
1295
1747
  }));
1748
+ const journalEntries = wireJournalEntryMethods(new Repository(models.JournalEntry, jePlugins, jePagination), models.JournalEntry, orgField, strictness);
1749
+ const fiscalPeriods = new Repository(models.FiscalPeriod, plugins.fiscalPeriod ?? [], fpPagination);
1750
+ const budgets = new Repository(models.Budget, plugins.budget ?? [], budgetPagination);
1751
+ const reconciliations = wireReconciliationMethods(new Repository(models.Reconciliation, plugins.reconciliation ?? [], reconPagination), models.Reconciliation, models.JournalEntry, orgField);
1752
+ const journalPagination = pagination.journal ?? {};
1296
1753
  return {
1297
1754
  accounts,
1298
- journalEntries: wireJournalEntryMethods(new Repository(models.JournalEntry, jePlugins, jePagination), models.JournalEntry, orgField, strictness),
1299
- fiscalPeriods: new Repository(models.FiscalPeriod, plugins.fiscalPeriod ?? [], fpPagination),
1300
- budgets: new Repository(models.Budget, plugins.budget ?? [], budgetPagination),
1301
- reconciliations: wireReconciliationMethods(new Repository(models.Reconciliation, plugins.reconciliation ?? [], reconPagination), models.Reconciliation, models.JournalEntry, orgField)
1755
+ journalEntries,
1756
+ fiscalPeriods,
1757
+ budgets,
1758
+ reconciliations,
1759
+ journals: wireJournalMethods(new Repository(models.Journal, plugins.journal ?? [], journalPagination), country, orgField)
1302
1760
  };
1303
1761
  }
1304
1762
  //#endregion
@@ -1992,6 +2450,68 @@ function createAccountingEngine(config) {
1992
2450
  return new AccountingEngine(config);
1993
2451
  }
1994
2452
  //#endregion
2453
+ //#region src/utils/repartition-tax.ts
2454
+ /**
2455
+ * Default role resolver used when the country pack doesn't override.
2456
+ * Walks `taxCodes` to find a code whose `direction` matches the role.
2457
+ */
2458
+ function defaultResolveRoleCode(role, _tax, country) {
2459
+ const direction = role === "collected" ? "collected" : role === "recoverable" ? "recoverable" : null;
2460
+ if (!direction) return void 0;
2461
+ for (const tc of Object.values(country.taxCodes)) if (tc.direction === direction) return tc.code;
2462
+ }
2463
+ /**
2464
+ * Build a `TaxLineGenerator` that expands each hit `taxCode` into one
2465
+ * journal item per repartition line. Taxes without `repartition` fall
2466
+ * back to a single-line generator using the direction-implied account.
2467
+ */
2468
+ function createRepartitionTaxGenerator(options) {
2469
+ const { country, resolveAccount, documentType = "invoice" } = options;
2470
+ return { generateTaxLines(input) {
2471
+ const code = input.taxCode;
2472
+ if (!code) return [];
2473
+ const tax = country.taxCodes[code];
2474
+ if (!tax) return [];
2475
+ const baseTax = Math.round(input.amount * tax.rate / 100);
2476
+ if (baseTax === 0) return [];
2477
+ const lines = tax.repartition && tax.repartition.length > 0 ? tax.repartition.filter((line) => !line.documentTypes || line.documentTypes.includes(documentType)) : [{
2478
+ factor: 1,
2479
+ accountRole: tax.direction === "recoverable" ? "recoverable" : "collected",
2480
+ gridCode: tax.reportLines?.[0]
2481
+ }];
2482
+ const generated = [];
2483
+ for (const rep of lines) {
2484
+ const signed = Math.round(baseTax * rep.factor);
2485
+ if (signed === 0) continue;
2486
+ const account = resolveAccount(rep.accountRole, tax, input);
2487
+ if (!account) throw new Error(`repartitionTax: cannot resolve account for role "${rep.accountRole}" on tax "${tax.code}"`);
2488
+ const absAmount = Math.abs(signed);
2489
+ let onCredit = rep.accountRole === "collected" || rep.accountRole === "transition";
2490
+ if (signed < 0) onCredit = !onCredit;
2491
+ generated.push({
2492
+ account,
2493
+ debit: onCredit ? 0 : absAmount,
2494
+ credit: onCredit ? absAmount : 0,
2495
+ label: rep.label ?? `${tax.name} ${rep.accountRole}`,
2496
+ taxDetails: [{
2497
+ taxCode: tax.code,
2498
+ taxName: tax.name,
2499
+ ...rep.gridCode != null ? { gridCode: String(rep.gridCode) } : {}
2500
+ }]
2501
+ });
2502
+ }
2503
+ return generated;
2504
+ } };
2505
+ }
2506
+ /**
2507
+ * Helper for packs that want the standard "role → account-type code"
2508
+ * mapping without writing their own resolver. Returns the function you
2509
+ * stuff into `CountryPackInput.resolveTaxRepartitionAccountCode`.
2510
+ */
2511
+ function defaultResolveTaxRepartitionAccountCode(country) {
2512
+ return (role, tax) => defaultResolveRoleCode(role, tax, country);
2513
+ }
2514
+ //#endregion
1995
2515
  //#region src/utils/dimensions.ts
1996
2516
  /**
1997
2517
  * Analytic Dimensions — Helpers for defining analytic dimensions
@@ -2051,4 +2571,4 @@ function buildDimensionIndexes(dimensions, orgField) {
2051
2571
  });
2052
2572
  }
2053
2573
  //#endregion
2054
- export { AccountingEngine, AccountingError, CATEGORIES, CATEGORY_KEYS, CURRENCIES, DEFAULT_BUCKETS, Errors, JOURNAL_CODES, JOURNAL_TYPES, Money, acquireSession, add, allocate, buildAccountTypeMap, buildDimensionFields, buildDimensionIndexes, buildItemFilters, buildRevaluationEntry, calculateTotal, closeFiscalPeriod, computeEndingBalance, computeRevaluation, createAccountingEngine, createModels, createRepositories, dateLockPlugin, defaultLogger, defineCountryPack, doubleEntryPlugin, exportToCsv, finalizeSession, fiscalLockPlugin, flattenJournalEntries, format, formatPlain, fromDecimal, generateAgedBalance, generateBalanceSheet, generateBudgetVsActual, generateCashFlow, generateDimensionBreakdown, generateGeneralLedger, generateIncomeStatement, generateRevaluation, generateTrialBalance, getCurrency, getCustomJournalTypes, getDateRange, getFiscalYearStart, getJournalType, getJournalTypeCodes, getMinorUnit, getNormalBalance, idempotencyPlugin, isBalanceSheet, isIncomeStatement, isValidCategory, isValidCurrency, isValidJournalType, isVirtualTaxAccount, multiply, parseCents, percentage, quickbooksFieldMap, registerJournalType, reopenFiscalPeriod, resolveModelNames, splitTaxExclusive, splitTaxInclusive, subtract, toDecimal, universalFieldMap };
2574
+ export { AccountingEngine, AccountingError, CATEGORIES, CATEGORY_KEYS, CURRENCIES, DEFAULT_BUCKETS, Errors, JOURNAL_CODES, JOURNAL_TYPES, Money, acquireSession, add, allocate, buildAccountTypeMap, buildDimensionFields, buildDimensionIndexes, buildItemFilters, buildRevaluationEntry, calculateTotal, closeFiscalPeriod, computeEndingBalance, computeRevaluation, createAccountingEngine, createLockPlugin, createModels, createRepartitionTaxGenerator, createRepositories, creditLimitPlugin, dailyLockPlugin, defaultLogger, defaultResolveTaxRepartitionAccountCode, 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, isBalanceSheet, isIncomeStatement, isValidCategory, isValidCurrency, isValidJournalType, isVirtualTaxAccount, multiply, parseCents, percentage, periodResolver, quickbooksFieldMap, registerJournalType, reopenFiscalPeriod, resolveModelNames, splitTaxExclusive, splitTaxInclusive, subtract, taxLockPlugin, toDecimal, universalFieldMap, watermarkResolver };