@classytic/ledger 0.4.2 → 0.5.1
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 +228 -188
- package/dist/constants/index.d.mts +1 -1
- package/dist/constants/index.mjs +2 -3
- package/dist/country/index.d.mts +1 -1
- package/dist/{journals-BfwnCFam.mjs → currencies-CsuBGfgs.mjs} +80 -1
- package/dist/{date-lock.plugin-DL6pe24p.mjs → date-lock.plugin-B6WyvqNG.mjs} +63 -11
- package/dist/errors-BmRjW38t.mjs +33 -0
- package/dist/exports/index.d.mts +1 -1
- package/dist/exports/index.mjs +1 -1
- package/dist/{fiscal-close-B2_7WMTe.mjs → fiscal-close-Dk3yRT9i.mjs} +14 -4
- package/dist/{idempotency.plugin-zU-GKJ0-.d.mts → idempotency.plugin-WcQLZU9n.d.mts} +38 -0
- package/dist/{index-CxZqRaOU.d.mts → index-GmfEFxVn.d.mts} +1 -1
- package/dist/index.d.mts +525 -344
- package/dist/index.mjs +1868 -170
- package/dist/{journals-DTipb_rz.d.mts → journals-C50E9mpo.d.mts} +1 -1
- 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/{trial-balance-DcQ0xj_4.d.mts → trial-balance-BZ7yOOFD.d.mts} +16 -4
- package/package.json +1 -11
- package/dist/currencies-W8kQAkm0.mjs +0 -80
- package/dist/engine-scgOvxHJ.d.mts +0 -130
- package/dist/errors-B_dyYZc_.mjs +0 -26
- package/dist/journal-entry.schema-JqrfbvB4.d.mts +0 -103
- package/dist/logger-UbTdBb1x.d.mts +0 -14
- package/dist/reconciliation.repository-D-D_ITL-.d.mts +0 -135
- package/dist/reconciliation.repository-fPwFKvrk.mjs +0 -542
- package/dist/reconciliation.schema-BA1lPv4t.mjs +0 -666
- package/dist/repositories/index.d.mts +0 -2
- package/dist/repositories/index.mjs +0 -2
- package/dist/schemas/index.d.mts +0 -71
- package/dist/schemas/index.mjs +0 -2
- package/dist/tenant-guard-r17Se3Bb.mjs +0 -13
- /package/dist/{categories-DWogBUgQ.mjs → categories-BkKdv16V.mjs} +0 -0
- /package/dist/{core-8Xfnpn6g.d.mts → core-BkGjuVZj.d.mts} +0 -0
- /package/dist/{exports-DoGQQtMQ.mjs → exports-BP-0Ni5W.mjs} +0 -0
- /package/dist/{index-J-XIbXH-.d.mts → index-D1ZjgVxn.d.mts} +0 -0
|
@@ -1,17 +1,41 @@
|
|
|
1
|
-
import { n as Errors } from "./errors-
|
|
1
|
+
import { n as Errors } from "./errors-BmRjW38t.mjs";
|
|
2
2
|
//#region src/plugins/double-entry.plugin.ts
|
|
3
3
|
function doubleEntryPlugin(options = {}) {
|
|
4
4
|
const { onlyOnPost = true, JournalEntryModel, AccountModel, orgField } = options;
|
|
5
5
|
function validateItems(items, data) {
|
|
6
|
+
const lineErrors = [];
|
|
6
7
|
for (let i = 0; i < items.length; i++) {
|
|
7
8
|
const d = items[i].debit ?? 0;
|
|
8
9
|
const c = items[i].credit ?? 0;
|
|
9
|
-
if (d > 0 && c > 0)
|
|
10
|
-
|
|
10
|
+
if (d > 0 && c > 0) lineErrors.push({
|
|
11
|
+
path: `journalItems.${i}`,
|
|
12
|
+
issue: "line cannot have both debit and credit greater than zero",
|
|
13
|
+
value: {
|
|
14
|
+
debit: d,
|
|
15
|
+
credit: c
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
if (d === 0 && c === 0) lineErrors.push({
|
|
19
|
+
path: `journalItems.${i}`,
|
|
20
|
+
issue: "line cannot have both debit and credit equal to zero",
|
|
21
|
+
value: {
|
|
22
|
+
debit: 0,
|
|
23
|
+
credit: 0
|
|
24
|
+
}
|
|
25
|
+
});
|
|
11
26
|
}
|
|
27
|
+
if (lineErrors.length > 0) throw Errors.validation(`Invalid journal line(s): ${lineErrors.map((e) => `${e.path} — ${e.issue}`).join("; ")}`, lineErrors);
|
|
12
28
|
const totalDebit = items.reduce((s, i) => s + (i.debit ?? 0), 0);
|
|
13
29
|
const totalCredit = items.reduce((s, i) => s + (i.credit ?? 0), 0);
|
|
14
|
-
if (totalDebit !== totalCredit) throw Errors.validation(`Double-entry violation: debits (${totalDebit}) ≠ credits (${totalCredit}). Difference: ${Math.abs(totalDebit - totalCredit)}
|
|
30
|
+
if (totalDebit !== totalCredit) throw Errors.validation(`Double-entry violation: debits (${totalDebit}) ≠ credits (${totalCredit}). Difference: ${Math.abs(totalDebit - totalCredit)}`, [{
|
|
31
|
+
path: "journalItems",
|
|
32
|
+
issue: "debits must equal credits",
|
|
33
|
+
value: {
|
|
34
|
+
totalDebit,
|
|
35
|
+
totalCredit,
|
|
36
|
+
difference: totalDebit - totalCredit
|
|
37
|
+
}
|
|
38
|
+
}]);
|
|
15
39
|
data.totalDebit = totalDebit;
|
|
16
40
|
data.totalCredit = totalCredit;
|
|
17
41
|
}
|
|
@@ -33,23 +57,51 @@ function doubleEntryPlugin(options = {}) {
|
|
|
33
57
|
};
|
|
34
58
|
/** Verify all journal item accounts exist and belong to the same org */
|
|
35
59
|
const validateAccounts = async (items, data, context) => {
|
|
36
|
-
const
|
|
37
|
-
|
|
60
|
+
const missingIdxs = [];
|
|
61
|
+
items.forEach((item, idx) => {
|
|
62
|
+
if (item.account == null || item.account === "") missingIdxs.push(idx);
|
|
63
|
+
});
|
|
64
|
+
if (missingIdxs.length > 0) throw Errors.validation(`Posted entry has items with missing accounts at index(es): ${missingIdxs.join(", ")}.`, missingIdxs.map((i) => ({
|
|
65
|
+
path: `journalItems.${i}.account`,
|
|
66
|
+
issue: "account is required on posted entries"
|
|
67
|
+
})));
|
|
68
|
+
const accountIds = items.map((i) => i.account);
|
|
38
69
|
const selectFields = orgField ? `_id ${orgField}` : "_id";
|
|
39
70
|
const accounts = await AccountModel?.find({ _id: { $in: accountIds } }).select(selectFields).session(context.session ?? null).lean();
|
|
40
71
|
const foundIds = new Set(accounts.map((a) => String(a._id)));
|
|
41
|
-
const
|
|
42
|
-
|
|
72
|
+
const missingFieldErrors = [];
|
|
73
|
+
items.forEach((item, idx) => {
|
|
74
|
+
if (!foundIds.has(String(item.account))) missingFieldErrors.push({
|
|
75
|
+
path: `journalItems.${idx}.account`,
|
|
76
|
+
issue: "account does not exist",
|
|
77
|
+
value: item.account
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
if (missingFieldErrors.length > 0) throw Errors.validation(`${missingFieldErrors.length} item(s) reference non-existent accounts.`, missingFieldErrors);
|
|
43
81
|
if (orgField && data[orgField] != null) {
|
|
44
82
|
const dataOrg = String(data[orgField]);
|
|
45
|
-
const
|
|
46
|
-
|
|
83
|
+
const accountOrgById = new Map(accounts.map((a) => [String(a._id), String(a[orgField])]));
|
|
84
|
+
const crossTenantFieldErrors = [];
|
|
85
|
+
items.forEach((item, idx) => {
|
|
86
|
+
const acctOrg = accountOrgById.get(String(item.account));
|
|
87
|
+
if (acctOrg !== void 0 && acctOrg !== dataOrg) crossTenantFieldErrors.push({
|
|
88
|
+
path: `journalItems.${idx}.account`,
|
|
89
|
+
issue: "account belongs to another organization",
|
|
90
|
+
value: {
|
|
91
|
+
account: item.account,
|
|
92
|
+
expectedOrg: dataOrg,
|
|
93
|
+
actualOrg: acctOrg
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
if (crossTenantFieldErrors.length > 0) throw Errors.validation(`${crossTenantFieldErrors.length} item(s) reference accounts from another organization.`, crossTenantFieldErrors);
|
|
47
98
|
}
|
|
48
99
|
};
|
|
49
100
|
const validateUpdate = async (context) => {
|
|
50
101
|
const data = context.data;
|
|
51
102
|
if (!data) return;
|
|
52
|
-
|
|
103
|
+
const internalOp = context._ledgerInternal;
|
|
104
|
+
if (JournalEntryModel && !internalOp) {
|
|
53
105
|
const id = context.id;
|
|
54
106
|
if (id) {
|
|
55
107
|
if ((await JournalEntryModel.findById(id).select("state").session(context.session ?? null).lean())?.state === "posted") {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/utils/errors.ts
|
|
2
|
+
var AccountingError = class extends Error {
|
|
3
|
+
status;
|
|
4
|
+
code;
|
|
5
|
+
fields;
|
|
6
|
+
constructor(message, status = 400, code = "ACCOUNTING_ERROR", fields) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "AccountingError";
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.code = code;
|
|
11
|
+
if (fields && fields.length > 0) this.fields = Object.freeze([...fields]);
|
|
12
|
+
}
|
|
13
|
+
/** Serialize to a plain object for API responses and logs. */
|
|
14
|
+
toJSON() {
|
|
15
|
+
return {
|
|
16
|
+
name: this.name,
|
|
17
|
+
message: this.message,
|
|
18
|
+
status: this.status,
|
|
19
|
+
code: this.code,
|
|
20
|
+
...this.fields ? { fields: this.fields } : {}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
/** Convenience factory functions. */
|
|
25
|
+
const Errors = {
|
|
26
|
+
validation: (msg, fields) => new AccountingError(msg, 400, "VALIDATION_ERROR", fields),
|
|
27
|
+
notFound: (msg, fields) => new AccountingError(msg, 404, "NOT_FOUND", fields),
|
|
28
|
+
conflict: (msg, fields) => new AccountingError(msg, 409, "CONFLICT", fields),
|
|
29
|
+
immutable: (msg, fields) => new AccountingError(msg, 403, "IMMUTABLE_ENTRY", fields),
|
|
30
|
+
fiscal: (msg, fields) => new AccountingError(msg, 400, "FISCAL_ERROR", fields)
|
|
31
|
+
};
|
|
32
|
+
//#endregion
|
|
33
|
+
export { Errors as n, AccountingError as t };
|
package/dist/exports/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { _ as PopulatedJournalEntry, a as exportToCsv, c as getHeaders, d as serializeCsv, f as CsvOptions, g as PopulatedAccount, h as FlatJournalRow, i as quickbooksFieldMap, l as buildCsv, m as ExportFieldMap, n as flattenJournalEntry, o as extractAllRows, p as ExportField, r as universalFieldMap, s as extractRow, t as flattenJournalEntries, u as escapeCell, v as PopulatedJournalItem } from "../index-
|
|
1
|
+
import { _ as PopulatedJournalEntry, a as exportToCsv, c as getHeaders, d as serializeCsv, f as CsvOptions, g as PopulatedAccount, h as FlatJournalRow, i as quickbooksFieldMap, l as buildCsv, m as ExportFieldMap, n as flattenJournalEntry, o as extractAllRows, p as ExportField, r as universalFieldMap, s as extractRow, t as flattenJournalEntries, u as escapeCell, v as PopulatedJournalItem } from "../index-D1ZjgVxn.mjs";
|
|
2
2
|
export { CsvOptions, ExportField, ExportFieldMap, FlatJournalRow, PopulatedAccount, PopulatedJournalEntry, PopulatedJournalItem, buildCsv, escapeCell, exportToCsv, extractAllRows, extractRow, flattenJournalEntries, flattenJournalEntry, getHeaders, quickbooksFieldMap, serializeCsv, universalFieldMap };
|
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-BP-0Ni5W.mjs";
|
|
2
2
|
export { buildCsv, escapeCell, exportToCsv, extractAllRows, extractRow, flattenJournalEntries, flattenJournalEntry, getHeaders, quickbooksFieldMap, serializeCsv, universalFieldMap };
|
|
@@ -1,7 +1,17 @@
|
|
|
1
|
-
import { n as Errors } from "./errors-
|
|
2
|
-
import {
|
|
3
|
-
import { i as extractMainType } from "./categories-DWogBUgQ.mjs";
|
|
1
|
+
import { n as Errors } from "./errors-BmRjW38t.mjs";
|
|
2
|
+
import { i as extractMainType } from "./categories-BkKdv16V.mjs";
|
|
4
3
|
import mongoose from "mongoose";
|
|
4
|
+
//#region src/utils/tenant-guard.ts
|
|
5
|
+
/**
|
|
6
|
+
* Multi-tenant scope guard.
|
|
7
|
+
*
|
|
8
|
+
* Throws when orgField is configured (multi-tenant mode active) but
|
|
9
|
+
* organizationId is missing — preventing unscoped cross-tenant queries.
|
|
10
|
+
*/
|
|
11
|
+
function requireOrgScope(orgField, organizationId) {
|
|
12
|
+
if (orgField && !organizationId) throw Errors.validation("organizationId is required when multi-tenant mode is configured (orgField: \"" + orgField + "\"). Refusing to run unscoped query.");
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
5
15
|
//#region src/reports/aged-balance.ts
|
|
6
16
|
const DEFAULT_BUCKETS = [
|
|
7
17
|
{
|
|
@@ -1650,4 +1660,4 @@ async function reopenFiscalPeriod(opts, params) {
|
|
|
1650
1660
|
}
|
|
1651
1661
|
}
|
|
1652
1662
|
//#endregion
|
|
1653
|
-
export { DEFAULT_BUCKETS as C, isVirtualTaxAccount as S, getDateRange as _, defaultLogger as a, calculateTotal as b, buildRevaluationEntry as c, generateGeneralLedger as d, generateDimensionBreakdown as f, buildItemFilters as g, generateBalanceSheet as h, finalizeSession as i, computeRevaluation as l, generateBudgetVsActual as m, reopenFiscalPeriod as n, generateTrialBalance as o, generateCashFlow as p, acquireSession as r, generateRevaluation as s, closeFiscalPeriod as t, generateIncomeStatement as u, getFiscalYearStart as v, generateAgedBalance as w, computeEndingBalance as x, buildAccountTypeMap as y };
|
|
1663
|
+
export { DEFAULT_BUCKETS as C, isVirtualTaxAccount as S, requireOrgScope as T, getDateRange as _, defaultLogger as a, calculateTotal as b, buildRevaluationEntry as c, generateGeneralLedger as d, generateDimensionBreakdown as f, buildItemFilters as g, generateBalanceSheet as h, finalizeSession as i, computeRevaluation as l, generateBudgetVsActual as m, reopenFiscalPeriod as n, generateTrialBalance as o, generateCashFlow as p, acquireSession as r, generateRevaluation as s, closeFiscalPeriod as t, generateIncomeStatement as u, getFiscalYearStart as v, generateAgedBalance as w, computeEndingBalance as x, buildAccountTypeMap as y };
|
|
@@ -15,6 +15,44 @@ declare function dateLockPlugin(options: DateLockPluginOptions): {
|
|
|
15
15
|
apply(repo: RepositoryInstance): void;
|
|
16
16
|
};
|
|
17
17
|
//#endregion
|
|
18
|
+
//#region src/types/mongokit-augmentation.d.ts
|
|
19
|
+
/**
|
|
20
|
+
* Module augmentation for `@classytic/mongokit`.
|
|
21
|
+
*
|
|
22
|
+
* Ledger's state-transition methods (post, unpost, archive, reverseMark) tag
|
|
23
|
+
* their `repository.update()` call with a `_ledgerInternal` flag so the
|
|
24
|
+
* double-entry immutability guard can distinguish legitimate transitions from
|
|
25
|
+
* arbitrary edits. This file types that flag onto both `RepositoryContext`
|
|
26
|
+
* (what plugins observe) and `SessionOptions` (what callers pass) so consumers
|
|
27
|
+
* and plugin authors get full IntelliSense without casts.
|
|
28
|
+
*
|
|
29
|
+
* This file is side-effect only — importing it anywhere in the package is
|
|
30
|
+
* enough to activate the augmentation. `src/types/index.ts` re-exports it.
|
|
31
|
+
*/
|
|
32
|
+
type LedgerInternalOp = 'post' | 'unpost' | 'archive' | 'reverseMark';
|
|
33
|
+
declare module '@classytic/mongokit' {
|
|
34
|
+
interface RepositoryContext {
|
|
35
|
+
/**
|
|
36
|
+
* Set by ledger's repository methods (post, unpost, archive, reverseMark)
|
|
37
|
+
* to signal a legitimate internal state transition. Plugins observing
|
|
38
|
+
* `before:update` can read this to distinguish from arbitrary edits.
|
|
39
|
+
*
|
|
40
|
+
* External `repository.update()` callers cannot spoof this flag because
|
|
41
|
+
* it is only set by ledger's own repo methods, never surfaced in the
|
|
42
|
+
* public API.
|
|
43
|
+
*/
|
|
44
|
+
_ledgerInternal?: LedgerInternalOp;
|
|
45
|
+
}
|
|
46
|
+
interface SessionOptions {
|
|
47
|
+
/**
|
|
48
|
+
* Ledger-internal flag — see `RepositoryContext._ledgerInternal`.
|
|
49
|
+
* Typed here so ledger's repo methods can pass it without casts.
|
|
50
|
+
* Consumers should never set this directly.
|
|
51
|
+
*/
|
|
52
|
+
_ledgerInternal?: LedgerInternalOp;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
18
56
|
//#region src/plugins/double-entry.plugin.d.ts
|
|
19
57
|
interface DoubleEntryPluginOptions {
|
|
20
58
|
/** Only enforce on posted entries (default: true) */
|