Files
PacketStation/tests/server/test_packets_api.py
T
spencermandClaude Fable 5.1 00531cb329
ci / frontend (push) Successful in 37s
ci / server (push) Successful in 9m18s
ci / image (push) Successful in 35s
ci / e2e (push) Successful in 2m17s
Phase 6: Q.C. inspection, project folders, packet documents and the digital ITP
The Q.C. screen (KPIs, boards, project folders), the Q.C. tab on the packet
page (check items from the qc_check_items settings row, visual exam per weld,
NDE, hardness with the NACE label, notes), accept with a route and a
signature gated by qc_missing_empty, reject with a reason or an NCR number
closing the inspection into qc_history, Q.C. folder documents per project and
unit with superseding revisions, packet documents as a closed set with
take-out reasons, the digital ITP per unit from the frozen template (line
initials with CAS, unsign, N/A, customer codes, rejects with NCR, block
signatures, sign-off with the PDF hook, admin reopen, the ITP log), hold-point
warnings by department page group, the scoped policy check for unit and
project writes, migration 0007, tests, Playwright scenarios and docs.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-17 20:08:53 -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": "other", "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"][0]["removed"]["reason"] == "wrong file"
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