@classytic/ledger 0.1.3
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/LICENSE +21 -0
- package/README.md +108 -0
- package/dist/account.repository-1C2sZvB2.d.mts +29 -0
- package/dist/account.repository-1C2sZvB2.d.mts.map +1 -0
- package/dist/account.repository-Crf5DGO4.mjs +393 -0
- package/dist/account.repository-Crf5DGO4.mjs.map +1 -0
- package/dist/categories-BNJBd4ze.mjs +70 -0
- package/dist/categories-BNJBd4ze.mjs.map +1 -0
- package/dist/constants/index.d.mts +2 -0
- package/dist/constants/index.mjs +5 -0
- package/dist/core-Cx0baosR.d.mts +104 -0
- package/dist/core-Cx0baosR.d.mts.map +1 -0
- package/dist/country/index.d.mts +105 -0
- package/dist/country/index.d.mts.map +1 -0
- package/dist/country/index.mjs +27 -0
- package/dist/country/index.mjs.map +1 -0
- package/dist/currencies-BBk3NwXn.mjs +82 -0
- package/dist/currencies-BBk3NwXn.mjs.map +1 -0
- package/dist/currencies-Bkn3FNkC.d.mts +38 -0
- package/dist/currencies-Bkn3FNkC.d.mts.map +1 -0
- package/dist/engine-Cd73EOT6.d.mts +72 -0
- package/dist/engine-Cd73EOT6.d.mts.map +1 -0
- package/dist/errors-CeqRahE-.mjs +28 -0
- package/dist/errors-CeqRahE-.mjs.map +1 -0
- package/dist/exports/index.d.mts +2 -0
- package/dist/exports/index.mjs +3 -0
- package/dist/fiscal-close-CNOwv_ud.mjs +934 -0
- package/dist/fiscal-close-CNOwv_ud.mjs.map +1 -0
- package/dist/fiscal-close-CzUzpnMg.d.mts +270 -0
- package/dist/fiscal-close-CzUzpnMg.d.mts.map +1 -0
- package/dist/fiscal-period.schema-CbALaaKl.mjs +477 -0
- package/dist/fiscal-period.schema-CbALaaKl.mjs.map +1 -0
- package/dist/fiscal-period.schema-DI2scngu.d.mts +38 -0
- package/dist/fiscal-period.schema-DI2scngu.d.mts.map +1 -0
- package/dist/idempotency.plugin-BESs9YPD.d.mts +58 -0
- package/dist/idempotency.plugin-BESs9YPD.d.mts.map +1 -0
- package/dist/idempotency.plugin-C6r8RI8d.mjs +165 -0
- package/dist/idempotency.plugin-C6r8RI8d.mjs.map +1 -0
- package/dist/index.d.mts +308 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +171 -0
- package/dist/index.mjs.map +1 -0
- package/dist/journals-CI3Wb4EF.mjs +92 -0
- package/dist/journals-CI3Wb4EF.mjs.map +1 -0
- package/dist/logger-Cv6VVc4r.d.mts +15 -0
- package/dist/logger-Cv6VVc4r.d.mts.map +1 -0
- package/dist/money.d.mts +129 -0
- package/dist/money.d.mts.map +1 -0
- package/dist/money.mjs +197 -0
- package/dist/money.mjs.map +1 -0
- package/dist/plugins/index.d.mts +2 -0
- package/dist/plugins/index.mjs +3 -0
- package/dist/reports/index.d.mts +2 -0
- package/dist/reports/index.mjs +3 -0
- package/dist/repositories/index.d.mts +2 -0
- package/dist/repositories/index.mjs +3 -0
- package/dist/schemas/index.d.mts +2 -0
- package/dist/schemas/index.mjs +3 -0
- package/dist/session-Dh0s6zG4.mjs +87 -0
- package/dist/session-Dh0s6zG4.mjs.map +1 -0
- package/dist/universal-CMfrZ2hG.mjs +257 -0
- package/dist/universal-CMfrZ2hG.mjs.map +1 -0
- package/dist/universal-x33ZJODp.d.mts +137 -0
- package/dist/universal-x33ZJODp.d.mts.map +1 -0
- package/docs/country-packs.md +117 -0
- package/docs/engine.md +147 -0
- package/docs/exports.md +81 -0
- package/docs/money.md +81 -0
- package/docs/plugins.md +136 -0
- package/docs/reports.md +154 -0
- package/docs/repositories.md +239 -0
- package/docs/schemas.md +146 -0
- package/docs/subledger-integration.md +287 -0
- package/package.json +116 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { n as Errors } from "./errors-CeqRahE-.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/plugins/double-entry.plugin.ts
|
|
4
|
+
function doubleEntryPlugin(options = {}) {
|
|
5
|
+
const { onlyOnPost = true, JournalEntryModel, AccountModel, orgField } = options;
|
|
6
|
+
function validateItems(items, data) {
|
|
7
|
+
for (let i = 0; i < items.length; i++) {
|
|
8
|
+
const d = items[i].debit ?? 0;
|
|
9
|
+
const c = items[i].credit ?? 0;
|
|
10
|
+
if (d > 0 && c > 0) throw Errors.validation(`Invalid journal item at index ${i}: a line cannot have both debit (${d}) and credit (${c}) greater than zero.`);
|
|
11
|
+
if (d === 0 && c === 0) throw Errors.validation(`Invalid journal item at index ${i}: a line cannot have both debit and credit equal to zero.`);
|
|
12
|
+
}
|
|
13
|
+
const totalDebit = items.reduce((s, i) => s + (i.debit ?? 0), 0);
|
|
14
|
+
const totalCredit = items.reduce((s, i) => s + (i.credit ?? 0), 0);
|
|
15
|
+
if (totalDebit !== totalCredit) throw Errors.validation(`Double-entry violation: debits (${totalDebit}) ≠ credits (${totalCredit}). Difference: ${Math.abs(totalDebit - totalCredit)}`);
|
|
16
|
+
data.totalDebit = totalDebit;
|
|
17
|
+
data.totalCredit = totalCredit;
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
name: "accounting:double-entry",
|
|
21
|
+
apply(repo) {
|
|
22
|
+
const validate = async (context) => {
|
|
23
|
+
const data = context.data;
|
|
24
|
+
if (!data) return;
|
|
25
|
+
if (onlyOnPost && data.state !== "posted") return;
|
|
26
|
+
const items = data.journalItems;
|
|
27
|
+
if (data.state === "posted" && (!items || items.length < 2)) throw Errors.validation(`Cannot post entry: at least 2 journal items required, got ${items?.length ?? 0}.`);
|
|
28
|
+
if (!items || items.length === 0) return;
|
|
29
|
+
validateItems(items, data);
|
|
30
|
+
if (data.state === "posted") {
|
|
31
|
+
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.");
|
|
32
|
+
await validateAccounts(items, data, context);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
/** Verify all journal item accounts exist and belong to the same org */
|
|
36
|
+
const validateAccounts = async (items, data, context) => {
|
|
37
|
+
const accountIds = items.map((i) => i.account).filter((a) => a != null && a !== "");
|
|
38
|
+
if (accountIds.length === 0) throw Errors.validation("Posted entry has items with missing accounts.");
|
|
39
|
+
const selectFields = orgField ? `_id ${orgField}` : "_id";
|
|
40
|
+
const accounts = await AccountModel.find({ _id: { $in: accountIds } }).select(selectFields).session(context.session ?? null).lean();
|
|
41
|
+
const foundIds = new Set(accounts.map((a) => String(a._id)));
|
|
42
|
+
const missingCount = accountIds.filter((id) => !foundIds.has(String(id))).length;
|
|
43
|
+
if (missingCount > 0) throw Errors.validation(`${missingCount} item(s) reference non-existent accounts.`);
|
|
44
|
+
if (orgField && data[orgField] != null) {
|
|
45
|
+
const dataOrg = String(data[orgField]);
|
|
46
|
+
const crossTenant = accounts.filter((a) => String(a[orgField]) !== dataOrg);
|
|
47
|
+
if (crossTenant.length > 0) throw Errors.validation(`${crossTenant.length} item(s) reference accounts from another organization.`);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const validateUpdate = async (context) => {
|
|
51
|
+
const data = context.data;
|
|
52
|
+
if (!data) return;
|
|
53
|
+
if (JournalEntryModel) {
|
|
54
|
+
const id = context.id;
|
|
55
|
+
if (id) {
|
|
56
|
+
if ((await JournalEntryModel.findById(id).select("state").session(context.session ?? null).lean())?.state === "posted") {
|
|
57
|
+
if (data.state !== void 0 && data.state !== "posted") throw Errors.immutable("Cannot change state of a posted journal entry. Posted entries are immutable.");
|
|
58
|
+
const allowedKeys = new Set(["state"]);
|
|
59
|
+
if (Object.keys(data).some((k) => !allowedKeys.has(k))) throw Errors.immutable("Cannot modify a posted journal entry. Use reverse() to create a correcting entry instead.");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (onlyOnPost && data.state !== "posted") return;
|
|
64
|
+
const items = data.journalItems;
|
|
65
|
+
if (items !== void 0) {
|
|
66
|
+
if (items.length < 2) throw Errors.validation(`Cannot post entry: at least 2 journal items required, got ${items.length}.`);
|
|
67
|
+
validateItems(items, data);
|
|
68
|
+
if (AccountModel) await validateAccounts(items, data, context);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (!JournalEntryModel) throw new Error("doubleEntryPlugin: JournalEntryModel is required to validate partial updates that set state to \"posted\". Pass JournalEntryModel in plugin options.");
|
|
72
|
+
const id = context.id;
|
|
73
|
+
if (!id) throw new Error("doubleEntryPlugin: update context is missing \"id\". Cannot validate partial post without document ID.");
|
|
74
|
+
const existing = await JournalEntryModel.findById(id).select("journalItems").session(context.session ?? null).lean();
|
|
75
|
+
if (!existing) return;
|
|
76
|
+
const persistedItems = existing.journalItems;
|
|
77
|
+
if (!persistedItems || persistedItems.length < 2) throw Errors.validation(`Cannot post entry: at least 2 journal items required, got ${persistedItems?.length ?? 0}.`);
|
|
78
|
+
validateItems(persistedItems, data);
|
|
79
|
+
if (AccountModel) await validateAccounts(persistedItems, {
|
|
80
|
+
...data,
|
|
81
|
+
...existing
|
|
82
|
+
}, context);
|
|
83
|
+
};
|
|
84
|
+
repo.on("before:create", (payload) => validate(payload));
|
|
85
|
+
repo.on("before:update", (payload) => validateUpdate(payload));
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/plugins/fiscal-lock.plugin.ts
|
|
92
|
+
function fiscalLockPlugin(options) {
|
|
93
|
+
const { FiscalPeriodModel, JournalEntryModel, orgField } = options;
|
|
94
|
+
return {
|
|
95
|
+
name: "accounting:fiscal-lock",
|
|
96
|
+
apply(repo) {
|
|
97
|
+
const checkPeriod = async (context, isUpdate) => {
|
|
98
|
+
const data = context.data;
|
|
99
|
+
if (!data) return;
|
|
100
|
+
if (data.state !== "posted") return;
|
|
101
|
+
const session = context.session ?? null;
|
|
102
|
+
let entryDate;
|
|
103
|
+
let persistedDoc = null;
|
|
104
|
+
if (data.date) entryDate = new Date(data.date);
|
|
105
|
+
else if (!isUpdate) entryDate = /* @__PURE__ */ new Date();
|
|
106
|
+
else {
|
|
107
|
+
if (!context.id) throw new Error("fiscalLockPlugin: update context is missing \"id\". Cannot validate fiscal lock without document ID.");
|
|
108
|
+
if (!JournalEntryModel) throw new Error("fiscalLockPlugin: JournalEntryModel is required to validate partial updates that set state to \"posted\". Pass JournalEntryModel in plugin options.");
|
|
109
|
+
const selectFields = orgField ? `date ${orgField}` : "date";
|
|
110
|
+
persistedDoc = await JournalEntryModel.findById(context.id).select(selectFields).session(session).lean();
|
|
111
|
+
if (persistedDoc?.date) entryDate = new Date(persistedDoc.date);
|
|
112
|
+
}
|
|
113
|
+
if (!entryDate) return;
|
|
114
|
+
const query = {
|
|
115
|
+
startDate: { $lte: entryDate },
|
|
116
|
+
endDate: { $gte: entryDate },
|
|
117
|
+
closed: true
|
|
118
|
+
};
|
|
119
|
+
if (orgField) {
|
|
120
|
+
let orgValue = data[orgField] ?? context[orgField];
|
|
121
|
+
if (!orgValue && isUpdate) {
|
|
122
|
+
if (persistedDoc) orgValue = persistedDoc[orgField];
|
|
123
|
+
else if (context.id && JournalEntryModel) {
|
|
124
|
+
const persisted = await JournalEntryModel.findById(context.id).select(orgField).session(session).lean();
|
|
125
|
+
if (persisted) orgValue = persisted[orgField];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (!orgValue) throw new Error(`fiscalLockPlugin: orgField "${orgField}" is configured but could not be resolved from payload, context, or persisted document. Refusing to run unscoped fiscal period query.`);
|
|
129
|
+
query[orgField] = orgValue;
|
|
130
|
+
}
|
|
131
|
+
const closedPeriod = await FiscalPeriodModel.findOne(query).session(session).lean();
|
|
132
|
+
if (closedPeriod) {
|
|
133
|
+
const period = closedPeriod;
|
|
134
|
+
throw Errors.fiscal(`Cannot post entry dated ${entryDate.toISOString().split("T")[0]}: fiscal period "${period.name}" is closed.`);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
repo.on("before:create", (payload) => checkPeriod(payload, false));
|
|
138
|
+
repo.on("before:update", (payload) => checkPeriod(payload, true));
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
//#endregion
|
|
144
|
+
//#region src/plugins/idempotency.plugin.ts
|
|
145
|
+
function idempotencyPlugin(options) {
|
|
146
|
+
const { JournalEntryModel, orgField } = options;
|
|
147
|
+
return {
|
|
148
|
+
name: "accounting:idempotency",
|
|
149
|
+
apply(repo) {
|
|
150
|
+
repo.on("before:create", async (raw) => {
|
|
151
|
+
const context = raw;
|
|
152
|
+
const data = context.data;
|
|
153
|
+
if (!data?.idempotencyKey) return;
|
|
154
|
+
const query = { idempotencyKey: data.idempotencyKey };
|
|
155
|
+
if (orgField && data[orgField]) query[orgField] = data[orgField];
|
|
156
|
+
const existing = await JournalEntryModel.findOne(query).select("_id").session(context.session ?? null).lean();
|
|
157
|
+
if (existing) throw Errors.conflict(`Duplicate idempotency key: "${data.idempotencyKey}". Existing entry: ${existing._id}`);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
//#endregion
|
|
164
|
+
export { fiscalLockPlugin as n, doubleEntryPlugin as r, idempotencyPlugin as t };
|
|
165
|
+
//# sourceMappingURL=idempotency.plugin-C6r8RI8d.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idempotency.plugin-C6r8RI8d.mjs","names":[],"sources":["../src/plugins/double-entry.plugin.ts","../src/plugins/fiscal-lock.plugin.ts","../src/plugins/idempotency.plugin.ts"],"sourcesContent":["/**\r\n * Double-Entry Validation Plugin for @classytic/mongokit\r\n *\r\n * Ensures every journal entry posted via the repository satisfies:\r\n * sum(debits) === sum(credits)\r\n *\r\n * Plugs into the before:create and before:update hooks.\r\n */\r\n\r\nimport type { Model } from 'mongoose';\r\nimport { Errors } from '../utils/errors.js';\r\n\r\n/** Minimal interface matching @classytic/mongokit RepositoryInstance */\r\ninterface RepositoryInstance {\r\n on(event: string, listener: (data: unknown) => void | Promise<void>): unknown;\r\n}\r\n\r\nexport interface DoubleEntryPluginOptions {\r\n /** Only enforce on posted entries (default: true) */\r\n onlyOnPost?: boolean;\r\n /** Mongoose model — required to validate partial updates that only set state */\r\n JournalEntryModel?: Model<unknown>;\r\n /** Account model — when provided, posted creates verify account existence + tenant scoping */\r\n AccountModel?: Model<unknown>;\r\n /** Multi-tenant org field name (e.g. 'business'). Required for tenant-account integrity checks. */\r\n orgField?: string;\r\n}\r\n\r\nexport function doubleEntryPlugin(options: DoubleEntryPluginOptions = {}) {\r\n const { onlyOnPost = true, JournalEntryModel, AccountModel, orgField } = options;\r\n\r\n function validateItems(\r\n items: Array<{ debit?: number; credit?: number }>,\r\n data: Record<string, unknown>,\r\n ): void {\r\n // Each line must be debit OR credit (not both), and cannot be zero-value\r\n for (let i = 0; i < items.length; i++) {\r\n const d = items[i].debit ?? 0;\r\n const c = items[i].credit ?? 0;\r\n if (d > 0 && c > 0) {\r\n throw Errors.validation(\r\n `Invalid journal item at index ${i}: a line cannot have both debit (${d}) and credit (${c}) greater than zero.`,\r\n );\r\n }\r\n if (d === 0 && c === 0) {\r\n throw Errors.validation(\r\n `Invalid journal item at index ${i}: a line cannot have both debit and credit equal to zero.`,\r\n );\r\n }\r\n }\r\n\r\n const totalDebit = items.reduce((s, i) => s + (i.debit ?? 0), 0);\r\n const totalCredit = items.reduce((s, i) => s + (i.credit ?? 0), 0);\r\n\r\n // Integer cents — exact comparison, no floating-point drift possible.\r\n if (totalDebit !== totalCredit) {\r\n throw Errors.validation(\r\n `Double-entry violation: debits (${totalDebit}) ≠ credits (${totalCredit}). ` +\r\n `Difference: ${Math.abs(totalDebit - totalCredit)}`,\r\n );\r\n }\r\n\r\n // Sync totals onto the data object\r\n data.totalDebit = totalDebit;\r\n data.totalCredit = totalCredit;\r\n }\r\n\r\n return {\r\n name: 'accounting:double-entry',\r\n apply(repo: RepositoryInstance) {\r\n const validate = async (context: Record<string, unknown>) => {\r\n const data = context.data as Record<string, unknown> | undefined;\r\n if (!data) return;\r\n\r\n // Skip draft entries if configured\r\n if (onlyOnPost && data.state !== 'posted') return;\r\n\r\n const items = data.journalItems as Array<{ debit?: number; credit?: number; account?: unknown }> | undefined;\r\n\r\n // Posted entries must have at least 2 journal items\r\n if (data.state === 'posted' && (!items || items.length < 2)) {\r\n throw Errors.validation(\r\n `Cannot post entry: at least 2 journal items required, got ${items?.length ?? 0}.`,\r\n );\r\n }\r\n\r\n if (!items || items.length === 0) return;\r\n\r\n validateItems(items, data);\r\n\r\n // Account existence + tenant-account integrity (fail-closed for posted creates)\r\n if (data.state === 'posted') {\r\n if (!AccountModel) {\r\n throw new Error(\r\n 'doubleEntryPlugin: AccountModel is required to validate posted entries. ' +\r\n 'Pass AccountModel in plugin options to enable account existence and tenant integrity checks.',\r\n );\r\n }\r\n await validateAccounts(items, data, context);\r\n }\r\n };\r\n\r\n /** Verify all journal item accounts exist and belong to the same org */\r\n const validateAccounts = async (\r\n items: Array<{ account?: unknown }>,\r\n data: Record<string, unknown>,\r\n context: Record<string, unknown>,\r\n ) => {\r\n const accountIds = items\r\n .map(i => i.account)\r\n .filter(a => a != null && a !== '');\r\n\r\n if (accountIds.length === 0) {\r\n throw Errors.validation('Posted entry has items with missing accounts.');\r\n }\r\n\r\n const selectFields = orgField ? `_id ${orgField}` : '_id';\r\n const accounts = await AccountModel!.find({ _id: { $in: accountIds } })\r\n .select(selectFields)\r\n .session((context.session as import('mongoose').ClientSession) ?? null)\r\n .lean() as Array<Record<string, unknown>>;\r\n\r\n // Check all accounts exist\r\n const foundIds = new Set(accounts.map(a => String(a._id)));\r\n const missingCount = accountIds.filter(id => !foundIds.has(String(id))).length;\r\n if (missingCount > 0) {\r\n throw Errors.validation(\r\n `${missingCount} item(s) reference non-existent accounts.`,\r\n );\r\n }\r\n\r\n // Check tenant scoping\r\n if (orgField && data[orgField] != null) {\r\n const dataOrg = String(data[orgField]);\r\n const crossTenant = accounts.filter(a => String(a[orgField]) !== dataOrg);\r\n if (crossTenant.length > 0) {\r\n throw Errors.validation(\r\n `${crossTenant.length} item(s) reference accounts from another organization.`,\r\n );\r\n }\r\n }\r\n };\r\n\r\n const validateUpdate = async (context: Record<string, unknown>) => {\r\n const data = context.data as Record<string, unknown> | undefined;\r\n if (!data) return;\r\n\r\n // ── Immutability guard: block modifications to posted entries ──────\r\n // Allow: idempotent state re-set (state: 'posted')\r\n // Block: everything else — including reversed/reversedBy (only settable via\r\n // reverse() which uses entry.save() directly, bypassing this hook)\r\n if (JournalEntryModel) {\r\n const id = context.id;\r\n if (id) {\r\n // Check if target entry is already posted\r\n const target = await JournalEntryModel.findById(id)\r\n .select('state')\r\n .session((context.session as import('mongoose').ClientSession) ?? null)\r\n .lean() as Record<string, unknown> | null;\r\n\r\n if (target?.state === 'posted') {\r\n // Block any state transition away from 'posted' (immutable ledger)\r\n if (data.state !== undefined && data.state !== 'posted') {\r\n throw Errors.immutable(\r\n 'Cannot change state of a posted journal entry. Posted entries are immutable.',\r\n );\r\n }\r\n\r\n // Only allow idempotent state re-set on posted entries.\r\n // reversed/reversedBy are NOT allowed through repository.update() —\r\n // reverse() uses entry.save() directly to bypass the plugin, so any\r\n // attempt to set these flags through the generic update path is illegitimate.\r\n const allowedKeys = new Set(['state']);\r\n const dataKeys = Object.keys(data);\r\n const hasDisallowedKeys = dataKeys.some(k => !allowedKeys.has(k));\r\n\r\n if (hasDisallowedKeys) {\r\n throw Errors.immutable(\r\n 'Cannot modify a posted journal entry. Use reverse() to create a correcting entry instead.',\r\n );\r\n }\r\n }\r\n }\r\n }\r\n\r\n if (onlyOnPost && data.state !== 'posted') return;\r\n\r\n const items = data.journalItems as Array<{ debit?: number; credit?: number }> | undefined;\r\n\r\n if (items !== undefined) {\r\n // Items present in payload — validate directly\r\n if (items.length < 2) {\r\n throw Errors.validation(\r\n `Cannot post entry: at least 2 journal items required, got ${items.length}.`,\r\n );\r\n }\r\n validateItems(items, data);\r\n\r\n // Account existence + tenant-account integrity (when AccountModel provided)\r\n if (AccountModel) {\r\n await validateAccounts(items as Array<{ account?: unknown }>, data, context);\r\n }\r\n return;\r\n }\r\n\r\n // state → posted but no journalItems in payload: fetch the persisted doc\r\n if (!JournalEntryModel) {\r\n throw new Error(\r\n 'doubleEntryPlugin: JournalEntryModel is required to validate partial updates that set state to \"posted\". ' +\r\n 'Pass JournalEntryModel in plugin options.',\r\n );\r\n }\r\n\r\n const id = context.id;\r\n if (!id) {\r\n throw new Error(\r\n 'doubleEntryPlugin: update context is missing \"id\". Cannot validate partial post without document ID.',\r\n );\r\n }\r\n\r\n const existing = await JournalEntryModel.findById(id)\r\n .select('journalItems')\r\n .session((context.session as import('mongoose').ClientSession) ?? null)\r\n .lean() as Record<string, unknown> | null;\r\n\r\n if (!existing) return; // will 404 downstream\r\n\r\n const persistedItems = existing.journalItems as Array<{ debit?: number; credit?: number; account?: unknown }> | undefined;\r\n if (!persistedItems || persistedItems.length < 2) {\r\n throw Errors.validation(\r\n `Cannot post entry: at least 2 journal items required, got ${persistedItems?.length ?? 0}.`,\r\n );\r\n }\r\n\r\n validateItems(persistedItems, data);\r\n\r\n // Account existence + tenant-account integrity (when AccountModel provided)\r\n if (AccountModel) {\r\n await validateAccounts(persistedItems, { ...data, ...existing }, context);\r\n }\r\n };\r\n\r\n repo.on('before:create', (payload: unknown) => validate(payload as Record<string, unknown>));\r\n repo.on('before:update', (payload: unknown) => validateUpdate(payload as Record<string, unknown>));\r\n },\r\n };\r\n}\r\n","/**\r\n * Fiscal Lock Plugin for @classytic/mongokit\r\n *\r\n * Prevents journal entries from being created or posted\r\n * in a closed fiscal period.\r\n *\r\n * Requires a FiscalPeriod model to check against.\r\n */\r\n\r\nimport type { Model, ClientSession } from 'mongoose';\r\nimport { Errors } from '../utils/errors.js';\r\n\r\n/** Minimal interface matching @classytic/mongokit RepositoryInstance */\r\ninterface RepositoryInstance {\r\n on(event: string, listener: (data: unknown) => void | Promise<void>): unknown;\r\n}\r\n\r\nexport interface FiscalLockPluginOptions {\r\n /** Mongoose model for fiscal periods */\r\n FiscalPeriodModel: Model<unknown>;\r\n /** Mongoose model for journal entries — needed to look up persisted date on partial updates */\r\n JournalEntryModel?: Model<unknown>;\r\n /** Organization field name (for multi-tenant) */\r\n orgField?: string;\r\n}\r\n\r\nexport function fiscalLockPlugin(options: FiscalLockPluginOptions) {\r\n const { FiscalPeriodModel, JournalEntryModel, orgField } = options;\r\n\r\n return {\r\n name: 'accounting:fiscal-lock',\r\n apply(repo: RepositoryInstance) {\r\n const checkPeriod = async (context: Record<string, unknown>, isUpdate: boolean) => {\r\n const data = context.data as Record<string, unknown> | undefined;\r\n if (!data) return;\r\n\r\n // Only check when posting or creating posted entries\r\n if (data.state !== 'posted') return;\r\n\r\n const session = (context.session as ClientSession) ?? null;\r\n\r\n // Resolve the entry date (and org field from persisted doc if needed)\r\n let entryDate: Date | undefined;\r\n let persistedDoc: Record<string, unknown> | null = null;\r\n\r\n if (data.date) {\r\n entryDate = new Date(data.date as string | number | Date);\r\n } else if (!isUpdate) {\r\n // Create without explicit date — schema will default to now, so check against now\r\n entryDate = new Date();\r\n } else {\r\n // Partial update without date — fetch the persisted doc\r\n if (!context.id) {\r\n throw new Error(\r\n 'fiscalLockPlugin: update context is missing \"id\". Cannot validate fiscal lock without document ID.',\r\n );\r\n }\r\n if (!JournalEntryModel) {\r\n throw new Error(\r\n 'fiscalLockPlugin: JournalEntryModel is required to validate partial updates that set state to \"posted\". ' +\r\n 'Pass JournalEntryModel in plugin options.',\r\n );\r\n }\r\n const selectFields = orgField ? `date ${orgField}` : 'date';\r\n persistedDoc = await JournalEntryModel.findById(context.id)\r\n .select(selectFields)\r\n .session(session)\r\n .lean() as Record<string, unknown> | null;\r\n if (persistedDoc?.date) {\r\n entryDate = new Date(persistedDoc.date as string | number | Date);\r\n }\r\n }\r\n\r\n if (!entryDate) return; // No date to check against (new entry without date defaults to draft)\r\n\r\n // Build query\r\n const query: Record<string, unknown> = {\r\n startDate: { $lte: entryDate },\r\n endDate: { $gte: entryDate },\r\n closed: true,\r\n };\r\n\r\n // Multi-tenant scope — check payload, context, then persisted doc\r\n if (orgField) {\r\n let orgValue = data[orgField] ?? context[orgField];\r\n\r\n if (!orgValue && isUpdate) {\r\n // Org field not in payload or context — resolve from persisted doc\r\n if (persistedDoc) {\r\n orgValue = persistedDoc[orgField];\r\n } else if (context.id && JournalEntryModel) {\r\n const persisted = await JournalEntryModel.findById(context.id)\r\n .select(orgField)\r\n .session(session)\r\n .lean() as Record<string, unknown> | null;\r\n if (persisted) orgValue = persisted[orgField];\r\n }\r\n }\r\n\r\n if (!orgValue) {\r\n throw new Error(\r\n `fiscalLockPlugin: orgField \"${orgField}\" is configured but could not be resolved from ` +\r\n 'payload, context, or persisted document. Refusing to run unscoped fiscal period query.',\r\n );\r\n }\r\n\r\n query[orgField] = orgValue;\r\n }\r\n\r\n const closedPeriod = await FiscalPeriodModel.findOne(query).session(session).lean();\r\n\r\n if (closedPeriod) {\r\n const period = closedPeriod as Record<string, unknown>;\r\n throw Errors.fiscal(\r\n `Cannot post entry dated ${entryDate.toISOString().split('T')[0]}: ` +\r\n `fiscal period \"${period.name}\" is closed.`,\r\n );\r\n }\r\n };\r\n\r\n repo.on('before:create', (payload: unknown) => checkPeriod(payload as Record<string, unknown>, false));\r\n repo.on('before:update', (payload: unknown) => checkPeriod(payload as Record<string, unknown>, true));\r\n },\r\n };\r\n}\r\n","/**\r\n * Idempotency Plugin for @classytic/mongokit\r\n *\r\n * Prevents duplicate journal entries by checking for existing entries\r\n * with the same idempotency key before creation.\r\n */\r\n\r\nimport type { Model, ClientSession } from 'mongoose';\r\nimport { Errors } from '../utils/errors.js';\r\n\r\n/** Minimal interface matching @classytic/mongokit RepositoryInstance */\r\ninterface RepositoryInstance {\r\n on(event: string, listener: (data: unknown) => void | Promise<void>): unknown;\r\n}\r\n\r\nexport interface IdempotencyPluginOptions {\r\n /** Mongoose model for journal entries */\r\n JournalEntryModel: Model<unknown>;\r\n /** Multi-tenant org field name */\r\n orgField?: string;\r\n}\r\n\r\nexport function idempotencyPlugin(options: IdempotencyPluginOptions) {\r\n const { JournalEntryModel, orgField } = options;\r\n\r\n return {\r\n name: 'accounting:idempotency',\r\n apply(repo: RepositoryInstance) {\r\n repo.on('before:create', async (raw: unknown) => {\r\n const context = raw as Record<string, unknown>;\r\n const data = context.data as Record<string, unknown> | undefined;\r\n if (!data?.idempotencyKey) return;\r\n\r\n const query: Record<string, unknown> = {\r\n idempotencyKey: data.idempotencyKey,\r\n };\r\n if (orgField && data[orgField]) {\r\n query[orgField] = data[orgField];\r\n }\r\n\r\n const existing = await JournalEntryModel.findOne(query)\r\n .select('_id')\r\n .session((context.session as ClientSession) ?? null)\r\n .lean() as Record<string, unknown> | null;\r\n\r\n if (existing) {\r\n throw Errors.conflict(\r\n `Duplicate idempotency key: \"${data.idempotencyKey}\". Existing entry: ${existing._id}`,\r\n );\r\n }\r\n });\r\n },\r\n };\r\n}\r\n"],"mappings":"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"}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { _ as TaxMetadata, a as Cents, c as DateRange, d as JournalType, f as MainType, g as TaxDetail, h as StatementType, i as CategoryKey, l as EntryState, m as ObjectId, n as CashFlowCategory, o as Currency, p as NormalBalance, s as DateOption, t as AccountType, u as JournalItem, v as TotalAccountOp } from "./core-Cx0baosR.mjs";
|
|
2
|
+
import { _ as getNormalBalance, a as JOURNAL_CODES, b as isValidCategory, c as getJournalTypeCodes, d as CATEGORY_KEYS, i as isValidCurrency, l as isValidJournalType, n as getCurrency, o as JOURNAL_TYPES, r as getMinorUnit, t as CURRENCIES, u as CATEGORIES, v as isBalanceSheet, y as isIncomeStatement } from "./currencies-Bkn3FNkC.mjs";
|
|
3
|
+
import { CountryPack, CountryPackInput, TaxCode, TaxCodesByRegion, TaxReportLine, TaxReportTemplate, defineCountryPack } from "./country/index.mjs";
|
|
4
|
+
import { _ as PopulatedJournalEntry, a as exportToCsv, h as FlatJournalRow, m as ExportFieldMap, n as quickbooksFieldMap, p as ExportField, r as flattenJournalEntries, t as universalFieldMap } from "./universal-x33ZJODp.mjs";
|
|
5
|
+
import { Money, abs, add, allocate, equals, format, formatPlain, fromDecimal, isNegative, isPositive, isValid, isZero, max, min, multiply, negate, parseCents, percentage, round, splitTaxExclusive, splitTaxInclusive, subtract, toDecimal } from "./money.mjs";
|
|
6
|
+
import { n as defaultLogger, t as Logger } from "./logger-Cv6VVc4r.mjs";
|
|
7
|
+
import { a as SchemaOptions, i as MultiTenantConfig, n as AuditConfig, o as StrictnessConfig, r as JournalSchemaOptions, t as AccountingEngineConfig } from "./engine-Cd73EOT6.mjs";
|
|
8
|
+
import { n as createJournalEntrySchema, r as createAccountSchema, t as createFiscalPeriodSchema } from "./fiscal-period.schema-DI2scngu.mjs";
|
|
9
|
+
import { i as fiscalLockPlugin, n as idempotencyPlugin, o as doubleEntryPlugin } from "./idempotency.plugin-BESs9YPD.mjs";
|
|
10
|
+
import { C as ReportAccount, D as TaxReturnSummary, E as TaxReport, O as TrialBalanceReport, S as LedgerEntry, T as ReportGroup, _ as CashFlowReport, a as reopenFiscalPeriod, b as GeneralLedgerReport, d as generateIncomeStatement, g as BalanceSheetReport, h as generateTrialBalance, i as closeFiscalPeriod, k as TrialBalanceRow, l as generateGeneralLedger, p as generateBalanceSheet, s as generateCashFlow, v as CashFlowSection, w as ReportCategory, x as IncomeStatementReport, y as GeneralLedgerAccount } from "./fiscal-close-CzUzpnMg.mjs";
|
|
11
|
+
import { n as wireJournalEntryMethods, t as wireAccountMethods } from "./account.repository-1C2sZvB2.mjs";
|
|
12
|
+
import * as mongoose$1 from "mongoose";
|
|
13
|
+
import { ClientSession, Connection, Model } from "mongoose";
|
|
14
|
+
|
|
15
|
+
//#region src/engine.d.ts
|
|
16
|
+
declare class AccountingEngine {
|
|
17
|
+
readonly config: AccountingEngineConfig;
|
|
18
|
+
readonly country: CountryPack;
|
|
19
|
+
readonly currency: string;
|
|
20
|
+
readonly money: {
|
|
21
|
+
readonly round: typeof round;
|
|
22
|
+
readonly fromDecimal: typeof fromDecimal;
|
|
23
|
+
readonly toDecimal: typeof toDecimal;
|
|
24
|
+
readonly add: typeof add;
|
|
25
|
+
readonly subtract: typeof subtract;
|
|
26
|
+
readonly multiply: typeof multiply;
|
|
27
|
+
readonly percentage: typeof percentage;
|
|
28
|
+
readonly splitTaxInclusive: typeof splitTaxInclusive;
|
|
29
|
+
readonly splitTaxExclusive: typeof splitTaxExclusive;
|
|
30
|
+
readonly allocate: typeof allocate;
|
|
31
|
+
readonly equals: typeof equals;
|
|
32
|
+
readonly isZero: typeof isZero;
|
|
33
|
+
readonly isPositive: typeof isPositive;
|
|
34
|
+
readonly isNegative: typeof isNegative;
|
|
35
|
+
readonly abs: typeof abs;
|
|
36
|
+
readonly negate: typeof negate;
|
|
37
|
+
readonly min: typeof min;
|
|
38
|
+
readonly max: typeof max;
|
|
39
|
+
readonly format: typeof format;
|
|
40
|
+
readonly formatPlain: typeof formatPlain;
|
|
41
|
+
readonly isValid: typeof isValid;
|
|
42
|
+
readonly parseCents: typeof parseCents;
|
|
43
|
+
};
|
|
44
|
+
constructor(config: AccountingEngineConfig);
|
|
45
|
+
createAccountSchema(options?: SchemaOptions): mongoose$1.Schema<any, Model<any, any, any, any, any, any, any>, {}, {}, {}, {}, {
|
|
46
|
+
timestamps: true;
|
|
47
|
+
}, any, any, unknown, {
|
|
48
|
+
[x: string]: any;
|
|
49
|
+
} & Required<{
|
|
50
|
+
_id: unknown;
|
|
51
|
+
}> & {
|
|
52
|
+
__v: number;
|
|
53
|
+
}>;
|
|
54
|
+
createJournalEntrySchema(accountModelName: string, options?: JournalSchemaOptions): mongoose$1.Schema<any, Model<any, any, any, any, any, any, any>, {}, {}, {}, {}, {
|
|
55
|
+
timestamps: true;
|
|
56
|
+
}, any, any, unknown, {
|
|
57
|
+
[x: string]: any;
|
|
58
|
+
} & Required<{
|
|
59
|
+
_id: unknown;
|
|
60
|
+
}> & {
|
|
61
|
+
__v: number;
|
|
62
|
+
}>;
|
|
63
|
+
createFiscalPeriodSchema(options?: SchemaOptions): mongoose$1.Schema<any, Model<any, any, any, any, any, any, any>, {}, {}, {}, {}, {
|
|
64
|
+
timestamps: true;
|
|
65
|
+
}, any, any, unknown, {
|
|
66
|
+
[x: string]: any;
|
|
67
|
+
} & Required<{
|
|
68
|
+
_id: unknown;
|
|
69
|
+
}> & {
|
|
70
|
+
__v: number;
|
|
71
|
+
}>;
|
|
72
|
+
createReports(models: {
|
|
73
|
+
Account: Model<unknown>;
|
|
74
|
+
JournalEntry: Model<unknown>;
|
|
75
|
+
}): {
|
|
76
|
+
trialBalance: (params: {
|
|
77
|
+
organizationId?: unknown;
|
|
78
|
+
dateOption: "month" | "quarter" | "year" | "custom";
|
|
79
|
+
dateValue: unknown;
|
|
80
|
+
accountId?: string;
|
|
81
|
+
filters?: Record<string, unknown>;
|
|
82
|
+
}) => Promise<TrialBalanceReport>;
|
|
83
|
+
balanceSheet: (params: {
|
|
84
|
+
organizationId?: unknown;
|
|
85
|
+
dateOption: "month" | "quarter" | "year" | "custom";
|
|
86
|
+
dateValue: unknown;
|
|
87
|
+
businessName?: string;
|
|
88
|
+
filters?: Record<string, unknown>;
|
|
89
|
+
}) => Promise<BalanceSheetReport>;
|
|
90
|
+
incomeStatement: (params: {
|
|
91
|
+
organizationId?: unknown;
|
|
92
|
+
dateOption: "month" | "quarter" | "year" | "custom";
|
|
93
|
+
dateValue: unknown;
|
|
94
|
+
businessName?: string;
|
|
95
|
+
filters?: Record<string, unknown>;
|
|
96
|
+
}) => Promise<IncomeStatementReport>;
|
|
97
|
+
generalLedger: (params: {
|
|
98
|
+
organizationId?: unknown;
|
|
99
|
+
dateOption: "month" | "quarter" | "year" | "custom";
|
|
100
|
+
dateValue: unknown;
|
|
101
|
+
accountId?: string;
|
|
102
|
+
filters?: Record<string, unknown>;
|
|
103
|
+
}) => Promise<GeneralLedgerReport>;
|
|
104
|
+
cashFlow: (params: {
|
|
105
|
+
organizationId?: unknown;
|
|
106
|
+
dateOption: "month" | "quarter" | "year" | "custom";
|
|
107
|
+
dateValue: unknown;
|
|
108
|
+
businessName?: string;
|
|
109
|
+
filters?: Record<string, unknown>;
|
|
110
|
+
}) => Promise<CashFlowReport>;
|
|
111
|
+
};
|
|
112
|
+
/** Get all posting account types (accounts you can post transactions to) */
|
|
113
|
+
getPostingAccountTypes(): readonly AccountType[];
|
|
114
|
+
/** Validate an account type code */
|
|
115
|
+
isValidAccountType(code: string): boolean;
|
|
116
|
+
/** Get account type definition by code */
|
|
117
|
+
getAccountType(code: string): AccountType | undefined;
|
|
118
|
+
/** Get tax codes for a region */
|
|
119
|
+
getTaxCodesForRegion(region: string): TaxCode[];
|
|
120
|
+
/**
|
|
121
|
+
* Create a fully-configured journal entry repository with secure plugin wiring.
|
|
122
|
+
* This is the **recommended** way to set up journal entry repositories.
|
|
123
|
+
*
|
|
124
|
+
* Includes:
|
|
125
|
+
* - Double-entry plugin with account existence + tenant integrity validation
|
|
126
|
+
* - Fiscal lock plugin (when FiscalPeriodModel is provided)
|
|
127
|
+
* - post(), unpost(), reverse(), and duplicate() domain methods
|
|
128
|
+
*
|
|
129
|
+
* @param createRepository - The `createRepository` function from @classytic/mongokit
|
|
130
|
+
* @param models.JournalEntryModel - Mongoose model for journal entries
|
|
131
|
+
* @param models.AccountModel - Mongoose model for accounts (required for secure posted-create validation)
|
|
132
|
+
* @param models.FiscalPeriodModel - Mongoose model for fiscal periods (optional, enables fiscal lock)
|
|
133
|
+
* @param additionalPlugins - Extra plugins to include (e.g. timestampPlugin)
|
|
134
|
+
* @returns A wired repository with post(), unpost(), reverse(), duplicate(), and all plugins configured
|
|
135
|
+
*/
|
|
136
|
+
createJournalEntryRepository(createRepository: (model: Model<unknown>, plugins: any[]) => any, models: {
|
|
137
|
+
JournalEntryModel: Model<unknown>;
|
|
138
|
+
AccountModel: Model<unknown>;
|
|
139
|
+
FiscalPeriodModel?: Model<unknown>;
|
|
140
|
+
}, additionalPlugins?: any[]): any;
|
|
141
|
+
/**
|
|
142
|
+
* Wire post/reverse domain methods onto a mongokit Repository
|
|
143
|
+
* for journal entries. The repository must already be created via
|
|
144
|
+
* `createRepository(Model, plugins)` from @classytic/mongokit.
|
|
145
|
+
*
|
|
146
|
+
* **Note:** Prefer `createJournalEntryRepository()` which guarantees
|
|
147
|
+
* secure plugin wiring. This method only adds domain methods and does
|
|
148
|
+
* not validate plugin configuration.
|
|
149
|
+
*
|
|
150
|
+
* @param repository - An existing mongokit Repository instance
|
|
151
|
+
* @param JournalEntryModel - The Mongoose model for journal entries
|
|
152
|
+
* @returns The same repository, now with `.post()` and `.reverse()`
|
|
153
|
+
*/
|
|
154
|
+
wireJournalEntryRepository(repository: any, JournalEntryModel: Model<unknown>): any;
|
|
155
|
+
/**
|
|
156
|
+
* Wire seedAccounts/bulkCreate and posting-account validation onto a
|
|
157
|
+
* mongokit Repository for accounts. The repository must already be
|
|
158
|
+
* created via `createRepository(Model, plugins)` from @classytic/mongokit.
|
|
159
|
+
*
|
|
160
|
+
* @param repository - An existing mongokit Repository instance
|
|
161
|
+
* @param AccountModel - The Mongoose model for accounts
|
|
162
|
+
* @returns The same repository, now with `.seedAccounts()` and `.bulkCreate()`
|
|
163
|
+
*/
|
|
164
|
+
wireAccountRepository(repository: any, AccountModel: Model<unknown>): any;
|
|
165
|
+
}
|
|
166
|
+
declare function createAccountingEngine(config: AccountingEngineConfig): AccountingEngine;
|
|
167
|
+
//#endregion
|
|
168
|
+
//#region src/utils/date-range.d.ts
|
|
169
|
+
/**
|
|
170
|
+
* Compute start/end dates from a date option + value.
|
|
171
|
+
*
|
|
172
|
+
* Examples:
|
|
173
|
+
* getDateRange('month', '2025-03') → Mar 1 – Mar 31
|
|
174
|
+
* getDateRange('quarter', { quarter: 2, year: 2025 }) → Apr 1 – Jun 30
|
|
175
|
+
* getDateRange('year', 2025) → Jan 1 – Dec 31
|
|
176
|
+
* getDateRange('custom', { startDate, endDate })
|
|
177
|
+
*/
|
|
178
|
+
declare function getDateRange(option: DateOption, value: unknown): DateRange;
|
|
179
|
+
/** Get fiscal year start date for a given date and fiscal start month */
|
|
180
|
+
declare function getFiscalYearStart(date: Date, fiscalStartMonth?: number): Date;
|
|
181
|
+
//#endregion
|
|
182
|
+
//#region src/utils/account-helpers.d.ts
|
|
183
|
+
/**
|
|
184
|
+
* Check if an account type is a virtual tax sub-account.
|
|
185
|
+
* Returns true if the account's parent has `isVirtualTotal: true`.
|
|
186
|
+
* Works for any country pack — no code format assumptions.
|
|
187
|
+
*/
|
|
188
|
+
declare function isVirtualTaxAccount(accountType: AccountType, accountMap: Map<string, AccountType>): boolean;
|
|
189
|
+
/**
|
|
190
|
+
* Calculate a total from sub-accounts using the totalAccountTypes formula.
|
|
191
|
+
* @param formula - Array of { account, operation } instructions
|
|
192
|
+
* @param balanceMap - Map of account code → balance
|
|
193
|
+
*/
|
|
194
|
+
declare function calculateTotal(formula: readonly TotalAccountOp[], balanceMap: Map<string, number>): number;
|
|
195
|
+
/**
|
|
196
|
+
* Compute the ending balance for an account given its debits and credits.
|
|
197
|
+
* Uses the account's main type to determine normal balance direction.
|
|
198
|
+
*
|
|
199
|
+
* Assets & Expenses: debit - credit
|
|
200
|
+
* Liabilities, Equity & Income: credit - debit
|
|
201
|
+
*/
|
|
202
|
+
declare function computeEndingBalance(category: CategoryKey, totalDebit: number, totalCredit: number): number;
|
|
203
|
+
/**
|
|
204
|
+
* Build a lookup map from an array of account types.
|
|
205
|
+
*/
|
|
206
|
+
declare function buildAccountTypeMap(accountTypes: readonly AccountType[]): Map<string, AccountType>;
|
|
207
|
+
//#endregion
|
|
208
|
+
//#region src/utils/errors.d.ts
|
|
209
|
+
/**
|
|
210
|
+
* Typed error for the accounting package.
|
|
211
|
+
* Carries HTTP status + machine-readable code.
|
|
212
|
+
* Replaces all ad-hoc `(error as ...).status = N` patterns.
|
|
213
|
+
*/
|
|
214
|
+
declare class AccountingError extends Error {
|
|
215
|
+
readonly status: number;
|
|
216
|
+
readonly code: string;
|
|
217
|
+
constructor(message: string, status?: number, code?: string);
|
|
218
|
+
}
|
|
219
|
+
/** Convenience factory functions */
|
|
220
|
+
declare const Errors: {
|
|
221
|
+
readonly validation: (msg: string) => AccountingError;
|
|
222
|
+
readonly notFound: (msg: string) => AccountingError;
|
|
223
|
+
readonly conflict: (msg: string) => AccountingError;
|
|
224
|
+
readonly immutable: (msg: string) => AccountingError;
|
|
225
|
+
readonly fiscal: (msg: string) => AccountingError;
|
|
226
|
+
};
|
|
227
|
+
//#endregion
|
|
228
|
+
//#region src/utils/session.d.ts
|
|
229
|
+
interface SessionResult {
|
|
230
|
+
session: ClientSession | null;
|
|
231
|
+
ownSession: boolean;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Acquire a session: uses external if provided, otherwise creates an internal one.
|
|
235
|
+
* Returns { session, ownSession } so callers can commit/abort/end appropriately.
|
|
236
|
+
*
|
|
237
|
+
* When transactions are unavailable (no replica set / standalone), returns
|
|
238
|
+
* session=null and the function runs without transactional safety.
|
|
239
|
+
*/
|
|
240
|
+
declare function acquireSession(db: Connection, externalSession: ClientSession | undefined | null, logger?: Logger): Promise<SessionResult>;
|
|
241
|
+
/**
|
|
242
|
+
* Finalize an owned session: commit or abort, then always end.
|
|
243
|
+
*/
|
|
244
|
+
declare function finalizeSession(session: ClientSession | null, ownSession: boolean, success: boolean): Promise<void>;
|
|
245
|
+
//#endregion
|
|
246
|
+
//#region src/utils/filter-builder.d.ts
|
|
247
|
+
/**
|
|
248
|
+
* Filter Builder — Sanitizes user-supplied dimension filters for aggregation pipelines.
|
|
249
|
+
*
|
|
250
|
+
* Prevents injection of dangerous MongoDB operators while allowing
|
|
251
|
+
* standard equality and comparison filters on custom dimension fields.
|
|
252
|
+
*/
|
|
253
|
+
/**
|
|
254
|
+
* Build a sanitized filter object from user-supplied dimension filters.
|
|
255
|
+
* Blocks dangerous operators ($where, $expr, $function, etc.).
|
|
256
|
+
*
|
|
257
|
+
* @param filters - Key-value filters (e.g. { 'journalItems.departmentId': 'dept-1' })
|
|
258
|
+
* @returns Sanitized filter object safe for $match stages
|
|
259
|
+
* @throws Error if a blocked operator is used
|
|
260
|
+
*/
|
|
261
|
+
declare function buildItemFilters(filters?: Record<string, unknown>): Record<string, unknown>;
|
|
262
|
+
//#endregion
|
|
263
|
+
//#region src/types/contracts.d.ts
|
|
264
|
+
/** A single line produced by a subledger for posting */
|
|
265
|
+
interface SubledgerJournalItem {
|
|
266
|
+
/** Account type code (resolved to ObjectId by the app layer) */
|
|
267
|
+
accountCode: string;
|
|
268
|
+
/** Integer cents */
|
|
269
|
+
debit: number;
|
|
270
|
+
/** Integer cents */
|
|
271
|
+
credit: number;
|
|
272
|
+
/** Line-item description */
|
|
273
|
+
label?: string;
|
|
274
|
+
/** Extra dimension fields (departmentId, projectId, etc.) */
|
|
275
|
+
extraFields?: Record<string, unknown>;
|
|
276
|
+
}
|
|
277
|
+
/** The shape of a journal entry that a subledger produces */
|
|
278
|
+
interface SubledgerPostingInput {
|
|
279
|
+
journalType: string;
|
|
280
|
+
label: string;
|
|
281
|
+
date: Date;
|
|
282
|
+
journalItems: SubledgerJournalItem[];
|
|
283
|
+
/** Prevents duplicate postings on retry */
|
|
284
|
+
idempotencyKey?: string;
|
|
285
|
+
/** Arbitrary metadata for the entry (stored via extraFields) */
|
|
286
|
+
metadata?: Record<string, unknown>;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Contract that subledger posting adapters must implement.
|
|
290
|
+
*
|
|
291
|
+
* @typeParam TSource - The source document type (e.g., Invoice, Bill, PayrollRun)
|
|
292
|
+
*/
|
|
293
|
+
interface PostingContract<TSource = unknown> {
|
|
294
|
+
/** Unique name for this subledger (e.g. 'billing', 'inventory', 'payroll') */
|
|
295
|
+
readonly name: string;
|
|
296
|
+
/** Convert a source document into one or more journal entry inputs */
|
|
297
|
+
toJournalEntries(source: TSource): SubledgerPostingInput[];
|
|
298
|
+
/** Validate that the source document is ready to post. Throws on failure. */
|
|
299
|
+
validate(source: TSource): void;
|
|
300
|
+
}
|
|
301
|
+
/** Result of a subledger posting operation */
|
|
302
|
+
interface PostingResult {
|
|
303
|
+
journalEntryIds: (string | ObjectId)[];
|
|
304
|
+
idempotencyKeys?: string[];
|
|
305
|
+
}
|
|
306
|
+
//#endregion
|
|
307
|
+
export { type AccountType, AccountingEngine, type AccountingEngineConfig, AccountingError, type AuditConfig, type BalanceSheetReport, CATEGORIES, CATEGORY_KEYS, CURRENCIES, type CashFlowCategory, type CashFlowReport, type CashFlowSection, type CategoryKey, type Cents, type CountryPack, type CountryPackInput, type Currency, type DateOption, type DateRange, type EntryState, Errors, type ExportField, type ExportFieldMap, type FlatJournalRow, type GeneralLedgerAccount, type GeneralLedgerReport, type IncomeStatementReport, JOURNAL_CODES, JOURNAL_TYPES, type JournalItem, type JournalSchemaOptions, type JournalType, type LedgerEntry, type Logger, type MainType, Money, type MultiTenantConfig, type NormalBalance, type PopulatedJournalEntry, type PostingContract, type PostingResult, type ReportAccount, type ReportCategory, type ReportGroup, type SchemaOptions, type SessionResult, type StatementType, type StrictnessConfig, type SubledgerJournalItem, type SubledgerPostingInput, type TaxCode, type TaxCodesByRegion, type TaxDetail, type TaxMetadata, type TaxReport, type TaxReportLine, type TaxReportTemplate, type TaxReturnSummary, type TotalAccountOp, type TrialBalanceReport, type TrialBalanceRow, acquireSession, add, allocate, buildAccountTypeMap, buildItemFilters, calculateTotal, closeFiscalPeriod, computeEndingBalance, createAccountSchema, createAccountingEngine, createFiscalPeriodSchema, createJournalEntrySchema, defaultLogger, defineCountryPack, doubleEntryPlugin, exportToCsv, finalizeSession, fiscalLockPlugin, flattenJournalEntries, format, formatPlain, fromDecimal, generateBalanceSheet, generateCashFlow, generateGeneralLedger, generateIncomeStatement, generateTrialBalance, getCurrency, getDateRange, getFiscalYearStart, getJournalTypeCodes, getMinorUnit, getNormalBalance, idempotencyPlugin, isBalanceSheet, isIncomeStatement, isValidCategory, isValidCurrency, isValidJournalType, isVirtualTaxAccount, multiply, parseCents, percentage, quickbooksFieldMap, reopenFiscalPeriod, splitTaxExclusive, splitTaxInclusive, subtract, toDecimal, universalFieldMap, wireAccountMethods, wireJournalEntryMethods };
|
|
308
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/engine.ts","../src/utils/date-range.ts","../src/utils/account-helpers.ts","../src/utils/errors.ts","../src/utils/session.ts","../src/utils/filter-builder.ts","../src/types/contracts.ts"],"mappings":";;;;;;;;;;;;;;;cA0Ca,gBAAA;EAAA,SACF,MAAA,EAAQ,sBAAA;EAAA,SACR,OAAA,EAAS,WAAA;EAAA,SACT,QAAA;EAAA,SACA,KAAA;IAAA,uBAFoB,KAAA;IAAA;;;;;;;;;;;;;;;;;;;;;;cAIjB,MAAA,EAAQ,sBAAA;EAQpB,mBAAA,CAAoB,OAAA,GAAU,aAAA,cAAa,MAAA,MAAA,KAAA;;;;;;;;;EAI3C,wBAAA,CAAyB,gBAAA,UAA0B,OAAA,GAAU,oBAAA,cAAoB,MAAA,MAAA,KAAA;;;;;;;;;EAIjF,wBAAA,CAAyB,OAAA,GAAU,aAAA,cAAa,MAAA,MAAA,KAAA;;;;;;;;;EAMhD,aAAA,CAAc,MAAA;IACZ,OAAA,EAAS,KAAA;IACT,YAAA,EAAc,KAAA;EAAA;;MAWV,cAAA;MACA,UAAA;MACA,SAAA;MACA,SAAA;MACA,OAAA,GAAU,MAAA;IAAA,MACX,OAAA,CADiB,kBAAA;;MAQhB,cAAA;MACA,UAAA;MACA,SAAA;MACA,YAAA;MACA,OAAA,GAAU,MAAA;IAAA,MACX,OAAA,CADiB,kBAAA;;MAQhB,cAAA;MACA,UAAA;MACA,SAAA;MACA,YAAA;MACA,OAAA,GAAU,MAAA;IAAA,MACX,OAAA,CADiB,qBAAA;;MAQhB,cAAA;MACA,UAAA;MACA,SAAA;MACA,SAAA;MACA,OAAA,GAAU,MAAA;IAAA,MACX,OAAA,CADiB,mBAAA;;MAQhB,cAAA;MACA,UAAA;MACA,SAAA;MACA,YAAA;MACA,OAAA,GAAU,MAAA;IAAA,MACX,OAAA,CADiB,cAAA;EAAA;;EAYtB,sBAAA,CAAA,YAXK,WAAA;;EAgBL,kBAAA,CAAmB,IAAA;;EAKnB,cAAA,CAAe,IAAA,WAVO,WAAA;;EAetB,oBAAA,CAAqB,MAAA,WALM,OAAA;;;;;;;;;;;;;;;;;EA2B3B,4BAAA,CAEE,gBAAA,GAAmB,KAAA,EAAO,KAAA,WAAgB,OAAA,iBAC1C,MAAA;IACE,iBAAA,EAAmB,KAAA;IACnB,YAAA,EAAc,KAAA;IACd,iBAAA,GAAoB,KAAA;EAAA,GAGtB,iBAAA;EArI+E;;;;;;;;;;;;;EA0LjF,0BAAA,CAA2B,UAAA,OAAiB,iBAAA,EAAmB,KAAA;;;;;;;;;;EAgB/D,qBAAA,CAAsB,UAAA,OAAiB,YAAA,EAAc,KAAA;AAAA;AAAA,iBASvC,sBAAA,CAAuB,MAAA,EAAQ,sBAAA,GAAyB,gBAAA;;;;;;;;;;;;iBChQxD,YAAA,CAAa,MAAA,EAAQ,UAAA,EAAY,KAAA,YAAiB,SAAA;;iBA6DlD,kBAAA,CAAmB,IAAA,EAAM,IAAA,EAAM,gBAAA,YAAuB,IAAA;;;;;;;;iBChEtD,mBAAA,CAAoB,WAAA,EAAa,WAAA,EAAa,UAAA,EAAY,GAAA,SAAY,WAAA;;AF8BtF;;;;iBEPgB,cAAA,CACd,OAAA,WAAkB,cAAA,IAClB,UAAA,EAAY,GAAA;;;;;;;;iBAiBE,oBAAA,CACd,QAAA,EAAU,WAAA,EACV,UAAA,UACA,WAAA;;;;iBAYc,mBAAA,CAAoB,YAAA,WAAuB,WAAA,KAAgB,GAAA,SAAY,WAAA;;;;;;;;cChE1E,eAAA,SAAwB,KAAA;EAAA,SAC1B,MAAA;EAAA,SACA,IAAA;cAEG,OAAA,UAAiB,MAAA,WAAc,IAAA;AAAA;;cAShC,MAAA;EAAA,sCACa,eAAA;EAAA,oCACF,eAAA;EAAA,oCACA,eAAA;EAAA,qCACC,eAAA;EAAA,kCACH,eAAA;AAAA;;;UCJL,aAAA;EACf,OAAA,EAAS,aAAA;EACT,UAAA;AAAA;;;;;;;;iBAUoB,cAAA,CACpB,EAAA,EAAI,UAAA,EACJ,eAAA,EAAiB,aAAA,qBACjB,MAAA,GAAQ,MAAA,GACP,OAAA,CAAQ,aAAA;;;;iBA6CW,eAAA,CACpB,OAAA,EAAS,aAAA,SACT,UAAA,WACA,OAAA,YACC,OAAA;;;;;;;;;;;;;;;;;iBChEa,gBAAA,CAAiB,OAAA,GAAU,MAAA,oBAA0B,MAAA;;;;UCIpD,oBAAA;;EAEf,WAAA;;EAEA,KAAA;;EAEA,MAAA;;EAEA,KAAA;;EAEA,WAAA,GAAc,MAAA;AAAA;;UAMC,qBAAA;EACf,WAAA;EACA,KAAA;EACA,IAAA,EAAM,IAAA;EACN,YAAA,EAAc,oBAAA;;EAEd,cAAA;;EAEA,QAAA,GAAW,MAAA;AAAA;;;;;;UAUI,eAAA;;WAEN,IAAA;ENI0B;EMFnC,gBAAA,CAAiB,MAAA,EAAQ,OAAA,GAAU,qBAAA;;EAEnC,QAAA,CAAS,MAAA,EAAQ,OAAA;AAAA;;UAMF,aAAA;EACf,eAAA,YAA2B,QAAA;EAC3B,eAAA;AAAA"}
|