@classytic/ledger 0.7.0 → 0.9.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 (41) hide show
  1. package/README.md +221 -115
  2. package/dist/bridges/index.d.mts +2 -0
  3. package/dist/bridges/index.mjs +1 -0
  4. package/dist/constants/index.d.mts +1 -1
  5. package/dist/constants/index.mjs +2 -2
  6. package/dist/country/index.d.mts +1 -1
  7. package/dist/errors-BI5k4iak.mjs +121 -0
  8. package/dist/events/index.d.mts +2 -0
  9. package/dist/events/index.mjs +2 -0
  10. package/dist/exports/index.d.mts +1 -1
  11. package/dist/exports/index.mjs +1 -1
  12. package/dist/{fx-realization.plugin-CfYy1tB6.mjs → fx-realization.plugin-Bxlb8cIx.mjs} +45 -2
  13. package/dist/{index-BX8miYdu.d.mts → index-08IpHhrU.d.mts} +12 -1
  14. package/dist/{index-Bl0_ak5w.d.mts → index-Db0n_6Z8.d.mts} +1 -1
  15. package/dist/index-dqkjgpII.d.mts +104 -0
  16. package/dist/index.d.mts +344 -65
  17. package/dist/index.mjs +539 -110
  18. package/dist/{journals-C50E9mpo.d.mts → journals-DUpWwFt1.d.mts} +1 -1
  19. package/dist/opening-balance-1cixYh6Y.mjs +60 -0
  20. package/dist/outbox-store-DQbL-KYT.mjs +132 -0
  21. package/dist/outbox-store-UYC4eZpI.d.mts +249 -0
  22. package/dist/{partner-ledger-D9H5hegI.mjs → partner-ledger-BoebloHk.mjs} +2 -2
  23. package/dist/plugins/index.d.mts +1 -1
  24. package/dist/plugins/index.mjs +1 -1
  25. package/dist/reports/index.d.mts +1 -1
  26. package/dist/reports/index.mjs +1 -1
  27. package/dist/sync/index.d.mts +313 -0
  28. package/dist/sync/index.mjs +527 -0
  29. package/dist/sync-JvchM3FO.d.mts +152 -0
  30. package/dist/{trial-balance-DTc8kzTD.d.mts → trial-balance-DyNm5bFu.d.mts} +2 -2
  31. package/docs/country-packs.md +71 -47
  32. package/docs/engine.md +3 -2
  33. package/docs/subledger-integration.md +29 -8
  34. package/docs/sync.md +330 -0
  35. package/package.json +36 -14
  36. package/dist/errors-CSDQPNyt.mjs +0 -33
  37. /package/dist/{categories-BkKdv16V.mjs → categories-FJlrvzcl.mjs} +0 -0
  38. /package/dist/{core-BkGjuVZj.d.mts → core-DwjkrRkJ.d.mts} +0 -0
  39. /package/dist/{currencies-CsuBGfgs.mjs → currencies-Jo5oaM_4.mjs} +0 -0
  40. /package/dist/{exports-BP-0Ni5W.mjs → exports-C30yRapf.mjs} +0 -0
  41. /package/dist/{index-D1ZjgVxn.d.mts → index-J-XIbXH-.d.mts} +0 -0
@@ -1,2 +1,2 @@
1
- import { a as exportToCsv, c as getHeaders, d as serializeCsv, i as quickbooksFieldMap, l as buildCsv, n as flattenJournalEntry, o as extractAllRows, r as universalFieldMap, s as extractRow, t as flattenJournalEntries, u as escapeCell } from "../exports-BP-0Ni5W.mjs";
1
+ import { a as exportToCsv, c as getHeaders, d as serializeCsv, i as quickbooksFieldMap, l as buildCsv, n as flattenJournalEntry, o as extractAllRows, r as universalFieldMap, s as extractRow, t as flattenJournalEntries, u as escapeCell } from "../exports-C30yRapf.mjs";
2
2
  export { buildCsv, escapeCell, exportToCsv, extractAllRows, extractRow, flattenJournalEntries, flattenJournalEntry, getHeaders, quickbooksFieldMap, serializeCsv, universalFieldMap };
@@ -1,4 +1,4 @@
1
- import { n as Errors, t as AccountingError } from "./errors-CSDQPNyt.mjs";
1
+ import { a as IdempotencyConflictError, i as Errors, t as AccountingError } from "./errors-BI5k4iak.mjs";
2
2
  //#region src/plugins/double-entry.plugin.ts
3
3
  function doubleEntryPlugin(options = {}) {
4
4
  const { onlyOnPost = true, JournalEntryModel, AccountModel, orgField } = options;
@@ -132,7 +132,23 @@ function doubleEntryPlugin(options = {}) {
132
132
  ...existing
133
133
  }, context);
134
134
  };
135
+ const validateMany = async (context) => {
136
+ const docs = context.dataArray;
137
+ if (!docs || docs.length === 0) return;
138
+ for (const data of docs) {
139
+ if (onlyOnPost && data.state !== "posted") continue;
140
+ const items = data.journalItems;
141
+ if (data.state === "posted" && (!items || items.length < 2)) throw Errors.validation(`Cannot post entry: at least 2 journal items required, got ${items?.length ?? 0}.`);
142
+ if (!items || items.length === 0) continue;
143
+ validateItems(items, data);
144
+ if (data.state === "posted") {
145
+ if (!AccountModel) throw new Error("doubleEntryPlugin: AccountModel is required to validate posted entries. Pass AccountModel in plugin options to enable account existence and tenant integrity checks.");
146
+ await validateAccounts(items, data, context);
147
+ }
148
+ }
149
+ };
135
150
  repo.on("before:create", validate);
151
+ repo.on("before:createMany", validateMany);
136
152
  repo.on("before:update", validateUpdate);
137
153
  }
138
154
  };
@@ -150,7 +166,21 @@ function idempotencyPlugin(options) {
150
166
  const query = { idempotencyKey: data.idempotencyKey };
151
167
  if (orgField && data[orgField]) query[orgField] = data[orgField];
152
168
  const existing = await JournalEntryModel.findOne(query).select("_id").session(context.session ?? null).lean();
153
- if (existing) throw Errors.conflict(`Duplicate idempotency key: "${data.idempotencyKey}". Existing entry: ${existing._id}`);
169
+ if (existing) throw new IdempotencyConflictError(data.idempotencyKey, existing._id);
170
+ });
171
+ repo.on("before:createMany", async (context) => {
172
+ const docs = context.dataArray;
173
+ if (!docs || docs.length === 0) return;
174
+ const keys = docs.map((d) => d.idempotencyKey).filter((k) => !!k);
175
+ if (keys.length === 0) return;
176
+ const query = { idempotencyKey: { $in: keys } };
177
+ const firstOrg = orgField && docs[0]?.[orgField];
178
+ if (orgField && firstOrg) query[orgField] = firstOrg;
179
+ const existingDocs = await JournalEntryModel.find(query).select("idempotencyKey").session(context.session ?? null).lean();
180
+ if (existingDocs.length > 0) {
181
+ const existingKeys = existingDocs.map((d) => d.idempotencyKey);
182
+ throw Errors.conflict(`Duplicate idempotency keys: ${existingKeys.join(", ")}. ${existingDocs.length} entries already exist.`);
183
+ }
154
184
  });
155
185
  }
156
186
  };
@@ -215,7 +245,20 @@ function createLockPlugin(options) {
215
245
  const refPart = hit.externalRef ? ` (ref: ${hit.externalRef})` : "";
216
246
  throw Errors.locked(hit.scope, `Cannot post entry dated ${datePart}: ${hit.scope}${subTypePart} period "${hit.label}" is closed${refPart}.`);
217
247
  };
248
+ const runMany = async (context) => {
249
+ const docs = context.dataArray;
250
+ if (!docs || docs.length === 0) return;
251
+ for (const data of docs) {
252
+ if (data.state !== "posted") continue;
253
+ await run({
254
+ ...context,
255
+ data,
256
+ dataArray: void 0
257
+ }, false);
258
+ }
259
+ };
218
260
  repo.on("before:create", (ctx) => run(ctx, false));
261
+ repo.on("before:createMany", runMany);
219
262
  repo.on("before:update", (ctx) => run(ctx, true));
220
263
  }
221
264
  };
@@ -1,4 +1,4 @@
1
- import { t as AccountType } from "./core-BkGjuVZj.mjs";
1
+ import { t as AccountType } from "./core-DwjkrRkJ.mjs";
2
2
 
3
3
  //#region src/country/index.d.ts
4
4
  /**
@@ -72,6 +72,16 @@ interface CountryPack {
72
72
  readonly retainedEarningsDisplayCode?: string;
73
73
  /** Display code for current year net income line (e.g. '3680' CA, '3311' BD) */
74
74
  readonly currentYearEarningsCode?: string;
75
+ /**
76
+ * Account code used as the equity contra for opening balance entries.
77
+ * Defaults to `retainedEarningsAccountCode` when not set.
78
+ *
79
+ * Following Odoo convention, this is typically the retained earnings
80
+ * account itself (not a temporary "Opening Balance Equity" account).
81
+ * Country packs that prefer a separate temporary account (like ERPNext)
82
+ * can override this.
83
+ */
84
+ readonly openingBalanceEquityCode?: string;
75
85
  /** Group label code used to identify Cost of Sales in the income statement */
76
86
  readonly cogsGroupCode?: string;
77
87
  /** Override default English report section names */
@@ -102,6 +112,7 @@ interface CountryPackInput {
102
112
  retainedEarningsAccountCode?: string;
103
113
  retainedEarningsDisplayCode?: string;
104
114
  currentYearEarningsCode?: string;
115
+ openingBalanceEquityCode?: string;
105
116
  cogsGroupCode?: string;
106
117
  reportLabels?: {
107
118
  readonly assets?: string;
@@ -1,6 +1,6 @@
1
- import { ClientSession, Model } from "mongoose";
2
1
  import * as _classytic_mongokit0 from "@classytic/mongokit";
3
2
  import { Repository, RepositoryContext, RepositoryInstance } from "@classytic/mongokit";
3
+ import { ClientSession, Model } from "mongoose";
4
4
 
5
5
  //#region src/types/repositories.d.ts
6
6
  interface PostOptions {
@@ -0,0 +1,104 @@
1
+ //#region src/bridges/notification.bridge.d.ts
2
+ /**
3
+ * NotificationBridge — host-implemented delivery for ledger-originated alerts.
4
+ *
5
+ * The ledger generates operational alerts that hosts may want to route to
6
+ * email, Slack, in-app notifications, or an audit/compliance system:
7
+ *
8
+ * - `periodLocked` — a fiscal period was locked (audit sign-off)
9
+ * - `periodUnlocked` — a locked period was re-opened (requires elevated role)
10
+ * - `entryReversed` — a posted entry was reversed (accounting correction)
11
+ * - `reconciliationMismatch` — match debit/credit totals differ (FX gain/loss)
12
+ *
13
+ * This is deliberately thin — richer integrations should subscribe to the
14
+ * EventTransport (§11-14) and route via their own notification stack.
15
+ * NotificationBridge exists for hosts that want a direct callback without
16
+ * owning an event bus.
17
+ *
18
+ * All methods are optional. Skip the bridge entirely if the host uses events.
19
+ */
20
+ interface NotificationBridgeContext {
21
+ organizationId?: unknown;
22
+ actorId?: unknown;
23
+ correlationId?: string;
24
+ }
25
+ interface PeriodLockedNotification {
26
+ periodId: unknown;
27
+ startDate: Date;
28
+ endDate: Date;
29
+ lockedBy?: unknown;
30
+ }
31
+ interface EntryReversedNotification {
32
+ originalEntryId: unknown;
33
+ reversalEntryId: unknown;
34
+ reversalDate: Date;
35
+ reversedBy?: unknown;
36
+ reason?: string;
37
+ }
38
+ interface ReconciliationMismatchNotification {
39
+ matchingNumber: string;
40
+ account: unknown;
41
+ debitTotal: number;
42
+ creditTotal: number;
43
+ difference: number;
44
+ currency: string | null;
45
+ }
46
+ interface NotificationBridge {
47
+ onPeriodLocked?(payload: PeriodLockedNotification, ctx: NotificationBridgeContext): Promise<void>;
48
+ onPeriodUnlocked?(payload: PeriodLockedNotification, ctx: NotificationBridgeContext): Promise<void>;
49
+ onEntryReversed?(payload: EntryReversedNotification, ctx: NotificationBridgeContext): Promise<void>;
50
+ onReconciliationMismatch?(payload: ReconciliationMismatchNotification, ctx: NotificationBridgeContext): Promise<void>;
51
+ }
52
+ //#endregion
53
+ //#region src/bridges/source.bridge.d.ts
54
+ /**
55
+ * SourceBridge — host-implemented resolver for polymorphic external references.
56
+ *
57
+ * Ledger journal entries commonly carry a `reference`/`externalRef` pointing
58
+ * at a source document that lives outside the ledger package — an Invoice,
59
+ * Payment, Payroll Run, Stripe Charge, ERP voucher, etc. Storing these as
60
+ * opaque `String + sourceModel` (per PACKAGE_RULES §7) keeps the ledger
61
+ * transport-agnostic: the same schema works whether the source lives in the
62
+ * same Mongo, a different Mongo, Postgres, or an external REST API.
63
+ *
64
+ * Hosts implement `SourceBridge` to hydrate those refs when building
65
+ * enriched views (partner ledger with invoice details, audit trail with
66
+ * payment metadata, reconciliation UI with source documents, etc.).
67
+ *
68
+ * All methods are optional. Features that need resolution degrade gracefully
69
+ * when a bridge is not provided.
70
+ */
71
+ interface SourceRef {
72
+ sourceId: string;
73
+ sourceModel: string;
74
+ }
75
+ interface SourceBridgeContext {
76
+ organizationId?: unknown;
77
+ actorId?: unknown;
78
+ [key: string]: unknown;
79
+ }
80
+ interface SourceBridge {
81
+ /**
82
+ * Resolve a single external reference.
83
+ *
84
+ * Return `null` when the source cannot be found (deleted, permission
85
+ * denied, wrong model). Do not throw for missing sources — callers
86
+ * expect `null` for graceful degradation.
87
+ */
88
+ resolve?(sourceId: string, sourceModel: string, ctx: SourceBridgeContext): Promise<unknown | null>;
89
+ /**
90
+ * Batch resolver — avoids N+1 round-trips when enriching a list.
91
+ * Key the returned map by `sourceId`. Missing sources may be omitted or
92
+ * mapped to `null` at the implementer's discretion.
93
+ */
94
+ resolveMany?(refs: ReadonlyArray<SourceRef>, ctx: SourceBridgeContext): Promise<Map<string, unknown>>;
95
+ }
96
+ //#endregion
97
+ //#region src/bridges/index.d.ts
98
+ /** Collected bridges exposed as `engine.bridges`. */
99
+ interface LedgerBridges {
100
+ source?: SourceBridge;
101
+ notification?: NotificationBridge;
102
+ }
103
+ //#endregion
104
+ export { EntryReversedNotification as a, PeriodLockedNotification as c, SourceRef as i, ReconciliationMismatchNotification as l, SourceBridge as n, NotificationBridge as o, SourceBridgeContext as r, NotificationBridgeContext as s, LedgerBridges as t };