"""ConnectWise implementation of PSAProvider.""" from __future__ import annotations from app.services.psa.base import PSAProvider from app.services.psa.cache import psa_cache from app.services.psa.types import ( ConnectionTestResult, PSATicket, PSANote, PSAStatus, PSACompany, PSAMember, PSAConfiguration, ) from .client import ConnectWiseClient class ConnectWiseProvider(PSAProvider): """ConnectWise PSA provider implementation.""" def __init__(self, client: ConnectWiseClient): self.client = client async def test_connection(self) -> ConnectionTestResult: """Test the CW connection by fetching system info.""" try: info = await self.client.get("/system/info") return ConnectionTestResult( success=True, message="Connected successfully.", server_version=info.get("version", None), ) except Exception as e: return ConnectionTestResult( success=False, message=str(e), server_version=None, ) # ── Tickets ─────────────────────────────────────────────────────── async def get_ticket(self, ticket_id: str) -> PSATicket: """Fetch a single ticket by ID from ConnectWise.""" data = await self.client.get( f"/service/tickets/{ticket_id}", params={"fields": "id,summary,company,board,status,priority,closedFlag"}, ) return self._map_ticket(data) async def search_tickets(self, query: str, **filters) -> list[PSATicket]: """Search CW tickets by summary. Supports board_id and status_id filters.""" params: dict = { "fields": "id,summary,company,board,status,priority,closedFlag", "orderBy": "id desc", "pageSize": 25, } # Build CW condition query conditions: list[str] = [] if query: conditions.append(f"summary contains '{query}'") if filters.get("board_id"): conditions.append(f"board/id = {filters['board_id']}") if filters.get("status_id"): conditions.append(f"status/id = {filters['status_id']}") if not filters.get("include_closed", False): conditions.append("closedFlag = false") if conditions: params["conditions"] = " and ".join(conditions) data = await self.client.get("/service/tickets", params=params) return [ self._map_ticket(t) for t in (data if isinstance(data, list) else []) ] async def get_ticket_configurations( self, ticket_id: str ) -> list[PSAConfiguration]: """Get configurations (assets) attached to a ticket.""" data = await self.client.get( f"/service/tickets/{ticket_id}/configurations", params={"fields": "id,deviceIdentifier,type,company"}, ) return [ PSAConfiguration( id=str(c["id"]), name=c.get("deviceIdentifier", ""), type=c.get("type", {}).get("name") if c.get("type") else None, company_name=c.get("company", {}).get("name") if c.get("company") else None, ) for c in (data if isinstance(data, list) else []) ] # ── Board statuses (cached) ─────────────────────────────────────── async def get_ticket_statuses(self, board_id: int) -> list[PSAStatus]: """Get available statuses for a CW service board (cached 1 hour).""" cache_key = f"board_statuses:{board_id}" cached = psa_cache.get(cache_key) if cached is not None: return cached data = await self.client.get( f"/service/boards/{board_id}/statuses", params={"fields": "id,name,closedStatus", "pageSize": 100}, ) result = [ PSAStatus( id=s["id"], name=s["name"], is_closed=s.get("closedStatus", False), ) for s in (data if isinstance(data, list) else []) ] psa_cache.set(cache_key, result, ttl_seconds=3600) return result # ── Companies ───────────────────────────────────────────────────── async def list_companies(self, **filters) -> list[PSACompany]: """List companies from CW, optionally filtered by status.""" params: dict = { "fields": "id,name,status", "pageSize": 100, "orderBy": "name asc", } conditions: list[str] = [] if filters.get("status"): conditions.append(f"status/name = '{filters['status']}'") if conditions: params["conditions"] = " and ".join(conditions) data = await self.client.get("/company/companies", params=params) return [ PSACompany( id=str(c["id"]), name=c.get("name", ""), status=c.get("status", {}).get("name") if c.get("status") else None, ) for c in (data if isinstance(data, list) else []) ] async def get_company(self, company_id: str) -> PSACompany: """Fetch a single company by ID.""" data = await self.client.get( f"/company/companies/{company_id}", params={"fields": "id,name,status"}, ) return PSACompany( id=str(data["id"]), name=data.get("name", ""), status=data.get("status", {}).get("name") if data.get("status") else None, ) # ── Stubs for later slices ──────────────────────────────────────── async def post_note( self, ticket_id: str, text: str, note_type: str, member_id: str | None = None, ) -> PSANote: raise NotImplementedError("Implemented in Slice 4") async def update_ticket_status( self, ticket_id: str, status_id: int ) -> PSATicket: raise NotImplementedError("Implemented in Slice 4") async def list_members(self) -> list[PSAMember]: raise NotImplementedError("Implemented in Slice 5") # ── Private helpers ─────────────────────────────────────────────── @staticmethod def _map_ticket(data: dict) -> PSATicket: """Map a CW ticket JSON dict to a PSATicket.""" return PSATicket( id=str(data["id"]), summary=data.get("summary", ""), company_name=data.get("company", {}).get("name"), company_id=str(data["company"]["id"]) if data.get("company") else None, board_name=data.get("board", {}).get("name"), board_id=data.get("board", {}).get("id"), status_name=data.get("status", {}).get("name"), status_id=data.get("status", {}).get("id"), priority_name=data.get("priority", {}).get("name"), priority_id=data.get("priority", {}).get("id"), closed=data.get("closedFlag", False), )