@mulmoclaude/accounting-plugin 0.3.0 → 0.3.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/dist/server.js CHANGED
@@ -1485,27 +1485,45 @@ function unsupportedCountryError(received) {
1485
1485
  function unsupportedFiscalYearEndError(received) {
1486
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)`);
1487
1487
  }
1488
- /** Narrow a free-form `fiscalYearEnd` input. Absent default
1489
- * (back-compat with old callers and pre-field on-disk books); any
1490
- * other value must be a month number 1-12 or 400. */
1491
- function narrowFiscalYearEnd(raw) {
1492
- if (raw === void 0) return 12;
1493
- if (!isFiscalYearEnd(raw)) throw unsupportedFiscalYearEndError(raw);
1494
- return raw;
1495
- }
1496
- /** Boundary checks shared by updateBook. Throws on the first failure
1497
- * so the surrounding function stays under the cognitive-complexity
1498
- * threshold; each rule is also unit-testable independently via the
1499
- * service entry point. */
1488
+ /** Coerce + validate a free-form `fiscalYearEnd` from any ingress path
1489
+ * (REST body, MCP tool args, direct callers). The service is the
1490
+ * validation boundary, so this is deliberately tolerant of input SHAPE
1491
+ * but strict about the resulting value:
1492
+ * - absent / null / empty string → `undefined` (field omitted; the
1493
+ * caller decides default-vs-no-op);
1494
+ * - a number, or a numeric string ("8") from a hand-rolled client →
1495
+ * that month;
1496
+ * - a legacy calendar-quarter token ("Q1".."Q4") from a stale client
1497
+ * its closing month (same Q1→3 mapping the read side applies);
1498
+ * - anything else non-empty (a typo, garbage, an out-of-range or
1499
+ * non-integer number) 400, echoing the ORIGINAL value so the
1500
+ * bad payload can't be silently mistaken for the default. */
1501
+ function coerceFiscalYearEndInput(raw) {
1502
+ if (raw === void 0 || raw === null || raw === "") return void 0;
1503
+ let month = raw;
1504
+ if (typeof raw === "string") {
1505
+ const trimmed = raw.trim();
1506
+ if (trimmed === "") return void 0;
1507
+ if (/^Q[1-4]$/.test(trimmed)) return resolveFiscalYearEnd(trimmed);
1508
+ month = /^-?\d+$/.test(trimmed) ? Number(trimmed) : NaN;
1509
+ }
1510
+ if (!isFiscalYearEnd(month)) throw unsupportedFiscalYearEndError(raw);
1511
+ return month;
1512
+ }
1513
+ /** Boundary checks shared by updateBook (name / country only —
1514
+ * fiscalYearEnd is coerced + validated separately via
1515
+ * `coerceFiscalYearEndInput`). Throws on the first failure so the
1516
+ * surrounding function stays under the cognitive-complexity threshold;
1517
+ * each rule is also unit-testable independently via the service entry
1518
+ * point. */
1500
1519
  function validateUpdateBookInput(input) {
1501
1520
  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");
1502
1521
  if (input.country !== void 0 && input.country !== "" && !isSupportedCountryCode(input.country)) throw unsupportedCountryError(input.country);
1503
- if (input.fiscalYearEnd !== void 0 && !isFiscalYearEnd(input.fiscalYearEnd)) throw unsupportedFiscalYearEndError(input.fiscalYearEnd);
1504
1522
  }
1505
1523
  async function createBook(input, workspaceRoot) {
1506
1524
  if (typeof input.name !== "string" || input.name.trim() === "") throw new AccountingError(400, "name is required");
1507
1525
  if (input.country !== void 0 && !isSupportedCountryCode(input.country)) throw unsupportedCountryError(input.country);
1508
- const fiscalYearEnd = narrowFiscalYearEnd(input.fiscalYearEnd);
1526
+ const fiscalYearEnd = coerceFiscalYearEndInput(input.fiscalYearEnd) ?? 12;
1509
1527
  const config = await loadOrInitConfig(workspaceRoot);
1510
1528
  const bookId = input.id ?? await generateBookId(config, workspaceRoot);
1511
1529
  if (!isSafeBookId(bookId)) throw new AccountingError(400, `invalid book id ${JSON.stringify(bookId)} — allowed characters are A-Z a-z 0-9 _ - (1-64 chars; cannot start with _ or -)`);
@@ -1530,11 +1548,12 @@ async function updateBook(input, workspaceRoot) {
1530
1548
  const target = findBook(config, input.bookId);
1531
1549
  if (!target) throw new AccountingError(404, `book ${JSON.stringify(input.bookId)} not found`);
1532
1550
  validateUpdateBookInput(input);
1551
+ const fiscalYearEnd = coerceFiscalYearEndInput(input.fiscalYearEnd);
1533
1552
  const next = {
1534
1553
  ...target,
1535
1554
  ...input.name !== void 0 ? { name: input.name } : {},
1536
1555
  ...input.country !== void 0 && input.country !== "" ? { country: input.country } : {},
1537
- ...input.fiscalYearEnd !== void 0 ? { fiscalYearEnd: input.fiscalYearEnd } : {}
1556
+ ...fiscalYearEnd !== void 0 ? { fiscalYearEnd } : {}
1538
1557
  };
1539
1558
  if (input.country === "") delete next.country;
1540
1559
  await writeConfig({ books: config.books.map((book) => book.id === input.bookId ? next : book) }, workspaceRoot);
@@ -1882,15 +1901,6 @@ function asyncHandler(namespace, fallbackMessage, handler) {
1882
1901
  }
1883
1902
  //#endregion
1884
1903
  //#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
- }
1894
1904
  async function handleOpenBook(rest) {
1895
1905
  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.");
1896
1906
  const list = await listBooks();
@@ -1939,7 +1949,7 @@ var ACTION_HANDLERS = {
1939
1949
  name: String(rest.name ?? ""),
1940
1950
  currency: typeof rest.currency === "string" ? rest.currency : void 0,
1941
1951
  country: typeof rest.country === "string" ? rest.country : void 0,
1942
- fiscalYearEnd: coerceFiscalYearEnd(rest.fiscalYearEnd)
1952
+ fiscalYearEnd: rest.fiscalYearEnd
1943
1953
  });
1944
1954
  return {
1945
1955
  bookId: result.book.id,
@@ -1951,7 +1961,7 @@ var ACTION_HANDLERS = {
1951
1961
  bookId: String(rest.bookId ?? ""),
1952
1962
  name: typeof rest.name === "string" ? rest.name : void 0,
1953
1963
  country: typeof rest.country === "string" ? rest.country : void 0,
1954
- fiscalYearEnd: coerceFiscalYearEnd(rest.fiscalYearEnd)
1964
+ fiscalYearEnd: rest.fiscalYearEnd
1955
1965
  });
1956
1966
  return {
1957
1967
  bookId: result.book.id,