def test_list_includes_all_five_rule_types(ops_client): response = ops_client.get("/api/v1/data-quality/issues") assert response.status_code == 200 issues = response.json() rule_types = {i["rule_type"] for i in issues} assert rule_types == { "possible_duplicate_customer", "missing_required_field", "odometer_regression", "booking_overlap", "vehicle_status_conflict", } def test_scan_is_idempotent_once_seeded(ops_client): # The session fixture already ran a scan as part of seeding; running again # must not create duplicate open issues for the same (rule_type, entity). response = ops_client.post("/api/v1/data-quality/scan") assert response.status_code == 200 assert response.json()["created"] == {} def test_scan_requires_operations_manager(employee_client): response = employee_client.post("/api/v1/data-quality/scan") assert response.status_code == 403 def test_s2_duplicate_customer_issue_detail(ops_client): response = ops_client.get("/api/v1/data-quality/issues/DQ-DEMO-DUPLICATE") assert response.status_code == 200 body = response.json() assert body["entity_ref"] == "CUS-0012" assert body["entity_snapshot"]["public_ref"] == "CUS-0012" assert [s["public_ref"] for s in body["related_snapshots"]] == ["CUS-0178"] def test_s4_booking_overlap_issue_detail(ops_client): response = ops_client.get("/api/v1/data-quality/issues/DQ-DEMO-OVERLAP") assert response.status_code == 200 body = response.json() assert body["entity_ref"] == "MO-016" assert set(body["evidence"]["related_refs"]) == {"BK-DEMO-OVERLAP-A", "BK-DEMO-OVERLAP-B"} def test_defer_then_reject_are_rejected_on_closed_issue(ops_client): issues = ops_client.get( "/api/v1/data-quality/issues", params={"rule_type": "missing_required_field", "status": "open"}, ).json() target = issues[0]["public_ref"] deferred = ops_client.post(f"/api/v1/data-quality/issues/{target}/defer") assert deferred.status_code == 200 assert deferred.json()["status"] == "deferred" again = ops_client.post(f"/api/v1/data-quality/issues/{target}/reject") assert again.status_code == 409 assert again.json()["error"]["code"] == "ISSUE_NOT_OPEN" def test_merge_customers_requires_operations_manager(employee_client): response = employee_client.post( "/api/v1/data-quality/issues/DQ-DEMO-DUPLICATE/merge-customers", json={"survivor_ref": "CUS-0012"}, ) assert response.status_code == 403 def test_merge_customers_rejects_unrelated_survivor(ops_client): response = ops_client.post( "/api/v1/data-quality/issues/DQ-DEMO-DUPLICATE/merge-customers", json={"survivor_ref": "CUS-0099"}, ) assert response.status_code == 422 assert response.json()["error"]["code"] == "INVALID_SURVIVOR" def test_merge_customers_s2_scenario_rewires_and_audits(ops_client): before_bookings = ops_client.get( "/api/v1/bookings", params={"vehicle_ref": "MO-001"} ) # warm the client session; irrelevant vehicle, just a cheap authenticated call assert before_bookings.status_code == 200 response = ops_client.post( "/api/v1/data-quality/issues/DQ-DEMO-DUPLICATE/merge-customers", json={"survivor_ref": "CUS-0012", "field_overrides": {"city": "Turnhout"}}, ) assert response.status_code == 200 body = response.json() assert body["survivor_ref"] == "CUS-0012" assert body["loser_ref"] == "CUS-0178" issue = ops_client.get("/api/v1/data-quality/issues/DQ-DEMO-DUPLICATE").json() assert issue["status"] == "resolved" audit_events = ops_client.get( "/api/v1/audit", params={"action": "customer_merged"} ).json() assert len(audit_events) >= 1 # Already-resolved issue cannot be merged again. replay = ops_client.post( "/api/v1/data-quality/issues/DQ-DEMO-DUPLICATE/merge-customers", json={"survivor_ref": "CUS-0012"}, ) assert replay.status_code == 409