diff --git a/containers/backupchecks/src/backend/app/integrations/autotask/client.py b/containers/backupchecks/src/backend/app/integrations/autotask/client.py index ef29b0d..d30c07d 100644 --- a/containers/backupchecks/src/backend/app/integrations/autotask/client.py +++ b/containers/backupchecks/src/backend/app/integrations/autotask/client.py @@ -559,11 +559,16 @@ class AutotaskClient: def update_ticket_resolution_safe(self, ticket_id: int, resolution_text: str) -> Dict[str, Any]: - """Safely update the Ticket 'resolution' field without changing status. + """Safely update the Ticket 'resolution' field with conditional status update. Autotask Tickets require a full PUT update; therefore we must: - GET /Tickets/{id} to retrieve current stabilising fields (including classification/routing) - - PUT /Tickets with those stabilising fields unchanged, and only update 'resolution' + - Query time entries for the ticket + - PUT /Tickets with stabilising fields and conditional status + + Status logic (per API contract section 9): + - If NO time entries exist: set status to 5 (Complete) + - If time entries exist: keep current status unchanged IMPORTANT: - GET /Tickets/{id} returns the ticket object under the 'item' envelope in most tenants. @@ -639,14 +644,23 @@ class AutotaskClient: "Cannot safely update ticket resolution because required fields are missing: " + ", ".join(missing) ) + # Check for time entries as per API contract section 9 + # If no time entries exist, we can set status to 5 (Complete) + # If time entries exist, status remains unchanged + time_entries = self.query_time_entries_by_ticket_id(int(ticket_id)) + has_time_entries = len(time_entries) > 0 + + # Determine final status based on time entry check + # Status 5 = Complete (sets completedDate and resolvedDateTime) + final_status = resolved_status if has_time_entries else 5 + # Build payload with exact values from GET response (including null if that's what we got) payload: Dict[str, Any] = { "id": int(ticket_id), "issueType": resolved_issue_type, "subIssueType": resolved_sub_issue_type, "source": resolved_source, - # Keep status unchanged - "status": resolved_status, + "status": final_status, "resolution": str(resolution_text or ""), } @@ -955,3 +969,22 @@ class AutotaskClient: if limit and isinstance(limit, int) and limit > 0: return items[: int(limit)] return items + + def query_time_entries_by_ticket_id(self, ticket_id: int) -> List[Dict[str, Any]]: + """Query TimeEntries for a specific ticket. + + Uses POST /TimeEntries/query as per API contract section 6. + + Returns list of time entry items. Empty list if no time entries exist. + """ + + try: + tid = int(ticket_id) + except Exception: + tid = 0 + if tid <= 0: + return [] + + payload = {"filter": [{"op": "eq", "field": "ticketID", "value": tid}]} + data = self._request("POST", "TimeEntries/query", json_body=payload) + return self._as_items_list(data) diff --git a/docs/changelog-claude.md b/docs/changelog-claude.md index 7c8844f..0b214d2 100644 --- a/docs/changelog-claude.md +++ b/docs/changelog-claude.md @@ -4,6 +4,13 @@ This file documents all changes made to this project via Claude Code. ## [2026-02-05] +### Added +- Autotask conditional ticket status update based on time entries (API contract section 9): + - `query_time_entries_by_ticket_id()` - Query time entries for a ticket via POST /TimeEntries/query + - `update_ticket_resolution_safe()` now checks for time entries and conditionally sets status: + - If NO time entries exist: sets status to 5 (Complete) with completedDate and resolvedDateTime + - If time entries exist: keeps current status unchanged (ticket remains open) + ### Fixed - Autotask ticket resolution update now correctly preserves exact field values from GET response in PUT payload. The `issueType`, `subIssueType`, and `source` fields are copied with their exact values (including null)