diff --git a/.last-branch b/.last-branch index a340f78..800bf22 100644 --- a/.last-branch +++ b/.last-branch @@ -1 +1 @@ -v20260115-16-autotask-ticket-create-response-fix +v20260115-17-autotask-ticket-create-trackingid-lookup diff --git a/containers/backupchecks/src/backend/app/integrations/autotask/client.py b/containers/backupchecks/src/backend/app/integrations/autotask/client.py index a9dab64..847867a 100644 --- a/containers/backupchecks/src/backend/app/integrations/autotask/client.py +++ b/containers/backupchecks/src/backend/app/integrations/autotask/client.py @@ -426,6 +426,51 @@ class AutotaskClient: return data raise AutotaskError("Autotask did not return a ticket object.") + def _lookup_created_ticket_id( + self, + tracking_identifier: str, + company_id: Optional[int] = None, + ) -> Optional[int]: + """Lookup the most recently created ticket by tracking identifier. + + Some Autotask tenants return an empty body and omit Location headers on + successful POST /Tickets calls. In that case, we must lookup the created + ticket deterministically via query. + + We prefer filtering by CompanyID when available to reduce ambiguity. + """ + + tid = (tracking_identifier or "").strip() + if not tid: + return None + + filters: List[Dict[str, Any]] = [ + {"op": "eq", "field": "TrackingIdentifier", "value": tid}, + ] + if isinstance(company_id, int) and company_id > 0: + filters.append({"op": "eq", "field": "CompanyID", "value": int(company_id)}) + + # Order by createDate desc when supported; fall back to id desc. + search_payload: Dict[str, Any] = { + "filter": filters, + "maxRecords": 1, + "orderby": [ + {"field": "createDate", "direction": "desc"}, + {"field": "id", "direction": "desc"}, + ], + } + + params = {"search": json.dumps(search_payload)} + data = self._request("GET", "Tickets/query", params=params) + items = self._as_items_list(data) + if not items: + return None + + first = items[0] + if isinstance(first, dict) and str(first.get("id") or "").isdigit(): + return int(first["id"]) + return None + def create_ticket(self, payload: Dict[str, Any]) -> Dict[str, Any]: """Create a Ticket in Autotask. @@ -485,11 +530,30 @@ class AutotaskClient: if ticket_id is not None: return self.get_ticket(ticket_id) + # Deterministic fallback: query by tracking identifier (+ company) if present. + tracking_identifier = ( + payload.get("trackingIdentifier") + or payload.get("TrackingIdentifier") + or "" + ) + company_id: Optional[int] = None + for ck in ("companyID", "companyId", "CompanyID"): + if str(payload.get(ck) or "").isdigit(): + company_id = int(payload[ck]) + break + + looked_up_id = self._lookup_created_ticket_id(str(tracking_identifier), company_id=company_id) + if looked_up_id is not None: + return self.get_ticket(looked_up_id) + # Last-resort fallback: normalize first item if possible. items = self._as_items_list(data) if items: return items[0] raise AutotaskError( - f"Autotask did not return a created ticket object (HTTP {resp.status_code})." + "Autotask did not return a ticket id. " + "Ticket creation may still have succeeded; configure ticket TrackingIdentifier to be unique per run " + "to allow deterministic lookup. " + f"(HTTP {resp.status_code})." ) diff --git a/docs/changelog.md b/docs/changelog.md index 6715874..c68b209 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -117,6 +117,12 @@ Changes: - Added support for extracting the created ticket ID from itemId/id fields and from the Location header. - Added a follow-up GET /Tickets/{id} to always retrieve the full created ticket object (ensuring ticketNumber/id are available). +## v20260115-17-autotask-ticket-create-trackingid-lookup +- Reworked Autotask ticket creation flow to no longer rely on POST /Tickets response data for returning an ID. +- Added deterministic fallback lookup using Tickets/query filtered by TrackingIdentifier (and CompanyID when available). +- Ensured the created ticket is reliably retrieved via follow-up GET /Tickets/{id} so ticketNumber/id can always be stored. +- Eliminated false-negative ticket creation errors when Autotask returns an empty body and no Location header. + *** ## v0.1.21