From 077e1fb17619c66f2e9e88a51e9d9b9d003b9d3f Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Thu, 5 Feb 2026 17:01:43 +0100 Subject: [PATCH] Add cross-company ticket search for overarching issues The "Link existing ticket" dialog now searches across all companies when a ticket number is entered, enabling linking of overarching issues. Changes: - Added query_tickets_by_number() to Autotask client - Route searches both customer's company and cross-company when ticket number detected - Results are combined and deduplicated (customer tickets shown first) - Enables linking multi-company infrastructure issues to any job Co-Authored-By: Claude Sonnet 4.5 --- .../app/integrations/autotask/client.py | 41 +++++++++++++++++++ .../src/backend/app/main/routes_run_checks.py | 40 +++++++++++++++++- docs/changelog-claude.md | 8 ++++ 3 files changed, 87 insertions(+), 2 deletions(-) diff --git a/containers/backupchecks/src/backend/app/integrations/autotask/client.py b/containers/backupchecks/src/backend/app/integrations/autotask/client.py index d30c07d..1e353b1 100644 --- a/containers/backupchecks/src/backend/app/integrations/autotask/client.py +++ b/containers/backupchecks/src/backend/app/integrations/autotask/client.py @@ -970,6 +970,47 @@ class AutotaskClient: return items[: int(limit)] return items + def query_tickets_by_number( + self, + ticket_number: str, + *, + exclude_status_ids: Optional[List[int]] = None, + limit: int = 10, + ) -> List[Dict[str, Any]]: + """Query Tickets by ticket number across all companies. + + Uses POST /Tickets/query. + + This is useful for linking overarching issues that span multiple companies. + """ + + tnum = (ticket_number or "").strip() + if not tnum: + return [] + + flt: List[Dict[str, Any]] = [ + {"op": "eq", "field": "ticketNumber", "value": tnum}, + ] + + ex: List[int] = [] + for x in exclude_status_ids or []: + try: + v = int(x) + except Exception: + continue + if v > 0: + ex.append(v) + if ex: + flt.append({"op": "notIn", "field": "status", "value": ex}) + + data = self._request("POST", "Tickets/query", json_body={"filter": flt}) + items = self._as_items_list(data) + + # Respect limit if tenant returns more. + if limit and isinstance(limit, int) and limit > 0: + return items[: int(limit)] + return items + def query_time_entries_by_ticket_id(self, ticket_id: int) -> List[Dict[str, Any]]: """Query TimeEntries for a specific ticket. diff --git a/containers/backupchecks/src/backend/app/main/routes_run_checks.py b/containers/backupchecks/src/backend/app/main/routes_run_checks.py index 7b5146e..f29f85e 100644 --- a/containers/backupchecks/src/backend/app/main/routes_run_checks.py +++ b/containers/backupchecks/src/backend/app/main/routes_run_checks.py @@ -1576,6 +1576,11 @@ def api_run_checks_autotask_existing_tickets(): """List open (non-terminal) Autotask tickets for the selected run's customer. Phase 2.2: used by the Run Checks modal to link an existing PSA ticket. + + Search behaviour: + - Always searches tickets for the customer's company + - If search term looks like a ticket number (starts with T + digits), also searches + across all companies to enable linking overarching issues """ try: @@ -1640,20 +1645,43 @@ def api_run_checks_autotask_existing_tickets(): # Best-effort; list will still work without labels. pass + # First: query tickets for this customer's company tickets = client.query_tickets_for_company( int(customer.autotask_company_id), search=q, exclude_status_ids=sorted(AUTOTASK_TERMINAL_STATUS_IDS), limit=75, ) + + # Second: if search looks like a ticket number, also search across all companies + # This allows linking overarching issues that span multiple companies + cross_company_tickets = [] + if q and q.upper().startswith("T") and any(ch.isdigit() for ch in q): + try: + cross_company_tickets = client.query_tickets_by_number( + q, + exclude_status_ids=sorted(AUTOTASK_TERMINAL_STATUS_IDS), + limit=10, + ) + except Exception: + # Best-effort; main company query already succeeded + pass + except Exception as exc: return jsonify({"status": "error", "message": f"Autotask ticket lookup failed: {exc}"}), 400 + # Combine and deduplicate results + seen_ids = set() items = [] - for t in tickets or []: + + def add_ticket(t): if not isinstance(t, dict): - continue + return tid = t.get("id") + if tid in seen_ids: + return + seen_ids.add(tid) + tnum = (t.get("ticketNumber") or t.get("number") or "") title = (t.get("title") or "") st = t.get("status") @@ -1672,6 +1700,14 @@ def api_run_checks_autotask_existing_tickets(): } ) + # Add company tickets first (primary results) + for t in tickets or []: + add_ticket(t) + + # Then add cross-company tickets (secondary results for ticket number search) + for t in cross_company_tickets or []: + add_ticket(t) + # Sort: newest-ish first. Autotask query ordering isn't guaranteed, so we provide a stable sort. items.sort(key=lambda x: (x.get("ticketNumber") or ""), reverse=True) diff --git a/docs/changelog-claude.md b/docs/changelog-claude.md index 8a3f319..f60b857 100644 --- a/docs/changelog-claude.md +++ b/docs/changelog-claude.md @@ -4,6 +4,14 @@ This file documents all changes made to this project via Claude Code. ## [2026-02-05] +### Added +- Autotask "Link existing ticket" now supports cross-company ticket search: + - Added `query_tickets_by_number()` to search tickets by number across all companies + - When searching with a ticket number (e.g., "T20260205.0001"), results include: + - Tickets from the customer's company (primary results) + - Matching tickets from other companies (for overarching issues) + - Enables linking tickets for multi-company infrastructure issues + ### Changed - Autotask resolve confirmation and note messages now correctly indicate ticket closure status: - Frontend confirmation dialog explains conditional closure based on time entries