diff --git a/backend/app/services/ticket_service.py b/backend/app/services/ticket_service.py new file mode 100644 index 00000000..66e9552e --- /dev/null +++ b/backend/app/services/ticket_service.py @@ -0,0 +1,115 @@ +"""Ticket mutation service — wraps PSA provider, resolves is_rf_user flag.""" +from __future__ import annotations + +import logging +from uuid import UUID + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.psa_connection import PsaConnection +from app.models.psa_member_mapping import PsaMemberMapping +from app.schemas.psa_tickets import ( + PSAResourceSchema, + PSATicketCreatedSchema, + PSATicketStatusUpdateSchema, +) +from app.services.psa.registry import get_provider_for_account +from app.services.psa.types import TicketCreatePayload + +logger = logging.getLogger(__name__) + + +async def _get_mapped_member_ids(account_id: UUID, db: AsyncSession) -> set[int]: + """Return set of external_member_id ints that are mapped to RF users.""" + conn_result = await db.execute( + select(PsaConnection).where(PsaConnection.account_id == account_id) + ) + conn = conn_result.scalar_one_or_none() + if not conn: + return set() + mappings = await db.execute( + select(PsaMemberMapping).where(PsaMemberMapping.psa_connection_id == conn.id) + ) + return {int(m.external_member_id) for m in mappings.scalars().all() if m.external_member_id} + + +async def list_resources( + account_id: UUID, ticket_id: int, db: AsyncSession +) -> list[PSAResourceSchema]: + provider = await get_provider_for_account(account_id, db) + mapped_ids = await _get_mapped_member_ids(account_id, db) + resources = await provider.list_resources(ticket_id) + return [ + PSAResourceSchema( + member_id=r.member_id, + member_name=r.member_name, + member_identifier=r.member_identifier, + is_rf_user=r.member_id in mapped_ids, + ) + for r in resources + ] + + +async def add_resource( + account_id: UUID, ticket_id: int, member_id: int, db: AsyncSession +) -> PSAResourceSchema: + provider = await get_provider_for_account(account_id, db) + mapped_ids = await _get_mapped_member_ids(account_id, db) + resource = await provider.add_resource(ticket_id, member_id) + return PSAResourceSchema( + member_id=resource.member_id, + member_name=resource.member_name, + member_identifier=resource.member_identifier, + is_rf_user=resource.member_id in mapped_ids, + ) + + +async def remove_resource( + account_id: UUID, ticket_id: int, member_id: int, db: AsyncSession +) -> None: + provider = await get_provider_for_account(account_id, db) + await provider.remove_resource(ticket_id, member_id) + + +async def update_status( + account_id: UUID, ticket_id: int, status_id: int, db: AsyncSession +) -> PSATicketStatusUpdateSchema: + provider = await get_provider_for_account(account_id, db) + # get current status before updating + ticket = await provider.get_ticket(str(ticket_id)) + previous_status = ticket.status_name or "" + await provider.update_ticket_status(str(ticket_id), status_id) + # get new status name from statuses list + statuses = await provider.get_ticket_statuses(ticket.board_id or 0) + new_status = next((s.name for s in statuses if s.id == status_id), str(status_id)) + return PSATicketStatusUpdateSchema( + ticket_id=ticket_id, + previous_status=previous_status, + new_status=new_status, + ) + + +async def create_ticket( + account_id: UUID, payload: TicketCreatePayload, db: AsyncSession +) -> PSATicketCreatedSchema: + provider = await get_provider_for_account(account_id, db) + mapped_ids = await _get_mapped_member_ids(account_id, db) + result = await provider.create_ticket(payload) + return PSATicketCreatedSchema( + id=result.id, + summary=result.summary, + board_name=result.board_name, + status_name=result.status_name, + priority_name=result.priority_name, + company_name=result.company_name, + resources=[ + PSAResourceSchema( + member_id=r.member_id, + member_name=r.member_name, + member_identifier=r.member_identifier, + is_rf_user=r.member_id in mapped_ids, + ) + for r in result.resources + ], + )