From 8a733a356b3133676632de09e738c8c5635820df Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Thu, 5 Feb 2026 16:29:54 +0100 Subject: [PATCH] Fix Autotask resolution to preserve exact field values from GET response The issueType, subIssueType, and source fields must be sent in the PUT payload with their exact values from GET response, including null. This fixes HTTP 500 error where Autotask rejected picklist value 0. Changed _pick function to return (found, value) tuple to distinguish between "field missing" and "field is null". Co-Authored-By: Claude Sonnet 4.5 --- .../app/integrations/autotask/client.py | 52 ++++++++++++------- docs/changelog-claude.md | 5 ++ 2 files changed, 38 insertions(+), 19 deletions(-) diff --git a/containers/backupchecks/src/backend/app/integrations/autotask/client.py b/containers/backupchecks/src/backend/app/integrations/autotask/client.py index 3a02e6b..ef29b0d 100644 --- a/containers/backupchecks/src/backend/app/integrations/autotask/client.py +++ b/containers/backupchecks/src/backend/app/integrations/autotask/client.py @@ -597,37 +597,51 @@ class AutotaskClient: if not isinstance(ticket, dict) or not ticket: raise AutotaskError("Autotask did not return a ticket object.") - def _pick(d: Dict[str, Any], keys: List[str]) -> Any: + def _pick(d: Dict[str, Any], keys: List[str]) -> tuple[bool, Any]: + """Pick first available field from possible field names. + + Returns tuple: (found, value) + - found=True if field exists (even if value is None) + - found=False if field doesn't exist in dict + + This allows us to distinguish between "field missing" vs "field is null", + which is critical for Autotask PUT payloads that require exact values. + """ for k in keys: - if k in d and d.get(k) not in (None, ""): - return d.get(k) - return None + if k in d: + return (True, d[k]) + return (False, None) - # Required stabilising fields for safe resolution updates (validated via Postman tests) - resolved_issue_type = _pick(ticket, ["issueType", "issueTypeID", "issueTypeId"]) - resolved_sub_issue_type = _pick(ticket, ["subIssueType", "subIssueTypeID", "subIssueTypeId"]) - resolved_source = _pick(ticket, ["source", "sourceID", "sourceId"]) - resolved_status = _pick(ticket, ["status", "statusID", "statusId"]) + # Required stabilising fields for safe resolution updates (validated via Postman tests). + # Field names are camelCase as per API contract (docs/autotask_rest_api.md section 2.1). + # We must copy the EXACT values from GET response to PUT payload, even if null. + found_id, ticket_id = _pick(ticket, ["id"]) + found_issue_type, resolved_issue_type = _pick(ticket, ["issueType", "issueTypeID", "issueTypeId"]) + found_sub_issue_type, resolved_sub_issue_type = _pick(ticket, ["subIssueType", "subIssueTypeID", "subIssueTypeId"]) + found_source, resolved_source = _pick(ticket, ["source", "sourceID", "sourceId"]) + found_status, resolved_status = _pick(ticket, ["status", "statusID", "statusId"]) + # Validate that required fields exist in the response missing: List[str] = [] - if _pick(ticket, ["id"]) in (None, ""): + if not found_id or ticket_id in (None, ""): missing.append("id") - if resolved_issue_type in (None, ""): - missing.append("issueType") - if resolved_sub_issue_type in (None, ""): - missing.append("subIssueType") - if resolved_source in (None, ""): - missing.append("source") - if resolved_status in (None, ""): + if not found_status or resolved_status in (None, ""): missing.append("status") + if not found_issue_type: + missing.append("issueType") + if not found_sub_issue_type: + missing.append("subIssueType") + if not found_source: + missing.append("source") if missing: raise AutotaskError( "Cannot safely update ticket resolution because required fields are missing: " + ", ".join(missing) ) + # Build payload with exact values from GET response (including null if that's what we got) payload: Dict[str, Any] = { - "id": int(ticket.get("id")), + "id": int(ticket_id), "issueType": resolved_issue_type, "subIssueType": resolved_sub_issue_type, "source": resolved_source, @@ -648,7 +662,7 @@ class AutotaskClient: ] for f in optional_fields: if f in ticket: - payload[f] = ticket.get(f) + payload[f] = ticket[f] return self.update_ticket(payload) diff --git a/docs/changelog-claude.md b/docs/changelog-claude.md index d41ad62..7c8844f 100644 --- a/docs/changelog-claude.md +++ b/docs/changelog-claude.md @@ -4,6 +4,11 @@ This file documents all changes made to this project via Claude Code. ## [2026-02-05] +### 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) + from the GET response, as required by Autotask API. Previously these fields were being skipped or modified. + ### Added - Restored Autotask PSA integration from branch `v20260203-13-autotask-resolution-item-wrapper`: - `integrations/autotask/client.py` - Autotask REST API client with full support for: