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: