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 <noreply@anthropic.com>
This commit is contained in:
Ivo Oskamp 2026-02-05 16:29:54 +01:00
parent 8baf4e8ff1
commit 8a733a356b
2 changed files with 38 additions and 19 deletions

View File

@ -597,37 +597,51 @@ class AutotaskClient:
if not isinstance(ticket, dict) or not ticket: if not isinstance(ticket, dict) or not ticket:
raise AutotaskError("Autotask did not return a ticket object.") 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: for k in keys:
if k in d and d.get(k) not in (None, ""): if k in d:
return d.get(k) return (True, d[k])
return None return (False, None)
# Required stabilising fields for safe resolution updates (validated via Postman tests) # Required stabilising fields for safe resolution updates (validated via Postman tests).
resolved_issue_type = _pick(ticket, ["issueType", "issueTypeID", "issueTypeId"]) # Field names are camelCase as per API contract (docs/autotask_rest_api.md section 2.1).
resolved_sub_issue_type = _pick(ticket, ["subIssueType", "subIssueTypeID", "subIssueTypeId"]) # We must copy the EXACT values from GET response to PUT payload, even if null.
resolved_source = _pick(ticket, ["source", "sourceID", "sourceId"]) found_id, ticket_id = _pick(ticket, ["id"])
resolved_status = _pick(ticket, ["status", "statusID", "statusId"]) 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] = [] missing: List[str] = []
if _pick(ticket, ["id"]) in (None, ""): if not found_id or ticket_id in (None, ""):
missing.append("id") missing.append("id")
if resolved_issue_type in (None, ""): if not found_status or resolved_status 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, ""):
missing.append("status") 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: if missing:
raise AutotaskError( raise AutotaskError(
"Cannot safely update ticket resolution because required fields are missing: " + ", ".join(missing) "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] = { payload: Dict[str, Any] = {
"id": int(ticket.get("id")), "id": int(ticket_id),
"issueType": resolved_issue_type, "issueType": resolved_issue_type,
"subIssueType": resolved_sub_issue_type, "subIssueType": resolved_sub_issue_type,
"source": resolved_source, "source": resolved_source,
@ -648,7 +662,7 @@ class AutotaskClient:
] ]
for f in optional_fields: for f in optional_fields:
if f in ticket: if f in ticket:
payload[f] = ticket.get(f) payload[f] = ticket[f]
return self.update_ticket(payload) return self.update_ticket(payload)

View File

@ -4,6 +4,11 @@ This file documents all changes made to this project via Claude Code.
## [2026-02-05] ## [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 ### Added
- Restored Autotask PSA integration from branch `v20260203-13-autotask-resolution-item-wrapper`: - Restored Autotask PSA integration from branch `v20260203-13-autotask-resolution-item-wrapper`:
- `integrations/autotask/client.py` - Autotask REST API client with full support for: - `integrations/autotask/client.py` - Autotask REST API client with full support for: