@mulmoclaude/accounting-plugin 0.2.0 → 0.3.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.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ACCOUNTING_ACTIONS, ACCOUNTING_API, ACCOUNTING_BOOKS_CHANNEL, ACCOUNTING_DIRS, BOOK_EVENT_KINDS, FISCAL_YEAR_ENDS, SUPPORTED_COUNTRY_CODES, TIME_SERIES_GRANULARITIES, TIME_SERIES_METRICS, bookChannel, errorMessage, fiscalYearEndMonth, isFiscalYearEnd, isSupportedCountryCode } from "./shared.js";
1
+ import { ACCOUNTING_ACTIONS, ACCOUNTING_API, ACCOUNTING_BOOKS_CHANNEL, ACCOUNTING_DIRS, BOOK_EVENT_KINDS, FISCAL_YEAR_END_MONTHS, SUPPORTED_COUNTRY_CODES, TIME_SERIES_GRANULARITIES, TIME_SERIES_METRICS, bookChannel, errorMessage, fiscalYearEndMonth, isFiscalYearEnd, isSupportedCountryCode, resolveFiscalYearEnd } from "./shared.js";
2
2
  import { Router } from "express";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { promises } from "node:fs";
@@ -142,8 +142,27 @@ async function readJsonStrict(filePath) {
142
142
  throw err;
143
143
  }
144
144
  }
145
+ /** Migrate a legacy calendar-quarter `fiscalYearEnd` token ("Q1".."Q4")
146
+ * to its closing-month number in memory so every downstream consumer
147
+ * (reports, time-series, the UI selects) sees one shape. Absent stays
148
+ * absent — the field is optional and resolves to the default on read;
149
+ * we don't stamp an explicit December onto a book that never chose one.
150
+ * Nothing is written back here (no auto-migrate on disk). */
151
+ function normalizeBookFiscalYearEnd(book) {
152
+ if (book.fiscalYearEnd === void 0) return book;
153
+ const resolved = resolveFiscalYearEnd(book.fiscalYearEnd);
154
+ return book.fiscalYearEnd === resolved ? book : {
155
+ ...book,
156
+ fiscalYearEnd: resolved
157
+ };
158
+ }
145
159
  async function readConfig(workspaceRoot) {
146
- return readJsonStrict(configPath(workspaceRoot));
160
+ const config = await readJsonStrict(configPath(workspaceRoot));
161
+ if (!config) return null;
162
+ return {
163
+ ...config,
164
+ books: config.books.map(normalizeBookFiscalYearEnd)
165
+ };
147
166
  }
148
167
  async function writeConfig(config, workspaceRoot) {
149
168
  await writeJsonAtomic(configPath(workspaceRoot), config);
@@ -1464,13 +1483,13 @@ function unsupportedCountryError(received) {
1464
1483
  return new AccountingError(400, `unsupported country code ${JSON.stringify(received)} — must be one of: ${SUPPORTED_COUNTRY_CODES.join(", ")}`);
1465
1484
  }
1466
1485
  function unsupportedFiscalYearEndError(received) {
1467
- return new AccountingError(400, `unsupported fiscalYearEnd ${JSON.stringify(received)} — must be one of: ${FISCAL_YEAR_ENDS.join(", ")}`);
1486
+ return new AccountingError(400, `unsupported fiscalYearEnd ${JSON.stringify(received)} — must be a closing-month number ${FISCAL_YEAR_END_MONTHS.join(", ")} (1 = January … 12 = December)`);
1468
1487
  }
1469
- /** Narrow a free-form `fiscalYearEnd` input. Empty / absent → default
1488
+ /** Narrow a free-form `fiscalYearEnd` input. Absent → default
1470
1489
  * (back-compat with old callers and pre-field on-disk books); any
1471
- * other value must match the enum or 400. */
1490
+ * other value must be a month number 1-12 or 400. */
1472
1491
  function narrowFiscalYearEnd(raw) {
1473
- if (raw === void 0 || raw === "") return "Q4";
1492
+ if (raw === void 0) return 12;
1474
1493
  if (!isFiscalYearEnd(raw)) throw unsupportedFiscalYearEndError(raw);
1475
1494
  return raw;
1476
1495
  }
@@ -1481,7 +1500,7 @@ function narrowFiscalYearEnd(raw) {
1481
1500
  function validateUpdateBookInput(input) {
1482
1501
  if (input.name !== void 0 && (typeof input.name !== "string" || input.name.trim() === "")) throw new AccountingError(400, "name must be a non-empty string when supplied");
1483
1502
  if (input.country !== void 0 && input.country !== "" && !isSupportedCountryCode(input.country)) throw unsupportedCountryError(input.country);
1484
- if (input.fiscalYearEnd !== void 0 && input.fiscalYearEnd !== "" && !isFiscalYearEnd(input.fiscalYearEnd)) throw unsupportedFiscalYearEndError(input.fiscalYearEnd);
1503
+ if (input.fiscalYearEnd !== void 0 && !isFiscalYearEnd(input.fiscalYearEnd)) throw unsupportedFiscalYearEndError(input.fiscalYearEnd);
1485
1504
  }
1486
1505
  async function createBook(input, workspaceRoot) {
1487
1506
  if (typeof input.name !== "string" || input.name.trim() === "") throw new AccountingError(400, "name is required");
@@ -1515,7 +1534,7 @@ async function updateBook(input, workspaceRoot) {
1515
1534
  ...target,
1516
1535
  ...input.name !== void 0 ? { name: input.name } : {},
1517
1536
  ...input.country !== void 0 && input.country !== "" ? { country: input.country } : {},
1518
- ...input.fiscalYearEnd !== void 0 && input.fiscalYearEnd !== "" ? { fiscalYearEnd: input.fiscalYearEnd } : {}
1537
+ ...input.fiscalYearEnd !== void 0 ? { fiscalYearEnd: input.fiscalYearEnd } : {}
1519
1538
  };
1520
1539
  if (input.country === "") delete next.country;
1521
1540
  await writeConfig({ books: config.books.map((book) => book.id === input.bookId ? next : book) }, workspaceRoot);
@@ -1800,7 +1819,7 @@ async function loadTimeSeriesBookContext(requestedBookId, workspaceRoot) {
1800
1819
  const bookId = resolveBookId(config, requestedBookId);
1801
1820
  return {
1802
1821
  bookId,
1803
- fiscalYearEnd: findBook(config, bookId)?.fiscalYearEnd ?? "Q4",
1822
+ fiscalYearEnd: resolveFiscalYearEnd(findBook(config, bookId)?.fiscalYearEnd),
1804
1823
  accounts: await readAccounts(bookId, workspaceRoot)
1805
1824
  };
1806
1825
  }
@@ -1863,6 +1882,15 @@ function asyncHandler(namespace, fallbackMessage, handler) {
1863
1882
  }
1864
1883
  //#endregion
1865
1884
  //#region src/server/router.ts
1885
+ /** Coerce an inbound `fiscalYearEnd` to a number. The UI select and
1886
+ * the LLM tool (JSON-schema `number` enum) both send a number, but a
1887
+ * numeric string ("8") from a hand-rolled client is accepted too so
1888
+ * it isn't silently dropped to `undefined` (→ the December default).
1889
+ * Range validation stays authoritative in the service layer. */
1890
+ function coerceFiscalYearEnd(raw) {
1891
+ if (typeof raw === "number") return raw;
1892
+ if (typeof raw === "string" && raw.trim() !== "" && Number.isInteger(Number(raw))) return Number(raw);
1893
+ }
1866
1894
  async function handleOpenBook(rest) {
1867
1895
  if (typeof rest.bookId !== "string" || rest.bookId === "") throw new AccountingError(400, "openBook: bookId is required. Call 'getBooks' to enumerate, or 'createBook' first on a fresh workspace.");
1868
1896
  const list = await listBooks();
@@ -1911,7 +1939,7 @@ var ACTION_HANDLERS = {
1911
1939
  name: String(rest.name ?? ""),
1912
1940
  currency: typeof rest.currency === "string" ? rest.currency : void 0,
1913
1941
  country: typeof rest.country === "string" ? rest.country : void 0,
1914
- fiscalYearEnd: typeof rest.fiscalYearEnd === "string" ? rest.fiscalYearEnd : void 0
1942
+ fiscalYearEnd: coerceFiscalYearEnd(rest.fiscalYearEnd)
1915
1943
  });
1916
1944
  return {
1917
1945
  bookId: result.book.id,
@@ -1923,7 +1951,7 @@ var ACTION_HANDLERS = {
1923
1951
  bookId: String(rest.bookId ?? ""),
1924
1952
  name: typeof rest.name === "string" ? rest.name : void 0,
1925
1953
  country: typeof rest.country === "string" ? rest.country : void 0,
1926
- fiscalYearEnd: typeof rest.fiscalYearEnd === "string" ? rest.fiscalYearEnd : void 0
1954
+ fiscalYearEnd: coerceFiscalYearEnd(rest.fiscalYearEnd)
1927
1955
  });
1928
1956
  return {
1929
1957
  bookId: result.book.id,