From 19b2776eed4c3aeead6f05e6998be46b0d3b78c5 Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Thu, 5 Feb 2026 15:18:32 +0100 Subject: [PATCH] Fix Autotask resolution update for tickets without issueType/subIssueType/source Tickets may have issueType, subIssueType, and source fields with value 0 or null (meaning 'not set'). The previous validation incorrectly required these fields to have non-null values, causing "required fields are missing" errors. Now only id and status are required. Other stabilising fields are copied to the PUT payload when present in the GET response, preserving current values (including 0). Co-Authored-By: Claude Opus 4.5 --- .../app/integrations/autotask/client.py | 54 ++++++++++--------- docs/changelog-claude.md | 6 +++ 2 files changed, 36 insertions(+), 24 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..e95058a 100644 --- a/containers/backupchecks/src/backend/app/integrations/autotask/client.py +++ b/containers/backupchecks/src/backend/app/integrations/autotask/client.py @@ -598,27 +598,32 @@ class AutotaskClient: raise AutotaskError("Autotask did not return a ticket object.") def _pick(d: Dict[str, Any], keys: List[str]) -> Any: + """Pick the first available value from a list of possible field names. + + Returns the value (including 0) if found, or None if not present. + Note: 0 is a valid Autotask picklist value meaning 'not set'. + """ for k in keys: - if k in d and d.get(k) not in (None, ""): + if k in d: return d.get(k) return None + def _has_field(d: Dict[str, Any], keys: List[str]) -> bool: + """Check if any of the field names exist in the dict.""" + return any(k in d for k in keys) + # Required stabilising fields for safe resolution updates (validated via Postman tests) + # Note: These fields may have value 0 (not set) which is valid and must be preserved. 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"]) + # Only id and status are truly required - others may be 0/null in Autotask missing: List[str] = [] - if _pick(ticket, ["id"]) in (None, ""): + if _pick(ticket, ["id"]) is 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 resolved_status is None: missing.append("status") if missing: @@ -628,27 +633,28 @@ class AutotaskClient: payload: Dict[str, Any] = { "id": int(ticket.get("id")), - "issueType": resolved_issue_type, - "subIssueType": resolved_sub_issue_type, - "source": resolved_source, # Keep status unchanged "status": resolved_status, "resolution": str(resolution_text or ""), } - # Copy other stabilising fields when available (helps avoid tenant-specific validation errors) - optional_fields = [ - "companyID", - "queueID", - "title", - "priority", - "dueDateTime", - "ticketCategory", - "organizationalLevelAssociationID", + # Copy stabilising fields when present in the response (even if value is 0/null) + # This preserves the current ticket classification without unintended changes. + stabilising_fields = [ + ("issueType", ["issueType", "issueTypeID", "issueTypeId"]), + ("subIssueType", ["subIssueType", "subIssueTypeID", "subIssueTypeId"]), + ("source", ["source", "sourceID", "sourceId"]), + ("companyID", ["companyID"]), + ("queueID", ["queueID"]), + ("title", ["title"]), + ("priority", ["priority"]), + ("dueDateTime", ["dueDateTime"]), + ("ticketCategory", ["ticketCategory"]), + ("organizationalLevelAssociationID", ["organizationalLevelAssociationID"]), ] - for f in optional_fields: - if f in ticket: - payload[f] = ticket.get(f) + for field_name, keys in stabilising_fields: + if _has_field(ticket, keys): + payload[field_name] = _pick(ticket, keys) return self.update_ticket(payload) diff --git a/docs/changelog-claude.md b/docs/changelog-claude.md index d41ad62..e8358f7 100644 --- a/docs/changelog-claude.md +++ b/docs/changelog-claude.md @@ -4,6 +4,12 @@ This file documents all changes made to this project via Claude Code. ## [2026-02-05] +### Fixed +- Autotask ticket resolution update now correctly handles tickets where `issueType`, + `subIssueType`, or `source` fields are not set (value 0 or null). Previously this + caused "required fields are missing" errors. Now only `id` and `status` are required; + other stabilising fields are copied when present. + ### 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: