diff --git a/.last-branch b/.last-branch index 06a5a28..00b87aa 100644 --- a/.last-branch +++ b/.last-branch @@ -1 +1 @@ -v20260203-09-autotask-resolution-from-note +v20260203-10-autotask-resolution-field-aliases diff --git a/containers/backupchecks/src/backend/app/integrations/autotask/client.py b/containers/backupchecks/src/backend/app/integrations/autotask/client.py index 3b19942..3622f76 100644 --- a/containers/backupchecks/src/backend/app/integrations/autotask/client.py +++ b/containers/backupchecks/src/backend/app/integrations/autotask/client.py @@ -584,22 +584,43 @@ class AutotaskClient: if not isinstance(t, dict): raise AutotaskError("Autotask did not return a ticket object.") - stabilising_fields = [ - "id", - "companyID", - "queueID", - "title", - "priority", - "status", - "dueDateTime", - "ticketCategory", - "issueType", - "subIssueType", - "source", - "organizationalLevelAssociationID", - ] + # Some Autotask environments return slightly different field names (e.g. *ID vs *Id). + # We always source values from the fresh GET and send the canonical field names in the PUT payload. + field_sources: Dict[str, list[str]] = { + "id": ["id"], + "companyID": ["companyID", "companyId"], + "queueID": ["queueID", "queueId"], + "title": ["title"], + "priority": ["priority"], + "status": ["status"], + "dueDateTime": ["dueDateTime", "dueDate", "dueDateUtc"], + "ticketCategory": ["ticketCategory", "ticketCategoryID", "ticketCategoryId"], + "issueType": ["issueType", "issueTypeID", "issueTypeId"], + "subIssueType": ["subIssueType", "subIssueTypeID", "subIssueTypeId"], + "source": ["source", "sourceID", "sourceId"], + "organizationalLevelAssociationID": [ + "organizationalLevelAssociationID", + "organizationalLevelAssociationId", + ], + } + + def _get_first(ticket_obj: Dict[str, Any], keys: list[str]) -> Any: + for k in keys: + if k in ticket_obj: + return ticket_obj.get(k) + return None + + stabilising_fields = list(field_sources.keys()) + + resolved_values: Dict[str, Any] = {} + missing: list[str] = [] + for f in stabilising_fields: + v = _get_first(t, field_sources[f]) + # Treat None/"" as missing, but allow 0 for picklists. + if v in (None, ""): + missing.append(f) + resolved_values[f] = v - missing = [f for f in stabilising_fields if t.get(f) in (None, "")] if missing: raise AutotaskError( "Cannot safely update ticket resolution because required fields are missing: " + ", ".join(missing) @@ -614,11 +635,10 @@ class AutotaskClient: else: new_res = res_txt - payload: Dict[str, Any] = {k: t.get(k) for k in stabilising_fields} - # Ensure numeric ID is an int for PUT. - payload["id"] = int(t.get("id")) + payload: Dict[str, Any] = dict(resolved_values) + payload["id"] = int(resolved_values.get("id")) # Explicitly keep status unchanged. - payload["status"] = t.get("status") + payload["status"] = resolved_values.get("status") payload["resolution"] = new_res self.update_ticket(payload) diff --git a/docs/changelog.md b/docs/changelog.md index 745eae9..9edae79 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -512,6 +512,12 @@ Changes: - Resolution updates follow the validated safe pattern: GET the current Ticket first, then PUT with stabilising fields while keeping the status unchanged. - Added verification to ensure the resolution text is persisted after the update. +## v20260203-10-autotask-resolution-field-aliases + +- Fix: Resolution PUT now always reuses the latest classification/routing fields from GET /Tickets/{id}, including support for common field-name variants (*ID/*Id). +- Prevents failure when issueType/subIssueType/source are not present under the expected keys after ticket creation or later changes. +- Keeps ticket status unchanged while updating resolution, per validated Postman contract. + *** ## v0.1.21