"""Workflow state machine: happy paths, invalid transitions, closure locking, admin reopen, and rich-text sanitization.""" from .util import ( create_ncr, do_initial_disposition, hdr, to_closed, to_costing, to_operations, to_qc_inspection, user_id_by_email, ) async def test_full_lifecycle_direct_to_operations(client, team): ncr = await create_ncr(client, team) assert ncr["stage"] == "new_request" assert ncr["ncr_number"].startswith("NCR-") ncr = await to_operations(client, team, ncr["id"]) assert ncr["stage"] == "operations" assert ncr["secondary_review_needed"] is False assert ncr["qc_authority"] == "AS9100 8.7" r = await client.post( f"/api/ncrs/{ncr['id']}/operations-complete", headers=hdr(team["ops"]) ) body = r.json()["ncr"] assert body["stage"] == "qc_inspection" assert body["operations_complete"] is True assert body["operations_completed_by"]["email"] == team["ops"] # QC can save repeatedly without closing r = await client.post( f"/api/ncrs/{ncr['id']}/inspection", json={"qc_approval": "no", "inspection_notes": "First pass failed."}, headers=hdr(team["qc"]), ) assert r.json()["ncr"]["stage"] == "qc_inspection" r = await client.post( f"/api/ncrs/{ncr['id']}/inspection", json={"qc_approval": "yes", "inspection_notes": "Rework verified.", "qc_closed": True}, headers=hdr(team["qc"]), ) body = r.json()["ncr"] assert body["stage"] == "costing" assert body["qc_closed"] is True r = await client.post( f"/api/ncrs/{ncr['id']}/costing", json={ "labor_cost": "100.00", "material_cost": "50.25", "service_cost": "0", "other_cost": "10", }, headers=hdr(team["cost"]), ) body = r.json()["ncr"] assert body["stage"] == "closed" assert body["total_cost"] == "160.25" assert body["closed_at"] is not None # Transition history is complete and ordered stages = [t["to_stage"] for t in body["transitions"]] assert stages == ["new_request", "operations", "qc_inspection", "costing", "closed"] async def test_secondary_disposition_flow(client, team): ncr = await create_ncr(client, team) second_id = await user_id_by_email( client, team["dispo"], team["second"], "secondary_disposition_authority" ) # secondary review without assignees is rejected r = await do_initial_disposition(client, team, ncr["id"], secondary=True, secondary_ids=[]) assert r.status_code == 422 r = await do_initial_disposition( client, team, ncr["id"], secondary=True, secondary_ids=[second_id] ) body = r.json()["ncr"] assert body["stage"] == "secondary_disposition" assert [u["email"] for u in body["secondary_authorities"]] == [team["second"]] # assignee saves without releasing r = await client.post( f"/api/ncrs/{ncr['id']}/secondary-disposition", json={"disposition_notes": "

Updated by secondary.

", "release": False}, headers=hdr(team["second"]), ) assert r.json()["ncr"]["stage"] == "secondary_disposition" # then releases to operations r = await client.post( f"/api/ncrs/{ncr['id']}/secondary-disposition", json={"work_order": "WO-2002", "release": True}, headers=hdr(team["second"]), ) body = r.json()["ncr"] assert body["stage"] == "operations" assert body["work_order"] == "WO-2002" # earlier saved notes were not wiped by the release payload assert "Updated by secondary" in body["disposition_notes"] async def test_invalid_transitions_rejected(client, team): ncr = await create_ncr(client, team) # can't skip ahead from new_request r = await client.post(f"/api/ncrs/{ncr['id']}/operations-complete", headers=hdr(team["ops"])) assert r.status_code == 409 r = await client.post( f"/api/ncrs/{ncr['id']}/inspection", json={"qc_closed": True}, headers=hdr(team["qc"]), ) assert r.status_code == 409 r = await client.post( f"/api/ncrs/{ncr['id']}/costing", json={"labor_cost": "1", "material_cost": "1", "service_cost": "1", "other_cost": "1"}, headers=hdr(team["cost"]), ) assert r.status_code == 409 # once in operations, initial disposition can't run again await to_operations(client, team, ncr["id"]) r = await do_initial_disposition(client, team, ncr["id"]) assert r.status_code == 409 async def test_closed_ncr_is_fully_locked(client, team): ncr = await to_closed(client, team, (await create_ncr(client, team))["id"]) assert ncr["stage"] == "closed" for path, payload, user in [ ("initial-disposition", {"secondary_review_needed": False}, team["dispo"]), ("secondary-disposition", {"release": True}, team["second"]), ("operations-complete", None, team["ops"]), ("inspection", {"qc_closed": True}, team["qc"]), ("costing", {"labor_cost": "9", "material_cost": "9", "service_cost": "9", "other_cost": "9"}, team["cost"]), ]: r = await client.post( f"/api/ncrs/{ncr['id']}/{path}", json=payload, headers=hdr(user), ) assert r.status_code == 409, f"{path}: {r.status_code} {r.text}" assert "closed" in r.json()["detail"].lower() async def test_admin_reopen_with_reason(client, team): ncr = await to_closed(client, team, (await create_ncr(client, team))["id"]) # non-admin cannot reopen r = await client.post( f"/api/ncrs/{ncr['id']}/reopen", json={"to_stage": "costing", "reason": "Costs were entered incorrectly."}, headers=hdr(team["cost"]), ) assert r.status_code == 403 # reason is required (min length) r = await client.post( f"/api/ncrs/{ncr['id']}/reopen", json={"to_stage": "costing", "reason": ""}, headers=hdr(team["admin"]), ) assert r.status_code == 422 # reopening to 'closed' is not a valid target r = await client.post( f"/api/ncrs/{ncr['id']}/reopen", json={"to_stage": "closed", "reason": "does not make sense"}, headers=hdr(team["admin"]), ) assert r.status_code == 422 r = await client.post( f"/api/ncrs/{ncr['id']}/reopen", json={"to_stage": "costing", "reason": "Costs were entered incorrectly."}, headers=hdr(team["admin"]), ) body = r.json()["ncr"] assert body["stage"] == "costing" assert body["closed_at"] is None # costs preserved for correction assert body["labor_cost"] == "125.50" # reopen is recorded with its reason in the transition history + audit trail reopen_t = [t for t in body["transitions"] if t["action"] == "reopen"] assert len(reopen_t) == 1 assert "Costs were entered incorrectly." in reopen_t[0]["note"] audit = await client.get(f"/api/ncrs/{ncr['id']}/audit", headers=hdr(team["admin"])) actions = [a["action"] for a in audit.json()["items"]] assert "reopen" in actions # workflow resumes: costing can close it again r = await client.post( f"/api/ncrs/{ncr['id']}/costing", json={"labor_cost": "200", "material_cost": "0", "service_cost": "0", "other_cost": "0"}, headers=hdr(team["cost"]), ) assert r.json()["ncr"]["stage"] == "closed" async def test_reopen_only_from_closed(client, team): ncr = await create_ncr(client, team) r = await client.post( f"/api/ncrs/{ncr['id']}/reopen", json={"to_stage": "new_request", "reason": "not closed yet"}, headers=hdr(team["admin"]), ) assert r.status_code == 409 async def test_rich_text_is_sanitized(client, team): ncr = await create_ncr(client, team) r = await do_initial_disposition( client, team, ncr["id"], notes='

Keep

link', ) notes = r.json()["ncr"]["disposition_notes"] assert "