@mulmoclaude/accounting-plugin 1.2.2 → 2.0.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/dist/server/bodyFields.d.ts +10 -3
- package/dist/server/bodyFields.d.ts.map +1 -1
- package/dist/server/io.d.ts.map +1 -1
- package/dist/server/journal.d.ts +5 -5
- package/dist/server/journal.d.ts.map +1 -1
- package/dist/server/report.d.ts +2 -2
- package/dist/server/report.d.ts.map +1 -1
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/service.d.ts +24 -24
- package/dist/server/service.d.ts.map +1 -1
- package/dist/server/snapshotCache.d.ts.map +1 -1
- package/dist/server/timeSeries.d.ts +1 -1
- package/dist/server/timeSeries.d.ts.map +1 -1
- package/dist/server.cjs +74 -55
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +74 -55
- package/dist/server.js.map +1 -1
- package/dist/shared/fiscalYear.d.ts.map +1 -1
- package/dist/shared/types.d.ts +14 -14
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/{shared-xYPtGNvH.js → shared-BZAAF-I4.js} +8 -9
- package/dist/shared-BZAAF-I4.js.map +1 -0
- package/dist/{shared-KHOWwwiZ.cjs → shared-BpFVwa6Y.cjs} +8 -9
- package/dist/shared-BpFVwa6Y.cjs.map +1 -0
- package/dist/shared.cjs +1 -1
- package/dist/shared.js +1 -1
- package/dist/style.css +10 -10
- package/dist/vue/View.vue.d.ts.map +1 -1
- package/dist/vue/api.d.ts +16 -16
- package/dist/vue/api.d.ts.map +1 -1
- package/dist/vue/components/AccountEditor.vue.d.ts +2 -2
- package/dist/vue/components/AccountRow.vue.d.ts +2 -2
- package/dist/vue/components/AccountsList.vue.d.ts +2 -2
- package/dist/vue/components/AccountsModal.vue.d.ts +2 -2
- package/dist/vue/components/BalanceSheet.vue.d.ts +1 -1
- package/dist/vue/components/BookSettings.vue.d.ts +4 -4
- package/dist/vue/components/BookSettings.vue.d.ts.map +1 -1
- package/dist/vue/components/BookSwitcher.vue.d.ts +3 -3
- package/dist/vue/components/DateRangePicker.vue.d.ts +2 -2
- package/dist/vue/components/DateRangePicker.vue.d.ts.map +1 -1
- package/dist/vue/components/JournalEntryForm.vue.d.ts +4 -4
- package/dist/vue/components/JournalEntryForm.vue.d.ts.map +1 -1
- package/dist/vue/components/JournalList.vue.d.ts +6 -6
- package/dist/vue/components/JournalList.vue.d.ts.map +1 -1
- package/dist/vue/components/Ledger.vue.d.ts +3 -3
- package/dist/vue/components/Ledger.vue.d.ts.map +1 -1
- package/dist/vue/components/NewBookForm.vue.d.ts +2 -2
- package/dist/vue/components/OpeningBalancesForm.vue.d.ts +1 -1
- package/dist/vue/components/OpeningBalancesForm.vue.d.ts.map +1 -1
- package/dist/vue/components/ProfitLoss.vue.d.ts +3 -3
- package/dist/vue/components/ProfitLoss.vue.d.ts.map +1 -1
- package/dist/vue.cjs +22 -12
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.js +22 -12
- package/dist/vue.js.map +1 -1
- package/package.json +5 -5
- package/dist/shared-KHOWwwiZ.cjs.map +0 -1
- package/dist/shared-xYPtGNvH.js.map +0 -1
package/dist/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as resolveFiscalYearEnd, B as BALANCE_SHEET_ACCOUNT_TYPES, D as isFiscalYearEnd, F as isUnknownArray, H as ACCOUNTING_API, I as ACCOUNTING_BOOKS_CHANNEL, L as BOOK_EVENT_KINDS, M as errorMessage, N as hasStringProp, P as isRecord, R as bookChannel, S as FISCAL_YEAR_END_MONTHS, T as fiscalYearEndMonth, U as ACCOUNTING_ACTIONS, V as JOURNAL_ENTRY_KINDS, _ as SUPPORTED_COUNTRY_CODES, j as ACCOUNTING_DIRS, n as TIME_SERIES_METRICS, t as TIME_SERIES_GRANULARITIES, y as isSupportedCountryCode, z as ACCOUNT_TYPES } from "./shared-
|
|
1
|
+
import { A as resolveFiscalYearEnd, B as BALANCE_SHEET_ACCOUNT_TYPES, D as isFiscalYearEnd, F as isUnknownArray, H as ACCOUNTING_API, I as ACCOUNTING_BOOKS_CHANNEL, L as BOOK_EVENT_KINDS, M as errorMessage, N as hasStringProp, P as isRecord, R as bookChannel, S as FISCAL_YEAR_END_MONTHS, T as fiscalYearEndMonth, U as ACCOUNTING_ACTIONS, V as JOURNAL_ENTRY_KINDS, _ as SUPPORTED_COUNTRY_CODES, j as ACCOUNTING_DIRS, n as TIME_SERIES_METRICS, t as TIME_SERIES_GRANULARITIES, y as isSupportedCountryCode, z as ACCOUNT_TYPES } from "./shared-BZAAF-I4.js";
|
|
2
2
|
import { Router } from "express";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { promises } from "node:fs";
|
|
@@ -7,18 +7,36 @@ import { isEnoent, writeJsonAtomic } from "@mulmoclaude/core/files";
|
|
|
7
7
|
//#region src/server/bodyFields.ts
|
|
8
8
|
var optionalString = (value) => typeof value === "string" ? value : void 0;
|
|
9
9
|
var optionalRecord = (value) => isRecord(value) ? value : void 0;
|
|
10
|
+
var YEAR_MONTH_RE = /^\d{4}-(0[1-9]|1[0-2])$/;
|
|
11
|
+
var YEAR_MONTH_DAY_RE = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/;
|
|
12
|
+
/** `YYYY-MM-DD` that names a day the calendar actually has. The regex alone
|
|
13
|
+
* admits `2026-02-30`, and `Date.UTC` rolls that forward to March instead of
|
|
14
|
+
* refusing it, so the round trip is what says the date is real. */
|
|
15
|
+
var isCalendarDate = (value) => {
|
|
16
|
+
if (!YEAR_MONTH_DAY_RE.test(value)) return false;
|
|
17
|
+
const [year, month, day] = value.split("-").map(Number);
|
|
18
|
+
if (year === void 0 || month === void 0 || day === void 0) return false;
|
|
19
|
+
return new Date(Date.UTC(year, month - 1, day)).getUTCDate() === day;
|
|
20
|
+
};
|
|
10
21
|
/** Rebuilt field by field rather than narrowed with a predicate, so the
|
|
11
22
|
* returned object is one this function actually proved. A half-formed
|
|
12
23
|
* `{ kind: "month" }` reads as absent and the caller raises its own
|
|
13
24
|
* "period is required" — previously it reached the report builders and
|
|
14
|
-
* produced an `undefined-01` date.
|
|
25
|
+
* produced an `undefined-01` date.
|
|
26
|
+
*
|
|
27
|
+
* The FORMAT is checked here too, not just the type: `typeof === "string"`
|
|
28
|
+
* let `{ kind: "month", period: "banana" }` through, which came back as a
|
|
29
|
+
* balance sheet dated `"banana-NaN"` and made the snapshot cache write
|
|
30
|
+
* `banana.json` / `0NaN-NaN.json` into the book (#2765). Malformed reads as
|
|
31
|
+
* absent so it lands on the caller's existing 400, which already spells out
|
|
32
|
+
* both accepted shapes for the LLM to repair its payload from. */
|
|
15
33
|
var optionalReportPeriod = (value) => {
|
|
16
34
|
const period = optionalRecord(value);
|
|
17
|
-
if (period?.kind === "month" && typeof period.period === "string") return {
|
|
35
|
+
if (period?.kind === "month" && typeof period.period === "string" && YEAR_MONTH_RE.test(period.period)) return {
|
|
18
36
|
kind: "month",
|
|
19
37
|
period: period.period
|
|
20
38
|
};
|
|
21
|
-
if (period?.kind === "range" && typeof period.from === "string" && typeof period.to === "string") return {
|
|
39
|
+
if (period?.kind === "range" && typeof period.from === "string" && typeof period.to === "string" && isCalendarDate(period.from) && isCalendarDate(period.to)) return {
|
|
22
40
|
kind: "range",
|
|
23
41
|
from: period.from,
|
|
24
42
|
to: period.to
|
|
@@ -94,6 +112,7 @@ function localDateString(now = /* @__PURE__ */ new Date()) {
|
|
|
94
112
|
function isValidCalendarDate(date) {
|
|
95
113
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) return false;
|
|
96
114
|
const [year, month, day] = date.split("-").map((segment) => parseInt(segment, 10));
|
|
115
|
+
if (year === void 0 || month === void 0 || day === void 0) return false;
|
|
97
116
|
const parsed = new Date(Date.UTC(year, month - 1, day));
|
|
98
117
|
return parsed.getUTCFullYear() === year && parsed.getUTCMonth() === month - 1 && parsed.getUTCDate() === day;
|
|
99
118
|
}
|
|
@@ -598,6 +617,7 @@ async function invalidateSnapshotsFrom(bookId, fromPeriod, workspaceRoot) {
|
|
|
598
617
|
const match = /^(\d{4}-\d{2})\.json$/.exec(name);
|
|
599
618
|
if (!match) continue;
|
|
600
619
|
const [, period] = match;
|
|
620
|
+
if (period === void 0) continue;
|
|
601
621
|
if (period >= fromPeriod) {
|
|
602
622
|
await promises.rm(path.join(snapshotsDir(bookId, workspaceRoot), name), { force: true });
|
|
603
623
|
removed.push(period);
|
|
@@ -834,48 +854,38 @@ function computeCurrentEarnings(accounts, balanceByCode) {
|
|
|
834
854
|
}
|
|
835
855
|
return earnings;
|
|
836
856
|
}
|
|
857
|
+
function buildBalanceSheetSection(type, accounts, balanceByCode, currentEarnings) {
|
|
858
|
+
const rows = accounts.filter((account) => account.type === type).map((account) => ({
|
|
859
|
+
accountCode: account.code,
|
|
860
|
+
accountName: account.name,
|
|
861
|
+
balance: naturalSign$1(type, balanceByCode.get(account.code) ?? 0)
|
|
862
|
+
})).filter((row) => Math.abs(row.balance) > ZERO_TOLERANCE);
|
|
863
|
+
if (type === "equity" && Math.abs(currentEarnings) > ZERO_TOLERANCE) rows.push({
|
|
864
|
+
accountCode: CURRENT_EARNINGS_ACCOUNT_CODE,
|
|
865
|
+
accountName: "Current period earnings",
|
|
866
|
+
balance: currentEarnings
|
|
867
|
+
});
|
|
868
|
+
return {
|
|
869
|
+
type,
|
|
870
|
+
rows,
|
|
871
|
+
total: rows.reduce((sum, row) => sum + row.balance, 0)
|
|
872
|
+
};
|
|
873
|
+
}
|
|
837
874
|
function buildBalanceSheet(input) {
|
|
838
875
|
const balanceByCode = new Map(input.balances.map((row) => [row.accountCode, row.netDebit]));
|
|
839
876
|
const currentEarnings = computeCurrentEarnings(input.accounts, balanceByCode);
|
|
840
|
-
const
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
"equity"
|
|
845
|
-
]) {
|
|
846
|
-
const rows = [];
|
|
847
|
-
let total = 0;
|
|
848
|
-
for (const account of input.accounts) {
|
|
849
|
-
if (account.type !== type) continue;
|
|
850
|
-
const presented = naturalSign$1(type, balanceByCode.get(account.code) ?? 0);
|
|
851
|
-
if (Math.abs(presented) <= ZERO_TOLERANCE) continue;
|
|
852
|
-
rows.push({
|
|
853
|
-
accountCode: account.code,
|
|
854
|
-
accountName: account.name,
|
|
855
|
-
balance: presented
|
|
856
|
-
});
|
|
857
|
-
total += presented;
|
|
858
|
-
}
|
|
859
|
-
if (type === "equity" && Math.abs(currentEarnings) > ZERO_TOLERANCE) {
|
|
860
|
-
rows.push({
|
|
861
|
-
accountCode: CURRENT_EARNINGS_ACCOUNT_CODE,
|
|
862
|
-
accountName: "Current period earnings",
|
|
863
|
-
balance: currentEarnings
|
|
864
|
-
});
|
|
865
|
-
total += currentEarnings;
|
|
866
|
-
}
|
|
867
|
-
sections.push({
|
|
868
|
-
type,
|
|
869
|
-
rows,
|
|
870
|
-
total
|
|
871
|
-
});
|
|
872
|
-
}
|
|
873
|
-
const assetTotal = sections[0].total;
|
|
874
|
-
const liabEquityTotal = sections[1].total + sections[2].total;
|
|
877
|
+
const section = (type) => buildBalanceSheetSection(type, input.accounts, balanceByCode, currentEarnings);
|
|
878
|
+
const assets = section("asset");
|
|
879
|
+
const liabilities = section("liability");
|
|
880
|
+
const equity = section("equity");
|
|
875
881
|
return {
|
|
876
882
|
asOf: input.asOf,
|
|
877
|
-
sections
|
|
878
|
-
|
|
883
|
+
sections: [
|
|
884
|
+
assets,
|
|
885
|
+
liabilities,
|
|
886
|
+
equity
|
|
887
|
+
],
|
|
888
|
+
imbalance: assets.total - (liabilities.total + equity.total)
|
|
879
889
|
};
|
|
880
890
|
}
|
|
881
891
|
function buildProfitLoss(input) {
|
|
@@ -977,7 +987,7 @@ function fmtYmd(year, month, day) {
|
|
|
977
987
|
return `${year}-${pad2(month)}-${pad2(day)}`;
|
|
978
988
|
}
|
|
979
989
|
function parseYmd(value) {
|
|
980
|
-
const [year, month, day] = value.split("-").map((segment) => parseInt(segment, 10));
|
|
990
|
+
const [year = NaN, month = NaN, day = NaN] = value.split("-").map((segment) => parseInt(segment, 10));
|
|
981
991
|
return {
|
|
982
992
|
year,
|
|
983
993
|
month,
|
|
@@ -1157,7 +1167,7 @@ function publishBooksChanged() {
|
|
|
1157
1167
|
//#endregion
|
|
1158
1168
|
//#region src/server/snapshotCache.ts
|
|
1159
1169
|
function previousPeriod(period) {
|
|
1160
|
-
const [year, month] = period.split("-").map((segment) => parseInt(segment, 10));
|
|
1170
|
+
const [year = NaN, month = NaN] = period.split("-").map((segment) => parseInt(segment, 10));
|
|
1161
1171
|
if (month === 1) return `${(year - 1).toString().padStart(4, "0")}-12`;
|
|
1162
1172
|
return `${year.toString().padStart(4, "0")}-${(month - 1).toString().padStart(2, "0")}`;
|
|
1163
1173
|
}
|
|
@@ -1183,12 +1193,12 @@ async function buildEmptySnapshot(bookId, period, workspaceRoot) {
|
|
|
1183
1193
|
async function getOrBuildSnapshot(bookId, period, workspaceRoot) {
|
|
1184
1194
|
const cached = await readSnapshot(bookId, period, workspaceRoot);
|
|
1185
1195
|
if (cached) return cached;
|
|
1186
|
-
const
|
|
1187
|
-
if (
|
|
1196
|
+
const [earliestPeriod] = await listJournalPeriods(bookId, workspaceRoot);
|
|
1197
|
+
if (earliestPeriod === void 0 || period < earliestPeriod) return buildEmptySnapshot(bookId, period, workspaceRoot);
|
|
1188
1198
|
const { entries } = await readJournalMonth(bookId, period, workspaceRoot);
|
|
1189
1199
|
const monthDelta = aggregateBalances(entries);
|
|
1190
1200
|
let priorBalances = [];
|
|
1191
|
-
if (period >
|
|
1201
|
+
if (period > earliestPeriod) priorBalances = (await getOrBuildSnapshot(bookId, previousPeriod(period), workspaceRoot)).balances;
|
|
1192
1202
|
const snap = {
|
|
1193
1203
|
period,
|
|
1194
1204
|
balances: mergeBalances(priorBalances, monthDelta),
|
|
@@ -1923,7 +1933,7 @@ async function setOpeningBalances(input, workspaceRoot) {
|
|
|
1923
1933
|
}
|
|
1924
1934
|
function endDateOfPeriod(period) {
|
|
1925
1935
|
if (period.kind === "month") {
|
|
1926
|
-
const [year, month] = period.period.split("-").map((segment) => parseInt(segment, 10));
|
|
1936
|
+
const [year = NaN, month = NaN] = period.period.split("-").map((segment) => parseInt(segment, 10));
|
|
1927
1937
|
const last = new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
1928
1938
|
return `${period.period}-${String(last).padStart(2, "0")}`;
|
|
1929
1939
|
}
|
|
@@ -2092,6 +2102,7 @@ async function handleGetReport(rest) {
|
|
|
2092
2102
|
const periodInput = optionalReportPeriod(rest.period);
|
|
2093
2103
|
const bookId = optionalString(rest.bookId);
|
|
2094
2104
|
const periodRequired = (label) => new AccountingError(400, `${label}: period is required — { kind: "month", period: "YYYY-MM" } or { kind: "range", from: "YYYY-MM-DD", to: "YYYY-MM-DD" }`);
|
|
2105
|
+
if (rest.period !== void 0 && rest.period !== null && !periodInput) throw periodRequired(`getReport ${kind || "(no kind)"}`);
|
|
2095
2106
|
if (kind === "balance") {
|
|
2096
2107
|
if (!periodInput) throw periodRequired("getReport balance");
|
|
2097
2108
|
return getBalanceSheetReport({
|
|
@@ -2217,11 +2228,11 @@ var MESSAGE_BUILDERS = {
|
|
|
2217
2228
|
},
|
|
2218
2229
|
[ACCOUNTING_ACTIONS.addEntries]: (fields) => {
|
|
2219
2230
|
const entries = (isUnknownArray(fields.entries) ? fields.entries : []).map(describeEntry);
|
|
2220
|
-
|
|
2221
|
-
if (
|
|
2222
|
-
|
|
2223
|
-
const idFragment =
|
|
2224
|
-
return `Posted a journal entry on ${
|
|
2231
|
+
const [firstEntry, ...furtherEntries] = entries;
|
|
2232
|
+
if (!firstEntry) return "Posted 0 journal entries.";
|
|
2233
|
+
if (furtherEntries.length === 0) {
|
|
2234
|
+
const idFragment = firstEntry.id ? ` (id: ${firstEntry.id})` : "";
|
|
2235
|
+
return `Posted a journal entry on ${firstEntry.date ?? "the requested date"}${idFragment}.`;
|
|
2225
2236
|
}
|
|
2226
2237
|
const summary = entries.map((entry) => `${entry.date ?? "?"} (id: ${entry.id ?? "?"})`).join(", ");
|
|
2227
2238
|
return `Posted ${entries.length} journal entries: ${summary}.`;
|
|
@@ -2249,14 +2260,22 @@ var MESSAGE_BUILDERS = {
|
|
|
2249
2260
|
return `Updated ${bookName ? JSON.stringify(bookName) : "the book"}${country ? ` (country: ${country})` : ""}.`;
|
|
2250
2261
|
}
|
|
2251
2262
|
};
|
|
2263
|
+
/** Read a record entry under a user/LLM-controlled key. The
|
|
2264
|
+
* `Object.hasOwn` gate is load-bearing: a bare `record[key]` resolves
|
|
2265
|
+
* inherited prototype members, so a crafted action ("constructor",
|
|
2266
|
+
* "toString") would dispatch to an unexpected target instead of
|
|
2267
|
+
* reading as absent. */
|
|
2268
|
+
function ownEntry(record, key) {
|
|
2269
|
+
return Object.hasOwn(record, key) ? record[key] : void 0;
|
|
2270
|
+
}
|
|
2252
2271
|
function previewMessage(action, fields) {
|
|
2253
|
-
const head =
|
|
2272
|
+
const head = ownEntry(MESSAGE_BUILDERS, action)?.(fields);
|
|
2254
2273
|
return head ? `${head} ${VIEW_VISIBLE_TRAILER}` : VIEW_VISIBLE_TRAILER;
|
|
2255
2274
|
}
|
|
2256
2275
|
async function dispatch(body) {
|
|
2257
2276
|
const { action, ...rest } = body;
|
|
2258
|
-
|
|
2259
|
-
|
|
2277
|
+
const handler = ownEntry(ACTION_HANDLERS, action);
|
|
2278
|
+
if (!handler) throw new AccountingError(400, `unknown action ${JSON.stringify(action)}`);
|
|
2260
2279
|
const result = await handler(rest);
|
|
2261
2280
|
const handlerFields = isRecord(result) ? result : { value: result };
|
|
2262
2281
|
const dataField = PREVIEW_ACTIONS.has(action) ? { data: {
|