Files
PacketStation/tests/server/test_packets_api.py
T
spencermandClaude Fable 5.1 5a4174e0c2
ci / frontend (push) Successful in 34s
ci / server (push) Successful in 7m15s
ci / image (push) Successful in 39s
ci / e2e (push) Successful in 2m4s
Phase 5: Supervisor and Stall screens, weld map, heat table, clock and CO verification
The shop at parity with the old build. Supervisor screen (department chips, CO headline, numbers, Change orders
on hold, station grid with clocks and CO tags, boards) and Stall screen (station picker, hero, My packets,
Submitted). Shop steps on the packet page: assign with station, welder picker and stamp; pull back; Start, Pause,
Resume; Submit with a PIN signature and the missing steps as links; review with Send to Q.C., Return to station
and Skip Q.C. route; a lead's read-only review. Weld map (pdf.js viewer with pan, pinch, zoom, full screen; marks
with leader lines; weld sheet; weld record with field CAS; carried marks). Heat map (heat table placed, dragged,
sized, removed; legacy amber tags; borrowed heat map read-only). Checklist tab with templates from settings.
VOID stamps on voided pages. Change orders: CO-n labels, supervisor verify with signature, approval copies the
heat table to the new sheet and the BOM merge keeps the shop's values. Server clock of record with VISUAL ticket
hours beside it. Presence per packet over SSE. Migration 0006 (bfx, bfy, carried_from, checklist_templates).
New endpoints /api/shop/board, /api/shop/stations, /api/shop/stall, /api/departments/{code}/people.

Tests: policy oracle for every shop action, role and status; clock arithmetic; concurrent inserts; CO apply;
frontend geometry, heat table, clock, checklist, weld sheet, screens; seven Playwright scenarios. Docs updated.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-16 22:46:56 -06:00

487 lines
26 KiB
Python

"""The packet API: hand drafts, header CAS, drawing pages, transitions, filing and the seal, the boards, assets."""
from __future__ import annotations
from collections.abc import Iterator
from typing import Any
import pytest
from fakedir import FakeDirectory
from fastapi.testclient import TestClient
from pkthelpers import (
SIGN,
as_user,
attach_drawing,
get,
happy_path,
make_draft,
make_unit,
ok_step,
phase2_client,
ready_draft,
seed_users,
set_header,
step,
tiny_pdf,
upload,
)
from pshelpers import app_conn, needs_pg
from psycopg.types.json import Jsonb
from packetstation import events
from packetstation.settings import Settings
pytestmark = needs_pg
@pytest.fixture
def pg_client2(pg_settings: Settings, fake_dir: FakeDirectory, dev_visual: None) -> Iterator[TestClient]:
with phase2_client(pg_settings, fake_dir) as client:
seed_users(client)
yield client
@pytest.fixture
def unit(pg_settings: Settings) -> tuple[int, int]:
with app_conn(pg_settings) as conn:
return make_unit(conn)
def _one(settings: Settings, sql: str, params: Any = ()) -> Any:
with app_conn(settings) as conn:
return conn.execute(sql, params).fetchone()
def _all(settings: Settings, sql: str, params: Any = ()) -> list[Any]:
with app_conn(settings) as conn:
return conn.execute(sql, params).fetchall()
# ---------------------------------------------------------------- hand drafts
def test_hand_draft_by_clerical(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None:
as_user(pg_client2, "clerk.clara")
pkt = make_draft(pg_client2, unit[1])
assert pkt["status"] == "draft" and pkt["source_kind"] == "hand" and pkt["source"] == {"kind": "hand"}
assert pkt["flags"] == ["hand"] and pkt["drawing_state"] == "none" and pkt["version"] == 1
assert pkt["flag"] == "Made by hand - check it and send it on"
assert pkt["holder"] == "Clerical" and pkt["dept"] == {"code": "03", "name": "Weld Parts", "weld": True}
wo = pkt["work_orders"][0]
assert (wo["wo_base"], wo["lot"], wo["split"], wo["sub"]) == ("L670001", "001", "0", "0")
assert pkt["unit"]["number"].endswith("001") and pkt["unit"]["project"]["customer"] == "Acme Refining"
assert [i["key"] for i in pkt["checklist_items"]][:2] == ["dwg", "heat"]
assert "D-1001 Rev B" in pkt["checklist_items"][0]["text"] and len(pkt["qc_items"]) == 10
assert "edit_header" in pkt["can"] and "send_to_warehouse" not in pkt["can"]
assert pkt["blocked"]["send_to_warehouse"] == [
"Say whether this packet needs a heat map.",
"Attach the drawing first.",
]
log = pg_client2.get(f"/api/packets/{pkt['id']}/log").json()
assert [r["action"] for r in log] == ["Packet created by hand"] and log[0]["by"]["display_name"] == "Clara Clerk"
row = _one(pg_settings, "SELECT action, detail FROM audit_events WHERE action = 'packet.created'")
assert row[1]["source"] == "hand" and row[1]["work_orders"] == 1
def test_hand_draft_rules(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None:
from pkthelpers import draft_body
as_user(pg_client2, "wh.wade")
r = pg_client2.post("/api/packets", json=draft_body(unit[1]))
assert r.status_code == 403 and r.json()["code"] == "forbidden"
as_user(pg_client2, "clerk.clara")
r = pg_client2.post("/api/packets", json=draft_body(unit[1], dept_code="QC"))
assert r.status_code == 422 and r.json()["code"] == "invalid_dept"
r = pg_client2.post("/api/packets", json=draft_body(unit[1], work_orders=[{"wo_base": 670001}]))
assert r.status_code == 422 # work order keys are text, never numbers
with app_conn(pg_settings) as conn:
conn.execute("UPDATE units SET status = 'closed' WHERE id = %s", (unit[1],))
r = pg_client2.post("/api/packets", json=draft_body(unit[1]))
assert r.status_code == 409 and r.json()["code"] == "unit_closed"
r = pg_client2.post("/api/packets", json=draft_body(unit[1]) | {"pad": "x" * 210_000})
assert r.status_code == 413 and r.json()["code"] == "too_large"
assert pg_client2.get("/api/packets/999999").status_code == 404
# ---------------------------------------------------------------- header CAS
def test_patch_header_cas(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None:
as_user(pg_client2, "clerk.clara")
pkt = make_draft(pg_client2, unit[1])
pid = pkt["id"]
r = pg_client2.patch(f"/api/packets/{pid}", json={"set": {"title": "New title"}, "base": {"title": pkt["title"]}})
assert r.status_code == 200 and r.json()["title"] == "New title" and r.json()["version"] == 2
r = pg_client2.patch(f"/api/packets/{pid}", json={"set": {"title": "Third"}, "base": {"title": "Spool 4 inlet"}})
assert r.status_code == 409 and r.json()["code"] == "field_conflict"
assert r.json()["current"] == {"title": "New title"}
# heat map chips
r = pg_client2.patch(f"/api/packets/{pid}", json={"set": {"heat_map": True}, "base": {"heat_map": None}})
assert r.status_code == 200 and r.json()["heat_map"] is True
assert pg_client2.get(f"/api/packets/{pid}/log").json()[0]["action"] == "Heat map required"
r = pg_client2.patch(f"/api/packets/{pid}", json={"set": {"heat_map": False}, "base": {"heat_map": True}})
assert r.status_code == 200 and r.json()["heat_map"] is False
r = pg_client2.patch(f"/api/packets/{pid}", json={"set": {"status": "filed"}})
assert r.status_code == 422
as_user(pg_client2, "wh.wade")
r = pg_client2.patch(f"/api/packets/{pid}", json={"set": {"title": "Mine"}})
assert r.status_code == 403 and r.json()["code"] == "forbidden"
assert r.json()["detail"] == (
"Your access doesn't allow changing the packet details on this packet. It's with Clerical right now."
)
# ---------------------------------------------------------------- drawing pages and assets
def test_attach_drawing_needs_signature(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None:
as_user(pg_client2, "clerk.clara")
pkt = make_draft(pg_client2, unit[1])
a = upload(pg_client2, tiny_pdf(2), name="D-1001 B.pdf")
assert a.status_code == 201, a.text
asset = a.json()
assert asset["mime"] == "application/pdf" and asset["pages"] == 2 and asset["url"] == f"/_blob/{asset['id']}"
url = f"/api/packets/{pkt['id']}/pages"
r = pg_client2.post(url, json={"asset_id": asset["id"], "type": "drawing"})
assert r.status_code == 400 and r.json()["code"] == "signature_required"
assert r.json()["meaning"] == "Attach a drawing by hand"
r = pg_client2.post(url, json={"asset_id": asset["id"], "sign": {"factor": "password", "secret": "nope"}})
assert r.status_code == 403 and r.json()["code"] == "wrong_secret" and r.json()["attempts_left"] == 2
fails = _one(pg_settings, "SELECT stepup_failures FROM sessions WHERE revoked_at IS NULL ORDER BY created_at DESC")
assert fails[0] == 1 # the wrong attempt was kept although the write was refused
r = pg_client2.post(url, json={"asset_id": asset["id"], "sign": SIGN})
assert r.status_code == 200, r.text
out = r.json()
assert [(p["n"], p["pn"], p["dwg"]) for p in out["pages"]] == [(1, 1, "D-1001"), (2, 2, "D-1001")]
assert out["drawing_state"] == "hand" and out["pages"][0]["url"] == f"/_blob/{asset['id']}"
log = pg_client2.get(f"/api/packets/{pkt['id']}/log").json()[0]
assert log["action"] == "Drawing attached by hand" and log["signed"] is True
r = pg_client2.post(url, json={"asset_id": asset["id"], "sign": SIGN})
assert r.status_code == 409 and r.json()["code"] == "already_attached"
r = pg_client2.request("DELETE", f"/api/packets/{pkt['id']}/pages/2", json={"reason": "wrong sheet"})
assert r.status_code == 200 and r.json()["pages"] == [] and r.json()["drawing_state"] == "none"
def test_assets_blob_and_limits(
pg_client2: TestClient, pg_settings: Settings, fake_dir: FakeDirectory, dev_visual: None
) -> None:
as_user(pg_client2, "clerk.clara")
data = tiny_pdf(1)
a = upload(pg_client2, data, name="..\\evil/name.pdf").json()
assert a["original_name"] == "name.pdf" and a["size"] == len(data)
again = upload(pg_client2, data, name="other.pdf").json()
assert again["id"] == a["id"]
assert pg_client2.get(f"/api/assets/{a['id']}").json()["sha256"] == a["sha256"]
b = pg_client2.get(a["url"])
assert b.status_code == 200 and b.content == data
assert b.headers["x-content-type-options"] == "nosniff"
assert b.headers["content-disposition"] == 'inline; filename="name.pdf"'
assert b.headers["cache-control"] == "private, max-age=3600" and a["sha256"] in b.headers["etag"]
assert pg_client2.get(a["url"] + "?download=1").headers["content-disposition"].startswith("attachment;")
assert pg_client2.get("/_blob/00000000-0000-0000-0000-000000000000").status_code == 404
r = upload(pg_client2, b"just some text, not a document", name="x.txt", mime="text/plain")
assert r.status_code == 415 and r.json()["code"] == "unsupported_type"
r = upload(pg_client2, b"%PDF-1.7\nthis is not really a pdf at all\n%%EOF\n", name="bad.pdf")
assert r.status_code == 422 and r.json()["code"] == "bad_pdf"
small = pg_settings.model_copy(update={"upload_max_bytes": 2048})
with phase2_client(small, fake_dir) as c2:
as_user(c2, "clerk.clara")
r = upload(c2, b"%PDF-1.4\n" + b"0" * 5000)
assert r.status_code == 413 and r.json()["code"] == "too_large"
with TestClient(c2.app) as anon:
assert anon.get(a["url"]).status_code == 401
# ---------------------------------------------------------------- transitions
def test_send_to_warehouse(
pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int], monkeypatch: pytest.MonkeyPatch
) -> None:
as_user(pg_client2, "clerk.clara")
pkt = make_draft(pg_client2, unit[1])
r = step(pg_client2, pkt, "send_to_warehouse")
assert r.status_code == 403 and r.json() == {
"detail": "Say whether this packet needs a heat map.",
"code": "precondition",
}
pkt = set_header(pg_client2, pkt["id"], heat_map=False).json()
r = step(pg_client2, pkt, "send_to_warehouse")
assert r.status_code == 403 and r.json()["detail"] == "Attach the drawing first."
pkt = attach_drawing(pg_client2, pkt["id"])
assert "send_to_warehouse" in pkt["can"] and "send_to_warehouse" not in pkt["blocked"]
r = pg_client2.post(
f"/api/packets/{pkt['id']}/transitions",
json={"action": "send_to_warehouse", "expect": {"status": "warehouse", "dept": "03"}},
)
assert r.status_code == 409 and r.json()["code"] == "stale_step"
assert r.json()["current"] == {"status": "draft", "dept": "03", "version": pkt["version"]}
published: list[dict[str, Any]] = []
real = events.publish
monkeypatch.setattr(events, "publish", lambda conn, payload: (published.append(payload), real(conn, payload)))
out = ok_step(pg_client2, pkt, "send_to_warehouse")
assert out["status"] == "warehouse" and out["version"] == pkt["version"] + 1 and out["holder"] == "Warehouse"
assert out["wh"]["stage"] == "incoming" and out["wh"]["by"]["display_name"] == "Clara Clerk"
assert out["wh_sent_at"] and out["wh_skipped"] is False
assert published == [
{"event": "changed", "packet_id": pkt["id"], "version": out["version"], "status": "warehouse", "dept": "03"}
]
tr = _one(
pg_settings,
"SELECT action, from_status, to_status, by FROM packet_transitions WHERE packet_id = %s",
(pkt["id"],),
)
assert tr[:3] == ("send_to_warehouse", "draft", "warehouse")
lg = _one(
pg_settings, "SELECT action, transition_id FROM packet_log WHERE packet_id = %s ORDER BY id DESC", (pkt["id"],)
)
assert lg[0] == "Sent to warehouse" and lg[1] is not None
au = _one(pg_settings, "SELECT detail FROM audit_events WHERE action = 'packet.send_to_warehouse'")
assert au[0]["from"] == "draft" and au[0]["to"] == "warehouse"
as_user(pg_client2, "clerk.clara")
r = pg_client2.patch(f"/api/packets/{pkt['id']}", json={"set": {"title": "x"}})
assert r.status_code == 403 and r.json()["code"] == "wrong_status"
def test_happy_path_file_and_reopen(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None:
pkt = happy_path(pg_client2, pg_settings, unit[1])
pid = pkt["id"]
seal = _one(pg_settings, "SELECT key_id, signature_id, broken_at FROM packet_seals WHERE packet_id = %s", (pid,))
assert seal[1] is not None and seal[2] is None
view = pg_client2.get(f"/api/packets/{pid}/seal").json()
assert view["valid"] is True and view["seal"]["key_id"] == seal[0]
actions = [
r[0]
for r in _all(pg_settings, "SELECT action FROM packet_transitions WHERE packet_id = %s ORDER BY id", (pid,))
]
assert actions == [
"send_to_warehouse",
"wh_release",
"assign",
"start",
"submit",
"send_to_qc",
"qc_accept",
"file",
]
assert pg_client2.get(f"/api/packets/{pid}/log").json()[0]["action"] == "Filed in the data book"
# filed: every write is refused, Admin included
r = pg_client2.patch(f"/api/packets/{pid}", json={"set": {"drawing": "D-2"}})
assert r.status_code == 403 and r.json()["code"] == "filed"
r = pg_client2.patch(f"/api/packets/{pid}", json={"set": {"title": "x"}})
assert r.status_code == 403 and r.json()["code"] == "filed"
as_user(pg_client2, "admin.ann")
r = pg_client2.patch(
f"/api/packets/{pid}", json={"set": {"drawing": "D-2"}, "it_reason": "Fix a typo in the drawing"}
)
assert r.status_code == 403 and r.json()["code"] == "filed"
# the data book date: Clerical and Q.C., not the warehouse
for sam, code in (("clerk.clara", 200), ("qc.quinn", 200), ("wh.wade", 403)):
as_user(pg_client2, sam)
r = pg_client2.post(f"/api/packets/{pid}/databook", json={})
assert r.status_code == code, (sam, r.text)
assert pg_client2.get(f"/api/packets/{pid}/seal").json()["valid"] is True
# reopen by Admin with a reason and a signature breaks the seal
as_user(pg_client2, "clerk.clara")
r = pg_client2.post(f"/api/packets/{pid}/reopen", json={"reason": "Wrong heat number filed", "sign": SIGN})
assert r.status_code == 403 and r.json()["code"] == "forbidden"
as_user(pg_client2, "admin.ann")
r = pg_client2.post(f"/api/packets/{pid}/reopen", json={"reason": "short", "sign": SIGN})
assert r.status_code == 400 and r.json()["code"] == "reason_required"
r = pg_client2.post(f"/api/packets/{pid}/reopen", json={"reason": "Wrong heat number filed"})
assert r.status_code == 400 and r.json()["code"] == "signature_required"
r = pg_client2.post(f"/api/packets/{pid}/reopen", json={"reason": "Wrong heat number filed", "sign": SIGN})
assert r.status_code == 200, r.text
assert r.json()["status"] == "complete" and r.json()["seal"]["broken_at"] is not None
view = pg_client2.get(f"/api/packets/{pid}/seal").json()
assert view["seal"] is None and view["seals"][0]["broken_reason"] == "Wrong heat number filed"
def test_co_hold_blocks_filing(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None:
as_user(pg_client2, "clerk.clara")
pkt = make_draft(pg_client2, unit[1])
pid = pkt["id"]
with app_conn(pg_settings) as conn:
conn.execute("UPDATE packets SET status = 'complete', routed_to = 'shipping' WHERE id = %s", (pid,))
co = conn.execute(
"INSERT INTO change_orders (packet_id, co_no, origin, review) VALUES (%s, 1, 'manual', 'approved')"
" RETURNING id",
(pid,),
).fetchone()[0]
conn.execute("INSERT INTO co_required_depts (co_id, dept_code) VALUES (%s, '03')", (co,))
pkt = get(pg_client2, pid)
assert pkt["open_cos"] == [1] and "file" in pkt["blocked"]
r = step(pg_client2, pkt, "file", sign=SIGN)
assert r.status_code == 403 and r.json()["code"] == "co_hold"
assert r.json()["detail"] == "Change order CO-1 must be verified first by: 03 Weld Parts."
as_user(pg_client2, "admin.ann")
r = step(pg_client2, pkt, "file", sign=SIGN, it_reason="Close out the old job now")
assert r.status_code == 403 and r.json()["code"] == "co_hold"
card = pg_client2.get("/api/packets", params={"status": "complete"}).json()["packets"][0]
assert card["open_cos"] == [1] and "file" in card["blocked"]
def test_admin_bypass_needs_it_reason(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None:
as_user(pg_client2, "clerk.clara")
pkt = make_draft(pg_client2, unit[1])
as_user(pg_client2, "admin.ann")
r = pg_client2.patch(f"/api/packets/{pkt['id']}", json={"set": {"title": "IT title"}})
assert r.status_code == 400 and r.json()["code"] == "it_reason_required"
r = pg_client2.patch(
f"/api/packets/{pkt['id']}", json={"set": {"title": "IT title"}, "it_reason": "Ticket 4411 fix"}
)
assert r.status_code == 200 and r.json()["title"] == "IT title"
log = pg_client2.get(f"/api/packets/{pkt['id']}/log").json()[0]
assert log["it_reason"] == "Ticket 4411 fix" and log["by"]["display_name"] == "Ann Admin"
au = _one(pg_settings, "SELECT it_reason, detail FROM audit_events WHERE action = 'packet.edit_header'")
assert au[0] == "Ticket 4411 fix" and au[1]["fields"] == ["title"]
def test_return_to_clerical_only_when_wh_skipped(
pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]
) -> None:
as_user(pg_client2, "clerk.clara")
pkt = ready_draft(pg_client2, unit[1])
r = step(pg_client2, pkt, "release_to_shop", dept="QC")
assert r.status_code == 422 and r.json()["code"] == "invalid_dept"
pkt = ok_step(pg_client2, pkt, "release_to_shop", dept="06", note="Rush job")
assert pkt["status"] == "ready" and pkt["dept"]["code"] == "06" and pkt["wh_skipped"] is True
assert pkt["stage"]["dept_code"] == "06" and pkt["return_note"] == "Rush job"
as_user(pg_client2, "lead.lou")
r = step(pg_client2, pkt, "return_to_clerical", reason="Not ours")
assert r.status_code == 403 and r.json()["code"] == "forbidden" # lead.lou leads 03, the packet is in 06
with app_conn(pg_settings) as conn:
conn.execute("UPDATE packets SET dept_code = '03' WHERE id = %s", (pkt["id"],))
pkt = get(pg_client2, pkt["id"])
r = step(pg_client2, pkt, "return_to_clerical", reason="short")
assert r.status_code == 400 and r.json()["code"] == "reason_required"
out = ok_step(pg_client2, pkt, "return_to_clerical", reason="Missing the second sheet")
assert out["status"] == "draft" and out["return_note"] == "Missing the second sheet" and out["stage"] is None
def test_assign_validation_and_draft_to_qc(
pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]
) -> None:
as_user(pg_client2, "clerk.clara")
pkt = ok_step(pg_client2, ready_draft(pg_client2, unit[1]), "release_to_shop", dept="03")
with app_conn(pg_settings) as conn:
other = conn.execute("SELECT id FROM stations WHERE dept_code = '06' LIMIT 1").fetchone()[0]
andy = conn.execute("SELECT id FROM users WHERE sam_account = 'weld.andy'").fetchone()[0]
as_user(pg_client2, "sup.wes")
r = step(pg_client2, pkt, "assign", station_id=other, welder_id=str(andy))
assert r.status_code == 422 and r.json()["code"] == "invalid_station"
r = step(pg_client2, pkt, "assign")
assert r.status_code == 403 and r.json()["detail"] == "Pick a station and a welder."
as_user(pg_client2, "clerk.clara")
q = ok_step(pg_client2, ready_draft(pg_client2, unit[1]), "send_draft_to_qc", note="Inspect as is")
assert q["status"] == "qc" and q["wh_skipped"] is True and q["qc"]["sent_note"] == "Inspect as is"
assert q["holder"] == "Q.C."
def test_move_and_remove(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None:
from pkthelpers import add_unit
with app_conn(pg_settings) as conn:
u2 = add_unit(conn, unit[0], "002")
u3 = add_unit(conn, unit[0], "7A", status="closed")
as_user(pg_client2, "clerk.clara")
pkt = make_draft(pg_client2, unit[1])
r = pg_client2.post(f"/api/packets/{pkt['id']}/move", json={"unit_id": u3})
assert r.status_code == 409 and r.json()["code"] == "unit_closed"
r = pg_client2.post(f"/api/packets/{pkt['id']}/move", json={"unit_id": u2, "reason": "Belongs to unit two"})
assert r.status_code == 200 and r.json()["unit"]["id"] == u2 and r.json()["unit"]["lot"] == "002"
assert pg_client2.get(f"/api/packets/{pkt['id']}/log").json()[0]["action"] == "Moved to another unit"
assert _one(pg_settings, "SELECT 1 FROM audit_events WHERE action = 'packet.moved'") is not None
r = pg_client2.request("DELETE", f"/api/packets/{pkt['id']}", json={"reason": "dup"})
assert r.status_code == 400 and r.json()["code"] == "reason_required"
r = pg_client2.request("DELETE", f"/api/packets/{pkt['id']}", json={"reason": "Made twice by mistake"})
assert r.status_code == 200 and r.json()["removed"]["reason"] == "Made twice by mistake"
assert get(pg_client2, pkt["id"])["removed"] is not None and get(pg_client2, pkt["id"])["can"] == []
assert pg_client2.get("/api/packets").json()["packets"] == []
r = pg_client2.patch(f"/api/packets/{pkt['id']}", json={"set": {"title": "x"}})
assert r.status_code == 403 and r.json()["code"] == "removed"
assert pg_client2.get("/api/packets", params={"removed": 1}).json()["packets"] == []
as_user(pg_client2, "admin.ann")
cards = pg_client2.get("/api/packets", params={"removed": 1}).json()["packets"]
assert [c["id"] for c in cards] == [pkt["id"]]
r = pg_client2.patch(f"/api/packets/{pkt['id']}", json={"set": {"title": "x"}, "it_reason": "Restore the title"})
assert r.status_code == 403 and r.json()["code"] == "removed"
def test_list_filters_and_cards(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None:
with app_conn(pg_settings) as conn:
_, other_unit = make_unit(conn, number="L777")
as_user(pg_client2, "clerk.clara")
a = make_draft(pg_client2, unit[1], drawing="D-55", work_orders=[{"wo_base": "007123", "lot": "7A"}])
b = ready_draft(pg_client2, other_unit, title="Other one")
ok_step(pg_client2, b, "send_to_warehouse")
cards = pg_client2.get("/api/packets").json()["packets"]
assert {c["id"] for c in cards} == {a["id"], b["id"]}
card = next(c for c in cards if c["id"] == a["id"])
assert card["holder"] == "Clerical" and card["flag"] == "Made by hand - check it and send it on"
assert card["can"] == [] and set(card["blocked"]) == {"send_to_warehouse", "release_to_shop", "send_draft_to_qc"}
assert card["unit"]["id"] == unit[1] and card["dept"] == {"code": "03", "name": "Weld Parts"}
assert card["wo_count"] == 1 and card["open_shorts"] == 0 and card["open_cos"] == []
bcard = next(c for c in cards if c["id"] == b["id"])
assert bcard["wh_stage"] == "incoming" and bcard["flag"] is None and bcard["holder"] == "Warehouse"
def ids(**params: Any) -> list[int]:
return [c["id"] for c in pg_client2.get("/api/packets", params=params).json()["packets"]]
assert ids(status="warehouse") == [b["id"]]
assert ids(status="draft,warehouse", unit_id=unit[1]) == [a["id"]]
assert ids(q="d-55") == [a["id"]] and ids(q="007123") == [a["id"]]
assert ids(q="L777 other") == [b["id"]] and ids(q="L777 D-55") == []
assert ids(flag="1") == [a["id"]] and ids(flag="0") == [b["id"]]
as_user(pg_client2, "wh.wade")
wcard = next(c for c in pg_client2.get("/api/packets", params={"status": "warehouse"}).json()["packets"])
assert set(wcard["can"]) == {"wh_send_back", "wh_release"} # transitions only, no edits
def test_pm_reads_but_cannot_write(pg_client2: TestClient, unit: tuple[int, int]) -> None:
as_user(pg_client2, "clerk.clara")
pkt = make_draft(pg_client2, unit[1])
as_user(pg_client2, "pm.paula")
view = get(pg_client2, pkt["id"])
assert view["can"] == [] and view["holder"] == "Clerical"
assert pg_client2.get("/api/packets").json()["packets"][0]["id"] == pkt["id"]
r = pg_client2.patch(f"/api/packets/{pkt['id']}", json={"set": {"title": "x"}})
assert r.status_code == 403 and r.json()["code"] == "forbidden"
r = step(pg_client2, view, "send_to_warehouse")
assert r.status_code == 403 and r.json()["code"] == "forbidden"
def test_work_orders_put_and_docs(pg_client2: TestClient, unit: tuple[int, int]) -> None:
as_user(pg_client2, "clerk.clara")
pkt = make_draft(pg_client2, unit[1])
url = f"/api/packets/{pkt['id']}/work-orders"
wos = {"work_orders": [{"wo_base": "0070001", "lot": "7A", "split": "", "sub": "01"}]}
assert pg_client2.put(url, json=wos).status_code == 428
r = pg_client2.put(url, json=wos, headers={"If-Match": str(pkt["version"] + 5)})
assert r.status_code == 409 and r.json()["code"] == "version_conflict"
r = pg_client2.put(url, json=wos, headers={"If-Match": str(pkt["version"])})
assert r.status_code == 200
wo = r.json()["work_orders"]
assert [(w["wo_base"], w["lot"], w["sub"]) for w in wo] == [("0070001", "7A", "01")]
a = upload(pg_client2, tiny_pdf(1)).json()
r = pg_client2.post(f"/api/packets/{pkt['id']}/docs", json={"asset_id": a["id"], "kind": "mtr", "title": "MTR 1"})
assert r.status_code == 200 and r.json()["docs"][0]["title"] == "MTR 1"
doc_id = r.json()["docs"][0]["id"]
as_user(pg_client2, "wh.wade")
r = pg_client2.request("DELETE", f"/api/packets/{pkt['id']}/docs/{doc_id}", json={})
assert r.status_code == 403
as_user(pg_client2, "qc.quinn")
r = pg_client2.request("DELETE", f"/api/packets/{pkt['id']}/docs/{doc_id}", json={"reason": "wrong file"})
assert r.status_code == 200 and r.json()["docs"] == []
def test_seal_detects_tampering(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None:
pkt = happy_path(pg_client2, pg_settings, unit[1])
with app_conn(pg_settings) as conn:
conn.execute("UPDATE packets SET title = 'changed behind the app' WHERE id = %s", (pkt["id"],))
conn.execute("UPDATE bom_lines SET heat = 'X' WHERE packet_id = %s", (pkt["id"],))
conn.execute(
"UPDATE packet_stages SET checklist = %s WHERE packet_id = %s", (Jsonb({"dwg": {"done": False}}), pkt["id"])
)
assert pg_client2.get(f"/api/packets/{pkt['id']}/seal").json()["valid"] is False