@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.
- package/README.md +221 -115
- package/dist/bridges/index.d.mts +2 -0
- package/dist/bridges/index.mjs +1 -0
- package/dist/constants/index.d.mts +1 -1
- package/dist/constants/index.mjs +2 -2
- package/dist/country/index.d.mts +1 -1
- package/dist/errors-BI5k4iak.mjs +121 -0
- package/dist/events/index.d.mts +2 -0
- package/dist/events/index.mjs +2 -0
- package/dist/exports/index.d.mts +1 -1
- package/dist/exports/index.mjs +1 -1
- package/dist/{fx-realization.plugin-CfYy1tB6.mjs → fx-realization.plugin-Bxlb8cIx.mjs} +45 -2
- package/dist/{index-BX8miYdu.d.mts → index-08IpHhrU.d.mts} +12 -1
- package/dist/{index-Bl0_ak5w.d.mts → index-Db0n_6Z8.d.mts} +1 -1
- package/dist/index-dqkjgpII.d.mts +104 -0
- package/dist/index.d.mts +344 -65
- package/dist/index.mjs +539 -110
- package/dist/{journals-C50E9mpo.d.mts → journals-DUpWwFt1.d.mts} +1 -1
- package/dist/opening-balance-1cixYh6Y.mjs +60 -0
- package/dist/outbox-store-DQbL-KYT.mjs +132 -0
- package/dist/outbox-store-UYC4eZpI.d.mts +249 -0
- package/dist/{partner-ledger-D9H5hegI.mjs → partner-ledger-BoebloHk.mjs} +2 -2
- package/dist/plugins/index.d.mts +1 -1
- package/dist/plugins/index.mjs +1 -1
- package/dist/reports/index.d.mts +1 -1
- package/dist/reports/index.mjs +1 -1
- package/dist/sync/index.d.mts +313 -0
- package/dist/sync/index.mjs +527 -0
- package/dist/sync-JvchM3FO.d.mts +152 -0
- package/dist/{trial-balance-DTc8kzTD.d.mts → trial-balance-DyNm5bFu.d.mts} +2 -2
- package/docs/country-packs.md +71 -47
- package/docs/engine.md +3 -2
- package/docs/subledger-integration.md +29 -8
- package/docs/sync.md +330 -0
- package/package.json +36 -14
- package/dist/errors-CSDQPNyt.mjs +0 -33
- /package/dist/{categories-BkKdv16V.mjs → categories-FJlrvzcl.mjs} +0 -0
- /package/dist/{core-BkGjuVZj.d.mts → core-DwjkrRkJ.d.mts} +0 -0
- /package/dist/{currencies-CsuBGfgs.mjs → currencies-Jo5oaM_4.mjs} +0 -0
- /package/dist/{exports-BP-0Ni5W.mjs → exports-C30yRapf.mjs} +0 -0
- /package/dist/{index-D1ZjgVxn.d.mts → index-J-XIbXH-.d.mts} +0 -0
package/dist/exports/index.mjs
CHANGED
|
@@ -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-
|
|
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 {
|
|
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
|
|
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-
|
|
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 };
|