From 5a217cad5a51475605aa6b204bc153ff382e395c Mon Sep 17 00:00:00 2001 From: Sonny May Date: Fri, 13 Nov 2026 15:22:36 +0000 Subject: [PATCH] fix: record initial ticket_history row when a ticket is created POST /tickets inserted the ticket but never wrote a history row, so the audit trail for API-created tickets started at the first status change and the original status was lost. The seed data already models creation as old_status=null -> initial status; the endpoint now does the same. The Tickets page also rendered null old_status as an empty pill; it now shows "Created". Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0171zWzo7vSa9Ls6xtBaJgNJ --- README.md | 2 +- backend/main.py | 21 ++++++++++++++++---- backend/test_main.py | 36 ++++++++++++++++++++++++++++++++++ frontend/src/pages/Tickets.jsx | 2 +- 4 files changed, 55 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 31193cb..e0af2f6 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,7 @@ Free-tier dynos sleep after 15 min of inactivity. The frontend warms the backend | RMAs | `GET/POST/PUT /rmas` | | AI | `POST /ai/suggest` | -Status changes on tickets automatically append a row to `ticket_history`. +Ticket creation and every status change automatically append a row to `ticket_history` (creation is logged with `old_status = null`). Full OpenAPI spec available at `http://localhost:8000/docs` when the backend is running. diff --git a/backend/main.py b/backend/main.py index a434e29..723e6db 100644 --- a/backend/main.py +++ b/backend/main.py @@ -225,10 +225,23 @@ def get_ticket(id: str): @app.post("/tickets") def create_ticket(t: Ticket): - return db_post( - "tickets", - clean_empty_strings(model_data(t), "customer_id", "device_id", "assigned_user_id"), - ) + data = clean_empty_strings(model_data(t), "customer_id", "device_id", "assigned_user_id") + result = db_post("tickets", data) + # Record the initial status so the audit trail starts at creation + # (old_status=None), matching the history rows in the seed data. + # Supabase returns the inserted rows because of Prefer: return=representation. + created = result[0] if isinstance(result, list) and result else result + if isinstance(created, dict) and created.get("id"): + db_post( + "ticket_history", + { + "ticket_id": created["id"], + "old_status": None, + "new_status": created.get("status", data["status"]), + "changed_by": "Agent", + }, + ) + return result @app.put("/tickets/{id}") diff --git a/backend/test_main.py b/backend/test_main.py index ebd5e80..e032bf7 100644 --- a/backend/test_main.py +++ b/backend/test_main.py @@ -438,6 +438,42 @@ def fake_get(table, params=""): ) in posts +def test_create_ticket_records_initial_status_history(client, monkeypatch): + test_client, main = client + posts = [] + + def fake_post(table, data): + posts.append((table, data)) + if table == "tickets": + return [{"id": "t9", **data, "status": "In Progress"}] + return [data] + + monkeypatch.setattr(main, "db_post", fake_post) + + res = test_client.post("/tickets", json={"title": "VPN down", "status": "In Progress"}) + + assert res.status_code == 200 + assert res.json()[0]["id"] == "t9" + assert [table for table, _ in posts] == ["tickets", "ticket_history"] + assert posts[1][1] == { + "ticket_id": "t9", + "old_status": None, + "new_status": "In Progress", + "changed_by": "Agent", + } + + +def test_create_ticket_skips_history_when_insert_returns_no_row(client, monkeypatch): + test_client, main = client + posts = [] + monkeypatch.setattr(main, "db_post", lambda table, data: posts.append(table) or []) + + res = test_client.post("/tickets", json={"title": "VPN down"}) + + assert res.status_code == 200 + assert posts == ["tickets"] + + def test_ticket_routes_convert_blank_relationship_ids_to_null(client, monkeypatch): test_client, main = client calls = [] diff --git a/frontend/src/pages/Tickets.jsx b/frontend/src/pages/Tickets.jsx index cabc416..7119f2d 100644 --- a/frontend/src/pages/Tickets.jsx +++ b/frontend/src/pages/Tickets.jsx @@ -206,7 +206,7 @@ export default function Tickets() {

Status History

{detailLoading ?

Loading history…

: history.length === 0 ?

No history yet

: history.map(h => (
- {h.old_status} + {h.old_status ?? "Created"} → {h.new_status} {new Date(h.changed_at).toLocaleString()}