// Integration tests for the items / purchase orders / receiving API (Task 2 of
// docs/superpowers/plans/2026-08-03-inventory.md; spec:
// docs/superpowers/specs/2026-08-03-inventory-design.md). Mirrors
// tests/server/ap-bills.test.ts structurally (same createTestDb, app.inject,
// seeded chart of accounts) with item / purchase order 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 version
// matters — an error that moves value between two inventory accounts sums to a
// serene zero). 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 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 } });
}
function del(app: FastifyInstance, companyId: string, url: string) {
  return app.inject({ method: "DELETE", 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() is internal
// (REVOKE EXECUTE ... FROM PUBLIC), so this stands in for Task 3's post_build()
// when the build wash entry is exercised below. Session-company guards still
// apply — this is about EXECUTE privilege, not about tenancy.
let sysA: Client;

// accounts created on top of the helper's seeded chart (1000 Cash, 1100 A/R,
// 1200 Due from Affiliate, 2000 A/P, 2200 Due to Affiliate, 3000 Opening
// Equity, 4000 Revenue, 6000 Operating Expense — per company).
let invA = "", cogsA = "", inv2A = "", 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");
  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,
): Promise<string> {
  const res = await post(app, companyId, "/api/c/accounts", { code, name, type });
  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: an error that moves value BETWEEN two
  // inventory accounts nets to zero in the aggregate while both accounts are
  // individually wrong. This company has two inventory accounts on purpose.
  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;
}

async function countRows(companyId: string, table: string): Promise<number> {
  const client = companyId === db.companyA ? cA : cB;
  const res = await client.query(
    `SELECT count(*)::int AS n FROM ${table} WHERE company_id = $1`, [companyId],
  );
  return res.rows[0].n as number;
}

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<{ id: string; body: Record<string, unknown> }> {
  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;
  }
  const res = await post(app, companyId, "/api/c/items", payload);
  expect(res.statusCode).toBe(201);
  return { id: res.json().id, body: res.json() };
}

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;
}

interface PoLineInput {
  itemId?: string;
  description?: string;
  quantity: string;
  unitCost: string;
}

async function createPo(
  companyId: string, vendorId: string, lines: PoLineInput[],
  extra: Record<string, unknown> = {},
): Promise<Record<string, any>> {
  const res = await post(app, companyId, "/api/c/purchase-orders", {
    vendorId, orderDate: "2024-03-01", lines, ...extra,
  });
  expect(res.statusCode).toBe(201);
  return res.json();
}

/** Creates a draft bill and issues it; returns the issue response. */
async function billAndIssue(
  companyId: string, vendorId: string, lines: unknown[],
  extra: Record<string, unknown> = {},
): Promise<{ billId: string; issue: { statusCode: number; body: any } }> {
  const create = await post(app, companyId, "/api/c/bills", {
    vendorId, billDate: "2024-03-15", lines, ...extra,
  });
  expect(create.statusCode).toBe(201);
  const billId = create.json().billId;
  const issue = await post(app, companyId, `/api/c/bills/${billId}/issue`);
  return { billId, issue: { statusCode: issue.statusCode, body: issue.json() } };
}

// ===========================================================================
// items
// ===========================================================================

describe("items", () => {
  it("creates a stocked item and reports a derived zero on-hand position", async () => {
    const res = await post(app, db.companyA, "/api/c/items", {
      name: "Widget", sku: "W-1", kind: "inventory",
      inventoryAccountId: invA, cogsAccountId: cogsA, incomeAccountId: String(db.accounts["4000"]!.A),
      salesPrice: "19.99", purchaseCost: "5.00", description: "A widget",
    });
    expect(res.statusCode).toBe(201);
    expect(res.json()).toMatchObject({
      name: "Widget", sku: "W-1", kind: "inventory",
      inventoryAccountId: invA, inventoryAccountName: "Inventory",
      cogsAccountId: cogsA, cogsAccountName: "Cost of Goods Sold",
      salesPrice: "19.9900", purchaseCost: "5.0000", active: true,
      qtyOnHand: "0.0000", averageCost: "0.000000", value: "0.0000",
    });
  });

  it("creates a non-inventory item (cost account, no inventory account)", async () => {
    const res = await post(app, db.companyA, "/api/c/items", {
      name: "Drop-shipped Part", kind: "non_inventory", cogsAccountId: cogsA,
    });
    expect(res.statusCode).toBe(201);
    expect(res.json()).toMatchObject({
      kind: "non_inventory", inventoryAccountId: null,
      // not stocked: no quantity at all, rather than a misleading zero
      qtyOnHand: null, averageCost: null, value: null,
    });
  });

  it("creates a service item with no accounts at all", async () => {
    const res = await post(app, db.companyA, "/api/c/items", {
      name: "Installation Labor", kind: "service",
    });
    expect(res.statusCode).toBe(201);
    expect(res.json()).toMatchObject({ kind: "service", cogsAccountId: null, inventoryAccountId: null });
  });

  it("422s in English when a stocked item has no inventory account", async () => {
    const res = await post(app, db.companyA, "/api/c/items", {
      name: "No Home Widget", kind: "inventory", cogsAccountId: cogsA,
    });
    expect(res.statusCode).toBe(422);
    expect(res.json().error).toMatch(/holds this item's value on the balance sheet/i);
  });

  it("422s in English when a stocked item has no cost account", async () => {
    const res = await post(app, db.companyA, "/api/c/items", {
      name: "No Cost Widget", kind: "inventory", inventoryAccountId: invA,
    });
    expect(res.statusCode).toBe(422);
    expect(res.json().error).toMatch(/what this item costs you/i);
  });

  it("422s in English when a non-stocked item is given an inventory account", async () => {
    const res = await post(app, db.companyA, "/api/c/items", {
      name: "Confused Service", kind: "service", inventoryAccountId: invA,
    });
    expect(res.statusCode).toBe(422);
    expect(res.json().error).toMatch(/only an item you stock has an inventory account/i);
  });

  it("422s in English when the inventory account isn't an asset account", async () => {
    const res = await post(app, db.companyA, "/api/c/items", {
      name: "Wrong Inventory Account Widget", kind: "inventory",
      inventoryAccountId: String(db.accounts["6000"]!.A), cogsAccountId: cogsA,
    });
    expect(res.statusCode).toBe(422);
    expect(res.json().error).toMatch(/inventory account has to be an asset account/i);
  });

  it("422s in English when the cost account isn't an expense account", async () => {
    const res = await post(app, db.companyA, "/api/c/items", {
      name: "Wrong Cost Account Widget", kind: "inventory",
      inventoryAccountId: invA, cogsAccountId: String(db.accounts["4000"]!.A),
    });
    expect(res.statusCode).toBe(422);
    expect(res.json().error).toMatch(/cost account has to be an expense account/i);
  });

  it("422s in English when the income account isn't an income account", async () => {
    const res = await post(app, db.companyA, "/api/c/items", {
      name: "Wrong Income Account Widget", kind: "inventory",
      inventoryAccountId: invA, cogsAccountId: cogsA,
      incomeAccountId: String(db.accounts["1000"]!.A),
    });
    expect(res.statusCode).toBe(422);
    expect(res.json().error).toMatch(/income account has to be an income account/i);
  });

  it("409s in English on a duplicate item name", async () => {
    await createItem(db.companyA, { name: "Duplicate Name Widget" });
    const res = await post(app, db.companyA, "/api/c/items", {
      name: "Duplicate Name Widget", kind: "inventory",
      inventoryAccountId: invA, cogsAccountId: cogsA,
    });
    expect(res.statusCode).toBe(409);
    expect(res.json().error).toMatch(/already have an item with that name/i);
  });

  it("409s in English on a duplicate SKU", async () => {
    await createItem(db.companyA, { name: "SKU One Widget", sku: "DUP-SKU" });
    const res = await post(app, db.companyA, "/api/c/items", {
      name: "SKU Two Widget", sku: "DUP-SKU", kind: "inventory",
      inventoryAccountId: invA, cogsAccountId: cogsA,
    });
    expect(res.statusCode).toBe(409);
    expect(res.json().error).toMatch(/SKU/i);
  });

  it("allows the same item name in a different company", async () => {
    const res = await post(app, db.companyB, "/api/c/items", {
      name: "Widget", kind: "inventory", inventoryAccountId: invB, cogsAccountId: cogsB,
    });
    expect(res.statusCode).toBe(201);
  });

  it("400s in English for a bad kind", async () => {
    const res = await post(app, db.companyA, "/api/c/items", { name: "Bad Kind", kind: "widgetish" });
    expect(res.statusCode).toBe(400);
    expect(res.json().error).toMatch(/what kind of item/i);
  });

  it("400s in English for a price with too many decimals", async () => {
    const res = await post(app, db.companyA, "/api/c/items", {
      name: "Too Precise Widget", kind: "inventory",
      inventoryAccountId: invA, cogsAccountId: cogsA, purchaseCost: "1.234567",
    });
    expect(res.statusCode).toBe(400);
    expect(res.json().error).toMatch(/4 decimal places/i);
  });

  it("accepts a sub-cent purchase cost (a washer costs $0.0042)", async () => {
    const res = await post(app, db.companyA, "/api/c/items", {
      name: "Washer", kind: "inventory",
      inventoryAccountId: invA, cogsAccountId: cogsA, purchaseCost: "0.0042",
    });
    expect(res.statusCode).toBe(201);
    expect(res.json().purchaseCost).toBe("0.0042");
  });

  it("PATCH replaces name, sku, prices, accounts and active", async () => {
    const { id } = await createItem(db.companyA, { name: "Editable Widget", sku: "E-1" });
    const res = await patch(app, db.companyA, `/api/c/items/${id}`, {
      name: "Editable Widget Renamed", sku: "E-2", kind: "inventory",
      inventoryAccountId: invA, cogsAccountId: cogsA,
      incomeAccountId: String(db.accounts["4000"]!.A),
      salesPrice: "1.50", purchaseCost: "0.75", active: false,
    });
    expect(res.statusCode).toBe(200);
    expect(res.json()).toMatchObject({
      id, name: "Editable Widget Renamed", sku: "E-2",
      salesPrice: "1.5000", purchaseCost: "0.7500", active: false,
    });
  });

  it("GET list hides inactive items unless includeInactive is set", async () => {
    const active = await get(app, db.companyA, "/api/c/items");
    const names = active.json().items.map((i: { name: string }) => i.name);
    expect(names).not.toContain("Editable Widget Renamed");

    const all = await get(app, db.companyA, "/api/c/items?includeInactive=1");
    const allNames = all.json().items.map((i: { name: string }) => i.name);
    expect(allNames).toContain("Editable Widget Renamed");
  });

  it("GET list filters by kind", async () => {
    const res = await get(app, db.companyA, "/api/c/items?kind=service");
    expect(res.statusCode).toBe(200);
    const kinds = new Set(res.json().items.map((i: { kind: string }) => i.kind));
    expect([...kinds]).toEqual(["service"]);
  });

  it("lets a pure rename through even when an untouched account has been deactivated", async () => {
    const deadCogs = await createAccount(db.companyA, "5900", "Cost of Goods - Retired", "expense");
    const { id } = await createItem(db.companyA, {
      name: "Untouched Account Widget", kind: "service", cogsAccountId: deadCogs,
    });
    expect((await patch(app, db.companyA, `/api/c/accounts/${deadCogs}`,
      { active: false })).statusCode).toBe(200);

    // the cost account is deactivated and the caller isn't touching it
    const rename = await patch(app, db.companyA, `/api/c/items/${id}`, {
      name: "Untouched Account Widget Renamed", kind: "service", cogsAccountId: deadCogs,
    });
    expect(rename.statusCode).toBe(200);
    expect(rename.json().name).toBe("Untouched Account Widget Renamed");

    // ...but pointing it at a DIFFERENT deactivated account is still refused
    const deadTwo = await createAccount(db.companyA, "5910", "Cost of Goods - Retired Two", "expense");
    expect((await patch(app, db.companyA, `/api/c/accounts/${deadTwo}`,
      { active: false })).statusCode).toBe(200);
    const repoint = await patch(app, db.companyA, `/api/c/items/${id}`, {
      name: "Untouched Account Widget Renamed", kind: "service", cogsAccountId: deadTwo,
    });
    expect(repoint.statusCode).toBe(422);
    expect(repoint.json().error).toMatch(/couldn't find the account you picked for the cost account/i);
  });

  it("speaks English, not enum tokens, about item kinds", async () => {
    const res = await post(app, db.companyA, "/api/c/items", { name: "Token Check", kind: "nope" });
    expect(res.statusCode).toBe(400);
    expect(res.json().error).not.toMatch(/non_inventory/);
    expect(res.json().error).toMatch(/something you keep in stock/i);
  });

  it("404s for a made-up item id", async () => {
    const res = await get(app, db.companyA, "/api/c/items/999999999");
    expect(res.statusCode).toBe(404);
  });

  it("404s (not 500) when the item belongs to another company (RLS)", async () => {
    const { id } = await createItem(db.companyA, { name: "Company A Only Widget" });
    expect((await get(app, db.companyB, `/api/c/items/${id}`)).statusCode).toBe(404);
    const hijack = await patch(app, db.companyB, `/api/c/items/${id}`, {
      name: "Hijacked", kind: "inventory", inventoryAccountId: invB, cogsAccountId: cogsB,
    });
    expect(hijack.statusCode).toBe(404);
  });

  it("company B's item list never shows company A's items (RLS)", async () => {
    const res = await get(app, db.companyB, "/api/c/items?includeInactive=1");
    const names = res.json().items.map((i: { name: string }) => i.name);
    expect(names).not.toContain("Company A Only Widget");
    expect(names).not.toContain("Installation Labor");
  });

  it("has no delete route (items are deactivated, never deleted)", async () => {
    const { id } = await createItem(db.companyA, { name: "Undeletable Widget" });
    const res = await del(app, db.companyA, `/api/c/items/${id}`);
    expect(res.statusCode).toBe(404);
  });
});

// ===========================================================================
// purchase orders
// ===========================================================================

describe("purchase orders", () => {
  it("creates a draft with a derived total and no number", async () => {
    const vendorId = await createVendor(db.companyA, "PO Draft Vendor Co");
    const { id } = await createItem(db.companyA, { name: "Draft PO Widget" });
    const po = await createPo(db.companyA, vendorId, [
      { itemId: id, quantity: "10", unitCost: "5.00" },
      { description: "Freight", quantity: "1", unitCost: "25.00" },
    ], { memo: "spring restock", expectedDate: "2024-03-20" });

    expect(po).toMatchObject({
      poNo: null, status: "draft", state: "draft", total: "75.0000",
      vendorName: "PO Draft Vendor Co", memo: "spring restock", expectedDate: "2024-03-20",
      // one item line can be received; the freight line never can, and the
      // payload says so rather than leaving the UI to work it out
      receivableLines: 1, unreceivableLines: 1,
    });
    expect(po.lines).toHaveLength(2);
    expect(po.lines[0]).toMatchObject({
      quantity: "10.0000", unitCost: "5.0000", amount: "50.0000",
      qtyReceived: "0.0000", qtyOutstanding: "10.0000",
    });
  });

  it("PATCH replaces a draft's header and lines", async () => {
    const vendorId = await createVendor(db.companyA, "PO Patch Vendor Co");
    const { id } = await createItem(db.companyA, { name: "Patch PO Widget" });
    const po = await createPo(db.companyA, vendorId, [{ itemId: id, quantity: "1", unitCost: "1.00" }]);

    const res = await patch(app, db.companyA, `/api/c/purchase-orders/${po.id}`, {
      vendorId, orderDate: "2024-03-05", memo: "revised",
      lines: [{ itemId: id, quantity: "4", unitCost: "2.50" }],
    });
    expect(res.statusCode).toBe(200);
    expect(res.json()).toMatchObject({ memo: "revised", total: "10.0000", state: "draft" });
    expect(res.json().lines).toHaveLength(1);
  });

  it("deletes a never-issued draft", async () => {
    const vendorId = await createVendor(db.companyA, "PO Delete Vendor Co");
    const po = await createPo(db.companyA, vendorId, [{ description: "x", quantity: "1", unitCost: "1.00" }]);
    expect((await del(app, db.companyA, `/api/c/purchase-orders/${po.id}`)).statusCode).toBe(200);
    expect((await get(app, db.companyA, `/api/c/purchase-orders/${po.id}`)).statusCode).toBe(404);
  });

  it("issues a draft, assigns a per-company number, and POSTS NOTHING", async () => {
    const vendorId = await createVendor(db.companyA, "PO Issue Vendor Co");
    const { id } = await createItem(db.companyA, { name: "Issue PO Widget" });
    const po = await createPo(db.companyA, vendorId, [{ itemId: id, quantity: "3", unitCost: "9.00" }]);

    const entriesBefore = await countRows(db.companyA, "journal_entries");
    const movementsBefore = await countRows(db.companyA, "inventory_movements");
    const inventoryBefore = await accountBalance(db.companyA, invA);
    const apBefore = await accountBalance(db.companyA, String(db.accounts["2000"]!.A));

    const res = await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/issue`);
    expect(res.statusCode).toBe(200);
    expect(res.json().poNo).toBeTruthy();
    expect(res.json().summary).toMatch(/Nothing has been recorded in the books/i);

    // A purchase order is a promise, not a transaction (spec): no journal
    // entry, no stock movement, no balance changes anywhere.
    expect(await countRows(db.companyA, "journal_entries")).toBe(entriesBefore);
    expect(await countRows(db.companyA, "inventory_movements")).toBe(movementsBefore);
    expect(await accountBalance(db.companyA, invA)).toBe(inventoryBefore);
    expect(await accountBalance(db.companyA, String(db.accounts["2000"]!.A))).toBe(apBefore);
    await expectBooksTie();

    const detail = await get(app, db.companyA, `/api/c/purchase-orders/${po.id}`);
    expect(detail.json()).toMatchObject({ status: "open", state: "open" });
  });

  it("numbers purchase orders per company, starting at 1", async () => {
    const vendorId = await createVendor(db.companyB, "PO Numbering Vendor Co");
    const item = await createItem(db.companyB, { name: "Company B Widget" });
    const po = await createPo(db.companyB, vendorId, [{ itemId: item.id, quantity: "1", unitCost: "1.00" }]);
    const res = await post(app, db.companyB, `/api/c/purchase-orders/${po.id}/issue`);
    expect(res.statusCode).toBe(200);
    expect(res.json().poNo).toBe("1");
  });

  it("refuses a second issue in English", async () => {
    const vendorId = await createVendor(db.companyA, "PO Reissue Vendor Co");
    const po = await createPo(db.companyA, vendorId, [{ description: "x", quantity: "1", unitCost: "1.00" }]);
    expect((await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/issue`)).statusCode).toBe(200);
    const again = await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/issue`);
    expect(again.statusCode).toBe(409);
    expect(again.json().error).toMatch(/already been sent to the vendor/i);
  });

  it("refuses to issue a purchase order with nothing on it, in English", async () => {
    // The route's own zod schema requires at least one line, so an empty PO can
    // only be reached by deleting the lines of a draft — which is exactly what
    // issue_purchase_order()'s own guard is for (CLAUDE.md §12).
    const vendorId = await createVendor(db.companyA, "PO Empty Vendor Co");
    const po = await createPo(db.companyA, vendorId, [{ description: "x", quantity: "1", unitCost: "1.00" }]);
    await cA.query(
      "DELETE FROM purchase_order_lines WHERE company_id = $1 AND po_id = $2",
      [db.companyA, po.id],
    );
    const res = await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/issue`);
    expect(res.statusCode).toBe(422);
    expect(res.json().error).toMatch(/add at least one line/i);
  });

  it("refuses to edit or delete an issued purchase order, in English", async () => {
    const vendorId = await createVendor(db.companyA, "PO Frozen Vendor Co");
    const { id } = await createItem(db.companyA, { name: "Frozen PO Widget" });
    const po = await createPo(db.companyA, vendorId, [{ itemId: id, quantity: "2", unitCost: "3.00" }]);
    await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/issue`);

    const edit = await patch(app, db.companyA, `/api/c/purchase-orders/${po.id}`, {
      vendorId, orderDate: "2024-04-01", lines: [{ itemId: id, quantity: "99", unitCost: "3.00" }],
    });
    expect(edit.statusCode).toBe(409);
    expect(edit.json().error).toMatch(/already been sent to the vendor/i);

    // ...including a PATCH that leaves every header field alone and only
    // rewrites the lines, which the header-freeze block cannot see (it fires on
    // a CHANGED field) — the line trigger is what catches this one.
    const linesOnly = await patch(app, db.companyA, `/api/c/purchase-orders/${po.id}`, {
      vendorId, orderDate: "2024-03-01",
      lines: [{ itemId: id, quantity: "7", unitCost: "3.00" }],
    });
    expect(linesOnly.statusCode).toBe(409);
    expect(linesOnly.json().error).toMatch(/already been sent to the vendor/i);

    const drop = await del(app, db.companyA, `/api/c/purchase-orders/${po.id}`);
    expect(drop.statusCode).toBe(409);
    expect(drop.json().error).toMatch(/cancel it instead of deleting it/i);

    // and the lines themselves are frozen at the database, not merely by the
    // route refusing to call the UPDATE
    await expect(cA.query(
      "UPDATE purchase_order_lines SET qty_ordered = 99 WHERE company_id = $1 AND po_id = $2",
      [db.companyA, po.id],
    )).rejects.toThrow(/already been sent to the vendor/);
  });

  it("cancels a purchase order, and refuses a second cancel in English", async () => {
    const vendorId = await createVendor(db.companyA, "PO Cancel Vendor Co");
    const { id } = await createItem(db.companyA, { name: "Cancel PO Widget" });
    const po = await createPo(db.companyA, vendorId, [{ itemId: id, quantity: "5", unitCost: "2.00" }]);
    await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/issue`);

    const res = await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/cancel`);
    expect(res.statusCode).toBe(200);
    expect(res.json().summary).toMatch(/never touches them/i);

    const detail = await get(app, db.companyA, `/api/c/purchase-orders/${po.id}`);
    expect(detail.json()).toMatchObject({ status: "cancelled", state: "cancelled" });

    const again = await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/cancel`);
    expect(again.statusCode).toBe(409);
    expect(again.json().error).toMatch(/already been cancelled/i);
    await expectBooksTie();
  });

  it("returns a print payload with the company, the vendor and the lines", async () => {
    const vendorId = await createVendor(db.companyA, "PO Print Vendor Co");
    const { id } = await createItem(db.companyA, { name: "Print PO Widget" });
    const po = await createPo(db.companyA, vendorId, [
      { itemId: id, quantity: "2", unitCost: "12.50" },
      { description: "Setup fee", quantity: "1", unitCost: "40.00" },
    ]);
    await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/issue`);

    const res = await get(app, db.companyA, `/api/c/purchase-orders/${po.id}/print`);
    expect(res.statusCode).toBe(200);
    const body = res.json();
    expect(body.company.name).toBeTruthy();
    expect(body.vendor.name).toBe("PO Print Vendor Co");
    expect(body.purchaseOrder.no).toBeTruthy();
    expect(body.total).toBe("65.0000");
    expect(body.lines).toHaveLength(2);
    expect(body.lines[0]).toMatchObject({ itemName: "Print PO Widget", amount: "25.0000" });
    // Nothing received yet, so the whole order is still outstanding.
    expect(body.lines[0]).toMatchObject({ qtyReceived: "0.0000", qtyOutstanding: "2.0000" });
    // A description-only line's receipt can't be observed, so it reports
    // neither — an em dash on the paper, never a 0 that looks like a claim.
    expect(body.lines[1]).toMatchObject({ qtyReceived: null, qtyOutstanding: null });
  });

  it("the print payload carries receipt progress, so a partly-received order can be chased", async () => {
    // Review finding F7: reprinting a partly-received order used to hand the
    // vendor a document asking for the whole thing again.
    const vendorId = await createVendor(db.companyA, "PO Chase Vendor Co");
    const { id: itemId } = await createItem(db.companyA, { name: "Chase Widget" });
    const po = await createPo(db.companyA, vendorId, [{ itemId, quantity: "10", unitCost: "5.00" }]);
    await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/issue`);

    await billAndIssue(db.companyA, vendorId, [
      { itemId, poLineId: po.lines[0].id, quantity: "4", unitPrice: "5.00" },
    ]);

    const res = await get(app, db.companyA, `/api/c/purchase-orders/${po.id}/print`);
    expect(res.statusCode).toBe(200);
    expect(res.json().purchaseOrder.state).toBe("partial");
    expect(res.json().lines[0]).toMatchObject({
      quantity: "10.0000", qtyReceived: "4.0000", qtyOutstanding: "6.0000",
    });
    await expectBooksTie();
  });

  it("reports 'closed' on a MIXED order once its item lines are in, and says a line isn't tracked", async () => {
    const vendorId = await createVendor(db.companyA, "Mixed PO Vendor Co");
    const { id: itemId } = await createItem(db.companyA, { name: "Mixed PO Widget" });
    const po = await createPo(db.companyA, vendorId, [
      { itemId, quantity: "4", unitCost: "2.00" },
      { description: "Freight", quantity: "1", unitCost: "15.00" },
    ]);
    await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/issue`);

    const { issue } = await billAndIssue(db.companyA, vendorId, [
      { itemId, poLineId: po.lines[0].id, quantity: "4", unitPrice: "2.00" },
    ], { refNo: "MIXEDPO-1" });
    expect(issue.statusCode).toBe(200);
    await expectBooksTie();

    const detail = await get(app, db.companyA, `/api/c/purchase-orders/${po.id}`);
    expect(detail.json()).toMatchObject({
      state: "closed", receivableLines: 1, unreceivableLines: 1,
    });
    // the description line still shows an outstanding quantity — nothing can
    // ever mark it received, which is what unreceivableLines exists to explain
    expect(detail.json().lines[1]).toMatchObject({ itemId: null, qtyOutstanding: "1.0000" });
  });

  it("does not hand back the same quantity twice when 'receive' is clicked twice", async () => {
    const vendorId = await createVendor(db.companyA, "Double Click Vendor Co");
    const { id: itemId } = await createItem(db.companyA, { name: "Double Click Widget" });
    const po = await createPo(db.companyA, vendorId, [{ itemId, quantity: "50", unitCost: "2.00" }]);
    await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/issue`);

    // click one: the whole order, nothing already drafted
    const first = await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/receive`);
    expect(first.statusCode).toBe(200);
    expect(first.json().notice).toBeNull();
    expect(first.json().lines[0]).toMatchObject({ quantity: "50.0000", qtyOnDraftBills: "0.0000" });

    // the user starts the bill but doesn't enter it
    const draft = await post(app, db.companyA, "/api/c/bills", {
      vendorId, billDate: "2024-03-15",
      lines: [{ itemId, poLineId: po.lines[0].id, quantity: "50", unitPrice: "2.00" }],
    });
    expect(draft.statusCode).toBe(201);

    // click two: nothing left to put on a new bill, and it says why
    const second = await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/receive`);
    expect(second.statusCode).toBe(409);
    expect(second.json().error).toMatch(/already on a bill you haven't entered yet/i);
    // The STABLE discriminator. This is the one refusal with a way out (throw
    // the unentered draft away), and the UI must be able to find it without
    // matching on the sentence above — which is free to be reworded.
    expect(second.json().code).toBe("all-on-draft-bills");

    // the PO still reports nothing received — a draft has posted nothing
    const detail = await get(app, db.companyA, `/api/c/purchase-orders/${po.id}`);
    expect(detail.json()).toMatchObject({ state: "open" });
    expect(detail.json().lines[0]).toMatchObject({
      qtyReceived: "0.0000", qtyOnDraftBills: "50.0000",
      qtyOutstanding: "50.0000", qtyAvailableToReceive: "0.0000",
    });

    // and once entered, receiving is closed out properly — 50, not 100
    expect((await post(app, db.companyA, `/api/c/bills/${draft.json().billId}/issue`)).statusCode).toBe(200);
    await expectBooksTie();
    expect((await get(app, db.companyA, `/api/c/items/${itemId}`)).json().qtyOnHand).toBe("50.0000");
    expect((await get(app, db.companyA, `/api/c/purchase-orders/${po.id}`)).json().state).toBe("closed");

    // Same 409 status, DIFFERENT code — this one has no way out, and the UI
    // must not offer the throw-the-draft-away escape hatch for it.
    const third = await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/receive`);
    expect(third.statusCode).toBe(409);
    expect(third.json().code).toBe("fully-received");
    expect(third.json().error).toMatch(/has already been received/i);
  });

  it("codes the two pre-receipt refusals distinctly as well", async () => {
    const vendorId = await createVendor(db.companyA, "Receive Codes Vendor Co");
    const { id: itemId } = await createItem(db.companyA, { name: "Receive Codes Widget" });

    const draftPo = await createPo(db.companyA, vendorId, [{ itemId, quantity: "1", unitCost: "1.00" }]);
    const notSent = await post(app, db.companyA, `/api/c/purchase-orders/${draftPo.id}/receive`);
    expect(notSent.statusCode).toBe(409);
    expect(notSent.json().code).toBe("not-sent");

    const cancelledPo = await createPo(db.companyA, vendorId, [{ itemId, quantity: "1", unitCost: "1.00" }]);
    await post(app, db.companyA, `/api/c/purchase-orders/${cancelledPo.id}/issue`);
    await post(app, db.companyA, `/api/c/purchase-orders/${cancelledPo.id}/cancel`);
    const cancelled = await post(app, db.companyA, `/api/c/purchase-orders/${cancelledPo.id}/receive`);
    expect(cancelled.statusCode).toBe(409);
    expect(cancelled.json().code).toBe("cancelled");
  });

  it("throwing away the unentered draft gives the whole order back to 'receive'", async () => {
    // The way OUT of the all-drafted 409 above (Task 2's review found there
    // wasn't one): a draft bill has posted nothing, so discarding it hands the
    // quantity straight back to the purchase order.
    const vendorId = await createVendor(db.companyA, "Discard Draft Receipt Vendor Co");
    const { id: itemId } = await createItem(db.companyA, { name: "Discard Draft Widget" });
    const po = await createPo(db.companyA, vendorId, [{ itemId, quantity: "12", unitCost: "3.00" }]);
    await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/issue`);

    const draft = await post(app, db.companyA, "/api/c/bills", {
      vendorId, billDate: "2024-03-15",
      lines: [{ itemId, poLineId: po.lines[0].id, quantity: "12", unitPrice: "3.00" }],
    });
    expect(draft.statusCode).toBe(201);
    expect((await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/receive`)).statusCode).toBe(409);

    const discard = await del(app, db.companyA, `/api/c/bills/${draft.json().billId}`);
    expect(discard.statusCode).toBe(200);

    const again = await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/receive`);
    expect(again.statusCode).toBe(200);
    expect(again.json().notice).toBeNull();
    expect(again.json().lines[0]).toMatchObject({
      quantity: "12.0000", qtyOnDraftBills: "0.0000",
    });
    const detail = await get(app, db.companyA, `/api/c/purchase-orders/${po.id}`);
    expect(detail.json().state).toBe("open");
    expect(detail.json().lines[0]).toMatchObject({
      qtyReceived: "0.0000", qtyOnDraftBills: "0.0000", qtyAvailableToReceive: "12.0000",
    });
    // Nothing ever posted, so nothing moved and the tie never wobbled.
    expect((await get(app, db.companyA, `/api/c/items/${itemId}`)).json().qtyOnHand).toBe("0.0000");
    await expectBooksTie();
  });

  it("subtracts a PARTIAL draft from the prefill and says how much it subtracted", async () => {
    const vendorId = await createVendor(db.companyA, "Partial Draft Vendor Co");
    const { id: itemId } = await createItem(db.companyA, { name: "Partial Draft Widget" });
    const po = await createPo(db.companyA, vendorId, [{ itemId, quantity: "10", unitCost: "1.00" }]);
    await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/issue`);

    const draft = await post(app, db.companyA, "/api/c/bills", {
      vendorId, billDate: "2024-03-15",
      lines: [{ itemId, poLineId: po.lines[0].id, quantity: "3", unitPrice: "1.00" }],
    });
    expect(draft.statusCode).toBe(201);

    const prefill = await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/receive`);
    expect(prefill.statusCode).toBe(200);
    expect(prefill.json().lines[0]).toMatchObject({
      quantity: "7.0000", qtyOnDraftBills: "3.0000",
    });
    expect(prefill.json().notice).toBe(
      '3 of "Partial Draft Widget" are already on a bill you haven\'t entered yet, so they aren\'t on this one.');
  });

  it("explains a line it dropped because that line is entirely on an unentered bill", async () => {
    // The mixed case: one item line fully drafted, one description line still
    // open. Without a word about it the user clicks "receive" and gets a bill
    // that is quietly missing the item they wanted.
    const vendorId = await createVendor(db.companyA, "Dropped Line Vendor Co");
    const { id: itemId } = await createItem(db.companyA, { name: "Dropped Line Widget" });
    const po = await createPo(db.companyA, vendorId, [
      { itemId, quantity: "6", unitCost: "2.00" },
      { description: "Freight", quantity: "1", unitCost: "20.00" },
    ]);
    await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/issue`);

    const draft = await post(app, db.companyA, "/api/c/bills", {
      vendorId, billDate: "2024-03-15",
      lines: [{ itemId, poLineId: po.lines[0].id, quantity: "6", unitPrice: "2.00" }],
    });
    expect(draft.statusCode).toBe(201);

    const prefill = await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/receive`);
    expect(prefill.statusCode).toBe(200);
    // only the freight line comes back...
    expect(prefill.json().lines).toHaveLength(1);
    expect(prefill.json().lines[0]).toMatchObject({ itemId: null, needsCategory: true });
    // ...and the payload says why the widget isn't there
    expect(prefill.json().notice).toBe(
      '6 of "Dropped Line Widget" are already on a bill you haven\'t entered yet, so they aren\'t on this one.');
  });

  it("flags an over-receipt instead of swallowing it", async () => {
    const vendorId = await createVendor(db.companyA, "Over Receipt Vendor Co");
    const { id: itemId } = await createItem(db.companyA, { name: "Over Receipt Widget" });
    const po = await createPo(db.companyA, vendorId, [{ itemId, quantity: "100", unitCost: "1.00" }]);
    await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/issue`);

    const { issue } = await billAndIssue(db.companyA, vendorId, [
      { itemId, poLineId: po.lines[0].id, quantity: "105", unitPrice: "1.00" },
    ], { refNo: "OVER-1" });
    expect(issue.statusCode).toBe(200);
    await expectBooksTie();

    const detail = await get(app, db.companyA, `/api/c/purchase-orders/${po.id}`);
    expect(detail.json()).toMatchObject({ state: "closed", overReceivedLines: 1 });
    expect(detail.json().lines[0]).toMatchObject({
      qtyReceived: "105.0000", qtyOutstanding: "0.0000", overReceived: true,
    });

    const list = await get(app, db.companyA, `/api/c/purchase-orders?vendorId=${vendorId}`);
    expect(list.json().purchaseOrders[0]).toMatchObject({ overReceivedLines: 1 });
  });

  it("says what a partly-received order gives up when it is cancelled", async () => {
    const vendorId = await createVendor(db.companyA, "Partial Cancel Vendor Co");
    const { id: itemId } = await createItem(db.companyA, { name: "Partial Cancel Widget" });
    const po = await createPo(db.companyA, vendorId, [{ itemId, quantity: "10", unitCost: "3.00" }]);
    await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/issue`);

    const { issue } = await billAndIssue(db.companyA, vendorId, [
      { itemId, poLineId: po.lines[0].id, quantity: "4", unitPrice: "3.00" },
    ], { refNo: "PARTCANCEL-1" });
    expect(issue.statusCode).toBe(200);

    const res = await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/cancel`);
    expect(res.statusCode).toBe(200);
    expect(res.json().partiallyReceived).toBe(true);
    expect(res.json().summary).toMatch(/Some of this order had already arrived/);
    expect(res.json().summary).toMatch(/6 Partial Cancel Widget/);
    await expectBooksTie();
  });

  it("refuses to receive against a draft or a cancelled purchase order, in English", async () => {
    const vendorId = await createVendor(db.companyA, "PO Receive Guard Vendor Co");
    const { id } = await createItem(db.companyA, { name: "Receive Guard Widget" });
    const draft = await createPo(db.companyA, vendorId, [{ itemId: id, quantity: "1", unitCost: "1.00" }]);
    const onDraft = await post(app, db.companyA, `/api/c/purchase-orders/${draft.id}/receive`);
    expect(onDraft.statusCode).toBe(409);
    expect(onDraft.json().error).toMatch(/send this purchase order to the vendor/i);

    await post(app, db.companyA, `/api/c/purchase-orders/${draft.id}/issue`);
    await post(app, db.companyA, `/api/c/purchase-orders/${draft.id}/cancel`);
    const onCancelled = await post(app, db.companyA, `/api/c/purchase-orders/${draft.id}/receive`);
    expect(onCancelled.statusCode).toBe(409);
    expect(onCancelled.json().error).toMatch(/was cancelled/i);
  });

  it("400s in English for a line with neither an item nor a description", async () => {
    const vendorId = await createVendor(db.companyA, "PO Bad Line Vendor Co");
    const res = await post(app, db.companyA, "/api/c/purchase-orders", {
      vendorId, orderDate: "2024-03-01", lines: [{ quantity: "1", unitCost: "1.00" }],
    });
    expect(res.statusCode).toBe(400);
    expect(res.json().error).toMatch(/item or a description/i);
  });

  it("404s (not 500) for another company's purchase order (RLS)", async () => {
    const vendorId = await createVendor(db.companyA, "PO RLS Vendor Co");
    const po = await createPo(db.companyA, vendorId, [{ description: "x", quantity: "1", unitCost: "1.00" }]);
    expect((await get(app, db.companyB, `/api/c/purchase-orders/${po.id}`)).statusCode).toBe(404);
    expect((await post(app, db.companyB, `/api/c/purchase-orders/${po.id}/issue`)).statusCode).toBe(404);
    expect((await post(app, db.companyB, `/api/c/purchase-orders/${po.id}/cancel`)).statusCode).toBe(404);
    expect((await post(app, db.companyB, `/api/c/purchase-orders/${po.id}/receive`)).statusCode).toBe(404);
    expect((await get(app, db.companyB, `/api/c/purchase-orders/${po.id}/print`)).statusCode).toBe(404);
    expect((await del(app, db.companyB, `/api/c/purchase-orders/${po.id}`)).statusCode).toBe(404);

    const list = await get(app, db.companyB, "/api/c/purchase-orders");
    const ids = list.json().purchaseOrders.map((p: { id: string }) => p.id);
    expect(ids).not.toContain(po.id);
  });
});

// ===========================================================================
// receiving: PO -> prefilled bill -> issued bill -> stock and money together
// ===========================================================================

describe("receiving against a purchase order", () => {
  it("runs the whole lifecycle: two partial receipts, derived partial then closed, average cost", async () => {
    const vendorId = await createVendor(db.companyA, "Receiving Vendor Co");
    const { id: itemId } = await createItem(db.companyA, { name: "Receivable Widget" });
    const po = await createPo(db.companyA, vendorId, [
      { itemId, quantity: "10", unitCost: "5.00" },
    ]);
    await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/issue`);
    const poLineId = po.lines[0].id;

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

    // --- first receipt: 4 of 10 at the ordered cost --------------------------
    const prefill1 = await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/receive`);
    expect(prefill1.statusCode).toBe(200);
    expect(prefill1.json()).toMatchObject({ vendorId, vendorName: "Receiving Vendor Co" });
    expect(prefill1.json().lines[0]).toMatchObject({
      poLineId, itemId, itemName: "Receivable Widget",
      quantity: "10.0000", unitPrice: "5.0000", needsCategory: false,
    });

    const first = await billAndIssue(db.companyA, vendorId, [
      { itemId, poLineId, quantity: "4", unitPrice: "5.00" },
    ], { refNo: "RCV-1" });
    expect(first.issue.statusCode).toBe(200);
    await expectBooksTie();

    // the money went to the item's INVENTORY account, not to an expense
    expect(await accountBalance(db.companyA, invA))
      .toBe((Number(invBefore) + 20).toFixed(4));

    const afterFirst = await get(app, db.companyA, `/api/c/purchase-orders/${po.id}`);
    expect(afterFirst.json()).toMatchObject({ status: "open", state: "partial" });
    expect(afterFirst.json().lines[0]).toMatchObject({
      qtyReceived: "4.0000", qtyOutstanding: "6.0000",
    });

    const itemAfterFirst = await get(app, db.companyA, `/api/c/items/${itemId}`);
    expect(itemAfterFirst.json()).toMatchObject({
      qtyOnHand: "4.0000", averageCost: "5.000000", value: "20.0000",
    });

    // --- second receipt: the remaining 6, at a HIGHER actual cost -----------
    const prefill2 = await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/receive`);
    expect(prefill2.json().lines[0]).toMatchObject({ quantity: "6.0000", unitPrice: "5.0000" });

    const second = await billAndIssue(db.companyA, vendorId, [
      { itemId, poLineId, quantity: "6", unitPrice: "7.50" },
    ], { refNo: "RCV-2" });
    expect(second.issue.statusCode).toBe(200);
    await expectBooksTie();

    const afterSecond = await get(app, db.companyA, `/api/c/purchase-orders/${po.id}`);
    // every receivable line is now fully received -> DERIVED 'closed', while
    // the STORED status is still 'open' (nothing writes partial/closed)
    expect(afterSecond.json()).toMatchObject({ status: "open", state: "closed" });
    expect(afterSecond.json().lines[0]).toMatchObject({
      qtyReceived: "10.0000", qtyOutstanding: "0.0000",
    });

    // weighted average: (4 x 5.00 + 6 x 7.50) / 10 = 65.00 / 10 = 6.50
    const itemAfterSecond = await get(app, db.companyA, `/api/c/items/${itemId}`);
    expect(itemAfterSecond.json()).toMatchObject({
      qtyOnHand: "10.0000", averageCost: "6.500000", value: "65.0000",
    });
    expect(await accountBalance(db.companyA, invA))
      .toBe((Number(invBefore) + 65).toFixed(4));

    // the movement history reads back with a running quantity
    const movements = itemAfterSecond.json().movements;
    expect(movements).toHaveLength(2);
    expect(movements[0]).toMatchObject({ qty: "6.0000", reason: "receipt", runningQty: "10.0000" });
    expect(movements[1]).toMatchObject({ qty: "4.0000", reason: "receipt", runningQty: "4.0000" });
    expect(movements[0].sourceKind).toBe("bill_line");

    // nothing more to receive
    const nothingLeft = await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/receive`);
    expect(nothingLeft.statusCode).toBe(409);
    expect(nothingLeft.json().error).toMatch(/already been received/i);

    // ...and the PO list agrees
    const list = await get(app, db.companyA, "/api/c/purchase-orders?state=closed");
    const ids = list.json().purchaseOrders.map((p: { id: string }) => p.id);
    expect(ids).toContain(po.id);
  });

  it("a DRAFT receiving bill does not count as received (nothing has posted)", async () => {
    const vendorId = await createVendor(db.companyA, "Draft Receipt Vendor Co");
    const { id: itemId } = await createItem(db.companyA, { name: "Draft Receipt Widget" });
    const po = await createPo(db.companyA, vendorId, [{ itemId, quantity: "5", unitCost: "2.00" }]);
    await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/issue`);

    const create = await post(app, db.companyA, "/api/c/bills", {
      vendorId, billDate: "2024-03-15",
      lines: [{ itemId, poLineId: po.lines[0].id, quantity: "5", unitPrice: "2.00" }],
    });
    expect(create.statusCode).toBe(201);

    const detail = await get(app, db.companyA, `/api/c/purchase-orders/${po.id}`);
    expect(detail.json().state).toBe("open");
    expect(detail.json().lines[0].qtyReceived).toBe("0.0000");
  });

  it("posts a bill mixing a stocked item, a non-stocked item and a plain expense line", async () => {
    const vendorId = await createVendor(db.companyA, "Mixed Bill Vendor Co");
    const { id: stocked } = await createItem(db.companyA, { name: "Mixed Stocked Widget" });
    const { id: nonStocked } = await createItem(db.companyA, {
      name: "Mixed Non Stocked Part", kind: "non_inventory",
    });

    const invBefore = await accountBalance(db.companyA, invA);
    const cogsBefore = await accountBalance(db.companyA, cogsA);
    const expenseBefore = await accountBalance(db.companyA, String(db.accounts["6000"]!.A));

    const { billId, issue } = await billAndIssue(db.companyA, vendorId, [
      { itemId: stocked, quantity: "2", unitPrice: "3.00" },
      { itemId: nonStocked, quantity: "1", unitPrice: "10.00" },
      { categoryAccountId: String(db.accounts["6000"]!.A), quantity: "1", unitPrice: "25.00", description: "Shipping" },
    ], { refNo: "MIX-1" });
    expect(issue.statusCode).toBe(200);
    await expectBooksTie();

    expect(await accountBalance(db.companyA, invA)).toBe((Number(invBefore) + 6).toFixed(4));
    // the non-stocked item behaves exactly like a category line, using its own
    // cost account
    expect(await accountBalance(db.companyA, cogsA)).toBe((Number(cogsBefore) + 10).toFixed(4));
    expect(await accountBalance(db.companyA, String(db.accounts["6000"]!.A)))
      .toBe((Number(expenseBefore) + 25).toFixed(4));

    const detail = await get(app, db.companyA, `/api/c/bills/${billId}`);
    expect(detail.json().total).toBe("41.0000");
    expect(detail.json().lines[0]).toMatchObject({ itemKind: "inventory", categoryAccountId: invA });
    expect(detail.json().lines[1]).toMatchObject({ itemKind: "non_inventory", categoryAccountId: cogsA });
    expect(detail.json().lines[2]).toMatchObject({ itemId: null, poLineId: null });

    // only the stocked line moved stock
    const movementCount = await cA.query(
      `SELECT count(*)::int AS n FROM inventory_movements m
        JOIN bill_lines l ON l.company_id = m.company_id AND l.id = m.source_id
       WHERE m.company_id = $1 AND m.source_kind = 'bill_line' AND l.bill_id = $2`,
      [db.companyA, billId],
    );
    expect(movementCount.rows[0].n).toBe(1);
  });

  it("keeps two inventory accounts separately tied on one bill", async () => {
    const vendorId = await createVendor(db.companyA, "Two Accounts Vendor Co");
    const { id: raw } = await createItem(db.companyA, { name: "Raw Material", inventoryAccountId: invA });
    const { id: finished } = await createItem(db.companyA, {
      name: "Finished Good", inventoryAccountId: inv2A,
    });

    const rawBefore = await accountBalance(db.companyA, invA);
    const finishedBefore = await accountBalance(db.companyA, inv2A);

    const { issue } = await billAndIssue(db.companyA, vendorId, [
      { itemId: raw, quantity: "10", unitPrice: "1.00" },
      { itemId: finished, quantity: "2", unitPrice: "20.00" },
    ], { refNo: "TWO-ACCT-1" });
    expect(issue.statusCode).toBe(200);
    await expectBooksTie();

    expect(await accountBalance(db.companyA, invA)).toBe((Number(rawBefore) + 10).toFixed(4));
    expect(await accountBalance(db.companyA, inv2A)).toBe((Number(finishedBefore) + 40).toFixed(4));
  });

  it("keeps a fractional quantity's amount on whole cents, and PAYABLE (0.5 x 99.99)", async () => {
    // 0.5 x 99.99 is 49.9950 exactly. An earlier draft stored that, which made
    // the bill unpayable (payments are whole cents) and left half a cent open in
    // A/P forever. Money is whole cents; the movement takes the amount as its
    // value and derives the unit cost from it.
    const vendorId = await createVendor(db.companyA, "Fractional Vendor Co");
    const { id: itemId } = await createItem(db.companyA, { name: "Fractional Widget" });

    const create = await post(app, db.companyA, "/api/c/bills", {
      vendorId, billDate: "2024-03-15", refNo: "FRAC-1",
      lines: [{ itemId, quantity: "0.5", unitPrice: "99.99" }],
    });
    expect(create.statusCode).toBe(201);
    expect(create.json().lines[0].amount).toBe("50.0000");

    const billId = create.json().billId;
    expect((await post(app, db.companyA, `/api/c/bills/${billId}/issue`)).statusCode).toBe(200);
    await expectBooksTie();

    const item = await get(app, db.companyA, `/api/c/items/${itemId}`);
    expect(item.json()).toMatchObject({
      qtyOnHand: "0.5000", averageCost: "100.000000", value: "50.0000",
    });

    const pay = await post(app, db.companyA, "/api/c/bill-payments", {
      vendorId, paymentDate: "2024-03-20", amount: "50.00",
      moneyAccountId: String(db.accounts["1000"]!.A), method: "electronic",
      applications: [{ billId, amount: "50.00" }],
    });
    expect(pay.statusCode).toBe(201);
    expect((await get(app, db.companyA, `/api/c/bills/${billId}`)).json())
      .toMatchObject({ open: "0.0000", state: "paid" });
    await expectBooksTie();
  });

  it("handles the headline sub-cent case end to end: 333 washers at $0.0042", async () => {
    // 333 x 0.0042 = 1.3986 -> $1.40. The movement's value IS that 1.40, and
    // the unit cost is derived from it: round(1.40 / 333, 6) = 0.004204. Note
    // round(333 x 0.004204, 4) = 1.3999 — no six-decimal unit cost reproduces
    // 1.4000 from 333 units, which is exactly why the value is given rather
    // than recomputed.
    const vendorId = await createVendor(db.companyA, "Sub Cent Vendor Co");
    const { id: itemId } = await createItem(db.companyA, { name: "Sub Cent Washer" });

    const create = await post(app, db.companyA, "/api/c/bills", {
      vendorId, billDate: "2024-03-15", refNo: "SUBCENT-1",
      lines: [{ itemId, quantity: "333", unitPrice: "0.0042" }],
    });
    expect(create.statusCode).toBe(201);
    expect(create.json().lines[0].amount).toBe("1.4000");

    const billId = create.json().billId;
    expect((await post(app, db.companyA, `/api/c/bills/${billId}/issue`)).statusCode).toBe(200);
    await expectBooksTie();

    const item = await get(app, db.companyA, `/api/c/items/${itemId}`);
    expect(item.json()).toMatchObject({
      qtyOnHand: "333.0000", averageCost: "0.004204", value: "1.4000",
    });
    const movement = item.json().movements[0];
    expect(movement).toMatchObject({ qty: "333.0000", unitCost: "0.004204", value: "1.4000" });

    // ...and the bill is payable in full, to the cent.
    expect((await get(app, db.companyA, `/api/c/bills/${billId}`)).json().open).toBe("1.4000");
    const pay = await post(app, db.companyA, "/api/c/bill-payments", {
      vendorId, paymentDate: "2024-03-20", amount: "1.40",
      moneyAccountId: String(db.accounts["1000"]!.A), method: "electronic",
      applications: [{ billId, amount: "1.40" }],
    });
    expect(pay.statusCode).toBe(201);
    expect((await get(app, db.companyA, `/api/c/bills/${billId}`)).json())
      .toMatchObject({ open: "0.0000", state: "paid" });
    await expectBooksTie();
  });

  it("422s in English when a quantity and a price multiply out to nothing", async () => {
    const vendorId = await createVendor(db.companyA, "Rounds To Nothing Vendor Co");
    const { id: itemId } = await createItem(db.companyA, { name: "Rounds To Nothing Widget" });
    const res = await post(app, db.companyA, "/api/c/bills", {
      vendorId, billDate: "2024-03-15",
      lines: [{ itemId, quantity: "0.0001", unitPrice: "0.0001" }],
    });
    expect(res.statusCode).toBe(422);
    expect(res.json().error).toBe("That line works out to $0.00 — check the quantity and the price.");

    // ...and the same for a plain category line (whose unit price is still
    // capped at two decimals, so the tiny number has to be the quantity)
    const plain = await post(app, db.companyA, "/api/c/bills", {
      vendorId, billDate: "2024-03-15",
      lines: [{ categoryAccountId: String(db.accounts["6000"]!.A), quantity: "0.0001", unitPrice: "0.01" }],
    });
    expect(plain.statusCode).toBe(422);
    expect(plain.json().error).toBe("That line works out to $0.00 — check the quantity and the price.");
  });

  it("422s in English when a stocked item's inventory account has been turned off", async () => {
    const vendorId = await createVendor(db.companyA, "Dead Account Vendor Co");
    const deadAccount = await createAccount(db.companyA, "1390", "Inventory - Retired", "asset");
    const { id: itemId } = await createItem(db.companyA, {
      name: "Dead Account Widget", inventoryAccountId: deadAccount,
    });
    expect((await patch(app, db.companyA, `/api/c/accounts/${deadAccount}`,
      { active: false })).statusCode).toBe(200);

    const res = await post(app, db.companyA, "/api/c/bills", {
      vendorId, billDate: "2024-03-15",
      lines: [{ itemId, quantity: "1", unitPrice: "5.00" }],
    });
    expect(res.statusCode).toBe(422);
    expect(res.json().error).toMatch(/is turned off — turn it back on/i);
  });

  it("leaves A/P reporting untouched: an inventory bill is an ordinary payable", async () => {
    // Everything downstream of a bill sums bill_lines.amount — bill_open_balance(),
    // ap_aging(), the vendor list's derived balance, the dashboard. An item line
    // changes WHICH account the debit lands on, never what the vendor is owed,
    // so all of it has to read exactly as it did before.
    const vendorId = await createVendor(db.companyA, "Payable Reporting Vendor Co");
    const { id: itemId } = await createItem(db.companyA, { name: "Payable Reporting Widget" });
    const { billId, issue } = await billAndIssue(db.companyA, vendorId, [
      { itemId, quantity: "3", unitPrice: "11.00" },
      { categoryAccountId: String(db.accounts["6000"]!.A), unitPrice: "7.00", description: "Freight" },
    ], { refNo: "PAYREP-1" });
    expect(issue.statusCode).toBe(200);

    const bill = await get(app, db.companyA, `/api/c/bills/${billId}`);
    expect(bill.json()).toMatchObject({ total: "40.0000", open: "40.0000", state: "open" });

    const vendors = await get(app, db.companyA, "/api/c/vendors");
    const vendor = vendors.json().find((v: { id: string }) => v.id === vendorId);
    expect(vendor.balance).toBe("40.0000");

    const aging = await get(app, db.companyA, "/api/c/reports/ap-aging");
    expect(aging.statusCode).toBe(200);
    const agingVendor = aging.json().vendors.find((v: { vendorId: string }) => v.vendorId === vendorId);
    expect(agingVendor.total).toBe("40.0000");

    // ...and it is payable exactly like any other bill
    const pay = await post(app, db.companyA, "/api/c/bill-payments", {
      vendorId, paymentDate: "2024-03-20", amount: "40.00",
      moneyAccountId: String(db.accounts["1000"]!.A), method: "electronic",
      applications: [{ billId, amount: "40.00" }],
    });
    expect(pay.statusCode).toBe(201);
    await expectBooksTie();
    const paid = await get(app, db.companyA, `/api/c/bills/${billId}`);
    expect(paid.json()).toMatchObject({ open: "0.0000", state: "paid" });
  });

  it("freezes an item's kind and inventory account once it has stock history", async () => {
    const vendorId = await createVendor(db.companyA, "Frozen Item Vendor Co");
    const { id: itemId } = await createItem(db.companyA, { name: "Frozen Field Widget" });
    const { issue } = await billAndIssue(db.companyA, vendorId, [
      { itemId, quantity: "1", unitPrice: "1.00" },
    ], { refNo: "FROZEN-1" });
    expect(issue.statusCode).toBe(200);

    const kindChange = await patch(app, db.companyA, `/api/c/items/${itemId}`, {
      name: "Frozen Field Widget", kind: "service",
    });
    expect(kindChange.statusCode).toBe(409);
    expect(kindChange.json().error).toMatch(/already has stock history/i);

    const accountChange = await patch(app, db.companyA, `/api/c/items/${itemId}`, {
      name: "Frozen Field Widget", kind: "inventory",
      inventoryAccountId: inv2A, cogsAccountId: cogsA,
    });
    expect(accountChange.statusCode).toBe(409);
    expect(accountChange.json().error).toMatch(/already has stock history/i);

    // ...but the fields that are NOT frozen still change
    const rename = await patch(app, db.companyA, `/api/c/items/${itemId}`, {
      name: "Frozen Field Widget Renamed", kind: "inventory",
      inventoryAccountId: invA, cogsAccountId: cogsA, salesPrice: "9.00",
    });
    expect(rename.statusCode).toBe(200);
    expect(rename.json()).toMatchObject({ name: "Frozen Field Widget Renamed", salesPrice: "9.0000" });
    await expectBooksTie();
  });
});

// ===========================================================================
// refusals — every one in English
// ===========================================================================

describe("receiving refusals", () => {
  it("422s in English for a stocked-item line with no quantity", async () => {
    const vendorId = await createVendor(db.companyA, "No Qty Vendor Co");
    const { id: itemId } = await createItem(db.companyA, { name: "No Qty Widget" });
    const res = await post(app, db.companyA, "/api/c/bills", {
      vendorId, billDate: "2024-03-15",
      lines: [{ itemId, unitPrice: "5.00" }],
    });
    expect(res.statusCode).toBe(422);
    expect(res.json().error).toBe('Enter how many of "No Qty Widget" you received.');
  });

  it("422s in English for a plain line coded straight to an inventory account", async () => {
    const vendorId = await createVendor(db.companyA, "Direct Inventory Vendor Co");
    const res = await post(app, db.companyA, "/api/c/bills", {
      vendorId, billDate: "2024-03-15",
      lines: [{ categoryAccountId: invA, unitPrice: "100.00" }],
    });
    expect(res.statusCode).toBe(422);
    expect(res.json().error).toMatch(/is an inventory account — pick the item you're buying instead/i);
  });

  it("refuses a line coded to an inventory account at the DATABASE, not only in the API", async () => {
    // The API's own refusal above is the friendly restatement; this is the
    // guard. 0018's deferred per-account check cannot catch this case at all
    // (it fires on movements, and an entry with no movements never reaches it),
    // which is exactly why 0019 adds a trigger.
    const vendorId = await createVendor(db.companyA, "Direct Inventory DB Vendor Co");
    const create = await post(app, db.companyA, "/api/c/bills", {
      vendorId, billDate: "2024-03-15",
      lines: [{ categoryAccountId: String(db.accounts["6000"]!.A), unitPrice: "10.00" }],
    });
    const billId = create.json().billId;
    await expect(cA.query(
      `INSERT INTO bill_lines (company_id, bill_id, quantity, unit_price, amount, category_account_id)
       VALUES ($1, $2, 1, 100, 100, $3)`,
      [db.companyA, billId, invA],
    )).rejects.toThrow(/is an inventory account — pick the item you are buying instead/);
  });

  it("422s in English when a stocked item line names a different category account", async () => {
    const vendorId = await createVendor(db.companyA, "Wrong Category Vendor Co");
    const { id: itemId } = await createItem(db.companyA, { name: "Wrong Category Widget" });
    const res = await post(app, db.companyA, "/api/c/bills", {
      vendorId, billDate: "2024-03-15",
      lines: [{ itemId, quantity: "1", unitPrice: "5.00", categoryAccountId: cogsA }],
    });
    expect(res.statusCode).toBe(422);
    expect(res.json().error).toMatch(/its own inventory account/i);
  });

  it("refuses a document-only bill's stocked line at DRAFT time, not only at issue", async () => {
    // Review finding: bills used to accept this with a 201 and fail only at
    // issue. The UI saves and issues a document-only bill in one click, so
    // that left an orphan draft behind every attempt. Invoices have refused
    // at draft since Task 3; this asserts bills now match.
    const vendorId = await createVendor(db.companyA, "Doc Only Draft Vendor Co");
    const { id: itemId } = await createItem(db.companyA, { name: "Doc Only Draft Widget" });

    const draft = await post(app, db.companyA, "/api/c/bills", {
      vendorId, billDate: "2024-03-15", documentOnly: true,
      lines: [{ itemId, quantity: "5", unitPrice: "2.00" }],
    });
    expect(draft.statusCode).toBe(422);
    expect(draft.json().error).toMatch(/recorded for tracking only/i);
    expect(draft.json().error).toMatch(/can't bring stock in/i);

    // Nothing was created, so there is no orphan to clean up.
    const list = await get(app, db.companyA, "/api/c/bills?status=draft");
    expect(list.json().bills.filter((b: { vendorId: string }) => b.vendorId === vendorId)).toHaveLength(0);
  });

  it("refuses editing a document-only DRAFT into one that carries stock", async () => {
    const vendorId = await createVendor(db.companyA, "Doc Only Patch Vendor Co");
    const { id: itemId } = await createItem(db.companyA, { name: "Doc Only Patch Widget" });

    const draft = await post(app, db.companyA, "/api/c/bills", {
      vendorId, billDate: "2024-03-15", documentOnly: true,
      lines: [{ description: "Legacy freight", unitPrice: "10.00", categoryAccountId: String(db.accounts["6000"]!.A) }],
    });
    expect(draft.statusCode).toBe(201);

    const patched = await patch(app, db.companyA, `/api/c/bills/${draft.json().billId}`, {
      vendorId, billDate: "2024-03-15",
      lines: [{ itemId, quantity: "5", unitPrice: "2.00" }],
    });
    expect(patched.statusCode).toBe(422);
    expect(patched.json().error).toMatch(/recorded for tracking only/i);

    // …and a document-only bill with NO stocked line still edits freely.
    const stillFine = await patch(app, db.companyA, `/api/c/bills/${draft.json().billId}`, {
      vendorId, billDate: "2024-03-15",
      lines: [{ description: "Legacy freight", unitPrice: "12.00", categoryAccountId: String(db.accounts["6000"]!.A) }],
    });
    expect(stillFine.statusCode).toBe(200);
  });

  it("still refuses a document-only bill's stocked line AT ISSUE, which is the real guard", async () => {
    // The route now refuses this at draft (see the two tests above), so the
    // stocked line has to be written directly to reach issue_bill() at all —
    // same technique as the amount-mismatch test below. issue_bill() remains
    // the guard that actually cannot be bypassed (CLAUDE.md §12); the route's
    // refusal is only the friendlier, earlier version of it.
    const vendorId = await createVendor(db.companyA, "Document Only Vendor Co");
    const { id: itemId } = await createItem(db.companyA, { name: "Document Only Widget" });
    const create = await post(app, db.companyA, "/api/c/bills", {
      vendorId, billDate: "2024-03-15", documentOnly: true, refNo: "DOC-1",
      lines: [{ description: "Legacy line", unitPrice: "4.00", categoryAccountId: String(db.accounts["6000"]!.A) }],
    });
    expect(create.statusCode).toBe(201);
    await cA.query(
      `INSERT INTO bill_lines (company_id, bill_id, quantity, unit_price, amount,
                               category_account_id, item_id)
       VALUES ($1, $2, 3, 4, 12, $3, $4)`,
      [db.companyA, create.json().billId, invA, itemId],
    );

    const issue = await post(app, db.companyA, `/api/c/bills/${create.json().billId}/issue`);
    expect(issue.statusCode).toBe(422);
    expect(issue.json().error).toMatch(/recorded for tracking only, so it can't bring stock in/i);
    await expectBooksTie();
  });

  it("refuses at the DATABASE when a stocked line's amount doesn't match quantity x cost", async () => {
    // The API computes an item line's amount to four decimals precisely so this
    // can never happen through it — so this writes the bad row directly, which
    // is what issue_bill()'s assertion (0019) exists to catch. Without it the
    // transaction would still be refused, but by 0018's deferred per-account
    // check, with a message that names an account and no line.
    const vendorId = await createVendor(db.companyA, "Bad Amount Vendor Co");
    const { id: itemId } = await createItem(db.companyA, { name: "Bad Amount Widget" });
    const create = await post(app, db.companyA, "/api/c/bills", {
      vendorId, billDate: "2024-03-15", refNo: "BADAMT-1",
      lines: [{ categoryAccountId: String(db.accounts["6000"]!.A), unitPrice: "1.00" }],
    });
    const billId = create.json().billId;
    await cA.query(
      `INSERT INTO bill_lines (company_id, bill_id, quantity, unit_price, amount,
                               category_account_id, item_id)
       VALUES ($1, $2, 3, 1, 5, $3, $4)`,
      [db.companyA, billId, invA, itemId],
    );

    const issue = await post(app, db.companyA, `/api/c/bills/${billId}/issue`);
    expect(issue.statusCode).toBe(422);
    expect(issue.json().error).toMatch(/the quantity, the cost and the amount have to agree/i);
    await expectBooksTie();
  });

  it("400s in English for a purchase order link with no item", async () => {
    const vendorId = await createVendor(db.companyA, "Orphan PO Line Vendor Co");
    const { id: itemId } = await createItem(db.companyA, { name: "Orphan PO Line Widget" });
    const po = await createPo(db.companyA, vendorId, [{ itemId, quantity: "1", unitCost: "1.00" }]);
    await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/issue`);

    const res = await post(app, db.companyA, "/api/c/bills", {
      vendorId, billDate: "2024-03-15",
      lines: [{ poLineId: po.lines[0].id, categoryAccountId: cogsA, unitPrice: "1.00" }],
    });
    expect(res.statusCode).toBe(400);
    expect(res.json().error).toMatch(/pick the item you're receiving/i);
  });

  it("422s in English when the bill and the purchase order have different vendors", async () => {
    const vendorA = await createVendor(db.companyA, "PO Owner Vendor Co");
    const vendorB = await createVendor(db.companyA, "Other Vendor Co");
    const { id: itemId } = await createItem(db.companyA, { name: "Vendor Mismatch Widget" });
    const po = await createPo(db.companyA, vendorA, [{ itemId, quantity: "1", unitCost: "1.00" }]);
    await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/issue`);

    const res = await post(app, db.companyA, "/api/c/bills", {
      vendorId: vendorB, billDate: "2024-03-15",
      lines: [{ itemId, poLineId: po.lines[0].id, quantity: "1", unitPrice: "1.00" }],
    });
    expect(res.statusCode).toBe(422);
    expect(res.json().error).toMatch(/belong to different vendors/i);
  });

  it("422s in English when a bill line points at a purchase order line that isn't its item", async () => {
    const vendorId = await createVendor(db.companyA, "Item Mismatch Vendor Co");
    const { id: ordered } = await createItem(db.companyA, { name: "Ordered Widget" });
    const { id: other } = await createItem(db.companyA, { name: "Some Other Widget" });
    const po = await createPo(db.companyA, vendorId, [{ itemId: ordered, quantity: "1", unitCost: "1.00" }]);
    await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/issue`);

    const res = await post(app, db.companyA, "/api/c/bills", {
      vendorId, billDate: "2024-03-15",
      lines: [{ itemId: other, poLineId: po.lines[0].id, quantity: "1", unitPrice: "1.00" }],
    });
    expect(res.statusCode).toBe(422);
    expect(res.json().error).toMatch(/different item than the purchase order line/i);
  });

  it("409s in English when receiving against a purchase order that was never sent", async () => {
    const vendorId = await createVendor(db.companyA, "Unsent PO Vendor Co");
    const { id: itemId } = await createItem(db.companyA, { name: "Unsent PO Widget" });
    const po = await createPo(db.companyA, vendorId, [{ itemId, quantity: "1", unitCost: "1.00" }]);

    const res = await post(app, db.companyA, "/api/c/bills", {
      vendorId, billDate: "2024-03-15",
      lines: [{ itemId, poLineId: po.lines[0].id, quantity: "1", unitPrice: "1.00" }],
    });
    expect(res.statusCode).toBe(409);
    expect(res.json().error).toMatch(/hasn't been sent to the vendor yet/i);
  });

  it("422s in English for an item id that doesn't exist (or belongs to another company)", async () => {
    const vendorId = await createVendor(db.companyA, "Ghost Item Vendor Co");
    const { id: bItem } = await createItem(db.companyB, { name: "Company B Ghost Widget" });
    const res = await post(app, db.companyA, "/api/c/bills", {
      vendorId, billDate: "2024-03-15",
      lines: [{ itemId: bItem, quantity: "1", unitPrice: "1.00" }],
    });
    expect(res.statusCode).toBe(422);
    expect(res.json().error).toMatch(/names an item we couldn't find/i);
  });

  it("still refuses a BARE entry reversal of a stock-bearing entry, and now names the void", async () => {
    // POST /api/c/entries/:id/reverse knows nothing about inventory: it would
    // mirror the money and leave the quantity on hand untouched. Voiding the
    // BILL is the path that works (see the void suite below), and the message
    // now says so.
    const vendorId = await createVendor(db.companyA, "Bare Reverse Vendor Co");
    const { id: itemId } = await createItem(db.companyA, { name: "Bare Reverse Widget" });
    const { billId, issue } = await billAndIssue(db.companyA, vendorId, [
      { itemId, quantity: "2", unitPrice: "5.00" },
    ], { refNo: "VOID-1" });
    expect(issue.statusCode).toBe(200);

    const entryId = (await get(app, db.companyA, `/api/c/bills/${billId}`)).json().postedEntryId;
    const res = await post(app, db.companyA, `/api/c/entries/${entryId}/reverse`, {
      date: "2024-03-20",
    });
    expect(res.statusCode).toBe(409);
    expect(res.json().error).toMatch(/moved stock/i);
    expect(res.json().error).toMatch(/void the document it came from/i);

    // and the refusal left everything exactly as it was
    await expectBooksTie();
    const item = await get(app, db.companyA, `/api/c/items/${itemId}`);
    expect(item.json().qtyOnHand).toBe("2.0000");
  });
});

// ===========================================================================
// the draft-to-issue races (adversarial review F1, F1b, F3)
// ===========================================================================

describe("nothing changes underneath a draft bill", () => {
  it("refuses to un-stock an item that a draft bill line already names", async () => {
    // THE HOLE: a bill line's category account is chosen at draft time and
    // frozen on the row, while issue_bill decides what moves stock from the
    // item's kind AT ISSUE. Flip the kind in between and the bill debits an
    // inventory account with no movement behind it.
    const vendorId = await createVendor(db.companyA, "Kind Flip Vendor Co");
    const flipAccount = await createAccount(db.companyA, "1320", "Inventory - Flip", "asset");
    const { id: itemId } = await createItem(db.companyA, {
      name: "Kind Flip Widget", inventoryAccountId: flipAccount,
    });

    const create = await post(app, db.companyA, "/api/c/bills", {
      vendorId, billDate: "2024-03-15", refNo: "FLIP-1",
      lines: [{ itemId, quantity: "10", unitPrice: "1.00" }],
    });
    expect(create.statusCode).toBe(201);
    const billId = create.json().billId;

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

    // ...and it is the DATABASE that refuses it, not the route: the app role
    // cannot do it by hand either.
    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 a bill that hasn't been entered 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 a bill that hasn't been entered yet/);

    // the bill then issues normally, and everything ties
    expect((await post(app, db.companyA, `/api/c/bills/${billId}/issue`)).statusCode).toBe(200);
    await expectBooksTie();
    expect(await accountBalance(db.companyA, flipAccount)).toBe("10.0000");

    const ties = await cA.query(
      "SELECT account_id, difference FROM check_inventory_ties($1) WHERE account_id = $2",
      [db.companyA, flipAccount],
    );
    expect(ties.rows).toHaveLength(1);
    expect(ties.rows[0].difference).toBe("0.0000");
  });

  it("proves why that guard has to live on the item: an un-stocked account leaves the tie's field of view", async () => {
    // This is F1b's mechanism, demonstrated on an item with NO draft bill so
    // the guard above doesn't fire. check_inventory_ties() builds its account
    // set from items.inventory_account_id, so the moment an item stops being
    // stocked its account stops being looked at AT ALL — a wrong balance there
    // would report a difference of 0.0000 not because it is right but because
    // nobody asks. No check performed while issuing a bill can see that, which
    // is why the refusal is on the item and not at issue time.
    const orphanAccount = await createAccount(db.companyA, "1330", "Inventory - Orphan", "asset");
    const { id: itemId } = await createItem(db.companyA, {
      name: "Orphan Account Widget", inventoryAccountId: orphanAccount,
    });

    const before = await cA.query(
      "SELECT account_id FROM check_inventory_ties($1) WHERE account_id = $2",
      [db.companyA, orphanAccount],
    );
    expect(before.rows).toHaveLength(1);

    const flip = await patch(app, db.companyA, `/api/c/items/${itemId}`, {
      name: "Orphan Account Widget", kind: "non_inventory", cogsAccountId: cogsA,
    });
    expect(flip.statusCode).toBe(200);

    const after = await cA.query(
      "SELECT account_id FROM check_inventory_ties($1) WHERE account_id = $2",
      [db.companyA, orphanAccount],
    );
    expect(after.rows).toHaveLength(0);
    await expectBooksTie();
  });

  it("refuses at issue when a draft line's account has BECOME an inventory account", async () => {
    // The other direction of the same race: the line was written against a
    // perfectly ordinary asset account, and somebody then created an item that
    // claims it. Nothing about the line changed; the world did.
    const vendorId = await createVendor(db.companyA, "Adopted Account Vendor Co");
    const adopted = await createAccount(db.companyA, "1340", "Supplies On Hand", "asset");

    const create = await post(app, db.companyA, "/api/c/bills", {
      vendorId, billDate: "2024-03-15", refNo: "ADOPT-1",
      lines: [{ categoryAccountId: adopted, unitPrice: "700.00" }],
    });
    expect(create.statusCode).toBe(201);

    await createItem(db.companyA, { name: "Adopting Widget", inventoryAccountId: adopted });

    const issue = await post(app, db.companyA, `/api/c/bills/${create.json().billId}/issue`);
    expect(issue.statusCode).toBe(422);
    expect(issue.json().error).toMatch(/is an inventory account — pick the item you are buying instead/i);

    // nothing posted, and the newly-claimed account ties at zero
    expect(await accountBalance(db.companyA, adopted)).toBe("0.0000");
    await expectBooksTie();
  });

  it("refuses at issue when the purchase order was cancelled after the draft was written", async () => {
    const vendorId = await createVendor(db.companyA, "Cancel Race Vendor Co");
    const { id: itemId } = await createItem(db.companyA, { name: "Cancel Race Widget" });
    const po = await createPo(db.companyA, vendorId, [{ itemId, quantity: "10", unitCost: "2.00" }]);
    await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/issue`);

    const create = await post(app, db.companyA, "/api/c/bills", {
      vendorId, billDate: "2024-03-15", refNo: "CANCELRACE-1",
      lines: [{ itemId, poLineId: po.lines[0].id, quantity: "10", unitPrice: "2.00" }],
    });
    expect(create.statusCode).toBe(201);

    expect((await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/cancel`)).statusCode).toBe(200);

    const issue = await post(app, db.companyA, `/api/c/bills/${create.json().billId}/issue`);
    expect(issue.statusCode).toBe(409);
    expect(issue.json().error).toMatch(/was cancelled/i);

    // and the cancelled order does NOT report the stock as received
    const detail = await get(app, db.companyA, `/api/c/purchase-orders/${po.id}`);
    expect(detail.json().lines[0].qtyReceived).toBe("0.0000");
    const item = await get(app, db.companyA, `/api/c/items/${itemId}`);
    expect(item.json().qtyOnHand).toBe("0.0000");
    await expectBooksTie();
  });
});

// ===========================================================================
// adopting an inventory account (re-review NEW-1)
// ===========================================================================

describe("an account can only become an inventory account if nothing is hiding in it", () => {
  it("refuses re-adoption of an account that collected an ordinary posting while un-stocked", async () => {
    // (a) create on 1350 -> flip away while it still has no movements
    // (allowed) -> post an ordinary $700 category line into 1350 -> flip back.
    const readopt = await createAccount(db.companyA, "1350", "Inventory - Readopt", "asset");
    const { id: itemId } = await createItem(db.companyA, {
      name: "Readopt Widget", inventoryAccountId: readopt,
    });
    expect((await patch(app, db.companyA, `/api/c/items/${itemId}`, {
      name: "Readopt Widget", kind: "non_inventory", cogsAccountId: cogsA,
    })).statusCode).toBe(200);

    const vendorId = await createVendor(db.companyA, "Readopt Vendor Co");
    const { issue } = await billAndIssue(db.companyA, vendorId, [
      { categoryAccountId: readopt, unitPrice: "700.00", description: "Bought some stuff" },
    ], { refNo: "READOPT-1" });
    expect(issue.statusCode).toBe(200);
    expect(await accountBalance(db.companyA, readopt)).toBe("700.0000");

    const back = await patch(app, db.companyA, `/api/c/items/${itemId}`, {
      name: "Readopt Widget", kind: "inventory",
      inventoryAccountId: readopt, cogsAccountId: cogsA,
    });
    expect(back.statusCode).toBe(422);
    expect(back.json().error).toMatch(/already holds \$700\.00 that no stock is recorded behind/);
    expect(back.json().error).toMatch(/Point this item at a new inventory account/);
    await expectBooksTie();
  });

  it("refuses the FIRST item on an account already funded by imported history", async () => {
    // (b) the documented migration path: history put $5,000 in "Inventory"
    // long before any item existed.
    const history = await createAccount(db.companyA, "1360", "Inventory - History", "asset");
    const vendorId = await createVendor(db.companyA, "History Vendor Co");
    const { issue } = await billAndIssue(db.companyA, vendorId, [
      { categoryAccountId: history, unitPrice: "5000.00", description: "Migrated history" },
    ], { refNo: "HIST-1" });
    expect(issue.statusCode).toBe(200);
    expect(await accountBalance(db.companyA, history)).toBe("5000.0000");

    const res = await post(app, db.companyA, "/api/c/items", {
      name: "History Widget", kind: "inventory",
      inventoryAccountId: history, cogsAccountId: cogsA,
    });
    expect(res.statusCode).toBe(422);
    expect(res.json().error).toBe(
      "'Inventory - History' already holds $5000.00 that no stock is recorded behind. " +
      "Point this item at a new inventory account and record your opening quantities there — " +
      "that moves the value across and keeps your stock and your books matched.");

    // the app role cannot do it by hand either — this is a database rule
    await expect(cA.query(
      `INSERT INTO items (company_id, name, kind, inventory_account_id, cogs_account_id)
       VALUES ($1, 'History Widget Direct', 'inventory', $2, $3)`,
      [db.companyA, history, cogsA],
    )).rejects.toThrow(/already holds \$5000\.00 that no stock is recorded behind/);
    await expectBooksTie();
  });

  it("names a sub-cent shortfall truthfully instead of rendering it as $0.00", async () => {
    const crumbs = await createAccount(db.companyA, "1375", "Inventory - Crumbs", "asset");
    await cA.query("BEGIN");
    const e = await cA.query(
      `INSERT INTO journal_entries (company_id, entry_date, memo, created_by)
       VALUES ($1, '2024-02-01', 'rounding residue', $2) RETURNING id`,
      [db.companyA, db.userA]);
    await cA.query(
      `INSERT INTO journal_lines (company_id, entry_id, account_id, debit, credit)
       VALUES ($1,$2,$3,0.0001,0), ($1,$2,$4,0,0.0001)`,
      [db.companyA, e.rows[0].id, crumbs, String(db.accounts["3000"]!.A)]);
    await cA.query("SELECT post_entry($1, $2)", [db.companyA, e.rows[0].id]);
    await cA.query("COMMIT");

    const res = await post(app, db.companyA, "/api/c/items", {
      name: "Crumbs Widget", kind: "inventory",
      inventoryAccountId: crumbs, cogsAccountId: cogsA,
    });
    expect(res.statusCode).toBe(422);
    expect(res.json().error).toMatch(/already holds \$0\.0001 that no stock is recorded behind/);
    expect(res.json().error).not.toMatch(/\$0\.00 that/);
  });

  it("allows a fresh $0 account, and a second item joining a properly-backed one", async () => {
    const fresh = await createAccount(db.companyA, "1370", "Inventory - Fresh", "asset");
    const { id: first } = await createItem(db.companyA, {
      name: "Fresh Account Widget", inventoryAccountId: fresh,
    });

    const vendorId = await createVendor(db.companyA, "Fresh Account Vendor Co");
    const { issue } = await billAndIssue(db.companyA, vendorId, [
      { itemId: first, quantity: "100", unitPrice: "5.00" },
    ], { refNo: "FRESH-1" });
    expect(issue.statusCode).toBe(200);
    expect(await accountBalance(db.companyA, fresh)).toBe("500.0000");
    await expectBooksTie();

    // a second item adopting an account holding $500 of properly-backed stock
    const second = await post(app, db.companyA, "/api/c/items", {
      name: "Fresh Account Widget Two", kind: "inventory",
      inventoryAccountId: fresh, cogsAccountId: cogsA,
    });
    expect(second.statusCode).toBe(201);
    await expectBooksTie();
  });

  it("runs the migration reclassification end to end and lands on a perfect tie", async () => {
    // History funded "1380 Inventory (history)" with $5,000 and no items exist.
    const oldAccount = await createAccount(db.companyA, "1380", "Inventory - Imported", "asset");
    const vendorId = await createVendor(db.companyA, "Reclassify Vendor Co");
    expect((await billAndIssue(db.companyA, vendorId, [
      { categoryAccountId: oldAccount, unitPrice: "5000.00", description: "Imported history" },
    ], { refNo: "RECLASS-1" })).issue.statusCode).toBe(200);
    expect(await accountBalance(db.companyA, oldAccount)).toBe("5000.0000");

    const equity = String(db.accounts["3000"]!.A);
    const equityBefore = await accountBalance(db.companyA, equity);

    // 1. point the items at a NEW account (balance $0, so adoption is allowed)
    const newAccount = await createAccount(db.companyA, "1385", "Inventory - Stock", "asset");
    const { id: bolts } = await createItem(db.companyA, {
      name: "Reclass Bolts", inventoryAccountId: newAccount,
    });
    const { id: nuts } = await createItem(db.companyA, {
      name: "Reclass Nuts", inventoryAccountId: newAccount,
    });

    // 2. opening quantities per item, against Opening Balance Equity
    //    (Dr new inventory / Cr equity) — 400 bolts at 7.50 = 3,000 and
    //    2,000 nuts at 1.00 = 2,000, together the $5,000 history carried.
    await cA.query(
      "SELECT record_adjustment($1, $2, '2024-01-01'::date, 400::numeric, 7.50::numeric, $3, $4, $5)",
      [db.companyA, bolts, equity, "opening quantity", db.userA]);
    await cA.query(
      "SELECT record_adjustment($1, $2, '2024-01-01'::date, 2000::numeric, 1.00::numeric, $3, $4, $5)",
      [db.companyA, nuts, equity, "opening quantity", db.userA]);

    // 3. one reclassifying entry draining the old account (Dr equity / Cr old)
    await cA.query("BEGIN");
    const entry = await cA.query(
      `INSERT INTO journal_entries (company_id, entry_date, memo, created_by)
       VALUES ($1, '2024-01-01', 'Reclassify imported inventory to stock accounts', $2)
       RETURNING id`, [db.companyA, db.userA]);
    const entryId = entry.rows[0].id;
    await cA.query(
      `INSERT INTO journal_lines (company_id, entry_id, account_id, debit, credit)
       VALUES ($1,$2,$3,5000,0), ($1,$2,$4,0,5000)`,
      [db.companyA, entryId, equity, oldAccount]);
    await cA.query("SELECT post_entry($1, $2)", [db.companyA, entryId]);
    await cA.query("COMMIT");

    // the old account is drained, the new one is backed to the penny, and the
    // equity account is exactly where it started
    expect(await accountBalance(db.companyA, oldAccount)).toBe("0.0000");
    expect(await accountBalance(db.companyA, newAccount)).toBe("5000.0000");
    expect(await accountBalance(db.companyA, equity)).toBe(equityBefore);
    await expectBooksTie();

    const perAccount = await cA.query(
      "SELECT code, ledger_value, movement_value, difference FROM check_inventory_ties($1) WHERE account_id = $2",
      [db.companyA, newAccount]);
    expect(perAccount.rows[0]).toMatchObject({
      code: "1385", ledger_value: "5000.0000", movement_value: "5000.0000", difference: "0.0000",
    });
    // the drained account is nobody's inventory account, so it isn't 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);

    expect((await get(app, db.companyA, `/api/c/items/${bolts}`)).json())
      .toMatchObject({ qtyOnHand: "400.0000", averageCost: "7.500000", value: "3000.0000" });
  });
});

// ===========================================================================
// the other half of the tie: money may not enter an inventory account without
// the stock behind it (re-review M1)
// ===========================================================================

describe("no entry can put unbacked money into an inventory account", () => {
  it("refuses a plain TRANSFER whose destination is an inventory account", async () => {
    // The reviewer's repro, and the reason this guard is structural rather than
    // one more named door: entries.ts refuses an asset CATEGORY for money-in and
    // money-out, but a transfer requires both sides to be assets, so an
    // inventory account is a perfectly legal destination.
    const gate = await createAccount(db.companyA, "1395", "Inventory - Gate", "asset");
    await createItem(db.companyA, { name: "Gate Widget", inventoryAccountId: gate });

    const res = await post(app, db.companyA, "/api/c/entries", {
      kind: "transfer", date: "2024-04-01", amount: "900.00",
      fromAccountId: String(db.accounts["1000"]!.A), toAccountId: gate,
    });
    expect(res.statusCode).toBe(422);
    expect(res.json().error).toBe(
      "You can't move money into or out of 'Inventory - Gate' directly — it is an inventory account. " +
      "Receive stock on a bill, or record an inventory adjustment, so the quantity and the value move together.");

    expect(await accountBalance(db.companyA, gate)).toBe("0.0000");
    await expectBooksTie();
  });

  it("refuses a transfer OUT of an inventory account too", async () => {
    const gate = await createAccount(db.companyA, "1396", "Inventory - Gate Out", "asset");
    const { id: itemId } = await createItem(db.companyA, {
      name: "Gate Out Widget", inventoryAccountId: gate,
    });
    const vendorId = await createVendor(db.companyA, "Gate Out Vendor Co");
    expect((await billAndIssue(db.companyA, vendorId, [
      { itemId, quantity: "10", unitPrice: "10.00" },
    ], { refNo: "GATEOUT-1" })).issue.statusCode).toBe(200);
    await expectBooksTie();

    const res = await post(app, db.companyA, "/api/c/entries", {
      kind: "transfer", date: "2024-04-01", amount: "50.00",
      fromAccountId: gate, toAccountId: String(db.accounts["1000"]!.A),
    });
    expect(res.statusCode).toBe(422);
    expect(res.json().error).toMatch(/it is an inventory account/);
    expect(await accountBalance(db.companyA, gate)).toBe("100.0000");
    await expectBooksTie();
  });

  it("refuses a hand-written journal entry at the DATABASE, not just at the route", async () => {
    const gate = await createAccount(db.companyA, "1397", "Inventory - Gate Raw", "asset");
    await createItem(db.companyA, { name: "Gate Raw Widget", inventoryAccountId: gate });

    await expect((async () => {
      await cA.query("BEGIN");
      const e = await cA.query(
        `INSERT INTO journal_entries (company_id, entry_date, memo, created_by)
         VALUES ($1, '2024-04-01', 'sneaky', $2) RETURNING id`, [db.companyA, db.userA]);
      await cA.query(
        `INSERT INTO journal_lines (company_id, entry_id, account_id, debit, credit)
         VALUES ($1,$2,$3,900,0), ($1,$2,$4,0,900)`,
        [db.companyA, e.rows[0].id, gate, String(db.accounts["1000"]!.A)]);
      await cA.query("SELECT post_entry($1, $2)", [db.companyA, e.rows[0].id]);
      await cA.query("COMMIT");
    })()).rejects.toThrow(/it is an inventory account/);
    await cA.query("ROLLBACK").catch(() => { /* already rolled back */ });

    expect(await accountBalance(db.companyA, gate)).toBe("0.0000");
    await expectBooksTie();
  });

  it("ALLOWS the same-account build wash: movements net zero, the entry posts zero", async () => {
    // Task 3's shape, built by hand here because post_build() doesn't exist
    // yet: a component and its assembly share one inventory account, so the
    // build posts Dr <inventory> / Cr <inventory> for the build value — two
    // lines, each with exactly one non-zero side, netting zero on the account.
    // The movements net zero on it too. 0 = 0, so this must pass.
    const washAccount = await createAccount(db.companyA, "1398", "Inventory - Wash", "asset");
    const { id: component } = await createItem(db.companyA, {
      name: "Wash Component", inventoryAccountId: washAccount,
    });
    const { id: assembly } = await createItem(db.companyA, {
      name: "Wash Assembly", inventoryAccountId: washAccount,
    });

    const vendorId = await createVendor(db.companyA, "Wash Vendor Co");
    expect((await billAndIssue(db.companyA, vendorId, [
      { itemId: component, quantity: "10", unitPrice: "1.00" },
    ], { refNo: "WASH-1" })).issue.statusCode).toBe(200);
    await expectBooksTie();

    await sysA.query("BEGIN");
    const entry = await sysA.query(
      `INSERT INTO journal_entries (company_id, entry_date, memo, created_by)
       VALUES ($1, '2024-04-01', 'Build 5 Wash Assembly', $2) RETURNING id`,
      [db.companyA, db.userA]);
    const entryId = entry.rows[0].id;
    // components out, at the running average (the caller may not name a cost)
    const out = await sysA.query(
      `SELECT record_movement($1, $2::bigint, '2024-04-01'::date, -5::numeric, NULL,
                              'build_out', NULL, NULL, $3::bigint) AS id`,
      [db.companyA, component, entryId]);
    const outValue = (await sysA.query(
      "SELECT value FROM inventory_movements WHERE company_id=$1 AND id=$2",
      [db.companyA, out.rows[0].id])).rows[0].value;
    expect(outValue).toBe("-5.0000");
    // the assembly in, at exactly what the components cost
    await sysA.query(
      `SELECT record_movement($1, $2::bigint, '2024-04-01'::date, 5::numeric, NULL,
                              'build_in', NULL, NULL, $3::bigint, $4::numeric)`,
      [db.companyA, assembly, entryId, "5.0000"]);
    // the wash
    await sysA.query(
      `INSERT INTO journal_lines (company_id, entry_id, account_id, debit, credit)
       VALUES ($1,$2,$3,5,0), ($1,$2,$3,0,5)`,
      [db.companyA, entryId, washAccount]);
    await sysA.query("SELECT post_entry($1, $2)", [db.companyA, entryId]);
    await sysA.query("COMMIT");

    // the account's balance is untouched, both items moved, and it all ties
    expect(await accountBalance(db.companyA, washAccount)).toBe("10.0000");
    expect((await get(app, db.companyA, `/api/c/items/${component}`)).json().qtyOnHand).toBe("5.0000");
    expect((await get(app, db.companyA, `/api/c/items/${assembly}`)).json())
      .toMatchObject({ qtyOnHand: "5.0000", value: "5.0000", averageCost: "1.000000" });
    await expectBooksTie();
  });

  it("ALLOWS an inventory adjustment, which carries its movement with it", async () => {
    const adjAccount = await createAccount(db.companyA, "1399", "Inventory - Adjust", "asset");
    const { id: itemId } = await createItem(db.companyA, {
      name: "Adjust Gate Widget", inventoryAccountId: adjAccount,
    });
    const res = await cA.query(
      "SELECT record_adjustment($1, $2, '2024-04-01'::date, 12::numeric, 2.50::numeric, $3, $4, $5) AS entry_id",
      [db.companyA, itemId, String(db.accounts["3000"]!.A), "opening count", db.userA]);
    expect(res.rows[0].entry_id).toBeTruthy();

    expect(await accountBalance(db.companyA, adjAccount)).toBe("30.0000");
    expect((await get(app, db.companyA, `/api/c/items/${itemId}`)).json())
      .toMatchObject({ qtyOnHand: "12.0000", value: "30.0000" });
    await expectBooksTie();
  });
});

// ===========================================================================
// undoing a receipt (adversarial review F4)
// ===========================================================================

describe("voiding a bill that brought stock in", () => {
  it("gives the stock back, reopens the purchase order, and keeps the tie", async () => {
    const vendorId = await createVendor(db.companyA, "Undo Receipt Vendor Co");
    const { id: itemId } = await createItem(db.companyA, { name: "Undo Receipt Widget" });
    const po = await createPo(db.companyA, vendorId, [{ itemId, quantity: "10", unitCost: "5.00" }]);
    await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/issue`);

    const invBefore = await accountBalance(db.companyA, invA);
    const apBefore = await accountBalance(db.companyA, String(db.accounts["2000"]!.A));

    const { billId, issue } = await billAndIssue(db.companyA, vendorId, [
      { itemId, poLineId: po.lines[0].id, quantity: "10", unitPrice: "5.00" },
    ], { refNo: "UNDO-1" });
    expect(issue.statusCode).toBe(200);
    expect((await get(app, db.companyA, `/api/c/purchase-orders/${po.id}`)).json().state).toBe("closed");
    await expectBooksTie();

    const voided = await post(app, db.companyA, `/api/c/bills/${billId}/void`, { date: "2024-03-20" });
    expect(voided.statusCode).toBe(200);
    await expectBooksTie();

    // the stock came back...
    const item = await get(app, db.companyA, `/api/c/items/${itemId}`);
    expect(item.json()).toMatchObject({ qtyOnHand: "0.0000", value: "0.0000" });
    expect(item.json().movements).toHaveLength(2);
    expect(item.json().movements[0]).toMatchObject({
      qty: "-10.0000", value: "-50.0000", unitCost: "5.000000", runningQty: "0.0000",
    });

    // ...the money came back...
    expect(await accountBalance(db.companyA, invA)).toBe(invBefore);
    expect(await accountBalance(db.companyA, String(db.accounts["2000"]!.A))).toBe(apBefore);

    // ...the bill is void and carries its reversal...
    const bill = await get(app, db.companyA, `/api/c/bills/${billId}`);
    expect(bill.json()).toMatchObject({ status: "void", state: "void" });
    expect(bill.json().voidEntryId).toBeTruthy();

    // ...and the purchase order is open again with nothing received.
    const reopened = await get(app, db.companyA, `/api/c/purchase-orders/${po.id}`);
    expect(reopened.json()).toMatchObject({ status: "open", state: "open" });
    expect(reopened.json().lines[0]).toMatchObject({
      qtyReceived: "0.0000", qtyOutstanding: "10.0000",
    });

    // ...so it can be received again
    const again = await post(app, db.companyA, `/api/c/purchase-orders/${po.id}/receive`);
    expect(again.statusCode).toBe(200);
    expect(again.json().lines[0].quantity).toBe("10.0000");
  });

  it("refuses in English when the stock it brought in has already been used", async () => {
    const vendorId = await createVendor(db.companyA, "Consumed Stock Vendor Co");
    const { id: itemId } = await createItem(db.companyA, { name: "Consumed Widget" });
    const { billId, issue } = await billAndIssue(db.companyA, vendorId, [
      { itemId, quantity: "10", unitPrice: "5.00" },
    ], { refNo: "CONSUMED-1" });
    expect(issue.statusCode).toBe(200);

    // consume 6 of the 10 (an inventory adjustment is Task 2's only outbound
    // path; a sale would do the same thing in Task 3)
    await cA.query(
      "SELECT record_adjustment($1, $2, '2024-03-16'::date, -6::numeric, NULL, $3, $4, $5)",
      [db.companyA, itemId, String(db.accounts["6000"]!.A), "shrinkage", db.userA],
    );
    await expectBooksTie();

    const voided = await post(app, db.companyA, `/api/c/bills/${billId}/void`, { date: "2024-03-20" });
    expect(voided.statusCode).toBe(409);
    expect(voided.json().error).toBe(
      "You can't undo this receipt — 6 of the 10 of 'Consumed Widget' it brought in have already been used. Correct it with an inventory adjustment instead.");

    // the refusal changed nothing
    const item = await get(app, db.companyA, `/api/c/items/${itemId}`);
    expect(item.json().qtyOnHand).toBe("4.0000");
    expect((await get(app, db.companyA, `/api/c/bills/${billId}`)).json().state).toBe("open");
    await expectBooksTie();
  });

  it("gives back the ORIGINAL cost, not the running average", async () => {
    // Receive 10 at 5.00 then 10 at 7.00 (average 6.00) and void the first.
    // Reversing at the average would credit 60.00 against a 50.00 debit — the
    // tie would refuse it — and would leave the survivors mispriced. What is
    // left has to be exactly the second receipt: 10 at 7.00.
    const vendorId = await createVendor(db.companyA, "Original Cost Vendor Co");
    const { id: itemId } = await createItem(db.companyA, { name: "Original Cost Widget" });

    const first = await billAndIssue(db.companyA, vendorId, [
      { itemId, quantity: "10", unitPrice: "5.00" },
    ], { refNo: "ORIGCOST-1" });
    expect(first.issue.statusCode).toBe(200);
    const second = await billAndIssue(db.companyA, vendorId, [
      { itemId, quantity: "10", unitPrice: "7.00" },
    ], { refNo: "ORIGCOST-2" });
    expect(second.issue.statusCode).toBe(200);

    expect((await get(app, db.companyA, `/api/c/items/${itemId}`)).json())
      .toMatchObject({ qtyOnHand: "20.0000", averageCost: "6.000000", value: "120.0000" });

    const voided = await post(app, db.companyA, `/api/c/bills/${first.billId}/void`, { date: "2024-03-20" });
    expect(voided.statusCode).toBe(200);
    await expectBooksTie();

    expect((await get(app, db.companyA, `/api/c/items/${itemId}`)).json())
      .toMatchObject({ qtyOnHand: "10.0000", averageCost: "7.000000", value: "70.0000" });
  });

  it("still refuses to void a bill with payments applied", async () => {
    const vendorId = await createVendor(db.companyA, "Paid Receipt Vendor Co");
    const { id: itemId } = await createItem(db.companyA, { name: "Paid Receipt Widget" });
    const { billId, issue } = await billAndIssue(db.companyA, vendorId, [
      { itemId, quantity: "2", unitPrice: "5.00" },
    ], { refNo: "PAIDRCV-1" });
    expect(issue.statusCode).toBe(200);

    const pay = await post(app, db.companyA, "/api/c/bill-payments", {
      vendorId, paymentDate: "2024-03-20", amount: "10.00",
      moneyAccountId: String(db.accounts["1000"]!.A), method: "electronic",
      applications: [{ billId, amount: "10.00" }],
    });
    expect(pay.statusCode).toBe(201);

    const voided = await post(app, db.companyA, `/api/c/bills/${billId}/void`);
    expect(voided.statusCode).toBe(409);
    expect(voided.json().error).toMatch(/has payments applied/i);
    await expectBooksTie();
  });
});

// ===========================================================================
// period locks — LAST, because period_locks is append-only and every date
// above is in 2024. The lock is set through 2023-12-31 so it can only ever
// affect the bills created inside this block.
// ===========================================================================

describe("period locks", () => {
  it("refuses a receiving bill dated inside a locked period, in English", async () => {
    await db.admin.query(
      `INSERT INTO period_locks (company_id, locked_through, created_by) VALUES ($1, $2, $3)`,
      [db.companyA, "2023-12-31", db.userA],
    );

    const vendorId = await createVendor(db.companyA, "Locked Period Vendor Co");
    const { id: itemId } = await createItem(db.companyA, { name: "Locked Period Widget" });
    const create = await post(app, db.companyA, "/api/c/bills", {
      vendorId, billDate: "2023-06-01", refNo: "LOCK-1",
      lines: [{ itemId, quantity: "5", unitPrice: "2.00" }],
    });
    expect(create.statusCode).toBe(201);

    const issue = await post(app, db.companyA, `/api/c/bills/${create.json().billId}/issue`);
    expect(issue.statusCode).toBe(409);
    expect(issue.json().error).toMatch(/that year is locked \(through 2023-12-31\)/i);

    // nothing moved, and the books still tie
    const item = await get(app, db.companyA, `/api/c/items/${itemId}`);
    expect(item.json().qtyOnHand).toBe("0.0000");
    await expectBooksTie();
  });
});

// ===========================================================================
// everHeldStock, and inventory_out_of_tie on the dashboard
//
// Two questions the UI has to be able to ask that the item list alone cannot
// answer. Placed last on purpose: the first case needs a company that has never
// held stock, and company B is that company until this block funds it.
// ===========================================================================

describe("GET /api/c/items — everHeldStock", () => {
  it("is false for a company that has never held stock", async () => {
    // Company B has ITEMS (created above) and a purchase order — a PO posts
    // nothing and moves nothing, which is the point: intent is not stock.
    const res = await get(app, db.companyB, "/api/c/items");
    expect(res.statusCode).toBe(200);
    expect(res.json().items.length).toBeGreaterThan(0);
    expect(res.json().everHeldStock).toBe(false);
  });

  it("is true once stock has actually arrived", async () => {
    const vendorId = await createVendor(db.companyB, "Ever Held Stock Vendor Co");
    const { id: itemId } = await createItem(db.companyB, { name: "Ever Held Widget" });
    const { issue } = await billAndIssue(db.companyB, vendorId, [
      { itemId, quantity: "40", unitPrice: "2.50" },
    ], { refNo: "EVERHELD-1" });
    expect(issue.statusCode).toBe(200);
    await expectBooksTie(db.companyB);

    expect((await get(app, db.companyB, "/api/c/items")).json().everHeldStock).toBe(true);
  });

  it("STAYS true after the item is deactivated — the value is still on the books", async () => {
    // THE BUG THIS EXISTS FOR. An item with history can never be deleted, only
    // deactivated, and deactivating it does not take its value off the balance
    // sheet. A gate that reads the active list alone therefore hides the one
    // report that explains that value, at the moment it is the only thing that
    // would. everHeldStock is what the report gates ask instead
    // (web/src/items/useItemsPresence.ts).
    const vendorId = await createVendor(db.companyB, "Deactivated Stock Vendor Co");
    const { id: itemId } = await createItem(db.companyB, { name: "Retired Widget" });
    const { issue } = await billAndIssue(db.companyB, vendorId, [
      { itemId, quantity: "40", unitPrice: "2.50" },
    ], { refNo: "EVERHELD-2" });
    expect(issue.statusCode).toBe(200);

    const off = await patch(app, db.companyB, `/api/c/items/${itemId}`, {
      name: "Retired Widget", kind: "inventory",
      inventoryAccountId: invB, cogsAccountId: cogsB, active: false,
    });
    expect(off.statusCode).toBe(200);

    // Gone from the active list...
    const active = await get(app, db.companyB, "/api/c/items");
    expect(active.json().items.map((i: { name: string }) => i.name)).not.toContain("Retired Widget");
    // ...and its $100.00 is still sitting in the inventory account.
    expect(active.json().everHeldStock).toBe(true);
    expect(await accountBalance(db.companyB, invB)).not.toBe("0.0000");
    await expectBooksTie(db.companyB);
  });

  it("is answered per company, never leaked across the boundary", async () => {
    // Company B now holds stock; that must not be able to turn anything on for
    // a company that doesn't. (Both directions matter — this is the field a
    // whole report tab hangs off.)
    const bodyA = (await get(app, db.companyA, "/api/c/items")).json();
    const bodyB = (await get(app, db.companyB, "/api/c/items")).json();
    expect(typeof bodyA.everHeldStock).toBe("boolean");
    expect(bodyB.everHeldStock).toBe(true);
  });
});

describe("GET /api/c/reports/dashboard — health.inventoryOutOfTie", () => {
  it("puts the milestone's headline invariant on the health payload", async () => {
    // As-built decision 7 calls inventory_out_of_tie() "the single number for
    // the health panel and for tests". The tests and the valuation report used
    // it from day one; the health panel had no way to read it until now.
    for (const companyId of [db.companyA, db.companyB]) {
      const res = await get(app, companyId, "/api/c/reports/dashboard");
      expect(res.statusCode).toBe(200);
      const health = res.json().health;
      expect(health.inventoryOutOfTie).toBe("0.0000");
      expect(health.inventoryTies).toBe(true);
    }
  });

  it("reads 0.0000 for a company with no items at all, rather than failing", async () => {
    // check_inventory_ties() returns no rows for a company with no inventory
    // accounts, so the sum is 0.0000 — a company that keeps no stock pays
    // nothing for this check and is told nothing about it.
    const created = await post(app, db.companyA, "/api/companies", { name: "No Stock Co" });
    expect(created.statusCode).toBe(201);
    const fresh = created.json().id as string;

    const res = await get(app, fresh, "/api/c/reports/dashboard");
    expect(res.statusCode).toBe(200);
    expect(res.json().health.inventoryOutOfTie).toBe("0.0000");
    expect(res.json().health.inventoryTies).toBe(true);
  });
});
