// Bills of materials and builds — Task 3 of
// docs/superpowers/plans/2026-08-03-inventory.md (spec:
// docs/superpowers/specs/2026-08-03-inventory-design.md). Same route-module
// pattern as items.ts/purchaseOrders.ts: everything under /api/c/ (company
// scope from X-Company-Id via companyContextPlugin), every query inside
// db.withCompany() so RLS is the real guard (CLAUDE.md §4/§12).
//
//   GET  /api/c/items/:id/components          the assembly's parts list
//   PUT  /api/c/items/:id/components          replace the parts list
//   POST /api/c/builds/preview                what a build WOULD draw
//   POST /api/c/builds                        build it
//   POST /api/c/builds/:id/reverse            undo it
//   GET  /api/c/builds?limit=&offset=
//
// NOTHING HERE COMPUTES A COST. The preview calls build_preview() and the build
// calls post_build(), both in db/migrations/0023_inventory_selling_builds.sql,
// because a build owns two halves of one fact — the stock movements and the
// journal entry — and only something that writes both, in one transaction, may
// write either (0018's rule for record_movement). This module validates shapes,
// turns database English into HTTP status codes, and formats money as strings.
//
// A build costs EXACTLY its components: no labor, no overhead, no routings
// (spec non-goals). The preview says so on its face rather than leaving a
// manufacturer to discover it from a number that looks too low.

import type { FastifyInstance } from "fastify";
import { z } from "zod";
import type { Db } from "./db.js";
import { mapPgError } from "./errors.js";
import { findOwnerId } from "../../tools/lib/journal-load.js";
import { blankToUndefined } from "./lib/textNormalize.js";

// ---------------------------------------------------------------------------
// validation
// ---------------------------------------------------------------------------

const idSchema = z.string().regex(/^\d{1,18}$/, "That isn't something we recognise — go back and pick it again.");
const idParamSchema = z.object({ id: idSchema });

const isoDateSchema = z.iso.date({ error: "must be a date like 2024-01-15 (YYYY-MM-DD)" });
const optionalIsoDate = z.preprocess(
  (v) => (v === "" || v === undefined ? undefined : v),
  isoDateSchema.optional(),
);

const memoSchema = z.preprocess(blankToUndefined, z.string().max(1000).optional());

// assemblies.qty_per and builds.qty are both numeric(19,4). Same 8-digit
// integer cap the rest of the app uses, so an absurd quantity is a friendly 400
// rather than a raw Postgres overflow. A regex, never Number()/parseFloat()
// (CLAUDE.md §2).
const quantitySchema = z
  .string({ error: "Enter a quantity." })
  .regex(
    /^(0|[1-9]\d{0,7})(\.\d{1,4})?$/,
    "Enter a quantity like 1 or 0.5 (up to 4 decimal places, no leading zeros, no more than 8 digits before the decimal point).",
  )
  .refine((v) => /[1-9]/.test(v), { message: "Enter a quantity greater than zero." });

const componentsSchema = z.object({
  components: z.array(z.object({
    itemId: idSchema,
    qtyPer: quantitySchema,
  })),
});

const previewSchema = z.object({
  assemblyItemId: idSchema,
  qty: quantitySchema,
});

const buildSchema = z.object({
  assemblyItemId: idSchema,
  qty: quantitySchema,
  builtOn: isoDateSchema,
  memo: memoSchema,
});

const reverseSchema = z.object({ date: optionalIsoDate });

const listQuerySchema = z.object({
  limit: z.coerce.number().int().min(1).max(500).optional(),
  offset: z.coerce.number().int().min(0).optional(),
});

function zodMessage(issues: Array<{ message: string }>): string {
  return issues.map((i) => i.message).join("; ");
}

const NOT_FOUND_ITEM = { error: "We couldn't find that item." };
const NOT_FOUND_BUILD = { error: "We couldn't find that build." };
const NOT_FOUND_PART = { status: 422, error: "We couldn't find one of those parts." };

const NO_OWNER_ERROR = {
  status: 500,
  error: "This company doesn't have an owner user on file, so builds can't be attributed. This shouldn't happen.",
};

/** Mirrors entries.ts/invoices.ts's resolveOwner — every build and reversal is
 *  attributed to the company's owner user (no login in v1). */
async function resolveOwner(
  db: Db, companyId: string, log: { error: (obj: unknown, msg: string) => void },
): Promise<string | null> {
  try {
    return await findOwnerId(db.adminPool, companyId);
  } catch (err) {
    log.error({ err, companyId }, "resolveOwner: findOwnerId failed");
    return null;
  }
}

interface ComponentRow {
  component_item_id: string;
  sku: string | null;
  name: string;
  qty_per: string;
  qty_needed: string;
  qty_on_hand: string;
  average_cost: string;
  draw_value: string;
  sufficient: boolean;
}

function toComponent(r: ComponentRow) {
  return {
    itemId: r.component_item_id,
    sku: r.sku,
    name: r.name,
    qtyPer: r.qty_per,
    qtyNeeded: r.qty_needed,
    qtyOnHand: r.qty_on_hand,
    averageCost: r.average_cost,
    drawValue: r.draw_value,
    sufficient: r.sufficient,
  };
}

// ---------------------------------------------------------------------------
// route registration
// ---------------------------------------------------------------------------

export function registerBuildRoutes(fastify: FastifyInstance, db: Db): void {
  // -------------------------------------------------------------------------
  // GET /api/c/items/:id/components
  // -------------------------------------------------------------------------
  fastify.get("/api/c/items/:id/components", async (request, reply) => {
    const companyId = request.companyId!;
    const parsedParams = idParamSchema.safeParse(request.params);
    if (!parsedParams.success) {
      return reply.code(400).send({ error: zodMessage(parsedParams.error.issues) });
    }
    const itemId = parsedParams.data.id;

    try {
      const result = await db.withCompany(companyId, async (client) => {
        const itemRes = await client.query<{ id: string; name: string; kind: string }>(
          "SELECT id, name, kind FROM items WHERE company_id = $1 AND id = $2",
          [companyId, itemId],
        );
        const item = itemRes.rows[0];
        if (!item) return { found: false as const };

        // Quantity on hand and average cost come from 0018's derivations, at
        // query time — there is no cached column anywhere to read instead
        // (CLAUDE.md §3.4).
        const rowsRes = await client.query<{
          id: string; component_item_id: string; sku: string | null; name: string;
          qty_per: string; qty_on_hand: string; average_cost: string;
        }>(
          `SELECT a.id, a.component_item_id, i.sku, i.name, a.qty_per,
                  item_qty_on_hand($1, a.component_item_id) AS qty_on_hand,
                  item_average_cost($1, a.component_item_id) AS average_cost
             FROM assemblies a
             JOIN items i ON i.company_id = a.company_id AND i.id = a.component_item_id
            WHERE a.company_id = $1 AND a.assembly_item_id = $2
            ORDER BY i.name`,
          [companyId, itemId],
        );

        return {
          found: true as const,
          item: { id: item.id, name: item.name, kind: item.kind },
          components: rowsRes.rows.map((r) => ({
            id: r.id,
            itemId: r.component_item_id,
            sku: r.sku,
            name: r.name,
            qtyPer: r.qty_per,
            qtyOnHand: r.qty_on_hand,
            averageCost: r.average_cost,
          })),
        };
      });

      if (!result.found) return reply.code(404).send(NOT_FOUND_ITEM);
      const { found: _found, ...body } = result;
      return reply.send(body);
    } catch (err) {
      const mapped = mapPgError(err);
      if (mapped.status === 500) request.log.error({ err }, "components list failed");
      return reply.code(mapped.status).send({ error: mapped.message });
    }
  });

  // -------------------------------------------------------------------------
  // PUT /api/c/items/:id/components  (replaces the whole parts list)
  // -------------------------------------------------------------------------
  //
  // Replace-the-whole-set, like every other edit form in this app: the BOM
  // editor always resubmits every row, so "omitted" and "removed" are never
  // ambiguous. The recursion guard (0018's assemblies_no_recursion) and the
  // "a part has to be something you stock" rule are the DATABASE's, and they
  // are deliberately not pre-checked here — they raise in English and
  // errors.ts maps them, so there is exactly one definition of each rule.
  //
  // EXISTENCE is the one exception, and it is not a second definition of any
  // rule. assemblies_no_recursion() reads the component's name into a variable
  // that simply stays NULL when there is no such row, and raises before the
  // composite foreign key ever gets a say — so an unknown id (or another
  // company's, which RLS makes look identical) came back as "'<NULL>' is not a
  // stocked item, so it cannot be a part of 'Bracket'". §9 does not permit a
  // placeholder on a user's screen, so the parts are looked up first and the
  // refusal names what actually went wrong. The trigger stays exactly as it is:
  // it is the guard for everything a lookup cannot see.
  fastify.put("/api/c/items/:id/components", async (request, reply) => {
    const companyId = request.companyId!;
    const parsedParams = idParamSchema.safeParse(request.params);
    if (!parsedParams.success) {
      return reply.code(400).send({ error: zodMessage(parsedParams.error.issues) });
    }
    const parsed = componentsSchema.safeParse(request.body);
    if (!parsed.success) {
      return reply.code(400).send({ error: zodMessage(parsed.error.issues) });
    }
    const itemId = parsedParams.data.id;
    const body = parsed.data;

    const seen = new Set<string>();
    for (const c of body.components) {
      if (seen.has(c.itemId)) {
        return reply.code(422).send({
          error: "Each part can only be listed once — add up the quantity instead.",
        });
      }
      seen.add(c.itemId);
    }

    try {
      const result = await db.withCompany(companyId, async (client) => {
        const itemRes = await client.query<{ id: string; name: string; kind: string }>(
          "SELECT id, name, kind FROM items WHERE company_id = $1 AND id = $2",
          [companyId, itemId],
        );
        const item = itemRes.rows[0];
        if (!item) return { found: false as const };

        // RLS scopes this, so another company's part id looks exactly like an
        // id that never existed — which is the right answer for both.
        for (const c of body.components) {
          const partRes = await client.query<{ id: string }>(
            "SELECT id FROM items WHERE company_id = $1 AND id = $2",
            [companyId, c.itemId],
          );
          if (!partRes.rows[0]) return { found: true as const, ok: false as const, ...NOT_FOUND_PART };
        }

        await client.query(
          "DELETE FROM assemblies WHERE company_id = $1 AND assembly_item_id = $2",
          [companyId, itemId],
        );
        for (const c of body.components) {
          await client.query(
            `INSERT INTO assemblies (company_id, assembly_item_id, component_item_id, qty_per)
             VALUES ($1, $2, $3, $4)`,
            [companyId, itemId, c.itemId, c.qtyPer],
          );
        }

        const rowsRes = await client.query<{
          id: string; component_item_id: string; sku: string | null; name: string;
          qty_per: string; qty_on_hand: string; average_cost: string;
        }>(
          `SELECT a.id, a.component_item_id, i.sku, i.name, a.qty_per,
                  item_qty_on_hand($1, a.component_item_id) AS qty_on_hand,
                  item_average_cost($1, a.component_item_id) AS average_cost
             FROM assemblies a
             JOIN items i ON i.company_id = a.company_id AND i.id = a.component_item_id
            WHERE a.company_id = $1 AND a.assembly_item_id = $2
            ORDER BY i.name`,
          [companyId, itemId],
        );

        return {
          found: true as const,
          ok: true as const,
          item: { id: item.id, name: item.name, kind: item.kind },
          components: rowsRes.rows.map((r) => ({
            id: r.id,
            itemId: r.component_item_id,
            sku: r.sku,
            name: r.name,
            qtyPer: r.qty_per,
            qtyOnHand: r.qty_on_hand,
            averageCost: r.average_cost,
          })),
        };
      });

      if (!result.found) return reply.code(404).send(NOT_FOUND_ITEM);
      if (!result.ok) return reply.code(result.status).send({ error: result.error });
      const { found: _found, ok: _ok, ...body2 } = result;
      return reply.send(body2);
    } catch (err) {
      const mapped = mapPgError(err);
      if (mapped.status === 500) request.log.error({ err }, "set components failed");
      return reply.code(mapped.status).send({ error: mapped.message });
    }
  });

  // -------------------------------------------------------------------------
  // POST /api/c/builds/preview
  // -------------------------------------------------------------------------
  fastify.post("/api/c/builds/preview", async (request, reply) => {
    const companyId = request.companyId!;
    const parsed = previewSchema.safeParse(request.body);
    if (!parsed.success) {
      return reply.code(400).send({ error: zodMessage(parsed.error.issues) });
    }
    const body = parsed.data;

    try {
      const result = await db.withCompany(companyId, async (client) => {
        const itemRes = await client.query<{
          id: string; name: string; kind: string; inventory_account_id: string | null;
          inventory_account_name: string | null;
        }>(
          `SELECT i.id, i.name, i.kind, i.inventory_account_id, a.name AS inventory_account_name
             FROM items i
             LEFT JOIN accounts a ON a.company_id = i.company_id AND a.id = i.inventory_account_id
            WHERE i.company_id = $1 AND i.id = $2`,
          [companyId, body.assemblyItemId],
        );
        const item = itemRes.rows[0];
        if (!item) return { found: false as const };

        const rowsRes = await client.query<ComponentRow>(
          "SELECT * FROM build_preview($1, $2, $3::numeric)",
          [companyId, body.assemblyItemId, body.qty],
        );

        // Summed and divided in SQL, in NUMERIC — never in JS (CLAUDE.md §2).
        const totalsRes = await client.query<{
          total_value: string; unit_cost: string; all_sufficient: boolean; components: number;
        }>(
          `WITH p AS (SELECT * FROM build_preview($1, $2, $3::numeric))
           SELECT coalesce(sum(draw_value), 0)::numeric(19,4) AS total_value,
                  CASE WHEN $3::numeric = 0 THEN 0::numeric(19,6)
                       ELSE round(coalesce(sum(draw_value), 0) / $3::numeric, 6) END AS unit_cost,
                  coalesce(bool_and(sufficient), false) AS all_sufficient,
                  count(*)::int AS components
             FROM p`,
          [companyId, body.assemblyItemId, body.qty],
        );
        const totals = totalsRes.rows[0]!;

        return { found: true as const, item, rows: rowsRes.rows, totals };
      });

      if (!result.found) return reply.code(404).send(NOT_FOUND_ITEM);

      const { item, rows, totals } = result;
      const components = rows.map(toComponent);
      const short = components.filter((c) => !c.sufficient);

      // Every reason a build cannot go ahead, in the order the user meets them.
      // These mirror post_build()'s own refusals exactly — the database is still
      // the guard (§12); this is what lets the screen say so before the click.
      let blockedBecause: string | null = null;
      if (item.kind !== "inventory") {
        blockedBecause = `"${item.name}" isn't something you stock, so it can't be built.`;
      } else if (totals.components === 0) {
        blockedBecause = `"${item.name}" has no parts list yet — say what it's made from before you build it.`;
      } else if (short.length > 0) {
        const first = short[0]!;
        // formatQuantity, not the raw column: this sentence is read on the
        // panel above the button AND on the disabled button's own tooltip, and
        // "this build needs 8.0000 and you have 0.0000" is the same §9 leak
        // fixed on the build summary below (CLAUDE.md §9 — the trailing zeros
        // are a storage detail, not something a user should ever meet).
        blockedBecause = short.length === 1
          ? `You don't have enough "${first.name}" — this build needs ${formatQuantity(first.qtyNeeded)} and you have ${formatQuantity(first.qtyOnHand)}.`
          : `You don't have enough of ${short.length} of these parts yet.`;
      } else if (totals.total_value === "0.0000") {
        blockedBecause = `The parts for "${item.name}" have no value on hand, so this build would move no money at all.`;
      }

      return reply.send({
        assemblyItemId: item.id,
        assemblyName: item.name,
        assemblyKind: item.kind,
        inventoryAccountId: item.inventory_account_id,
        inventoryAccountName: item.inventory_account_name,
        qty: body.qty,
        components,
        totalValue: totals.total_value,
        // A build costs exactly its components — no labor, no overhead. Said
        // here so the screen can say it too (spec non-goals).
        unitCost: totals.unit_cost,
        canBuild: blockedBecause === null,
        blockedBecause,
      });
    } catch (err) {
      const mapped = mapPgError(err);
      if (mapped.status === 500) request.log.error({ err }, "build preview failed");
      return reply.code(mapped.status).send({ error: mapped.message });
    }
  });

  // -------------------------------------------------------------------------
  // POST /api/c/builds
  // -------------------------------------------------------------------------
  fastify.post("/api/c/builds", async (request, reply) => {
    const companyId = request.companyId!;
    const parsed = buildSchema.safeParse(request.body);
    if (!parsed.success) {
      return reply.code(400).send({ error: zodMessage(parsed.error.issues) });
    }
    const body = parsed.data;

    const ownerId = await resolveOwner(db, companyId, request.log);
    if (ownerId === null) {
      return reply.code(NO_OWNER_ERROR.status).send({ error: NO_OWNER_ERROR.error });
    }

    try {
      const result = await db.withCompany(companyId, async (client) => {
        // RLS makes another company's item id look exactly like an unknown one,
        // so look it up first for a clean 404 rather than an 'item % not found'
        // raised from inside the definer function.
        const itemRes = await client.query<{ id: string }>(
          "SELECT id FROM items WHERE company_id = $1 AND id = $2",
          [companyId, body.assemblyItemId],
        );
        if (!itemRes.rows[0]) return { found: false as const };

        const buildRes = await client.query<{ build_id: string }>(
          "SELECT post_build($1, $2, $3::numeric, $4::date, $5, $6) AS build_id",
          [companyId, body.assemblyItemId, body.qty, body.builtOn, body.memo ?? null, ownerId],
        );
        const buildId = buildRes.rows[0]!.build_id;

        const readRes = await client.query<{
          id: string; build_no: string; assembly_item_id: string; item_name: string;
          qty: string; built_on: string; status: string; entry_id: string;
          entry_no: string | null; memo: string | null; value: string; unit_cost: string;
        }>(
          `SELECT b.id, b.build_no, b.assembly_item_id, i.name AS item_name, b.qty,
                  b.built_on::text AS built_on, b.status, b.entry_id, e.entry_no, b.memo,
                  m.value, m.unit_cost
             FROM builds b
             JOIN items i ON i.company_id = b.company_id AND i.id = b.assembly_item_id
             JOIN journal_entries e ON e.company_id = b.company_id AND e.id = b.entry_id
             JOIN inventory_movements m
               ON m.company_id = b.company_id AND m.entry_id = b.entry_id
              AND m.reason = 'build_in' AND m.item_id = b.assembly_item_id
              AND m.reverses_movement_id IS NULL
            WHERE b.company_id = $1 AND b.id = $2`,
          [companyId, buildId],
        );
        return { found: true as const, row: readRes.rows[0]! };
      });

      if (!result.found) return reply.code(404).send(NOT_FOUND_ITEM);
      const r = result.row;
      return reply.code(201).send({
        buildId: r.id,
        buildNo: r.build_no,
        assemblyItemId: r.assembly_item_id,
        assemblyName: r.item_name,
        qty: r.qty,
        builtOn: r.built_on,
        status: r.status,
        entryId: r.entry_id,
        entryNo: r.entry_no,
        memo: r.memo,
        value: r.value,
        unitCost: r.unit_cost,
        summary: `Built ${formatQuantity(r.qty)} ${r.item_name} — worth ${formatUsd(r.value)} in parts.`,
      });
    } catch (err) {
      const mapped = mapPgError(err);
      if (mapped.status === 500) request.log.error({ err }, "post build failed");
      return reply.code(mapped.status).send({ error: mapped.message });
    }
  });

  // -------------------------------------------------------------------------
  // POST /api/c/builds/:id/reverse
  // -------------------------------------------------------------------------
  fastify.post("/api/c/builds/:id/reverse", async (request, reply) => {
    const companyId = request.companyId!;
    const parsedParams = idParamSchema.safeParse(request.params);
    if (!parsedParams.success) {
      return reply.code(400).send({ error: zodMessage(parsedParams.error.issues) });
    }
    const parsedBody = reverseSchema.safeParse(request.body ?? {});
    if (!parsedBody.success) {
      return reply.code(400).send({ error: zodMessage(parsedBody.error.issues) });
    }
    const buildId = parsedParams.data.id;
    const reversalDate = parsedBody.data.date ?? null;

    const ownerId = await resolveOwner(db, companyId, request.log);
    if (ownerId === null) {
      return reply.code(NO_OWNER_ERROR.status).send({ error: NO_OWNER_ERROR.error });
    }

    try {
      const result = await db.withCompany(companyId, async (client) => {
        const existingRes = await client.query<{ id: string; build_no: string; name: string }>(
          `SELECT b.id, b.build_no, i.name
             FROM builds b
             JOIN items i ON i.company_id = b.company_id AND i.id = b.assembly_item_id
            WHERE b.company_id = $1 AND b.id = $2`,
          [companyId, buildId],
        );
        const existing = existingRes.rows[0];
        if (!existing) return { found: false as const };

        const revRes = await client.query<{ entry_id: string }>(
          "SELECT reverse_build($1, $2, coalesce($3::date, current_date), $4) AS entry_id",
          [companyId, buildId, reversalDate, ownerId],
        );
        return {
          found: true as const,
          buildNo: existing.build_no,
          name: existing.name,
          entryId: revRes.rows[0]!.entry_id,
        };
      });

      if (!result.found) return reply.code(404).send(NOT_FOUND_BUILD);
      return reply.send({
        buildId,
        entryId: result.entryId,
        summary: `Undid build #${result.buildNo} — the ${result.name} it made are off the shelf and its parts are back on it.`,
      });
    } catch (err) {
      const mapped = mapPgError(err);
      if (mapped.status === 500) request.log.error({ err }, "reverse build failed");
      return reply.code(mapped.status).send({ error: mapped.message });
    }
  });

  // -------------------------------------------------------------------------
  // GET /api/c/builds
  // -------------------------------------------------------------------------
  fastify.get("/api/c/builds", async (request, reply) => {
    const companyId = request.companyId!;
    const parsedQuery = listQuerySchema.safeParse(request.query);
    if (!parsedQuery.success) {
      return reply.code(400).send({ error: zodMessage(parsedQuery.error.issues) });
    }
    const { limit, offset } = parsedQuery.data;

    try {
      return await db.withCompany(companyId, async (client) => {
        const res = await client.query<{
          id: string; build_no: string; assembly_item_id: string; item_name: string;
          qty: string; built_on: string; status: string; entry_id: string;
          entry_no: string | null; memo: string | null; value: string;
          full_count: string;
        }>(
          `SELECT b.id, b.build_no, b.assembly_item_id, i.name AS item_name, b.qty,
                  b.built_on::text AS built_on, b.status, b.entry_id, e.entry_no, b.memo,
                  coalesce((SELECT m.value FROM inventory_movements m
                             WHERE m.company_id = b.company_id AND m.entry_id = b.entry_id
                               AND m.reason = 'build_in' AND m.item_id = b.assembly_item_id
                               AND m.reverses_movement_id IS NULL), 0)::numeric(19,4) AS value,
                  count(*) OVER() AS full_count
             FROM builds b
             JOIN items i ON i.company_id = b.company_id AND i.id = b.assembly_item_id
             JOIN journal_entries e ON e.company_id = b.company_id AND e.id = b.entry_id
            WHERE b.company_id = $1
            ORDER BY b.built_on DESC, b.build_no DESC
            LIMIT $2 OFFSET $3`,
          [companyId, limit ?? 50, offset ?? 0],
        );
        return {
          total: res.rows[0] ? Number(res.rows[0].full_count) : 0,
          builds: res.rows.map((r) => ({
            buildId: r.id,
            buildNo: r.build_no,
            assemblyItemId: r.assembly_item_id,
            assemblyName: r.item_name,
            qty: r.qty,
            builtOn: r.built_on,
            status: r.status,
            entryId: r.entry_id,
            entryNo: r.entry_no,
            memo: r.memo,
            value: r.value,
          })),
        };
      });
    } catch (err) {
      const mapped = mapPgError(err);
      if (mapped.status === 500) request.log.error({ err }, "builds list failed");
      return reply.code(mapped.status).send({ error: mapped.message });
    }
  });
}

/**
 * "5.0000" -> "5", "0.5000" -> "0.5". A quantity column's trailing zeros are a
 * storage detail, and a sentence a user reads must not carry them ("Built
 * 5.0000 Northgate Cabinet" — caught in Task 5's live verification). Mirrors
 * server/src/purchaseOrders.ts's helper of the same name and
 * web/src/items/itemTypes.ts's fmtQuantity. A quantity is not money, so plain
 * string trimming is right here (CLAUDE.md §2 is about monetary values) — and
 * it is still string surgery, never a parsed number.
 */
function formatQuantity(raw: string): string {
  if (!/^-?\d+\.\d+$/.test(raw)) return raw;
  return raw.replace(/0+$/, "").replace(/\.$/, "");
}

/** Formats a NUMERIC(19,4) string from Postgres (e.g. "1234.0000") as
 *  "$1,234.00" — pure string surgery, never a parsed JS number (CLAUDE.md §2).
 *  Same helper invoices.ts/bills.ts carry. */
function formatUsd(amount: string): string {
  const negative = amount.startsWith("-");
  const unsigned = negative ? amount.slice(1) : amount;
  const [wholeRaw, fracRaw = ""] = unsigned.split(".");
  const whole = wholeRaw!.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
  const frac = (fracRaw + "00").slice(0, 2);
  return `${negative ? "-" : ""}$${whole}.${frac}`;
}
