// Integration tests for selling stocked items, bills of materials, builds,
// inventory adjustments and opening quantities (Task 3 of
// docs/superpowers/plans/2026-08-03-inventory.md; spec:
// docs/superpowers/specs/2026-08-03-inventory-design.md). Structurally a
// sibling of tests/server/inventory-api.test.ts (same createTestDb, app.inject,
// seeded chart) with the selling / building vocabulary.
//
// THE INVARIANT ASSERTED AFTER EVERY POSTING OPERATION is the ledger tie:
// inventory_out_of_tie() must be 0.0000 and check_inventory_ties() must report
// a zero difference for EVERY inventory account. The per-account half matters —
// an error that moves value between two inventory accounts sums to a serene
// zero in the aggregate while both accounts are individually wrong.
// ledger_out_of_balance() is asserted alongside it, because a tie that holds on
// unbalanced books proves nothing.

import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { createTestDb, type TestDb } from "../helpers/db.js";
// Where the database is, resolved once for the whole run: your ADMIN_URL /
// DATABASE_URL if you set one, otherwise the PostgreSQL Balanced Sum installed on
// this machine. Every file in this directory used to carry its own copy of that
// decision, hardcoded to the Docker container that no longer exists.
import { dbUrl } from "../helpers/test-db-admin.js";
import { buildApp } from "../../server/src/app.js";
import type { FastifyInstance } from "fastify";
import type { Client } from "pg";

async function buildTestApp(dbName: string): Promise<FastifyInstance> {
  return buildApp({
    databaseUrl: await dbUrl(dbName, "app", "app"),
    adminDatabaseUrl: await dbUrl(dbName),
  });
}

function post(app: FastifyInstance, companyId: string, url: string, payload?: unknown) {
  return app.inject({
    method: "POST", url, headers: { "x-company-id": companyId }, payload: payload ?? {},
  });
}
function put(app: FastifyInstance, companyId: string, url: string, payload?: unknown) {
  return app.inject({
    method: "PUT", url, headers: { "x-company-id": companyId }, payload: payload ?? {},
  });
}
function patch(app: FastifyInstance, companyId: string, url: string, payload?: unknown) {
  return app.inject({
    method: "PATCH", url, headers: { "x-company-id": companyId }, payload: payload ?? {},
  });
}
function get(app: FastifyInstance, companyId: string, url: string) {
  return app.inject({ method: "GET", url, headers: { "x-company-id": companyId } });
}

let db: TestDb;
let app: FastifyInstance;
let cA: Client;    // app-role connection scoped to company A (verification only)
let cB: Client;
// A company-scoped SUPERUSER connection. record_movement() and
// assert_build_posting() are internal (REVOKE EXECUTE ... FROM PUBLIC), so this
// is how the hand-written "what if somebody wrote this by hand" cases are
// exercised. Session-company guards still apply — this is about EXECUTE
// privilege, not about tenancy.
let sysA: Client;

let invA = "", inv2A = "", cogsA = "", cogs2A = "", incomeA = "", shrinkA = "";
let invB = "", cogsB = "";

beforeAll(async () => {
  db = await createTestDb();
  app = await buildTestApp(db.dbName);
  cA = await db.appClient(db.companyA);
  cB = await db.appClient(db.companyB);
  sysA = await db.sysClient(db.companyA);

  invA = await createAccount(db.companyA, "1300", "Inventory", "asset");
  inv2A = await createAccount(db.companyA, "1310", "Inventory - Finished Goods", "asset");
  cogsA = await createAccount(db.companyA, "5000", "Cost of Goods Sold", "expense");
  cogs2A = await createAccount(db.companyA, "5010", "Cost of Goods Sold - Parts", "expense");
  shrinkA = await createAccount(db.companyA, "6100", "Shrinkage", "expense");
  incomeA = String(db.accounts["4000"]!.A);
  invB = await createAccount(db.companyB, "1300", "Inventory", "asset");
  cogsB = await createAccount(db.companyB, "5000", "Cost of Goods Sold", "expense");
}, 90_000);

afterAll(async () => {
  await app.close();
  await db.close();
});

// ---------------------------------------------------------------------------
// helpers
// ---------------------------------------------------------------------------

async function createAccount(
  companyId: string, code: string, name: string, type: string, subtype?: string,
): Promise<string> {
  const res = await post(app, companyId, "/api/c/accounts",
    subtype === undefined ? { code, name, type } : { code, name, type, subtype });
  expect(res.statusCode).toBe(201);
  return res.json().id as string;
}

/** THE invariant. Asserted after every operation that posts anything. */
async function expectBooksTie(companyId: string = db.companyA): Promise<void> {
  const client = companyId === db.companyA ? cA : cB;
  const totals = await client.query(
    "SELECT ledger_out_of_balance($1) AS balance, inventory_out_of_tie($1) AS tie",
    [companyId],
  );
  expect(totals.rows[0].balance).toBe("0.0000");
  expect(totals.rows[0].tie).toBe("0.0000");

  // PER ACCOUNT, not just the total: value moved between two inventory
  // accounts nets to zero in the aggregate while both are individually wrong.
  const perAccount = await client.query(
    "SELECT account_id, code, difference FROM check_inventory_ties($1)", [companyId],
  );
  for (const row of perAccount.rows) {
    expect(`${row.code}:${row.difference}`).toBe(`${row.code}:0.0000`);
  }
}

async function accountBalance(companyId: string, accountId: string): Promise<string> {
  const client = companyId === db.companyA ? cA : cB;
  const res = await client.query(
    `SELECT coalesce(sum(l.debit - l.credit), 0)::numeric(19,4) AS balance
       FROM journal_lines l
       JOIN journal_entries e ON e.company_id = l.company_id AND e.id = l.entry_id
      WHERE l.company_id = $1 AND l.account_id = $2 AND e.status = 'posted'`,
    [companyId, accountId],
  );
  return res.rows[0].balance as string;
}

interface ItemInput {
  name: string;
  kind?: string;
  sku?: string;
  inventoryAccountId?: string;
  cogsAccountId?: string;
  incomeAccountId?: string;
  salesPrice?: string;
  purchaseCost?: string;
}

async function createItem(companyId: string, input: ItemInput): Promise<string> {
  const kind = input.kind ?? "inventory";
  const payload: Record<string, unknown> = { kind, ...input };
  if (kind === "inventory" && input.inventoryAccountId === undefined) {
    payload.inventoryAccountId = companyId === db.companyA ? invA : invB;
  }
  if (kind !== "service" && input.cogsAccountId === undefined) {
    payload.cogsAccountId = companyId === db.companyA ? cogsA : cogsB;
  }
  if (input.incomeAccountId === undefined) {
    payload.incomeAccountId = String(db.accounts["4000"]![companyId === db.companyA ? "A" : "B"]);
  }
  const res = await post(app, companyId, "/api/c/items", payload);
  expect(res.statusCode).toBe(201);
  return res.json().id as string;
}

async function createVendor(companyId: string, name: string): Promise<string> {
  const res = await post(app, companyId, "/api/c/vendors", { name });
  expect(res.statusCode).toBe(201);
  return res.json().id;
}

async function createCustomer(companyId: string, name: string): Promise<string> {
  const res = await post(app, companyId, "/api/c/customers", { name });
  expect(res.statusCode).toBe(201);
  return res.json().id;
}

/** Receives stock by entering and issuing a bill — the ONLY way stock comes in
 *  (spec as-built decision 1: there is no separate receipt document). */
async function receive(
  companyId: string, itemId: string, quantity: string, unitPrice: string,
  billDate = "2024-03-01", refNo?: string,
): Promise<void> {
  const vendorId = await createVendor(companyId, `Supplier ${refNo ?? Math.random()}`);
  const create = await post(app, companyId, "/api/c/bills", {
    vendorId, billDate, refNo, lines: [{ itemId, quantity, unitPrice }],
  });
  expect(create.statusCode).toBe(201);
  const issue = await post(app, companyId, `/api/c/bills/${create.json().billId}/issue`);
  expect(issue.statusCode).toBe(200);
}

interface InvoiceLine {
  itemId?: string;
  quantity?: string;
  unitPrice?: string;
  incomeAccountId?: string;
  description?: string;
  taxable?: boolean;
}

async function draftInvoice(
  companyId: string, customerId: string, lines: InvoiceLine[],
  extra: Record<string, unknown> = {},
): Promise<{ statusCode: number; body: any }> {
  const res = await post(app, companyId, "/api/c/invoices", {
    customerId, invoiceDate: "2024-06-01", lines, ...extra,
  });
  return { statusCode: res.statusCode, body: res.json() };
}

/** Drafts and issues an invoice; returns both responses. */
async function invoiceAndIssue(
  companyId: string, customerId: string, lines: InvoiceLine[],
  extra: Record<string, unknown> = {},
): Promise<{ invoiceId: string; issue: { statusCode: number; body: any } }> {
  const create = await draftInvoice(companyId, customerId, lines, extra);
  expect(create.statusCode).toBe(201);
  const invoiceId = create.body.invoiceId as string;
  const issue = await post(app, companyId, `/api/c/invoices/${invoiceId}/issue`);
  return { invoiceId, issue: { statusCode: issue.statusCode, body: issue.json() } };
}

async function itemPosition(
  companyId: string, itemId: string,
): Promise<{ qtyOnHand: string; averageCost: string; value: string }> {
  const res = await get(app, companyId, `/api/c/items/${itemId}`);
  expect(res.statusCode).toBe(200);
  const b = res.json();
  return { qtyOnHand: b.qtyOnHand, averageCost: b.averageCost, value: b.value };
}

// ===========================================================================
// selling stocked items
// ===========================================================================

describe("selling a stocked item", () => {
  it("posts the sale and its cost of sales on ONE entry, and everything ties", async () => {
    const itemId = await createItem(db.companyA, {
      name: "Sell Widget", salesPrice: "19.99",
    });
    await receive(db.companyA, itemId, "20", "5.00", "2024-03-01", "SELL-1");
    await expectBooksTie();

    const invBefore = await accountBalance(db.companyA, invA);
    const cogsBefore = await accountBalance(db.companyA, cogsA);
    const arBefore = await accountBalance(db.companyA, String(db.accounts["1100"]!.A));
    const incomeBefore = await accountBalance(db.companyA, incomeA);

    const customerId = await createCustomer(db.companyA, "Sell Widget Buyer Co");
    const { invoiceId, issue } = await invoiceAndIssue(db.companyA, customerId, [
      { itemId, quantity: "5", unitPrice: "19.99" },
    ]);
    expect(issue.statusCode).toBe(200);

    // 5 x 19.99 = 99.95 revenue; 5 x the 5.00 average = 25.00 of cost.
    expect(await accountBalance(db.companyA, String(db.accounts["1100"]!.A)))
      .toBe(add(arBefore, "99.9500"));
    expect(await accountBalance(db.companyA, incomeA)).toBe(sub(incomeBefore, "99.9500"));
    expect(await accountBalance(db.companyA, cogsA)).toBe(add(cogsBefore, "25.0000"));
    expect(await accountBalance(db.companyA, invA)).toBe(sub(invBefore, "25.0000"));
    await expectBooksTie();

    expect(await itemPosition(db.companyA, itemId)).toEqual({
      qtyOnHand: "15.0000", averageCost: "5.000000", value: "75.0000",
    });

    // ONE entry carries all four legs: AR, income, COGS and Inventory.
    const detail = await get(app, db.companyA, `/api/c/invoices/${invoiceId}`);
    const entryId = detail.json().postedEntryId;
    const lines = await cA.query(
      `SELECT a.code, l.debit, l.credit FROM journal_lines l
         JOIN accounts a ON a.company_id = l.company_id AND a.id = l.account_id
        WHERE l.company_id = $1 AND l.entry_id = $2 ORDER BY a.code`,
      [db.companyA, entryId]);
    expect(lines.rows).toEqual([
      { code: "1100", debit: "99.9500", credit: "0.0000" },   // A/R
      { code: "1300", debit: "0.0000", credit: "25.0000" },   // Inventory
      { code: "4000", debit: "0.0000", credit: "99.9500" },   // Revenue
      { code: "5000", debit: "25.0000", credit: "0.0000" },   // COGS
    ]);

    // ...and the movement rode on that same entry, dated the same day.
    const moves = await cA.query(
      `SELECT qty, unit_cost, value, reason, source_kind, moved_on::text AS moved_on
         FROM inventory_movements WHERE company_id = $1 AND entry_id = $2`,
      [db.companyA, entryId]);
    expect(moves.rows).toEqual([{
      qty: "-5.0000", unit_cost: "5.000000", value: "-25.0000",
      reason: "sale", source_kind: "invoice_line", moved_on: "2024-06-01",
    }]);
  });

  it("costs a sale at the average of TWO receipts — hand-computed", async () => {
    // Receipt 1: 10 @ 5.00  ->  value 50.00
    // Receipt 2: 10 @ 7.00  ->  value 70.00
    //   on hand 20, total value 120.00, average 120.00 / 20 = 6.000000
    // Sell 5   ->  cost 5 x 6.000000 = 30.00
    //   on hand 15, total value 90.00, average still 6.000000
    const itemId = await createItem(db.companyA, { name: "Average Widget" });
    await receive(db.companyA, itemId, "10", "5.00", "2024-03-01", "AVG-1");
    await receive(db.companyA, itemId, "10", "7.00", "2024-03-02", "AVG-2");
    await expectBooksTie();
    expect(await itemPosition(db.companyA, itemId)).toEqual({
      qtyOnHand: "20.0000", averageCost: "6.000000", value: "120.0000",
    });

    const cogsBefore = await accountBalance(db.companyA, cogsA);
    const invBefore = await accountBalance(db.companyA, invA);

    const customerId = await createCustomer(db.companyA, "Average Buyer Co");
    const { issue } = await invoiceAndIssue(db.companyA, customerId, [
      { itemId, quantity: "5", unitPrice: "12.00" },
    ]);
    expect(issue.statusCode).toBe(200);

    expect(await accountBalance(db.companyA, cogsA)).toBe(add(cogsBefore, "30.0000"));
    expect(await accountBalance(db.companyA, invA)).toBe(sub(invBefore, "30.0000"));
    expect(await itemPosition(db.companyA, itemId)).toEqual({
      qtyOnHand: "15.0000", averageCost: "6.000000", value: "90.0000",
    });
    await expectBooksTie();
  });

  it("fills the price from the item and targets the item's own income account", async () => {
    const otherIncome = await createAccount(db.companyA, "4100", "Product Sales", "income");
    const itemId = await createItem(db.companyA, {
      name: "Prefill Widget", salesPrice: "24.50", incomeAccountId: otherIncome,
    });
    await receive(db.companyA, itemId, "10", "3.00", "2024-03-01", "PREFILL-1");

    const customerId = await createCustomer(db.companyA, "Prefill Buyer Co");
    // No unitPrice, no incomeAccountId — both come from the item.
    const create = await draftInvoice(db.companyA, customerId, [{ itemId, quantity: "4" }]);
    expect(create.statusCode).toBe(201);
    expect(create.body.lines[0]).toMatchObject({
      unitPrice: "24.5000", incomeAccountId: otherIncome, amount: "98.0000",
    });

    const issue = await post(app, db.companyA, `/api/c/invoices/${create.body.invoiceId}/issue`);
    expect(issue.statusCode).toBe(200);
    expect(await accountBalance(db.companyA, otherIncome)).toBe("-98.0000");
    await expectBooksTie();
  });

  it("422s in English when the sale would go past what is on hand", async () => {
    const itemId = await createItem(db.companyA, { name: "Oversell Widget" });
    await receive(db.companyA, itemId, "12", "1.00", "2024-03-01", "OVERSELL-1");

    const customerId = await createCustomer(db.companyA, "Oversell Buyer Co");
    const { issue } = await invoiceAndIssue(db.companyA, customerId, [
      { itemId, quantity: "20", unitPrice: "5.00" },
    ]);
    expect(issue.statusCode).toBe(422);
    expect(issue.body.error).toBe("You only have 12 of Oversell Widget on hand — this would use 20.");

    // nothing posted, nothing moved
    expect((await itemPosition(db.companyA, itemId)).qtyOnHand).toBe("12.0000");
    await expectBooksTie();
  });

  it("422s in English for a stocked-item line with no quantity", async () => {
    const itemId = await createItem(db.companyA, { name: "No Qty Sale Widget", salesPrice: "9.00" });
    const customerId = await createCustomer(db.companyA, "No Qty Buyer Co");
    const res = await draftInvoice(db.companyA, customerId, [{ itemId, unitPrice: "9.00" }]);
    expect(res.statusCode).toBe(422);
    expect(res.body.error).toBe('Enter how many of "No Qty Sale Widget" you sold.');
  });

  it("422s in English when a document-only invoice tries to move stock", async () => {
    const itemId = await createItem(db.companyA, { name: "Doc Only Sale Widget" });
    await receive(db.companyA, itemId, "10", "1.00", "2024-03-01", "DOCONLY-1");
    const customerId = await createCustomer(db.companyA, "Doc Only Buyer Co");

    const res = await draftInvoice(db.companyA, customerId, [
      { itemId, quantity: "2", unitPrice: "5.00" },
    ], { documentOnly: true });
    expect(res.statusCode).toBe(422);
    expect(res.body.error).toBe(
      "This invoice is recorded for tracking only, so it can't move stock — " +
      "enter it as a normal invoice if it changes the quantity on hand.");
  });

  it("refuses a document-only stocked sale at the DATABASE, not only in the API", async () => {
    // The API refuses this at draft time; the database has to refuse it too, or
    // the guard is application-only (CLAUDE.md §12). A draft written by hand
    // gets past the route entirely.
    const itemId = await createItem(db.companyA, { name: "Doc Only Raw Widget" });
    await receive(db.companyA, itemId, "10", "1.00", "2024-03-01", "DOCONLYRAW-1");
    const customerId = await createCustomer(db.companyA, "Doc Only Raw Buyer Co");

    const inv = await cA.query(
      `INSERT INTO invoices (company_id, customer_id, invoice_date, due_date, document_only, created_by)
       VALUES ($1, $2, '2024-06-01', '2024-07-01', true, $3) RETURNING id`,
      [db.companyA, customerId, db.userA]);
    await cA.query(
      `INSERT INTO invoice_lines
         (company_id, invoice_id, quantity, unit_price, amount, income_account_id, item_id)
       VALUES ($1, $2, 2, 5.00, 10.00, $3, $4)`,
      [db.companyA, inv.rows[0].id, incomeA, itemId]);

    await expect(cA.query("SELECT issue_invoice($1, $2, $3)",
      [db.companyA, inv.rows[0].id, db.userA]))
      .rejects.toThrow(/recorded for tracking only, so it cannot move stock/);
    await expectBooksTie();
  });

  it("keeps sales tax and the cost-of-sales leg completely separate on one invoice", async () => {
    // Sales tax credits a LIABILITY account. It must not disturb the COGS leg,
    // and the COGS leg must not disturb the tax recompute.
    const enable = await put(app, db.companyA, "/api/c/settings/sales-tax", { enabled: true });
    expect(enable.statusCode).toBe(200);
    const rate = await post(app, db.companyA, "/api/c/tax-rates", {
      name: "CA Sales Tax", ratePercent: "7.2500",
    });
    expect(rate.statusCode).toBe(201);

    const itemId = await createItem(db.companyA, { name: "Taxed Widget", salesPrice: "100.00" });
    await receive(db.companyA, itemId, "10", "4.00", "2024-03-01", "TAX-1");

    const cogsBefore = await accountBalance(db.companyA, cogsA);
    const invBefore = await accountBalance(db.companyA, invA);

    const customerId = await createCustomer(db.companyA, "Taxed Buyer Co");
    const { invoiceId, issue } = await invoiceAndIssue(db.companyA, customerId, [
      { itemId, quantity: "3", unitPrice: "100.00", taxable: true },
    ], { taxRateId: rate.json().id });
    expect(issue.statusCode).toBe(200);

    // 3 x 100.00 = 300.00 taxable; 7.25% of 300.00 = 21.75. Cost is 3 x 4.00 =
    // 12.00 and is untouched by any of that.
    const detail = await get(app, db.companyA, `/api/c/invoices/${invoiceId}`);
    expect(detail.json()).toMatchObject({ taxAmount: "21.7500", total: "321.7500" });
    expect(await accountBalance(db.companyA, cogsA)).toBe(add(cogsBefore, "12.0000"));
    expect(await accountBalance(db.companyA, invA)).toBe(sub(invBefore, "12.0000"));
    await expectBooksTie();

    const lines = await cA.query(
      `SELECT a.code, a.type, l.debit, l.credit FROM journal_lines l
         JOIN accounts a ON a.company_id = l.company_id AND a.id = l.account_id
        WHERE l.company_id = $1 AND l.entry_id = $2 ORDER BY a.code`,
      [db.companyA, detail.json().postedEntryId]);
    // A/R 321.75 = 300.00 income + 21.75 tax; COGS 12.00 = Inventory 12.00.
    expect(lines.rows.map((r) => `${r.code} ${r.debit}/${r.credit}`)).toEqual([
      "1100 321.7500/0.0000",
      "1300 0.0000/12.0000",
      "2201 0.0000/21.7500",   // Sales Tax Payable, auto-created by the enable
      "4000 0.0000/300.0000",
      "5000 12.0000/0.0000",
    ]);

    expect((await put(app, db.companyA, "/api/c/settings/sales-tax", { enabled: false }))
      .statusCode).toBe(200);
  });

  it("leaves a non-stocked item line exactly as it always was: no movement, no cost", async () => {
    const itemId = await createItem(db.companyA, {
      name: "Service Call", kind: "service", salesPrice: "150.00",
    });
    const customerId = await createCustomer(db.companyA, "Service Buyer Co");
    const cogsBefore = await accountBalance(db.companyA, cogsA);

    const { invoiceId, issue } = await invoiceAndIssue(db.companyA, customerId, [
      { itemId, quantity: "2" },
    ]);
    expect(issue.statusCode).toBe(200);

    const detail = await get(app, db.companyA, `/api/c/invoices/${invoiceId}`);
    expect(detail.json().total).toBe("300.0000");
    const moves = await cA.query(
      "SELECT count(*)::int AS n FROM inventory_movements WHERE company_id = $1 AND entry_id = $2",
      [db.companyA, detail.json().postedEntryId]);
    expect(moves.rows[0].n).toBe(0);
    expect(await accountBalance(db.companyA, cogsA)).toBe(cogsBefore);
    await expectBooksTie();
  });

  it("inherits the reconciled-through guard on the NEW inventory leg", async () => {
    // A stocked invoice's cost leg touches an account the AR posting never did.
    // Reconciling that account and dating a sale inside the reconciled window
    // proves the new leg goes through post_entry()'s assert_not_reconciled()
    // like everything else.
    const rec = await createAccount(db.companyA, "1345", "Inventory - Reconciled", "asset");
    const itemId = await createItem(db.companyA, {
      name: "Reconciled Widget", inventoryAccountId: rec,
    });
    await receive(db.companyA, itemId, "10", "2.00", "2024-03-01", "RECON-1");

    await db.admin.query(
      `INSERT INTO account_reconciliations
         (company_id, money_account_id, statement_id, reconciled_through, statement_balance, created_by)
       VALUES ($1, $2, NULL, '2024-06-30', NULL, $3)`,
      [db.companyA, rec, db.userA]);

    const customerId = await createCustomer(db.companyA, "Reconciled Buyer Co");
    const { issue } = await invoiceAndIssue(db.companyA, customerId, [
      { itemId, quantity: "2", unitPrice: "9.00" },
    ]);
    expect(issue.statusCode).toBe(409);
    expect(issue.body.error).toMatch(/reconciled through 2024-06-30/);
    expect((await itemPosition(db.companyA, itemId)).qtyOnHand).toBe("10.0000");
    await expectBooksTie();

    // a later date sails through
    const later = await post(app, db.companyA, "/api/c/invoices", {
      customerId, invoiceDate: "2024-07-15",
      lines: [{ itemId, quantity: "2", unitPrice: "9.00" }],
    });
    expect(later.statusCode).toBe(201);
    const ok = await post(app, db.companyA, `/api/c/invoices/${later.json().invoiceId}/issue`);
    expect(ok.statusCode).toBe(200);
    await expectBooksTie();
  });

  it("404s (not 500) for another company's item on an invoice line (RLS)", async () => {
    const itemId = await createItem(db.companyA, { name: "RLS Sale Widget" });
    const customerId = await createCustomer(db.companyB, "RLS Buyer Co");
    const res = await draftInvoice(db.companyB, customerId, [
      { itemId, quantity: "1", unitPrice: "5.00" },
    ]);
    expect(res.statusCode).toBe(422);
    expect(res.body.error).toBe("One of these lines names an item we couldn't find.");
  });
});

// ===========================================================================
// nothing changes underneath a draft INVOICE (the sales half of 0020's guard)
// ===========================================================================

describe("nothing changes underneath a draft invoice", () => {
  it("refuses to un-stock an item that a draft invoice line already names", async () => {
    // Deliberately BEFORE any stock arrives. items_frozen_once_moved (0018)
    // fires first on an item that has movements (BEFORE triggers run in name
    // order, and "once_moved" sorts before "while_drafted"), so a movement-less
    // item is the only case where the NEW clause is the thing standing there —
    // which makes this the honest test of it.
    const flipAccount = await createAccount(db.companyA, "1321", "Inventory - Sales Flip", "asset");
    const itemId = await createItem(db.companyA, {
      name: "Sales Flip Widget", inventoryAccountId: flipAccount, salesPrice: "10.00",
    });

    const customerId = await createCustomer(db.companyA, "Sales Flip Buyer Co");
    const create = await draftInvoice(db.companyA, customerId, [
      { itemId, quantity: "4", unitPrice: "10.00" },
    ]);
    expect(create.statusCode).toBe(201);

    const flip = await patch(app, db.companyA, `/api/c/items/${itemId}`, {
      name: "Sales Flip Widget", kind: "non_inventory", cogsAccountId: cogsA,
    });
    expect(flip.statusCode).toBe(409);
    expect(flip.json().error).toBe(
      "'Sales Flip Widget' is on an invoice that hasn't been sent yet, so its item type " +
      "and inventory account can't change. Send or delete that invoice first.");

    // ...and it is the DATABASE that refuses it, not the route.
    await expect(cA.query(
      "UPDATE items SET kind = 'non_inventory', inventory_account_id = NULL WHERE company_id = $1 AND id = $2",
      [db.companyA, itemId],
    )).rejects.toThrow(/is on an invoice that hasn't been sent yet/);

    // repointing the inventory account while staying stocked is refused too
    await expect(cA.query(
      "UPDATE items SET inventory_account_id = $3 WHERE company_id = $1 AND id = $2",
      [db.companyA, itemId, inv2A],
    )).rejects.toThrow(/is on an invoice that hasn't been sent yet/);

    // the stock then arrives and the invoice issues normally, and it all ties
    await receive(db.companyA, itemId, "10", "1.00", "2024-03-01", "SALESFLIP-1");
    const issue = await post(app, db.companyA, `/api/c/invoices/${create.body.invoiceId}/issue`);
    expect(issue.statusCode).toBe(200);
    await expectBooksTie();
  });

  it("releases the item again once the draft is gone", async () => {
    const acct = await createAccount(db.companyA, "1322", "Inventory - Untouched", "asset");
    const itemId = await createItem(db.companyA, {
      name: "Untouched Widget", inventoryAccountId: acct, salesPrice: "10.00",
    });
    const customerId = await createCustomer(db.companyA, "Untouched Buyer Co");
    const create = await draftInvoice(db.companyA, customerId, [
      { itemId, quantity: "1", unitPrice: "10.00" },
    ]);
    expect(create.statusCode).toBe(201);

    const flip = await patch(app, db.companyA, `/api/c/items/${itemId}`, {
      name: "Untouched Widget", kind: "non_inventory", cogsAccountId: cogsA,
    });
    expect(flip.statusCode).toBe(409);
    expect(flip.json().error).toMatch(/is on an invoice that hasn't been sent yet/);

    // deleting the draft releases the item, exactly as the message says
    const del = await app.inject({
      method: "DELETE", url: `/api/c/invoices/${create.body.invoiceId}`,
      headers: { "x-company-id": db.companyA },
    });
    expect([200, 204, 404]).toContain(del.statusCode);
    if (del.statusCode === 404) {
      await cA.query("DELETE FROM invoice_lines WHERE company_id = $1 AND invoice_id = $2",
        [db.companyA, create.body.invoiceId]);
      await cA.query("DELETE FROM invoices WHERE company_id = $1 AND id = $2",
        [db.companyA, create.body.invoiceId]);
    }
    const flipAgain = await patch(app, db.companyA, `/api/c/items/${itemId}`, {
      name: "Untouched Widget", kind: "non_inventory", cogsAccountId: cogsA,
    });
    expect(flipAgain.statusCode).toBe(200);
    await expectBooksTie();
  });
});

// ===========================================================================
// voiding an invoice that sold stock
// ===========================================================================

describe("voiding an invoice that sold stock", () => {
  it("gives back the exact quantity AND the exact cost, even after the average has moved", async () => {
    // Receipt 1: 10 @ 5.00 -> 50.00.  Receipt 2: 10 @ 7.00 -> 70.00.
    //   20 on hand, 120.00 of value, average 6.000000.
    // Sell 5 -> cost 30.00. 15 on hand, 90.00, average still 6.000000.
    // Receipt 3: 10 @ 10.00 -> 100.00. 25 on hand, 190.00, average 7.600000.
    // VOID the sale -> the 30.00 that left comes back, NOT 5 x 7.60 = 38.00.
    //   30 on hand, 220.00 of value, average 220.00 / 30 = 7.333333 —
    //   which is exactly the position the books would be in if the sale had
    //   never happened (50 + 70 + 100 = 220 over 30 units).
    const itemId = await createItem(db.companyA, { name: "Void Average Widget" });
    await receive(db.companyA, itemId, "10", "5.00", "2024-03-01", "VOIDAVG-1");
    await receive(db.companyA, itemId, "10", "7.00", "2024-03-02", "VOIDAVG-2");

    const customerId = await createCustomer(db.companyA, "Void Average Buyer Co");
    const cogsBefore = await accountBalance(db.companyA, cogsA);
    const invBefore = await accountBalance(db.companyA, invA);

    const { invoiceId, issue } = await invoiceAndIssue(db.companyA, customerId, [
      { itemId, quantity: "5", unitPrice: "20.00" },
    ]);
    expect(issue.statusCode).toBe(200);
    expect(await accountBalance(db.companyA, cogsA)).toBe(add(cogsBefore, "30.0000"));
    await expectBooksTie();

    // the average moves AFTER the sale
    await receive(db.companyA, itemId, "10", "10.00", "2024-06-02", "VOIDAVG-3");
    expect(await itemPosition(db.companyA, itemId)).toEqual({
      qtyOnHand: "25.0000", averageCost: "7.600000", value: "190.0000",
    });
    await expectBooksTie();

    const invAfterThird = await accountBalance(db.companyA, invA);
    const voidRes = await post(app, db.companyA, `/api/c/invoices/${invoiceId}/void`,
      { date: "2024-06-10" });
    expect(voidRes.statusCode).toBe(200);

    // COGS is back where it started; Inventory got back exactly 30.00.
    expect(await accountBalance(db.companyA, cogsA)).toBe(cogsBefore);
    expect(await accountBalance(db.companyA, invA)).toBe(add(invAfterThird, "30.0000"));
    expect(await itemPosition(db.companyA, itemId)).toEqual({
      qtyOnHand: "30.0000", averageCost: "7.333333", value: "220.0000",
    });
    // ...and that is the same position as three receipts and no sale at all:
    // 50.00 + 70.00 + 100.00 = 220.00 over 30 units.
    expect(add(add("50.0000", "70.0000"), "100.0000")).toBe("220.0000");
    expect(sub(invBefore, invBefore)).toBe("0.0000");
    await expectBooksTie();

    // the compensating movement copied the ORIGINAL cost, not the new average
    const comp = await cA.query(
      `SELECT qty, unit_cost, value, reason FROM inventory_movements
        WHERE company_id = $1 AND item_id = $2 AND reverses_movement_id IS NOT NULL`,
      [db.companyA, itemId]);
    expect(comp.rows).toEqual([{
      qty: "5.0000", unit_cost: "6.000000", value: "30.0000", reason: "sale",
    }]);
  });

  it("can always be undone — a sale reversal only ever ADDS stock back", async () => {
    // The receipt rule ("you can't undo this, the stock has been used") has no
    // sales counterpart: putting units back can never drive the quantity below
    // zero. Proved on the hardest case — every unit sold, nothing left.
    const itemId = await createItem(db.companyA, { name: "Sold Out Widget" });
    await receive(db.companyA, itemId, "6", "3.00", "2024-03-01", "SOLDOUT-1");
    const customerId = await createCustomer(db.companyA, "Sold Out Buyer Co");
    const { invoiceId, issue } = await invoiceAndIssue(db.companyA, customerId, [
      { itemId, quantity: "6", unitPrice: "10.00" },
    ]);
    expect(issue.statusCode).toBe(200);
    expect(await itemPosition(db.companyA, itemId)).toEqual({
      qtyOnHand: "0.0000", averageCost: "0.000000", value: "0.0000",
    });
    await expectBooksTie();

    const voidRes = await post(app, db.companyA, `/api/c/invoices/${invoiceId}/void`,
      { date: "2024-06-05" });
    expect(voidRes.statusCode).toBe(200);
    expect(await itemPosition(db.companyA, itemId)).toEqual({
      qtyOnHand: "6.0000", averageCost: "3.000000", value: "18.0000",
    });
    await expectBooksTie();
  });

  it("still refuses to void an invoice with payments applied", async () => {
    const itemId = await createItem(db.companyA, { name: "Paid Sale Widget" });
    await receive(db.companyA, itemId, "5", "2.00", "2024-03-01", "PAIDSALE-1");
    const customerId = await createCustomer(db.companyA, "Paid Sale Buyer Co");
    const { invoiceId, issue } = await invoiceAndIssue(db.companyA, customerId, [
      { itemId, quantity: "5", unitPrice: "10.00" },
    ]);
    expect(issue.statusCode).toBe(200);

    const pay = await post(app, db.companyA, "/api/c/payments", {
      customerId, paymentDate: "2024-06-05", amount: "50.00",
      depositAccountId: String(db.accounts["1000"]!.A),
      applications: [{ invoiceId, amount: "50.00" }],
    });
    expect(pay.statusCode).toBe(201);

    const voidRes = await post(app, db.companyA, `/api/c/invoices/${invoiceId}/void`);
    expect(voidRes.statusCode).toBe(409);
    expect(voidRes.json().error).toMatch(/payments/i);
    expect((await itemPosition(db.companyA, itemId)).qtyOnHand).toBe("0.0000");
    await expectBooksTie();
  });

  it("still refuses a BARE entry reversal of a stock-bearing sale", async () => {
    const itemId = await createItem(db.companyA, { name: "Bare Reverse Sale Widget" });
    await receive(db.companyA, itemId, "5", "2.00", "2024-03-01", "BARESALE-1");
    const customerId = await createCustomer(db.companyA, "Bare Reverse Buyer Co");
    const { invoiceId, issue } = await invoiceAndIssue(db.companyA, customerId, [
      { itemId, quantity: "2", unitPrice: "10.00" },
    ]);
    expect(issue.statusCode).toBe(200);

    const detail = await get(app, db.companyA, `/api/c/invoices/${invoiceId}`);
    const res = await post(app, db.companyA,
      `/api/c/entries/${detail.json().postedEntryId}/reverse`, { date: "2024-06-10" });
    expect(res.statusCode).toBe(409);
    expect(res.json().error).toMatch(/moved stock/);
    await expectBooksTie();
  });
});

// ===========================================================================
// bills of materials
// ===========================================================================

describe("bills of materials", () => {
  it("sets and lists an assembly's parts, with each part's position", async () => {
    const bolt = await createItem(db.companyA, { name: "BOM Bolt" });
    const plate = await createItem(db.companyA, { name: "BOM Plate" });
    const bracket = await createItem(db.companyA, { name: "BOM Bracket" });
    await receive(db.companyA, bolt, "100", "0.25", "2024-03-01", "BOM-1");

    const set = await put(app, db.companyA, `/api/c/items/${bracket}/components`, {
      components: [{ itemId: bolt, qtyPer: "4" }, { itemId: plate, qtyPer: "1" }],
    });
    expect(set.statusCode).toBe(200);
    expect(set.json().components).toEqual([
      { id: expect.any(String), itemId: bolt, sku: null, name: "BOM Bolt",
        qtyPer: "4.0000", qtyOnHand: "100.0000", averageCost: "0.250000" },
      { id: expect.any(String), itemId: plate, sku: null, name: "BOM Plate",
        qtyPer: "1.0000", qtyOnHand: "0.0000", averageCost: "0.000000" },
    ]);

    const list = await get(app, db.companyA, `/api/c/items/${bracket}/components`);
    expect(list.statusCode).toBe(200);
    expect(list.json().components).toHaveLength(2);

    // replace-the-whole-set
    const replaced = await put(app, db.companyA, `/api/c/items/${bracket}/components`, {
      components: [{ itemId: bolt, qtyPer: "6" }],
    });
    expect(replaced.statusCode).toBe(200);
    expect(replaced.json().components).toHaveLength(1);
    expect(replaced.json().components[0].qtyPer).toBe("6.0000");
  });

  it("refuses a parts list that would make an item a part of itself, in English", async () => {
    const sub = await createItem(db.companyA, { name: "Recursion Sub" });
    const top = await createItem(db.companyA, { name: "Recursion Top" });
    expect((await put(app, db.companyA, `/api/c/items/${top}/components`, {
      components: [{ itemId: sub, qtyPer: "2" }],
    })).statusCode).toBe(200);

    const loop = await put(app, db.companyA, `/api/c/items/${sub}/components`, {
      components: [{ itemId: top, qtyPer: "1" }],
    });
    expect(loop.statusCode).toBe(422);
    expect(loop.json().error).toBe(
      "'Recursion Top' is already made from 'Recursion Sub' — adding it back would make the item a part of itself.");
  });

  it("refuses an item as a part of itself, in its own sentence", async () => {
    const self = await createItem(db.companyA, { name: "Self Part" });
    const res = await put(app, db.companyA, `/api/c/items/${self}/components`, {
      components: [{ itemId: self, qtyPer: "1" }],
    });
    expect(res.statusCode).toBe(422);
    expect(res.json().error).toBe("'Self Part' cannot be a part of itself.");
  });

  it("refuses a non-stocked part, and a non-stocked assembly", async () => {
    const labor = await createItem(db.companyA, { name: "BOM Labor", kind: "service" });
    const asm = await createItem(db.companyA, { name: "BOM Labored Assembly" });
    const bad = await put(app, db.companyA, `/api/c/items/${asm}/components`, {
      components: [{ itemId: labor, qtyPer: "1" }],
    });
    expect(bad.statusCode).toBe(422);
    expect(bad.json().error).toBe(
      "'BOM Labor' is not a stocked item, so it cannot be a part of 'BOM Labored Assembly'.");

    const part = await createItem(db.companyA, { name: "BOM Real Part" });
    const service = await createItem(db.companyA, { name: "BOM Service Assembly", kind: "service" });
    const bad2 = await put(app, db.companyA, `/api/c/items/${service}/components`, {
      components: [{ itemId: part, qtyPer: "1" }],
    });
    expect(bad2.statusCode).toBe(422);
    expect(bad2.json().error).toBe(
      "Only a stocked item can be built from parts — 'BOM Service Assembly' is not one.");
  });

  it("422s in English when the same part is listed twice", async () => {
    const part = await createItem(db.companyA, { name: "Dup Part" });
    const asm = await createItem(db.companyA, { name: "Dup Assembly" });
    const res = await put(app, db.companyA, `/api/c/items/${asm}/components`, {
      components: [{ itemId: part, qtyPer: "1" }, { itemId: part, qtyPer: "2" }],
    });
    expect(res.statusCode).toBe(422);
    expect(res.json().error).toBe("Each part can only be listed once — add up the quantity instead.");
  });

  it("names a part it cannot find, instead of leaking a placeholder", async () => {
    // REVIEW F3 (§9). assemblies_no_recursion() reads the component's name into
    // a variable that stays NULL when the row does not exist, and raises before
    // the composite foreign key ever gets a say — so an unknown id printed as
    // "'<NULL>' is not a stocked item". The route looks the parts up first.
    const asm = await createItem(db.companyA, { name: "Ghost Part Assembly" });
    const otherCompany = await createItem(db.companyB, { name: "B Only Part" });

    for (const bad of ["999999999", "0", otherCompany]) {
      const res = await put(app, db.companyA, `/api/c/items/${asm}/components`, {
        components: [{ itemId: bad, qtyPer: "1" }],
      });
      expect({ id: bad, status: res.statusCode }).toEqual({ id: bad, status: 422 });
      expect(res.json().error).toBe("We couldn't find one of those parts.");
      expect(res.json().error).not.toMatch(/NULL/);
    }

    // and nothing was written on the way to the refusal
    const list = await get(app, db.companyA, `/api/c/items/${asm}/components`);
    expect(list.json().components).toEqual([]);
  });

  it("404s (not 500) for another company's assembly (RLS)", async () => {
    const asm = await createItem(db.companyA, { name: "RLS Assembly" });
    expect((await get(app, db.companyB, `/api/c/items/${asm}/components`)).statusCode).toBe(404);
    expect((await put(app, db.companyB, `/api/c/items/${asm}/components`,
      { components: [] })).statusCode).toBe(404);
  });
});

// ===========================================================================
// builds
// ===========================================================================

describe("builds", () => {
  it("previews the component draw, the resulting unit cost, and whether it can go ahead", async () => {
    // 4 bolts at 0.25 and 1 plate at 3.00 per bracket; build 10.
    //   bolts:  40 needed of 100 on hand -> 40 x 0.25 = 10.00
    //   plates: 10 needed of  20 on hand -> 10 x 3.00 = 30.00
    //   total 40.00 over 10 brackets = 4.000000 each
    const bolt = await createItem(db.companyA, { name: "Preview Bolt" });
    const plate = await createItem(db.companyA, { name: "Preview Plate" });
    const bracket = await createItem(db.companyA, { name: "Preview Bracket" });
    await receive(db.companyA, bolt, "100", "0.25", "2024-03-01", "PREV-1");
    await receive(db.companyA, plate, "20", "3.00", "2024-03-01", "PREV-2");
    await put(app, db.companyA, `/api/c/items/${bracket}/components`, {
      components: [{ itemId: bolt, qtyPer: "4" }, { itemId: plate, qtyPer: "1" }],
    });

    const res = await post(app, db.companyA, "/api/c/builds/preview", {
      assemblyItemId: bracket, qty: "10",
    });
    expect(res.statusCode).toBe(200);
    expect(res.json()).toMatchObject({
      assemblyName: "Preview Bracket", qty: "10",
      totalValue: "40.0000", unitCost: "4.000000",
      canBuild: true, blockedBecause: null,
    });
    expect(res.json().components).toEqual([
      { itemId: bolt, sku: null, name: "Preview Bolt", qtyPer: "4.0000",
        qtyNeeded: "40.0000", qtyOnHand: "100.0000", averageCost: "0.250000",
        drawValue: "10.0000", sufficient: true },
      { itemId: plate, sku: null, name: "Preview Plate", qtyPer: "1.0000",
        qtyNeeded: "10.0000", qtyOnHand: "20.0000", averageCost: "3.000000",
        drawValue: "30.0000", sufficient: true },
    ]);

    // ...and says plainly when it cannot. 25 brackets needs exactly the 100
    // bolts on hand (enough, to the unit) but 25 plates against 20 (not).
    const tooMany = await post(app, db.companyA, "/api/c/builds/preview", {
      assemblyItemId: bracket, qty: "25",
    });
    expect(tooMany.json().canBuild).toBe(false);
    expect(tooMany.json().components.map((c: { name: string; sufficient: boolean }) =>
      `${c.name}:${c.sufficient}`)).toEqual(["Preview Bolt:true", "Preview Plate:false"]);
    // CLAUDE.md §9: this sentence is read on the panel above the Build button
    // AND on the disabled button's own tooltip, so it must not carry
    // numeric(19,4)'s trailing zeros ("needs 25.0000 and you have 20.0000" —
    // Task 5 review F3, the same leak already fixed on the build summary).
    expect(tooMany.json().blockedBecause).toBe(
      'You don\'t have enough "Preview Plate" — this build needs 25 and you have 20.');
  });

  it("keeps a FRACTIONAL shortfall exact in the blocking sentence", async () => {
    // Trimming trailing zeros must never trim meaning: half a plate short is
    // half a plate short, and 0.5 has to survive.
    const plate = await createItem(db.companyA, { name: "Fractional Plate" });
    const panel = await createItem(db.companyA, { name: "Fractional Panel" });
    await receive(db.companyA, plate, "2.5", "4.00", "2024-03-01", "FRACPREV-1");
    await put(app, db.companyA, `/api/c/items/${panel}/components`, {
      components: [{ itemId: plate, qtyPer: "1.5" }],
    });

    const res = await post(app, db.companyA, "/api/c/builds/preview", {
      assemblyItemId: panel, qty: "2",
    });
    expect(res.json().canBuild).toBe(false);
    // needs 3, has 2.5.
    expect(res.json().blockedBecause).toBe(
      'You don\'t have enough "Fractional Plate" — this build needs 3 and you have 2.5.');
  });

  it("posts a build across DIFFERENT inventory accounts: Dr assembly / Cr components", async () => {
    const bolt = await createItem(db.companyA, { name: "Two Acct Bolt" });          // 1300
    const bracket = await createItem(db.companyA, {                                  // 1310
      name: "Two Acct Bracket", inventoryAccountId: inv2A,
    });
    await receive(db.companyA, bolt, "100", "0.50", "2024-03-01", "TWOACCT-1");
    await put(app, db.companyA, `/api/c/items/${bracket}/components`, {
      components: [{ itemId: bolt, qtyPer: "4" }],
    });

    const invBefore = await accountBalance(db.companyA, invA);
    const inv2Before = await accountBalance(db.companyA, inv2A);

    const res = await post(app, db.companyA, "/api/c/builds", {
      assemblyItemId: bracket, qty: "10", builtOn: "2024-04-01", memo: "First run",
    });
    expect(res.statusCode).toBe(201);
    // 40 bolts at 0.50 = 20.00; 10 brackets at 2.000000 each.
    expect(res.json()).toMatchObject({
      buildNo: "1", qty: "10.0000", value: "20.0000", unitCost: "2.000000",
      status: "posted", summary: "Built 10 Two Acct Bracket — worth $20.00 in parts.",
    });

    expect(await accountBalance(db.companyA, invA)).toBe(sub(invBefore, "20.0000"));
    expect(await accountBalance(db.companyA, inv2A)).toBe(add(inv2Before, "20.0000"));
    expect(await itemPosition(db.companyA, bolt)).toMatchObject({ qtyOnHand: "60.0000" });
    expect(await itemPosition(db.companyA, bracket)).toEqual({
      qtyOnHand: "10.0000", averageCost: "2.000000", value: "20.0000",
    });
    await expectBooksTie();

    const lines = await cA.query(
      `SELECT a.code, l.debit, l.credit FROM journal_lines l
         JOIN accounts a ON a.company_id = l.company_id AND a.id = l.account_id
        WHERE l.company_id = $1 AND l.entry_id = $2 ORDER BY a.code`,
      [db.companyA, res.json().entryId]);
    expect(lines.rows).toEqual([
      { code: "1300", debit: "0.0000", credit: "20.0000" },
      { code: "1310", debit: "20.0000", credit: "0.0000" },
    ]);
  });

  it("posts a SAME-ACCOUNT build as a wash entry, leaving the account untouched", async () => {
    const wash = await createAccount(db.companyA, "1390", "Inventory - One Account", "asset");
    const bolt = await createItem(db.companyA, { name: "Wash Bolt", inventoryAccountId: wash });
    const bracket = await createItem(db.companyA, {
      name: "Wash Bracket", inventoryAccountId: wash,
    });
    await receive(db.companyA, bolt, "100", "1.00", "2024-03-01", "WASHB-1");
    await put(app, db.companyA, `/api/c/items/${bracket}/components`, {
      components: [{ itemId: bolt, qtyPer: "5" }],
    });

    const res = await post(app, db.companyA, "/api/c/builds", {
      assemblyItemId: bracket, qty: "4", builtOn: "2024-04-01",
    });
    expect(res.statusCode).toBe(201);
    // 20 bolts at 1.00 = 20.00.
    expect(res.json()).toMatchObject({ value: "20.0000", unitCost: "5.000000" });

    // the account's balance is untouched — and the build is still IN the
    // register, two lines, for exactly what it was worth.
    expect(await accountBalance(db.companyA, wash)).toBe("100.0000");
    const lines = await cA.query(
      `SELECT debit, credit FROM journal_lines
        WHERE company_id = $1 AND entry_id = $2 ORDER BY debit DESC`,
      [db.companyA, res.json().entryId]);
    expect(lines.rows).toEqual([
      { debit: "20.0000", credit: "0.0000" },
      { debit: "0.0000", credit: "20.0000" },
    ]);
    expect(await itemPosition(db.companyA, bolt)).toMatchObject({ qtyOnHand: "80.0000" });
    expect(await itemPosition(db.companyA, bracket)).toEqual({
      qtyOnHand: "4.0000", averageCost: "5.000000", value: "20.0000",
    });
    await expectBooksTie();
  });

  it("REFUSES A WRONG WASH — the one place the tie is blind", async () => {
    // Spec as-built decision 3: for a same-account build the per-account
    // deferred check degenerates to 0 = 0, so the wash's AMOUNT is
    // unconstrained by anything in Task 1. This proves it: a build worth $10
    // written with a $999,999 wash leaves inventory_out_of_tie() and
    // ledger_out_of_balance() BOTH at 0.0000 — and is refused anyway, at
    // COMMIT, by 0023's builds_entry_matches constraint trigger.
    const acct = await createAccount(db.companyA, "1391", "Inventory - Blind Spot", "asset");
    const bolt = await createItem(db.companyA, { name: "Blind Bolt", inventoryAccountId: acct });
    const bracket = await createItem(db.companyA, {
      name: "Blind Bracket", inventoryAccountId: acct,
    });
    await receive(db.companyA, bolt, "100", "1.00", "2024-03-01", "BLIND-1");

    async function handBuild(washAmount: string, buildNo: number): Promise<void> {
      await sysA.query("BEGIN");
      try {
        const entry = await sysA.query(
          `INSERT INTO journal_entries (company_id, entry_date, memo, created_by)
           VALUES ($1, '2024-04-01', 'hand-written build', $2) RETURNING id`,
          [db.companyA, db.userA]);
        const entryId = entry.rows[0].id;
        const build = await sysA.query(
          `INSERT INTO builds (company_id, build_no, assembly_item_id, qty, built_on,
                               entry_id, created_by)
           VALUES ($1, $2, $3, 2, '2024-04-01', $4, $5) RETURNING id`,
          [db.companyA, buildNo, bracket, entryId, db.userA]);
        const buildId = build.rows[0].id;
        await sysA.query(
          `SELECT record_movement($1, $2::bigint, '2024-04-01'::date, -10::numeric, NULL,
                                  'build_out', 'build', $3::bigint, $4::bigint)`,
          [db.companyA, bolt, buildId, entryId]);
        await sysA.query(
          `SELECT record_movement($1, $2::bigint, '2024-04-01'::date, 2::numeric, NULL,
                                  'build_in', 'build', $3::bigint, $4::bigint, $5::numeric)`,
          [db.companyA, bracket, buildId, entryId, "10.0000"]);
        await sysA.query(
          `INSERT INTO journal_lines (company_id, entry_id, account_id, debit, credit)
           VALUES ($1,$2,$3,$4,0), ($1,$2,$3,0,$4)`,
          [db.companyA, entryId, acct, washAmount]);
        await sysA.query("SELECT post_entry($1, $2)", [db.companyA, entryId]);
        await sysA.query("COMMIT");
      } catch (err) {
        await sysA.query("ROLLBACK").catch(() => { /* already rolled back */ });
        throw err;
      }
    }

    await expect(handBuild("999999.00", 900001)).rejects.toThrow(
      /a build's entry has to say exactly what the build was worth/);

    // A wash that is too SMALL is refused by the same rule, so this is not an
    // upper bound that a patient attacker walks under.
    await expect(handBuild("0.01", 900003)).rejects.toThrow(
      /a build's entry has to say exactly what the build was worth/);

    // nothing survived, and the honest version of the same build is accepted —
    // so the refusal is about the AMOUNT, not about the shape.
    expect(await accountBalance(db.companyA, acct)).toBe("100.0000");
    await expectBooksTie();
    await handBuild("10.0000", 900002);
    expect(await accountBalance(db.companyA, acct)).toBe("100.0000");
    expect(await itemPosition(db.companyA, bracket)).toMatchObject({
      qtyOnHand: "2.0000", value: "10.0000",
    });
    await expectBooksTie();
  });

  it("refuses a HALF-recorded build: stock left but nothing was recorded arriving", async () => {
    // The other side of the same rule. Once any stock has moved on a build's
    // entry, a build that recorded no finished units is half a fact — and the
    // entry it hangs off is otherwise perfectly legal (0022 only requires the
    // inventory legs to match the movements, which they do).
    const acct = await createAccount(db.companyA, "1392", "Inventory - Half Built", "asset");
    const bolt = await createItem(db.companyA, { name: "Half Bolt", inventoryAccountId: acct });
    const bracket = await createItem(db.companyA, {
      name: "Half Bracket", inventoryAccountId: acct,
    });
    await receive(db.companyA, bolt, "50", "1.00", "2024-03-01", "HALF-1");

    await expect((async () => {
      await sysA.query("BEGIN");
      try {
        const entry = await sysA.query(
          `INSERT INTO journal_entries (company_id, entry_date, memo, created_by)
           VALUES ($1, '2024-04-01', 'half a build', $2) RETURNING id`,
          [db.companyA, db.userA]);
        const entryId = entry.rows[0].id;
        const build = await sysA.query(
          `INSERT INTO builds (company_id, build_no, assembly_item_id, qty, built_on,
                               entry_id, created_by)
           VALUES ($1, 900101, $2, 2, '2024-04-01', $3, $4) RETURNING id`,
          [db.companyA, bracket, entryId, db.userA]);
        await sysA.query(
          `SELECT record_movement($1, $2::bigint, '2024-04-01'::date, -10::numeric, NULL,
                                  'build_out', 'build', $3::bigint, $4::bigint)`,
          [db.companyA, bolt, build.rows[0].id, entryId]);
        await sysA.query(
          `INSERT INTO journal_lines (company_id, entry_id, account_id, debit, credit)
           VALUES ($1,$2,$3,0,10), ($1,$2,$4,10,0)`,
          [db.companyA, entryId, acct, cogs2A]);
        await sysA.query("SELECT post_entry($1, $2)", [db.companyA, entryId]);
        await sysA.query("COMMIT");
      } catch (err) {
        await sysA.query("ROLLBACK").catch(() => { /* already rolled back */ });
        throw err;
      }
    })()).rejects.toThrow(/a build's journal entry has to say exactly what the build was worth|did not record what it was worth/);

    expect(await accountBalance(db.companyA, acct)).toBe("50.0000");
    await expectBooksTie();
  });

  it("refuses a build that would go past what is on hand, in English", async () => {
    const bolt = await createItem(db.companyA, { name: "Short Bolt" });
    const bracket = await createItem(db.companyA, { name: "Short Bracket" });
    await receive(db.companyA, bolt, "10", "1.00", "2024-03-01", "SHORT-1");
    await put(app, db.companyA, `/api/c/items/${bracket}/components`, {
      components: [{ itemId: bolt, qtyPer: "4" }],
    });

    const res = await post(app, db.companyA, "/api/c/builds", {
      assemblyItemId: bracket, qty: "5", builtOn: "2024-04-01",
    });
    expect(res.statusCode).toBe(422);
    expect(res.json().error).toBe("You only have 10 of Short Bolt on hand — this would use 20.");
    expect((await itemPosition(db.companyA, bolt)).qtyOnHand).toBe("10.0000");
    expect((await itemPosition(db.companyA, bracket)).qtyOnHand).toBe("0.0000");
    await expectBooksTie();
  });

  it("refuses to build an item with no parts list, in English", async () => {
    const lonely = await createItem(db.companyA, { name: "Partless Bracket" });
    const res = await post(app, db.companyA, "/api/c/builds", {
      assemblyItemId: lonely, qty: "1", builtOn: "2024-04-01",
    });
    expect(res.statusCode).toBe(422);
    expect(res.json().error).toBe(
      "'Partless Bracket' has no parts list yet — say what it is made from before you build it");
  });

  it("undoes a build both ways: the finished items come off, the parts go back on", async () => {
    const bolt = await createItem(db.companyA, { name: "Undo Bolt" });
    const bracket = await createItem(db.companyA, {
      name: "Undo Bracket", inventoryAccountId: inv2A,
    });
    await receive(db.companyA, bolt, "50", "2.00", "2024-03-01", "UNDOB-1");
    await put(app, db.companyA, `/api/c/items/${bracket}/components`, {
      components: [{ itemId: bolt, qtyPer: "5" }],
    });

    const invBefore = await accountBalance(db.companyA, invA);
    const inv2Before = await accountBalance(db.companyA, inv2A);

    const built = await post(app, db.companyA, "/api/c/builds", {
      assemblyItemId: bracket, qty: "4", builtOn: "2024-04-01",
    });
    expect(built.statusCode).toBe(201);
    expect(built.json().value).toBe("40.0000");   // 20 bolts at 2.00
    await expectBooksTie();

    const undo = await post(app, db.companyA, `/api/c/builds/${built.json().buildId}/reverse`,
      { date: "2024-04-05" });
    expect(undo.statusCode).toBe(200);
    expect(undo.json().summary).toMatch(/Undid build #/);

    expect(await accountBalance(db.companyA, invA)).toBe(invBefore);
    expect(await accountBalance(db.companyA, inv2A)).toBe(inv2Before);
    expect(await itemPosition(db.companyA, bolt)).toMatchObject({
      qtyOnHand: "50.0000", value: "100.0000",
    });
    expect(await itemPosition(db.companyA, bracket)).toEqual({
      qtyOnHand: "0.0000", averageCost: "0.000000", value: "0.0000",
    });
    await expectBooksTie();

    // a second undo is refused
    const again = await post(app, db.companyA, `/api/c/builds/${built.json().buildId}/reverse`,
      { date: "2024-04-06" });
    expect(again.statusCode).toBe(409);
    expect(again.json().error).toBe("That build has already been undone.");
  });

  it("refuses to undo a build whose output has already been sold, in the vocabulary of a build", async () => {
    const bolt = await createItem(db.companyA, { name: "Sold Build Bolt" });
    const bracket = await createItem(db.companyA, {
      name: "Sold Build Bracket", inventoryAccountId: inv2A, salesPrice: "50.00",
    });
    await receive(db.companyA, bolt, "50", "2.00", "2024-03-01", "SOLDB-1");
    await put(app, db.companyA, `/api/c/items/${bracket}/components`, {
      components: [{ itemId: bolt, qtyPer: "5" }],
    });
    const built = await post(app, db.companyA, "/api/c/builds", {
      assemblyItemId: bracket, qty: "4", builtOn: "2024-04-01",
    });
    expect(built.statusCode).toBe(201);

    const customerId = await createCustomer(db.companyA, "Sold Build Buyer Co");
    const { issue } = await invoiceAndIssue(db.companyA, customerId, [
      { itemId: bracket, quantity: "3", unitPrice: "50.00" },
    ]);
    expect(issue.statusCode).toBe(200);
    await expectBooksTie();

    const undo = await post(app, db.companyA, `/api/c/builds/${built.json().buildId}/reverse`,
      { date: "2024-06-10" });
    expect(undo.statusCode).toBe(409);
    expect(undo.json().error).toBe(
      "You can't undo build #" + built.json().buildNo +
      " — it made 4 of 'Sold Build Bracket' and only 1 are still on hand. " +
      "Record an inventory adjustment instead.");
    await expectBooksTie();
  });

  it("numbers builds per company, starting at 1", async () => {
    const bolt = await createItem(db.companyB, { name: "B Bolt" });
    const bracket = await createItem(db.companyB, { name: "B Bracket" });
    await receive(db.companyB, bolt, "50", "1.00", "2024-03-01", "BNUM-1");
    await put(app, db.companyB, `/api/c/items/${bracket}/components`, {
      components: [{ itemId: bolt, qtyPer: "2" }],
    });
    const res = await post(app, db.companyB, "/api/c/builds", {
      assemblyItemId: bracket, qty: "3", builtOn: "2024-04-01",
    });
    expect(res.statusCode).toBe(201);
    expect(res.json().buildNo).toBe("1");
    await expectBooksTie(db.companyB);
  });

  it("404s (not 500) for another company's assembly or build (RLS)", async () => {
    const asm = await createItem(db.companyA, { name: "RLS Build Assembly" });
    expect((await post(app, db.companyB, "/api/c/builds",
      { assemblyItemId: asm, qty: "1", builtOn: "2024-04-01" })).statusCode).toBe(404);
    expect((await post(app, db.companyB, "/api/c/builds/preview",
      { assemblyItemId: asm, qty: "1" })).statusCode).toBe(404);

    const list = await get(app, db.companyB, "/api/c/builds");
    expect(list.statusCode).toBe(200);
    const names = list.json().builds.map((b: { assemblyName: string }) => b.assemblyName);
    expect(names).not.toContain("Two Acct Bracket");
  });

  // Same §9 leak as the adjustment summary below — "Built 5.0000 Northgate
  // Cabinet" was on screen during Task 5's live verification. A fractional
  // build (half a batch) must still read exactly.
  it("writes the built quantity into the summary as English, not as numeric(19,4)", async () => {
    const rod = await createItem(db.companyA, { name: "Summary Rod" });
    const frame = await createItem(db.companyA, { name: "Summary Frame" });
    await receive(db.companyA, rod, "100", "1.00", "2024-03-01", "SUMBUILD-1");
    await put(app, db.companyA, `/api/c/items/${frame}/components`, {
      components: [{ itemId: rod, qtyPer: "4" }],
    });

    const whole = await post(app, db.companyA, "/api/c/builds", {
      assemblyItemId: frame, qty: "5", builtOn: "2024-04-01",
    });
    expect(whole.statusCode).toBe(201);
    expect(whole.json().summary).toBe("Built 5 Summary Frame — worth $20.00 in parts.");

    const half = await post(app, db.companyA, "/api/c/builds", {
      assemblyItemId: frame, qty: "0.5", builtOn: "2024-04-02",
    });
    expect(half.statusCode).toBe(201);
    expect(half.json().summary).toBe("Built 0.5 Summary Frame — worth $2.00 in parts.");
    await expectBooksTie();
  });
});

// ===========================================================================
// inventory adjustments
// ===========================================================================

describe("inventory adjustments", () => {
  it("adds stock found on the shelf, and takes stock that walked away", async () => {
    const itemId = await createItem(db.companyA, { name: "Adjust Widget" });
    await receive(db.companyA, itemId, "20", "2.00", "2024-03-01", "ADJ-1");

    const found = await post(app, db.companyA, "/api/c/inventory-adjustments", {
      itemId, date: "2024-04-01", quantity: "5", unitCost: "2.00",
      accountId: shrinkA, memo: "Found in the back",
    });
    expect(found.statusCode).toBe(201);
    expect(found.json()).toMatchObject({
      qty: "5.0000", value: "10.0000", qtyOnHand: "25.0000",
    });
    expect(found.json().summary).toBe("Added 5 Adjust Widget — $10.00 of value. 25 on hand.");
    await expectBooksTie();

    // 25 on hand at a flat 2.00; taking 3 out costs 6.00 at the running average
    const gone = await post(app, db.companyA, "/api/c/inventory-adjustments", {
      itemId, date: "2024-04-02", quantity: "-3",
      accountId: shrinkA, memo: "Breakage",
    });
    expect(gone.statusCode).toBe(201);
    expect(gone.json()).toMatchObject({
      qty: "-3.0000", value: "-6.0000", qtyOnHand: "22.0000",
    });
    await expectBooksTie();
    expect(await accountBalance(db.companyA, shrinkA)).toBe("-4.0000"); // -10 + 6
    expect(await itemPosition(db.companyA, itemId)).toEqual({
      qtyOnHand: "22.0000", averageCost: "2.000000", value: "44.0000",
    });
  });

  it("422s in English when a cost is given for stock going out", async () => {
    const itemId = await createItem(db.companyA, { name: "Out Cost Widget" });
    await receive(db.companyA, itemId, "10", "1.00", "2024-03-01", "OUTCOST-1");
    const res = await post(app, db.companyA, "/api/c/inventory-adjustments", {
      itemId, date: "2024-04-01", quantity: "-2", unitCost: "9.99",
      accountId: shrinkA, memo: "Trying to name a cost",
    });
    expect(res.statusCode).toBe(422);
    expect(res.json().error).toBe(
      "Stock going out is valued at what it already cost you on average, so leave the cost blank.");
  });

  it("422s in English when stock comes in with no cost", async () => {
    const itemId = await createItem(db.companyA, { name: "In No Cost Widget" });
    const res = await post(app, db.companyA, "/api/c/inventory-adjustments", {
      itemId, date: "2024-04-01", quantity: "2",
      accountId: shrinkA, memo: "No cost given",
    });
    expect(res.statusCode).toBe(422);
    expect(res.json().error).toBe(
      "Enter what this stock cost you — stock can't come in without a cost.");
  });

  it("400s in English without a reason", async () => {
    const itemId = await createItem(db.companyA, { name: "No Memo Widget" });
    const res = await post(app, db.companyA, "/api/c/inventory-adjustments", {
      itemId, date: "2024-04-01", quantity: "2", unitCost: "1.00", accountId: shrinkA,
    });
    expect(res.statusCode).toBe(400);
    expect(res.json().error).toMatch(/Say why the quantity is changing/);
  });

  it("refuses an inventory account on the other side, and an asset account, in English", async () => {
    const itemId = await createItem(db.companyA, { name: "Bad Side Widget" });
    await receive(db.companyA, itemId, "10", "1.00", "2024-03-01", "BADSIDE-1");

    const own = await post(app, db.companyA, "/api/c/inventory-adjustments", {
      itemId, date: "2024-04-01", quantity: "-1", accountId: invA, memo: "Wrong side",
    });
    expect(own.statusCode).toBe(422);
    expect(own.json().error).toMatch(/is this item's own inventory account/);

    const other = await post(app, db.companyA, "/api/c/inventory-adjustments", {
      itemId, date: "2024-04-01", quantity: "-1", accountId: inv2A, memo: "Wrong side",
    });
    expect(other.statusCode).toBe(422);
    expect(other.json().error).toMatch(/is another item's inventory account/);

    const cash = await post(app, db.companyA, "/api/c/inventory-adjustments", {
      itemId, date: "2024-04-01", quantity: "-1",
      accountId: String(db.accounts["1000"]!.A), memo: "Wrong side",
    });
    expect(cash.statusCode).toBe(422);
    expect(cash.json().error).toMatch(/posts to an expense, income or equity account/);
    await expectBooksTie();
  });

  it("refuses to take out more than is on hand", async () => {
    const itemId = await createItem(db.companyA, { name: "Adjust Oversell Widget" });
    await receive(db.companyA, itemId, "4", "1.00", "2024-03-01", "ADJOVER-1");
    const res = await post(app, db.companyA, "/api/c/inventory-adjustments", {
      itemId, date: "2024-04-01", quantity: "-9", accountId: shrinkA, memo: "Too many",
    });
    expect(res.statusCode).toBe(422);
    expect(res.json().error).toBe("You only have 4 of Adjust Oversell Widget on hand — this would use 9.");
    await expectBooksTie();
  });

  it("404s (not 500) for another company's item (RLS)", async () => {
    const itemId = await createItem(db.companyA, { name: "RLS Adjust Widget" });
    const res = await post(app, db.companyB, "/api/c/inventory-adjustments", {
      itemId, date: "2024-04-01", quantity: "1", unitCost: "1.00",
      accountId: String(db.accounts["6000"]!.B), memo: "Cross-company",
    });
    expect(res.statusCode).toBe(404);
  });

  // 0018's known limit 8(b), reached END TO END rather than only through the
  // mapper: stock received at zero cost carries no value, so §3.1 (no
  // zero-amount journal line) leaves an adjustment against it nowhere to go.
  // record_adjustment() refuses in good English — but until Task 5's review
  // errors.ts had no branch that could match it, so the route answered
  // "Something went wrong on our end. Nothing was saved.", which is both wrong
  // about whose fault it is and useless about what to do next.
  it("422s in the database's own English when the stock carries no value at all", async () => {
    const itemId = await createItem(db.companyA, { name: "Free Sample Widget" });
    // Free stock is real — a vendor's sample, a promotional unit. unitCostSchema
    // allows "0" precisely because of that.
    const free = await post(app, db.companyA, "/api/c/inventory-adjustments", {
      itemId, date: "2024-03-01", quantity: "10", unitCost: "0",
      accountId: shrinkA, memo: "Vendor samples, no charge",
    });
    expect(free.statusCode).toBe(422);
    expect(free.json().error).toContain("no value on hand");
    expect(free.json().error).not.toMatch(/went wrong on our end/i);
    await expectBooksTie();
  });

  // CLAUDE.md §9: the summary is a SENTENCE somebody reads. numeric(19,4)'s
  // trailing zeros are a storage detail and were leaking into it verbatim
  // ("Took 5.0000 Brass Hinge off the shelf ... 35.0000 left") — caught on
  // screen during Inventory Task 5's live verification. A genuinely fractional
  // quantity must still read exactly, so both directions are pinned here.
  it("writes quantities into the summary as English, not as numeric(19,4)", async () => {
    const itemId = await createItem(db.companyA, { name: "Summary Quantity Widget" });
    await receive(db.companyA, itemId, "40", "2.00", "2024-03-01", "SUMQTY-1");

    const gone = await post(app, db.companyA, "/api/c/inventory-adjustments", {
      itemId, date: "2024-04-01", quantity: "-5", accountId: shrinkA, memo: "Walked",
    });
    expect(gone.statusCode).toBe(201);
    expect(gone.json().summary)
      .toBe("Took 5 Summary Quantity Widget off the shelf — $10.00 of value. 35 left.");

    // ...and a real fraction keeps every digit that means something.
    const half = await post(app, db.companyA, "/api/c/inventory-adjustments", {
      itemId, date: "2024-04-02", quantity: "-0.5", accountId: shrinkA, memo: "Half a unit",
    });
    expect(half.statusCode).toBe(201);
    expect(half.json().summary)
      .toBe("Took 0.5 Summary Quantity Widget off the shelf — $1.00 of value. 34.5 left.");
    await expectBooksTie();
  });
});

// ===========================================================================
// opening quantities — ONE entry, both company shapes
// ===========================================================================

describe("opening quantities", () => {
  it("records a FRESH company's opening count against Opening Balance Equity", async () => {
    const acct = await createAccount(db.companyA, "1401", "Inventory - Fresh Opening", "asset");
    const bolts = await createItem(db.companyA, {
      name: "Fresh Opening Bolts", inventoryAccountId: acct,
    });
    const nuts = await createItem(db.companyA, {
      name: "Fresh Opening Nuts", inventoryAccountId: acct,
    });
    const equity = String(db.accounts["3000"]!.A);
    const equityBefore = await accountBalance(db.companyA, equity);

    // 400 bolts at 7.50 = 3,000.00 and 2,000 nuts at 1.00 = 2,000.00 -> 5,000.00
    const res = await post(app, db.companyA, "/api/c/inventory-opening", {
      date: "2024-01-01", sourceAccountId: equity, memo: "Opening count, 1 Jan",
      items: [
        { itemId: bolts, quantity: "400", unitCost: "7.50" },
        { itemId: nuts, quantity: "2000", unitCost: "1.00" },
      ],
    });
    expect(res.statusCode).toBe(201);
    expect(res.json()).toMatchObject({ itemCount: 2, totalValue: "5000.0000" });
    expect(res.json().summary).toBe(
      "Recorded 2 opening quantities worth $5,000.00, taken out of Opening Equity.");

    // ONE entry, two lines on the target and one on the source
    const lines = await cA.query(
      `SELECT a.code, l.debit, l.credit FROM journal_lines l
         JOIN accounts a ON a.company_id = l.company_id AND a.id = l.account_id
        WHERE l.company_id = $1 AND l.entry_id = $2 ORDER BY a.code`,
      [db.companyA, res.json().entryId]);
    expect(lines.rows).toEqual([
      { code: "1401", debit: "5000.0000", credit: "0.0000" },
      { code: "3000", debit: "0.0000", credit: "5000.0000" },
    ]);

    expect(await accountBalance(db.companyA, acct)).toBe("5000.0000");
    expect(await accountBalance(db.companyA, equity)).toBe(sub(equityBefore, "5000.0000"));
    expect(await itemPosition(db.companyA, bolts)).toEqual({
      qtyOnHand: "400.0000", averageCost: "7.500000", value: "3000.0000",
    });
    await expectBooksTie();

    const tie = await cA.query(
      "SELECT ledger_value, movement_value, difference FROM check_inventory_ties($1) WHERE account_id = $2",
      [db.companyA, acct]);
    expect(tie.rows[0]).toEqual({
      ledger_value: "5000.0000", movement_value: "5000.0000", difference: "0.0000",
    });
  });

  it("reclassifies a MIGRATED company's history-funded account in ONE entry", async () => {
    // CLAUDE.md §7 imports full history, so "1410 Inventory (imported)" already
    // carries $5,000 before any item exists — and 0021 refuses to let an item
    // adopt it. The cure is a reclassification onto a NEW account, and the whole
    // point of doing it as ONE entry is that the two-step version overstates
    // total assets in between.
    const oldAccount = await createAccount(db.companyA, "1410", "Inventory - Imported", "asset");
    const vendorId = await createVendor(db.companyA, "Migration Vendor Co");
    const bill = await post(app, db.companyA, "/api/c/bills", {
      vendorId, billDate: "2023-12-31", refNo: "MIG-1",
      lines: [{ categoryAccountId: oldAccount, unitPrice: "5000.00", description: "Imported history" }],
    });
    expect(bill.statusCode).toBe(201);
    expect((await post(app, db.companyA, `/api/c/bills/${bill.json().billId}/issue`)).statusCode)
      .toBe(200);
    expect(await accountBalance(db.companyA, oldAccount)).toBe("5000.0000");

    // 0021 refuses to point an item at it...
    const refused = await post(app, db.companyA, "/api/c/items", {
      name: "Migrated Widget Direct", kind: "inventory",
      inventoryAccountId: oldAccount, cogsAccountId: cogsA,
    });
    expect(refused.statusCode).toBe(422);
    expect(refused.json().error).toMatch(/already holds \$5000\.00 that no stock is recorded behind/);

    // ...so the items go on a NEW account and the opening count drains the old
    // one in a single entry.
    const newAccount = await createAccount(db.companyA, "1415", "Inventory - Stock", "asset");
    const bolts = await createItem(db.companyA, {
      name: "Migrated Bolts", inventoryAccountId: newAccount,
    });
    const nuts = await createItem(db.companyA, {
      name: "Migrated Nuts", inventoryAccountId: newAccount,
    });
    const equityBefore = await accountBalance(db.companyA, String(db.accounts["3000"]!.A));

    const res = await post(app, db.companyA, "/api/c/inventory-opening", {
      date: "2024-01-01", sourceAccountId: oldAccount,
      memo: "Reclassify imported inventory to stock accounts",
      items: [
        { itemId: bolts, quantity: "400", unitCost: "7.50" },
        { itemId: nuts, quantity: "2000", unitCost: "1.00" },
      ],
    });
    expect(res.statusCode).toBe(201);
    expect(res.json().totalValue).toBe("5000.0000");

    const lines = await cA.query(
      `SELECT a.code, l.debit, l.credit FROM journal_lines l
         JOIN accounts a ON a.company_id = l.company_id AND a.id = l.account_id
        WHERE l.company_id = $1 AND l.entry_id = $2 ORDER BY a.code`,
      [db.companyA, res.json().entryId]);
    expect(lines.rows).toEqual([
      { code: "1410", debit: "0.0000", credit: "5000.0000" },
      { code: "1415", debit: "5000.0000", credit: "0.0000" },
    ]);

    // The old account is drained, the new one is backed to the penny, equity is
    // untouched (it never entered the picture), and total assets never moved.
    expect(await accountBalance(db.companyA, oldAccount)).toBe("0.0000");
    expect(await accountBalance(db.companyA, newAccount)).toBe("5000.0000");
    expect(await accountBalance(db.companyA, String(db.accounts["3000"]!.A))).toBe(equityBefore);
    await expectBooksTie();

    const tie = await cA.query(
      "SELECT ledger_value, movement_value, difference FROM check_inventory_ties($1) WHERE account_id = $2",
      [db.companyA, newAccount]);
    expect(tie.rows[0]).toEqual({
      ledger_value: "5000.0000", movement_value: "5000.0000", difference: "0.0000",
    });
    // the drained account is nobody's inventory account, so it is not in the set
    const drained = await cA.query(
      "SELECT count(*)::int AS n FROM check_inventory_ties($1) WHERE account_id = $2",
      [db.companyA, oldAccount]);
    expect(drained.rows[0].n).toBe(0);
  });

  it("leaves a SHORT count's difference in the source account, and says so by arithmetic", async () => {
    // History said $2,000; the shelf says $1,600. The $400 difference is a
    // prior-period error and stays where it was — it is not hidden in an asset.
    const oldAccount = await createAccount(db.companyA, "1420", "Inventory - Short History", "asset");
    const vendorId = await createVendor(db.companyA, "Short History Vendor Co");
    const bill = await post(app, db.companyA, "/api/c/bills", {
      vendorId, billDate: "2023-12-31", refNo: "SHORTHIST-1",
      lines: [{ categoryAccountId: oldAccount, unitPrice: "2000.00", description: "History" }],
    });
    expect((await post(app, db.companyA, `/api/c/bills/${bill.json().billId}/issue`)).statusCode)
      .toBe(200);

    const newAccount = await createAccount(db.companyA, "1425", "Inventory - Short Stock", "asset");
    const itemId = await createItem(db.companyA, {
      name: "Short Count Widget", inventoryAccountId: newAccount,
    });

    const res = await post(app, db.companyA, "/api/c/inventory-opening", {
      date: "2024-01-01", sourceAccountId: oldAccount, memo: "Physical count came up short",
      items: [{ itemId, quantity: "160", unitCost: "10.00" }],
    });
    expect(res.statusCode).toBe(201);
    expect(res.json().totalValue).toBe("1600.0000");
    expect(await accountBalance(db.companyA, oldAccount)).toBe("400.0000");
    expect(await accountBalance(db.companyA, newAccount)).toBe("1600.0000");
    await expectBooksTie();
  });

  it("refuses an over-count that would push the source account negative", async () => {
    const oldAccount = await createAccount(db.companyA, "1430", "Inventory - Over History", "asset");
    const vendorId = await createVendor(db.companyA, "Over History Vendor Co");
    const bill = await post(app, db.companyA, "/api/c/bills", {
      vendorId, billDate: "2023-12-31", refNo: "OVERHIST-1",
      lines: [{ categoryAccountId: oldAccount, unitPrice: "1000.00", description: "History" }],
    });
    expect((await post(app, db.companyA, `/api/c/bills/${bill.json().billId}/issue`)).statusCode)
      .toBe(200);

    const newAccount = await createAccount(db.companyA, "1435", "Inventory - Over Stock", "asset");
    const itemId = await createItem(db.companyA, {
      name: "Over Count Widget", inventoryAccountId: newAccount,
    });

    const res = await post(app, db.companyA, "/api/c/inventory-opening", {
      date: "2024-01-01", sourceAccountId: oldAccount, memo: "Counted more than history had",
      items: [{ itemId, quantity: "150", unitCost: "10.00" }],
    });
    expect(res.statusCode).toBe(422);
    expect(res.json().error).toBe(
      "'Inventory - Over History' only holds $1000.00 as at 2024-01-01, but these opening " +
      "quantities come to $1500.00 — that would leave it with a negative balance. Check the " +
      "counts, check the date, or bring the difference in from Opening Balance Equity.");
    expect(await accountBalance(db.companyA, newAccount)).toBe("0.0000");
    await expectBooksTie();
  });

  it("refuses a source account that is some item's inventory account", async () => {
    const acct = await createAccount(db.companyA, "1440", "Inventory - Source Target", "asset");
    const itemId = await createItem(db.companyA, {
      name: "Source Target Widget", inventoryAccountId: acct,
    });
    const res = await post(app, db.companyA, "/api/c/inventory-opening", {
      date: "2024-01-01", sourceAccountId: invA, memo: "Wrong source",
      items: [{ itemId, quantity: "10", unitCost: "1.00" }],
    });
    expect(res.statusCode).toBe(422);
    expect(res.json().error).toBe(
      "'Inventory' is an item's inventory account, so the opening value cannot come out of it — " +
      "use Opening Balance Equity, or the account your imported history already put the value in.");
    await expectBooksTie();
  });

  it("refuses a BANK or CUSTOMER-BALANCES source, the way an adjustment already does", async () => {
    // REVIEW F1. record_adjustment() refuses an asset on the other side, and
    // 0018's decision 6 says exactly why: shrinkage posted to Cash silently
    // misstates the bank balance and corrupts the next reconciliation. 0022
    // then closed the manual-entry route into the same harm. This endpoint has
    // to name an asset source (a migrated company's history-funded account IS
    // one), so it must not become the door back into it.
    const bank = await createAccount(db.companyA, "1005", "Operating Checking", "asset", "bank");
    const ar = await createAccount(db.companyA, "1105", "Trade Receivables", "asset",
      "accounts-receivable");
    const vendorId = await createVendor(db.companyA, "Bank Source Vendor Co");
    for (const [acct, ref] of [[bank, "BANKSRC-1"], [ar, "ARSRC-1"]] as const) {
      const bill = await post(app, db.companyA, "/api/c/bills", {
        vendorId, billDate: "2023-12-31", refNo: ref,
        lines: [{ categoryAccountId: acct, unitPrice: "10000.00", description: "Funding" }],
      });
      expect((await post(app, db.companyA, `/api/c/bills/${bill.json().billId}/issue`))
        .statusCode).toBe(200);
    }

    const acct = await createAccount(db.companyA, "1470", "Inventory - Bank Source", "asset");
    const itemId = await createItem(db.companyA, {
      name: "Bank Source Widget", inventoryAccountId: acct,
    });

    const fromBank = await post(app, db.companyA, "/api/c/inventory-opening", {
      date: "2024-01-01", sourceAccountId: bank, memo: "Opening count",
      items: [{ itemId, quantity: "500", unitCost: "10.00" }],
    });
    expect(fromBank.statusCode).toBe(422);
    expect(fromBank.json().error).toBe(
      "'Operating Checking' is a bank account, so the opening value cannot come out of it — " +
      "use Opening Balance Equity, or the inventory account your imported history already put " +
      "the value in.");

    const fromAr = await post(app, db.companyA, "/api/c/inventory-opening", {
      date: "2024-01-01", sourceAccountId: ar, memo: "Opening count",
      items: [{ itemId, quantity: "500", unitCost: "10.00" }],
    });
    expect(fromAr.statusCode).toBe(422);
    expect(fromAr.json().error).toBe(
      "'Trade Receivables' is what customers owe you, so the opening value cannot come out of " +
      "it — use Opening Balance Equity, or the inventory account your imported history already " +
      "put the value in.");

    // neither balance moved, and nothing was recorded
    expect(await accountBalance(db.companyA, bank)).toBe("10000.0000");
    expect(await accountBalance(db.companyA, ar)).toBe("10000.0000");
    expect(await accountBalance(db.companyA, acct)).toBe("0.0000");
    await expectBooksTie();
  });

  it("checks the source account's balance AS AT the opening date, not today's", async () => {
    // REVIEW F2. The balance rule exists to stop an asset source being driven
    // negative. Summing every posted entry regardless of date defeats it: an
    // account funded in the future is "rich" today and bankrupt on the date the
    // opening entry actually lands, which shows up as a negative asset on every
    // balance sheet in between — and a movement-bearing entry cannot be
    // reversed, so it is not undoable. The realistic trigger is a migrated
    // company dating its opening count at the start of imported history.
    const late = await createAccount(db.companyA, "1475", "Inventory - Funded Later", "asset");
    const vendorId = await createVendor(db.companyA, "Late Funding Vendor Co");
    const bill = await post(app, db.companyA, "/api/c/bills", {
      vendorId, billDate: "2026-06-01", refNo: "LATEFUND-1",
      lines: [{ categoryAccountId: late, unitPrice: "3000.00", description: "History, late" }],
    });
    expect((await post(app, db.companyA, `/api/c/bills/${bill.json().billId}/issue`))
      .statusCode).toBe(200);
    expect(await accountBalance(db.companyA, late)).toBe("3000.0000");

    const acct = await createAccount(db.companyA, "1480", "Inventory - Backdated", "asset");
    const itemId = await createItem(db.companyA, {
      name: "Backdated Widget", inventoryAccountId: acct,
    });

    const res = await post(app, db.companyA, "/api/c/inventory-opening", {
      date: "2020-01-01", sourceAccountId: late, memo: "Opening count at the start of history",
      items: [{ itemId, quantity: "300", unitCost: "10.00" }],
    });
    expect(res.statusCode).toBe(422);
    expect(res.json().error).toBe(
      "'Inventory - Funded Later' only holds $0.00 as at 2020-01-01, but these opening " +
      "quantities come to $3000.00 — that would leave it with a negative balance. Check the " +
      "counts, check the date, or bring the difference in from Opening Balance Equity.");
    expect(await accountBalance(db.companyA, acct)).toBe("0.0000");
    await expectBooksTie();

    // dated on or after the day the value was really there, it goes through
    const ok = await post(app, db.companyA, "/api/c/inventory-opening", {
      date: "2026-06-01", sourceAccountId: late, memo: "Opening count at cutover",
      items: [{ itemId, quantity: "300", unitCost: "10.00" }],
    });
    expect(ok.statusCode).toBe(201);
    expect(await accountBalance(db.companyA, late)).toBe("0.0000");
    expect(await accountBalance(db.companyA, acct)).toBe("3000.0000");
    await expectBooksTie();
  });

  it("refuses a source account of a kind that makes no sense", async () => {
    const acct = await createAccount(db.companyA, "1445", "Inventory - Kind Check", "asset");
    const itemId = await createItem(db.companyA, {
      name: "Kind Check Widget", inventoryAccountId: acct,
    });
    const income = await post(app, db.companyA, "/api/c/inventory-opening", {
      date: "2024-01-01", sourceAccountId: incomeA, memo: "From revenue?",
      items: [{ itemId, quantity: "10", unitCost: "1.00" }],
    });
    expect(income.statusCode).toBe(422);
    expect(income.json().error).toBe(
      "opening quantities come from Opening Balance Equity on a fresh set of books, or from the " +
      "account your imported history already put the value in — 'Revenue' is an income account.");

    const liability = await post(app, db.companyA, "/api/c/inventory-opening", {
      date: "2024-01-01", sourceAccountId: String(db.accounts["2000"]!.A), memo: "From A/P?",
      items: [{ itemId, quantity: "10", unitCost: "1.00" }],
    });
    expect(liability.statusCode).toBe(422);
    expect(liability.json().error).toMatch(/is a liability account\.$/);
    await expectBooksTie();
  });

  it("refuses an item that already has stock history, and a duplicated item", async () => {
    const acct = await createAccount(db.companyA, "1450", "Inventory - Already Moved", "asset");
    const itemId = await createItem(db.companyA, {
      name: "Already Moved Widget", inventoryAccountId: acct,
    });
    await receive(db.companyA, itemId, "5", "1.00", "2024-03-01", "ALREADY-1");

    const res = await post(app, db.companyA, "/api/c/inventory-opening", {
      date: "2024-01-01", sourceAccountId: String(db.accounts["3000"]!.A),
      memo: "Late opening count",
      items: [{ itemId, quantity: "10", unitCost: "1.00" }],
    });
    expect(res.statusCode).toBe(409);
    expect(res.json().error).toBe(
      "'Already Moved Widget' already has stock history, so this is not its opening count — " +
      "record an inventory adjustment instead");

    const other = await createItem(db.companyA, {
      name: "Dup Opening Widget", inventoryAccountId: acct,
    });
    const dup = await post(app, db.companyA, "/api/c/inventory-opening", {
      date: "2024-01-01", sourceAccountId: String(db.accounts["3000"]!.A), memo: "Counted twice",
      items: [
        { itemId: other, quantity: "10", unitCost: "1.00" },
        { itemId: other, quantity: "5", unitCost: "1.00" },
      ],
    });
    expect(dup.statusCode).toBe(422);
    expect(dup.json().error).toBe("Each item can only be counted once.");
    await expectBooksTie();
  });

  it("422s (not 500) for another company's source account (RLS)", async () => {
    const itemId = await createItem(db.companyB, { name: "RLS Opening Widget" });
    const res = await post(app, db.companyB, "/api/c/inventory-opening", {
      date: "2024-01-01", sourceAccountId: String(db.accounts["3000"]!.A),
      memo: "Cross-company source",
      items: [{ itemId, quantity: "1", unitCost: "1.00" }],
    });
    expect(res.statusCode).toBe(422);
    expect(res.json().error).toBe("Pick the account this opening value comes from.");
    await expectBooksTie(db.companyB);
  });
});

// ===========================================================================
// the double-count guard (0025)
//
// CLAUDE.md §7: a company importing full transaction history must NOT also post
// an opening balance, because "history either replaces it or does not happen.
// Doing both double-counts everything." Opening QUANTITIES are that same trap:
// the imported history already recorded every purchase of the stock, so taking
// the opening value out of equity CREATES the value a second time.
//
// Nothing else catches it. The new inventory account is empty, so 0021's
// empty-or-backed rule passes; equity is a legal source, so 0023 passes; equity
// is not a bank account, so 0024 passes; and the ledger tie reads 0.0000
// throughout, because stock and ledger genuinely do agree — on a number that is
// counted twice. Total assets are simply overstated, permanently: the entry
// carries movements, so reverse_entry() correctly refuses to undo it.
//
// Company B is used for the imported-history cases so that company A's existing
// equity-sourced opening counts keep proving the no-history path is untouched.
// ===========================================================================

describe("opening quantities on a company that imported its history", () => {
  /** An entry that came in through the import pipeline: post_batch() (0003) is
   *  the only thing that ever writes journal_entries.source, from
   *  import_batches.source, which is exactly what makes it a precise signal. */
  async function seedImportedHistory(): Promise<void> {
    await cB.query("BEGIN");
    const er = await cB.query<{ id: string }>(
      `INSERT INTO journal_entries
         (company_id, entry_date, memo, source, external_ref, created_by)
       VALUES ($1, '2023-12-31', 'Imported: opening inventory', 'quickbooks_journal', 'QB-1', $2)
       RETURNING id`,
      [db.companyB, db.userB]);
    const entryId = er.rows[0]!.id;
    await cB.query(
      `INSERT INTO journal_lines (company_id, entry_id, account_id, debit, credit)
       VALUES ($1, $2, $3, '5000.0000', 0), ($1, $2, $4, 0, '5000.0000')`,
      [db.companyB, entryId, db.accounts["1200"]!.B, db.accounts["3000"]!.B]);
    await cB.query("SELECT post_entry($1, $2)", [db.companyB, entryId]);
    await cB.query("COMMIT");
  }

  it("refuses an EQUITY source, naming the risk and the alternative", async () => {
    await seedImportedHistory();
    const acct = await createAccount(db.companyB, "1405", "Inventory - Migrated New", "asset");
    const widget = await createItem(db.companyB, {
      name: "Double Count Widget", inventoryAccountId: acct,
    });

    const res = await post(app, db.companyB, "/api/c/inventory-opening", {
      date: "2024-01-01", sourceAccountId: String(db.accounts["3000"]!.B),
      memo: "Opening count", items: [{ itemId: widget, quantity: "10", unitCost: "5.00" }],
    });
    expect(res.statusCode).toBe(409);
    expect(res.json().error).toContain("This company brought in transaction history");
    expect(res.json().error).toContain("would count the same inventory twice");
    // ...and it says what to do instead, not merely that this is wrong.
    expect(res.json().error).toContain("use the account your imported history put the value in");
    // Flagged STRUCTURALLY so the screen can offer the answer without matching
    // on English prose.
    expect(res.json().needsAcknowledgement).toBe(true);

    // Nothing was written: no entry, no movement, no stock.
    expect(await itemPosition(db.companyB, widget)).toMatchObject({ qtyOnHand: "0.0000" });
    expect(await accountBalance(db.companyB, acct)).toBe("0.0000");
    await expectBooksTie(db.companyB);
  });

  it("goes ahead once the caller says it understands", async () => {
    const acct = await createAccount(db.companyB, "1406", "Inventory - Ack", "asset");
    const widget = await createItem(db.companyB, {
      name: "Acknowledged Widget", inventoryAccountId: acct,
    });
    const equity = String(db.accounts["3000"]!.B);
    const equityBefore = await accountBalance(db.companyB, equity);

    const res = await post(app, db.companyB, "/api/c/inventory-opening", {
      date: "2024-01-01", sourceAccountId: equity, memo: "Stock really is new",
      acknowledged: true,
      items: [{ itemId: widget, quantity: "10", unitCost: "5.00" }],
    });
    expect(res.statusCode).toBe(201);
    expect(res.json()).toMatchObject({ itemCount: 1, totalValue: "50.0000" });
    expect(await accountBalance(db.companyB, acct)).toBe("50.0000");
    expect(await accountBalance(db.companyB, equity)).toBe(sub(equityBefore, "50.0000"));
    await expectBooksTie(db.companyB);
  });

  it("leaves the MIGRATED shape alone — an asset source never trips it", async () => {
    // The right answer for a migrated company: drain the history-funded
    // account. It is an asset source, so the equity guard has nothing to say
    // about it, acknowledged or not.
    const oldAcct = await createAccount(db.companyB, "1407", "Inventory - Old History", "asset");
    const newAcct = await createAccount(db.companyB, "1408", "Inventory - New Home", "asset");
    const widget = await createItem(db.companyB, {
      name: "Migrated Path Widget", inventoryAccountId: newAcct,
    });
    // Fund the old account the way imported history would have.
    await cB.query("BEGIN");
    const er = await cB.query<{ id: string }>(
      `INSERT INTO journal_entries
         (company_id, entry_date, memo, source, external_ref, created_by)
       VALUES ($1, '2023-12-31', 'Imported: stock on hand', 'quickbooks_journal', 'QB-2', $2)
       RETURNING id`,
      [db.companyB, db.userB]);
    await cB.query(
      `INSERT INTO journal_lines (company_id, entry_id, account_id, debit, credit)
       VALUES ($1, $2, $3, '300.0000', 0), ($1, $2, $4, 0, '300.0000')`,
      [db.companyB, er.rows[0]!.id, oldAcct, db.accounts["3000"]!.B]);
    await cB.query("SELECT post_entry($1, $2)", [db.companyB, er.rows[0]!.id]);
    await cB.query("COMMIT");

    const res = await post(app, db.companyB, "/api/c/inventory-opening", {
      date: "2024-01-01", sourceAccountId: oldAcct, memo: "Reclassified at cutover",
      items: [{ itemId: widget, quantity: "60", unitCost: "5.00" }],
    });
    expect(res.statusCode).toBe(201);
    expect(res.json().needsAcknowledgement).toBeUndefined();
    // The value MOVED: old account drained, new one holds it, assets unchanged.
    expect(await accountBalance(db.companyB, oldAcct)).toBe("0.0000");
    expect(await accountBalance(db.companyB, newAcct)).toBe("300.0000");
    await expectBooksTie(db.companyB);
  });

  it("leaves a company with NO imported history completely unaffected", async () => {
    // Company A has never imported anything, so the equity path stays open with
    // no acknowledgement and no warning — the guard must not become a nag.
    const acct = await createAccount(db.companyA, "1409", "Inventory - No History", "asset");
    const widget = await createItem(db.companyA, {
      name: "No History Widget", inventoryAccountId: acct,
    });
    const res = await post(app, db.companyA, "/api/c/inventory-opening", {
      date: "2024-01-01", sourceAccountId: String(db.accounts["3000"]!.A),
      memo: "Fresh books", items: [{ itemId: widget, quantity: "4", unitCost: "2.50" }],
    });
    expect(res.statusCode).toBe(201);
    expect(res.json().needsAcknowledgement).toBeUndefined();
    expect(await accountBalance(db.companyA, acct)).toBe("10.0000");
    await expectBooksTie();
  });

  it("keeps refusing a bank source even WITH the acknowledgement (0024 still rules)", async () => {
    // The acknowledgement answers exactly one question. It is not a skeleton
    // key: draining a bank account is still refused, because that misstates a
    // balance somebody reconciles.
    const acct = await createAccount(db.companyB, "1410", "Inventory - Bank Try", "asset");
    const widget = await createItem(db.companyB, {
      name: "Bank Source Widget", inventoryAccountId: acct,
    });
    await cB.query(
      "UPDATE accounts SET subtype = 'bank' WHERE company_id = $1 AND id = $2",
      [db.companyB, db.accounts["1000"]!.B]);
    const res = await post(app, db.companyB, "/api/c/inventory-opening", {
      date: "2024-01-01", sourceAccountId: String(db.accounts["1000"]!.B),
      memo: "Out of the bank", acknowledged: true,
      items: [{ itemId: widget, quantity: "1", unitCost: "1.00" }],
    });
    expect(res.statusCode).toBe(422);
    expect(res.json().error).toContain("is a bank account, so the opening value cannot come out of it");
    await expectBooksTie(db.companyB);
  });
});

// ===========================================================================
// reports
// ===========================================================================

describe("inventory reports", () => {
  it("shows the valuation with the ledger tie on its face, per account", async () => {
    const res = await get(app, db.companyA, "/api/c/reports/inventory-valuation");
    expect(res.statusCode).toBe(200);
    const body = res.json();

    expect(body.outOfTie).toBe("0.0000");
    expect(body.ties).toBe(true);
    expect(body.accounts.length).toBeGreaterThan(1);
    for (const a of body.accounts) {
      expect(`${a.code}:${a.difference}`).toBe(`${a.code}:0.0000`);
      expect(a.ledgerValue).toBe(a.movementValue);
    }

    // every per-account ledger figure is the account's real balance
    for (const a of body.accounts) {
      expect(await accountBalance(db.companyA, a.accountId)).toBe(a.ledgerValue);
    }

    // and the report's own items sum to its own total
    const summed = body.items.reduce((acc: bigint, i: { value: string }) =>
      acc + toMicros(i.value), 0n);
    expect(fromMicros(summed)).toBe(body.totalValue);
  });

  it("reports item profitability from what actually posted, and drops a voided sale", async () => {
    // A fresh customer and item so the arithmetic below is only about them.
    // Receive 20 @ 4.00; sell 5 @ 15.00 -> revenue 75.00, cost 20.00, margin 55.00.
    const itemId = await createItem(db.companyA, { name: "Profit Widget", salesPrice: "15.00" });
    await receive(db.companyA, itemId, "20", "4.00", "2024-08-01", "PROFIT-1");
    const customerId = await createCustomer(db.companyA, "Profit Buyer Co");

    const sale = await post(app, db.companyA, "/api/c/invoices", {
      customerId, invoiceDate: "2024-08-10",
      lines: [{ itemId, quantity: "5", unitPrice: "15.00" }],
    });
    expect((await post(app, db.companyA, `/api/c/invoices/${sale.json().invoiceId}/issue`))
      .statusCode).toBe(200);

    const voided = await post(app, db.companyA, "/api/c/invoices", {
      customerId, invoiceDate: "2024-08-11",
      lines: [{ itemId, quantity: "2", unitPrice: "15.00" }],
    });
    expect((await post(app, db.companyA, `/api/c/invoices/${voided.json().invoiceId}/issue`))
      .statusCode).toBe(200);
    expect((await post(app, db.companyA, `/api/c/invoices/${voided.json().invoiceId}/void`,
      { date: "2024-08-12" })).statusCode).toBe(200);
    await expectBooksTie();

    const res = await get(app, db.companyA,
      "/api/c/reports/item-profitability?from=2024-08-01&to=2024-08-31");
    expect(res.statusCode).toBe(200);
    const row = res.json().items.find((i: { name: string }) => i.name === "Profit Widget");
    expect(row).toEqual({
      itemId, sku: null, name: "Profit Widget", kind: "inventory",
      unitsSold: "5.0000", revenue: "75.0000", cogs: "20.0000", margin: "55.0000",
    });

    // margin is revenue minus cost, everywhere, including the totals
    const totals = res.json();
    expect(sub(totals.revenueTotal, totals.cogsTotal)).toBe(totals.marginTotal);

    // outside the range it disappears entirely
    const outside = await get(app, db.companyA,
      "/api/c/reports/item-profitability?from=2025-01-01&to=2025-12-31");
    expect(outside.json().items).toEqual([]);
    expect(outside.json().marginTotal).toBe("0.0000");
  });

  it("keeps both reports inside their own company (RLS)", async () => {
    const a = await get(app, db.companyA, "/api/c/reports/inventory-valuation");
    const b = await get(app, db.companyB, "/api/c/reports/inventory-valuation");
    expect(b.statusCode).toBe(200);
    const aNames = a.json().items.map((i: { name: string }) => i.name);
    const bNames = b.json().items.map((i: { name: string }) => i.name);
    expect(aNames).toContain("Sell Widget");
    expect(bNames).not.toContain("Sell Widget");

    const profB = await get(app, db.companyB, "/api/c/reports/item-profitability");
    expect(profB.statusCode).toBe(200);
    expect(profB.json().items.map((i: { name: string }) => i.name)).not.toContain("Profit Widget");
  });
});

// ===========================================================================
// the whole loop, on one manufacturer, with the tie asserted after every step
// ===========================================================================

describe("receive → build → sell → void → adjust → undo the build", () => {
  it("walks the whole lifecycle and lands exactly where it started", async () => {
    // Its own two inventory accounts so every figure below is only about this
    // scenario. Hand-computed throughout:
    //
    //   1. receive 100 bolts @ 2.00          parts 200.00   finished   0.00
    //   2. build 10 brackets (5 bolts each)  parts 100.00   finished 100.00
    //        50 bolts x 2.00 = 100.00 -> 10 brackets at 10.000000 each
    //   3. sell 4 brackets @ 60.00           finished  60.00, COGS 40.00
    //        revenue 240.00, cost 4 x 10.00 = 40.00
    //   4. void that sale                    finished 100.00, COGS  0.00
    //        the 40.00 that left comes back, at the cost that left
    //   5. one bracket walks off             finished  90.00, shrinkage 10.00
    //   6. undoing the build is REFUSED — only 9 of the 10 it made are here
    //   7. the missing bracket is found      finished 100.00, shrinkage  0.00
    //   8. undo the build                    parts 200.00   finished   0.00
    const parts = await createAccount(db.companyA, "1301", "Inventory - Lifecycle Parts", "asset");
    const finished = await createAccount(db.companyA, "1311", "Inventory - Lifecycle Finished", "asset");
    const bolt = await createItem(db.companyA, {
      name: "Lifecycle Bolt", inventoryAccountId: parts,
    });
    const bracket = await createItem(db.companyA, {
      name: "Lifecycle Bracket", inventoryAccountId: finished, salesPrice: "60.00",
    });
    const cogsBefore = await accountBalance(db.companyA, cogsA);
    const shrinkBefore = await accountBalance(db.companyA, shrinkA);

    // 1
    await receive(db.companyA, bolt, "100", "2.00", "2024-03-01", "LIFE-1");
    expect(await accountBalance(db.companyA, parts)).toBe("200.0000");
    await expectBooksTie();

    // 2
    await put(app, db.companyA, `/api/c/items/${bracket}/components`, {
      components: [{ itemId: bolt, qtyPer: "5" }],
    });
    const built = await post(app, db.companyA, "/api/c/builds", {
      assemblyItemId: bracket, qty: "10", builtOn: "2024-03-05", memo: "Run 1",
    });
    expect(built.statusCode).toBe(201);
    expect(built.json()).toMatchObject({ value: "100.0000", unitCost: "10.000000" });
    expect(await accountBalance(db.companyA, parts)).toBe("100.0000");
    expect(await accountBalance(db.companyA, finished)).toBe("100.0000");
    expect(await itemPosition(db.companyA, bracket)).toEqual({
      qtyOnHand: "10.0000", averageCost: "10.000000", value: "100.0000",
    });
    await expectBooksTie();

    // 3
    const customerId = await createCustomer(db.companyA, "Lifecycle Buyer Co");
    const sale = await post(app, db.companyA, "/api/c/invoices", {
      customerId, invoiceDate: "2024-03-10",
      lines: [{ itemId: bracket, quantity: "4" }],   // price comes from the item
    });
    expect(sale.statusCode).toBe(201);
    expect(sale.json().total).toBe("240.0000");
    expect((await post(app, db.companyA, `/api/c/invoices/${sale.json().invoiceId}/issue`))
      .statusCode).toBe(200);
    expect(await accountBalance(db.companyA, finished)).toBe("60.0000");
    expect(await accountBalance(db.companyA, cogsA)).toBe(add(cogsBefore, "40.0000"));
    await expectBooksTie();

    // 4
    expect((await post(app, db.companyA, `/api/c/invoices/${sale.json().invoiceId}/void`,
      { date: "2024-03-12" })).statusCode).toBe(200);
    expect(await accountBalance(db.companyA, finished)).toBe("100.0000");
    expect(await accountBalance(db.companyA, cogsA)).toBe(cogsBefore);
    await expectBooksTie();

    // 5
    const lost = await post(app, db.companyA, "/api/c/inventory-adjustments", {
      itemId: bracket, date: "2024-03-15", quantity: "-1",
      accountId: shrinkA, memo: "One walked off the bench",
    });
    expect(lost.statusCode).toBe(201);
    expect(lost.json().value).toBe("-10.0000");
    expect(await accountBalance(db.companyA, finished)).toBe("90.0000");
    expect(await accountBalance(db.companyA, shrinkA)).toBe(add(shrinkBefore, "10.0000"));
    await expectBooksTie();

    // 6
    const tooEarly = await post(app, db.companyA,
      `/api/c/builds/${built.json().buildId}/reverse`, { date: "2024-03-16" });
    expect(tooEarly.statusCode).toBe(409);
    expect(tooEarly.json().error).toMatch(
      /it made 10 of 'Lifecycle Bracket' and only 9 are still on hand/);
    await expectBooksTie();

    // 7
    const found = await post(app, db.companyA, "/api/c/inventory-adjustments", {
      itemId: bracket, date: "2024-03-16", quantity: "1", unitCost: "10.00",
      accountId: shrinkA, memo: "Found it under the bench",
    });
    expect(found.statusCode).toBe(201);
    expect(await accountBalance(db.companyA, finished)).toBe("100.0000");
    expect(await accountBalance(db.companyA, shrinkA)).toBe(shrinkBefore);
    await expectBooksTie();

    // 8
    const undone = await post(app, db.companyA,
      `/api/c/builds/${built.json().buildId}/reverse`, { date: "2024-03-20" });
    expect(undone.statusCode).toBe(200);
    expect(await accountBalance(db.companyA, parts)).toBe("200.0000");
    expect(await accountBalance(db.companyA, finished)).toBe("0.0000");
    expect(await itemPosition(db.companyA, bolt)).toEqual({
      qtyOnHand: "100.0000", averageCost: "2.000000", value: "200.0000",
    });
    expect(await itemPosition(db.companyA, bracket)).toEqual({
      qtyOnHand: "0.0000", averageCost: "0.000000", value: "0.0000",
    });
    await expectBooksTie();

    // exactly where it started, on both accounts and both derivations
    const ties = await cA.query(
      `SELECT code, ledger_value, movement_value, difference
         FROM check_inventory_ties($1) WHERE account_id = ANY($2::bigint[]) ORDER BY code`,
      [db.companyA, [parts, finished]]);
    expect(ties.rows).toEqual([
      { code: "1301", ledger_value: "200.0000", movement_value: "200.0000", difference: "0.0000" },
      { code: "1311", ledger_value: "0.0000", movement_value: "0.0000", difference: "0.0000" },
    ]);
  });
});

// ===========================================================================
// period locks — LAST, because period_locks is append-only and every date used
// above would fall inside the lock this block sets (same reason and placement
// as tests/server/inventory-api.test.ts's own period-lock block).
// ===========================================================================

describe("period locks", () => {
  it("refuses a stocked SALE dated inside a locked period, in English", async () => {
    const itemId = await createItem(db.companyA, { name: "Locked Sale Widget" });
    await receive(db.companyA, itemId, "10", "2.00", "2024-09-01", "LOCKSALE-1");
    const customerId = await createCustomer(db.companyA, "Locked Sale Buyer Co");

    const create = await post(app, db.companyA, "/api/c/invoices", {
      customerId, invoiceDate: "2024-09-15",
      lines: [{ itemId, quantity: "2", unitPrice: "9.00" }],
    });
    expect(create.statusCode).toBe(201);

    await db.admin.query(
      "INSERT INTO period_locks (company_id, locked_through, created_by) VALUES ($1, $2, $3)",
      [db.companyA, "2024-09-30", db.userA]);

    const issue = await post(app, db.companyA, `/api/c/invoices/${create.json().invoiceId}/issue`);
    expect(issue.statusCode).toBe(409);
    expect(issue.json().error).toBe(
      "That year is locked (through 2024-09-30); record it in the current year or unlock the period first.");

    // nothing posted and nothing moved
    expect((await itemPosition(db.companyA, itemId)).qtyOnHand).toBe("10.0000");
    await expectBooksTie();
  });

  it("refuses a BUILD dated inside a locked period, in English", async () => {
    const bolt = await createItem(db.companyA, { name: "Locked Build Bolt" });
    const bracket = await createItem(db.companyA, { name: "Locked Build Bracket" });
    await receive(db.companyA, bolt, "50", "1.00", "2024-10-01", "LOCKBUILD-1");
    await put(app, db.companyA, `/api/c/items/${bracket}/components`, {
      components: [{ itemId: bolt, qtyPer: "5" }],
    });

    const res = await post(app, db.companyA, "/api/c/builds", {
      assemblyItemId: bracket, qty: "2", builtOn: "2024-09-15",
    });
    expect(res.statusCode).toBe(409);
    expect(res.json().error).toMatch(/That year is locked \(through 2024-09-30\)/);
    expect((await itemPosition(db.companyA, bracket)).qtyOnHand).toBe("0.0000");
    await expectBooksTie();
  });

  it("refuses an ADJUSTMENT and an OPENING COUNT inside a locked period too", async () => {
    const itemId = await createItem(db.companyA, { name: "Locked Adjust Widget" });
    await receive(db.companyA, itemId, "10", "1.00", "2024-10-01", "LOCKADJ-1");

    const adj = await post(app, db.companyA, "/api/c/inventory-adjustments", {
      itemId, date: "2024-09-15", quantity: "-1", accountId: shrinkA, memo: "Inside the lock",
    });
    expect(adj.statusCode).toBe(409);
    expect(adj.json().error).toMatch(/That year is locked \(through 2024-09-30\)/);

    const acct = await createAccount(db.companyA, "1460", "Inventory - Locked Opening", "asset");
    const fresh = await createItem(db.companyA, {
      name: "Locked Opening Widget", inventoryAccountId: acct,
    });
    const opening = await post(app, db.companyA, "/api/c/inventory-opening", {
      date: "2024-09-15", sourceAccountId: String(db.accounts["3000"]!.A),
      memo: "Inside the lock",
      items: [{ itemId: fresh, quantity: "10", unitCost: "1.00" }],
    });
    expect(opening.statusCode).toBe(409);
    expect(opening.json().error).toMatch(/That year is locked \(through 2024-09-30\)/);
    await expectBooksTie();
  });
});

// ---------------------------------------------------------------------------
// money arithmetic for the assertions above — integer only, never a JS float
// (CLAUDE.md §2). The same toMicros/fromMicros technique the server uses.
// ---------------------------------------------------------------------------

function toMicros(amount: string): bigint {
  const negative = amount.startsWith("-");
  const unsigned = negative ? amount.slice(1) : amount;
  const [wholeRaw, fracRaw = ""] = unsigned.split(".");
  const frac = (fracRaw + "0000").slice(0, 4);
  const value = BigInt(wholeRaw || "0") * 10000n + BigInt(frac || "0");
  return negative ? -value : value;
}

function fromMicros(micros: bigint): string {
  const negative = micros < 0n;
  const abs = negative ? -micros : micros;
  const whole = abs / 10000n;
  const frac = (abs % 10000n).toString().padStart(4, "0");
  return `${negative ? "-" : ""}${whole}.${frac}`;
}

function add(a: string, b: string): string {
  return fromMicros(toMicros(a) + toMicros(b));
}

function sub(a: string, b: string): string {
  return fromMicros(toMicros(a) - toMicros(b));
}
