@classytic/ledger 0.5.0 → 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 +2 -0
- package/dist/{date-lock.plugin-B2Jy0ukX.mjs → date-lock.plugin-B6WyvqNG.mjs} +2 -1
- package/dist/{idempotency.plugin-CK7LHnBn.d.mts → idempotency.plugin-WcQLZU9n.d.mts} +38 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +75 -21
- package/dist/plugins/index.d.mts +1 -1
- package/dist/plugins/index.mjs +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Embeddable double-entry accounting engine for MongoDB. Integer-cents arithmetic, plugin-based, country-agnostic, multi-tenant at every layer. Framework-agnostic — works with Express, Fastify, Nest, Arc, or any plain Mongoose app.
|
|
4
4
|
|
|
5
|
+
> **0.5.1** — Critical plugin-pipeline fixes. `post()`, `unpost()`, `archive()`, and the `reverse()` mark-as-reversed step now route through `repository.update()` so `before:update` / `after:update` hooks fire on every state transition (period locks, audit, observability are no longer silently bypassed). `reverse()` and `duplicate()` propagate every consumer-defined top-level field (`departmentId`, `projectId`, `sourceRef`, `branchTag`, `organizationId`, …). New typed `_ledgerInternal` flag on `RepositoryContext` lets plugin authors observe internal transitions without casts. See [CHANGELOG.md](CHANGELOG.md).
|
|
6
|
+
|
|
5
7
|
## Install
|
|
6
8
|
|
|
7
9
|
```bash
|
|
@@ -100,7 +100,8 @@ function doubleEntryPlugin(options = {}) {
|
|
|
100
100
|
const validateUpdate = async (context) => {
|
|
101
101
|
const data = context.data;
|
|
102
102
|
if (!data) return;
|
|
103
|
-
|
|
103
|
+
const internalOp = context._ledgerInternal;
|
|
104
|
+
if (JournalEntryModel && !internalOp) {
|
|
104
105
|
const id = context.id;
|
|
105
106
|
if (id) {
|
|
106
107
|
if ((await JournalEntryModel.findById(id).select("state").session(context.session ?? null).lean())?.state === "posted") {
|
|
@@ -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) */
|
package/dist/index.d.mts
CHANGED
|
@@ -4,7 +4,7 @@ import { a as TaxReportLine, i as TaxCodesByRegion, n as CountryPackInput, o as
|
|
|
4
4
|
import { _ as PopulatedJournalEntry, a as exportToCsv, h as FlatJournalRow, i as quickbooksFieldMap, m as ExportFieldMap, p as ExportField, r as universalFieldMap, t as flattenJournalEntries } from "./index-D1ZjgVxn.mjs";
|
|
5
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
6
|
import { $ as AgedBucketConfig, A as BudgetVsActualReport, B as IncomeStatementReport, C as DimensionBreakdownReport, D as generateCashFlow, F as BalanceSheetReport, G as TaxReport, H as ReportAccount, I as CashFlowReport, J as TrialBalanceRow, K as TaxReturnSummary, L as CashFlowSection, M as generateBudgetVsActual, O as BudgetVsActualOptions, P as generateBalanceSheet, Q as AgedBalanceRow, R as GeneralLedgerAccount, S as DimensionBreakdownParams, T as generateDimensionBreakdown, U as ReportCategory, V as LedgerEntry, W as ReportGroup, X as AgedBalanceParams, Y as AgedBalanceOptions, Z as AgedBalanceReport, a as RevaluationReport, b as reopenFiscalPeriod, c as RevaluationRate, d as computeRevaluation, et as DEFAULT_BUCKETS, h as generateGeneralLedger, i as RevaluationParams, j as BudgetVsActualRow, k as BudgetVsActualParams, l as RevaluationResult, n as generateTrialBalance, nt as Logger, o as generateRevaluation, p as generateIncomeStatement, q as TrialBalanceReport, r as RevaluationOptions, rt as defaultLogger, s as AccountForeignBalance, tt as generateAgedBalance, u as buildRevaluationEntry, w as DimensionBreakdownRow, x as DimensionBreakdownOptions, y as closeFiscalPeriod, z as GeneralLedgerReport } from "./trial-balance-BZ7yOOFD.mjs";
|
|
7
|
-
import { c as dateLockPlugin, i as fiscalLockPlugin, n as idempotencyPlugin, o as doubleEntryPlugin } from "./idempotency.plugin-
|
|
7
|
+
import { c as dateLockPlugin, i as fiscalLockPlugin, n as idempotencyPlugin, o as doubleEntryPlugin } from "./idempotency.plugin-WcQLZU9n.mjs";
|
|
8
8
|
import { ClientSession, Connection, Model } from "mongoose";
|
|
9
9
|
import { PaginationConfig, PluginType, Repository } from "@classytic/mongokit";
|
|
10
10
|
|
package/dist/index.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { Money, add, allocate, format, formatPlain, fromDecimal, multiply, parse
|
|
|
3
3
|
import { n as Errors, t as AccountingError } from "./errors-BmRjW38t.mjs";
|
|
4
4
|
import { C as DEFAULT_BUCKETS, S as isVirtualTaxAccount, T as requireOrgScope, _ as getDateRange, a as defaultLogger, b as calculateTotal, c as buildRevaluationEntry, d as generateGeneralLedger, f as generateDimensionBreakdown, g as buildItemFilters, h as generateBalanceSheet, i as finalizeSession, l as computeRevaluation, m as generateBudgetVsActual, n as reopenFiscalPeriod, o as generateTrialBalance, p as generateCashFlow, r as acquireSession, s as generateRevaluation, t as closeFiscalPeriod, u as generateIncomeStatement, v as getFiscalYearStart, w as generateAgedBalance, x as computeEndingBalance, y as buildAccountTypeMap } from "./fiscal-close-Dk3yRT9i.mjs";
|
|
5
5
|
import { c as getNormalBalance, d as isValidCategory, l as isBalanceSheet, n as CATEGORY_KEYS, t as CATEGORIES, u as isIncomeStatement } from "./categories-BkKdv16V.mjs";
|
|
6
|
-
import { i as doubleEntryPlugin, n as idempotencyPlugin, r as fiscalLockPlugin, t as dateLockPlugin } from "./date-lock.plugin-
|
|
6
|
+
import { i as doubleEntryPlugin, n as idempotencyPlugin, r as fiscalLockPlugin, t as dateLockPlugin } from "./date-lock.plugin-B6WyvqNG.mjs";
|
|
7
7
|
import { defineCountryPack } from "./country/index.mjs";
|
|
8
8
|
import { a as exportToCsv, i as quickbooksFieldMap, r as universalFieldMap, t as flattenJournalEntries } from "./exports-BP-0Ni5W.mjs";
|
|
9
9
|
import mongoose, { Schema } from "mongoose";
|
|
@@ -927,7 +927,43 @@ const ITEM_CORE_KEYS = new Set([
|
|
|
927
927
|
function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, strictness) {
|
|
928
928
|
const getByQuery = repository.getByQuery.bind(repository);
|
|
929
929
|
const create = repository.create.bind(repository);
|
|
930
|
+
const update = repository.update.bind(repository);
|
|
930
931
|
const withTransaction = repository.withTransaction.bind(repository);
|
|
932
|
+
const RESERVED_TOPLEVEL = new Set([
|
|
933
|
+
"_id",
|
|
934
|
+
"__v",
|
|
935
|
+
"id",
|
|
936
|
+
"journalType",
|
|
937
|
+
"state",
|
|
938
|
+
"date",
|
|
939
|
+
"label",
|
|
940
|
+
"journalItems",
|
|
941
|
+
"totalDebit",
|
|
942
|
+
"totalCredit",
|
|
943
|
+
"reversalOf",
|
|
944
|
+
"reversedBy",
|
|
945
|
+
"reversedByUser",
|
|
946
|
+
"reversed",
|
|
947
|
+
"stateChangedAt",
|
|
948
|
+
"createdAt",
|
|
949
|
+
"updatedAt",
|
|
950
|
+
"referenceNumber",
|
|
951
|
+
"idempotencyKey",
|
|
952
|
+
"postedBy",
|
|
953
|
+
"approvedBy",
|
|
954
|
+
"approvedAt"
|
|
955
|
+
]);
|
|
956
|
+
/** Copy non-reserved top-level fields from `source` onto `target`. */
|
|
957
|
+
function copyExtraTopLevel(source, target) {
|
|
958
|
+
const obj = typeof source.toObject === "function" ? source.toObject() : source;
|
|
959
|
+
for (const key of Object.keys(obj)) {
|
|
960
|
+
if (RESERVED_TOPLEVEL.has(key)) continue;
|
|
961
|
+
if (key in target) continue;
|
|
962
|
+
const value = obj[key];
|
|
963
|
+
if (value === void 0 || value === null) continue;
|
|
964
|
+
target[key] = value;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
931
967
|
/** Build a tenant-scoped query for a single entry by ID (injection-safe) */
|
|
932
968
|
function buildQuery(id, orgId) {
|
|
933
969
|
validateScalarId(id, "entry ID");
|
|
@@ -993,11 +1029,16 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
|
|
|
993
1029
|
const totalDebit = entry.journalItems.reduce((s, i) => s + (i.debit || 0), 0);
|
|
994
1030
|
const totalCredit = entry.journalItems.reduce((s, i) => s + (i.credit || 0), 0);
|
|
995
1031
|
if (totalDebit !== totalCredit) throw Errors.validation(`Entry is not balanced. Debit: ${totalDebit}, Credit: ${totalCredit}`);
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1032
|
+
const patch = {
|
|
1033
|
+
state: "posted",
|
|
1034
|
+
stateChangedAt: /* @__PURE__ */ new Date()
|
|
1035
|
+
};
|
|
1036
|
+
if (options.actorId) patch.postedBy = options.actorId;
|
|
1037
|
+
const updateOptions = {
|
|
1038
|
+
_ledgerInternal: "post",
|
|
1039
|
+
...options.session ? { session: options.session } : {}
|
|
1040
|
+
};
|
|
1041
|
+
return await update(entry._id, patch, updateOptions) ?? entry;
|
|
1001
1042
|
};
|
|
1002
1043
|
/**
|
|
1003
1044
|
* Unpost an entry (posted → draft).
|
|
@@ -1012,10 +1053,14 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
|
|
|
1012
1053
|
if (!entry) throw Errors.notFound("Entry not found");
|
|
1013
1054
|
if (entry.state !== "posted") throw Errors.validation("Only posted entries can be unposted");
|
|
1014
1055
|
if (entry.reversed) throw Errors.validation("Cannot unpost a reversed entry. The reversal entry is still posted and linked to this entry. Reverse the reversal entry first, or create a new correcting entry instead.");
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1056
|
+
const updateOptions = {
|
|
1057
|
+
_ledgerInternal: "unpost",
|
|
1058
|
+
...options.session ? { session: options.session } : {}
|
|
1059
|
+
};
|
|
1060
|
+
return await update(entry._id, {
|
|
1061
|
+
state: "draft",
|
|
1062
|
+
stateChangedAt: /* @__PURE__ */ new Date()
|
|
1063
|
+
}, updateOptions) ?? entry;
|
|
1019
1064
|
};
|
|
1020
1065
|
/**
|
|
1021
1066
|
* Archive a draft entry (draft → archived).
|
|
@@ -1028,10 +1073,14 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
|
|
|
1028
1073
|
const entry = await findEntry(buildQuery(id, orgId), { session: options.session });
|
|
1029
1074
|
if (!entry) throw Errors.notFound("Entry not found");
|
|
1030
1075
|
if (entry.state !== "draft") throw Errors.validation("Only draft entries can be archived");
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1076
|
+
const updateOptions = {
|
|
1077
|
+
_ledgerInternal: "archive",
|
|
1078
|
+
...options.session ? { session: options.session } : {}
|
|
1079
|
+
};
|
|
1080
|
+
return await update(entry._id, {
|
|
1081
|
+
state: "archived",
|
|
1082
|
+
stateChangedAt: /* @__PURE__ */ new Date()
|
|
1083
|
+
}, updateOptions) ?? entry;
|
|
1035
1084
|
};
|
|
1036
1085
|
/**
|
|
1037
1086
|
* Duplicate an entry as a new draft.
|
|
@@ -1061,7 +1110,7 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
|
|
|
1061
1110
|
};
|
|
1062
1111
|
})
|
|
1063
1112
|
};
|
|
1064
|
-
|
|
1113
|
+
copyExtraTopLevel(entry, duplicateData);
|
|
1065
1114
|
return await create(duplicateData, options.session ? { session: options.session } : {});
|
|
1066
1115
|
};
|
|
1067
1116
|
/**
|
|
@@ -1113,15 +1162,20 @@ function wireJournalEntryMethods(repository, _JournalEntryModel, orgField, stric
|
|
|
1113
1162
|
reversalOf: entry._id,
|
|
1114
1163
|
stateChangedAt: /* @__PURE__ */ new Date()
|
|
1115
1164
|
};
|
|
1116
|
-
|
|
1165
|
+
copyExtraTopLevel(entry, reversalData);
|
|
1117
1166
|
if (options.actorId) reversalData.postedBy = options.actorId;
|
|
1118
1167
|
const reversalEntry = await create(reversalData, session ? { session } : {});
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1168
|
+
const markPatch = {
|
|
1169
|
+
reversed: true,
|
|
1170
|
+
reversedBy: reversalEntry._id
|
|
1171
|
+
};
|
|
1172
|
+
if (options.actorId) markPatch.reversedByUser = options.actorId;
|
|
1173
|
+
const markOptions = {
|
|
1174
|
+
_ledgerInternal: "reverseMark",
|
|
1175
|
+
...session ? { session } : {}
|
|
1176
|
+
};
|
|
1123
1177
|
return {
|
|
1124
|
-
original: entry,
|
|
1178
|
+
original: await update(entry._id, markPatch, markOptions) ?? entry,
|
|
1125
1179
|
reversal: reversalEntry
|
|
1126
1180
|
};
|
|
1127
1181
|
};
|
package/dist/plugins/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as DoubleEntryPluginOptions, c as dateLockPlugin, i as fiscalLockPlugin, n as idempotencyPlugin, o as doubleEntryPlugin, r as FiscalLockPluginOptions, s as DateLockPluginOptions, t as IdempotencyPluginOptions } from "../idempotency.plugin-
|
|
1
|
+
import { a as DoubleEntryPluginOptions, c as dateLockPlugin, i as fiscalLockPlugin, n as idempotencyPlugin, o as doubleEntryPlugin, r as FiscalLockPluginOptions, s as DateLockPluginOptions, t as IdempotencyPluginOptions } from "../idempotency.plugin-WcQLZU9n.mjs";
|
|
2
2
|
import { RepositoryInstance } from "@classytic/mongokit";
|
|
3
3
|
|
|
4
4
|
//#region src/utils/tax-hooks.d.ts
|
package/dist/plugins/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as doubleEntryPlugin, n as idempotencyPlugin, r as fiscalLockPlugin, t as dateLockPlugin } from "../date-lock.plugin-
|
|
1
|
+
import { i as doubleEntryPlugin, n as idempotencyPlugin, r as fiscalLockPlugin, t as dateLockPlugin } from "../date-lock.plugin-B6WyvqNG.mjs";
|
|
2
2
|
//#region src/utils/tax-hooks.ts
|
|
3
3
|
/**
|
|
4
4
|
* Apply a tax hook to journal items.
|