@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.
Files changed (58) hide show
  1. package/dist/server/bodyFields.d.ts +10 -3
  2. package/dist/server/bodyFields.d.ts.map +1 -1
  3. package/dist/server/io.d.ts.map +1 -1
  4. package/dist/server/journal.d.ts +5 -5
  5. package/dist/server/journal.d.ts.map +1 -1
  6. package/dist/server/report.d.ts +2 -2
  7. package/dist/server/report.d.ts.map +1 -1
  8. package/dist/server/router.d.ts.map +1 -1
  9. package/dist/server/service.d.ts +24 -24
  10. package/dist/server/service.d.ts.map +1 -1
  11. package/dist/server/snapshotCache.d.ts.map +1 -1
  12. package/dist/server/timeSeries.d.ts +1 -1
  13. package/dist/server/timeSeries.d.ts.map +1 -1
  14. package/dist/server.cjs +74 -55
  15. package/dist/server.cjs.map +1 -1
  16. package/dist/server.js +74 -55
  17. package/dist/server.js.map +1 -1
  18. package/dist/shared/fiscalYear.d.ts.map +1 -1
  19. package/dist/shared/types.d.ts +14 -14
  20. package/dist/shared/types.d.ts.map +1 -1
  21. package/dist/{shared-xYPtGNvH.js → shared-BZAAF-I4.js} +8 -9
  22. package/dist/shared-BZAAF-I4.js.map +1 -0
  23. package/dist/{shared-KHOWwwiZ.cjs → shared-BpFVwa6Y.cjs} +8 -9
  24. package/dist/shared-BpFVwa6Y.cjs.map +1 -0
  25. package/dist/shared.cjs +1 -1
  26. package/dist/shared.js +1 -1
  27. package/dist/style.css +10 -10
  28. package/dist/vue/View.vue.d.ts.map +1 -1
  29. package/dist/vue/api.d.ts +16 -16
  30. package/dist/vue/api.d.ts.map +1 -1
  31. package/dist/vue/components/AccountEditor.vue.d.ts +2 -2
  32. package/dist/vue/components/AccountRow.vue.d.ts +2 -2
  33. package/dist/vue/components/AccountsList.vue.d.ts +2 -2
  34. package/dist/vue/components/AccountsModal.vue.d.ts +2 -2
  35. package/dist/vue/components/BalanceSheet.vue.d.ts +1 -1
  36. package/dist/vue/components/BookSettings.vue.d.ts +4 -4
  37. package/dist/vue/components/BookSettings.vue.d.ts.map +1 -1
  38. package/dist/vue/components/BookSwitcher.vue.d.ts +3 -3
  39. package/dist/vue/components/DateRangePicker.vue.d.ts +2 -2
  40. package/dist/vue/components/DateRangePicker.vue.d.ts.map +1 -1
  41. package/dist/vue/components/JournalEntryForm.vue.d.ts +4 -4
  42. package/dist/vue/components/JournalEntryForm.vue.d.ts.map +1 -1
  43. package/dist/vue/components/JournalList.vue.d.ts +6 -6
  44. package/dist/vue/components/JournalList.vue.d.ts.map +1 -1
  45. package/dist/vue/components/Ledger.vue.d.ts +3 -3
  46. package/dist/vue/components/Ledger.vue.d.ts.map +1 -1
  47. package/dist/vue/components/NewBookForm.vue.d.ts +2 -2
  48. package/dist/vue/components/OpeningBalancesForm.vue.d.ts +1 -1
  49. package/dist/vue/components/OpeningBalancesForm.vue.d.ts.map +1 -1
  50. package/dist/vue/components/ProfitLoss.vue.d.ts +3 -3
  51. package/dist/vue/components/ProfitLoss.vue.d.ts.map +1 -1
  52. package/dist/vue.cjs +22 -12
  53. package/dist/vue.cjs.map +1 -1
  54. package/dist/vue.js +22 -12
  55. package/dist/vue.js.map +1 -1
  56. package/package.json +5 -5
  57. package/dist/shared-KHOWwwiZ.cjs.map +0 -1
  58. 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-xYPtGNvH.js";
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 sections = [];
841
- for (const type of [
842
- "asset",
843
- "liability",
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
- imbalance: assetTotal - liabEquityTotal
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 periods = await listJournalPeriods(bookId, workspaceRoot);
1187
- if (periods.length === 0 || period < periods[0]) return buildEmptySnapshot(bookId, period, workspaceRoot);
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 > periods[0]) priorBalances = (await getOrBuildSnapshot(bookId, previousPeriod(period), workspaceRoot)).balances;
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
- if (entries.length === 0) return "Posted 0 journal entries.";
2221
- if (entries.length === 1) {
2222
- const [entry] = entries;
2223
- const idFragment = entry.id ? ` (id: ${entry.id})` : "";
2224
- return `Posted a journal entry on ${entry.date ?? "the requested date"}${idFragment}.`;
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 = Object.hasOwn(MESSAGE_BUILDERS, action) ? MESSAGE_BUILDERS[action](fields) : void 0;
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
- if (!Object.hasOwn(ACTION_HANDLERS, action)) throw new AccountingError(400, `unknown action ${JSON.stringify(action)}`);
2259
- const handler = ACTION_HANDLERS[action];
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: {