"""Helpers the MCP tools need that the HTTP surface gets for free. An HTTP caller sends a UUID path param that FastAPI has already parsed and a date that pydantic has already validated. An AI client sends whatever the user said — often the human-readable report code ("BB-2026-001", or "bb-2026-1") and a date as a bare string. These two helpers make that work without loosening the service layer's types. Both raise the platform's typed exceptions, so a bad argument comes back to the client as a clear 4xx-shaped error rather than a traceback. """ from __future__ import annotations import re import uuid from datetime import date from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.core.exceptions import NotFoundError, ValidationError from app.domains.bounty.models import BountyReport _CODE_RE = re.compile(r"^BB-(\d{4})-(\d{1,6})$", re.IGNORECASE) def _normalize_code(raw: str) -> str | None: """``bb-2026-1`` → ``BB-2026-001``. Returns None if it isn't a code.""" match = _CODE_RE.match(raw.strip()) if match is None: return None year, seq = match.groups() return f"BB-{year}-{int(seq):03d}" async def resolve_my_report_id( db: AsyncSession, raw: str, *, person_id: uuid.UUID, ) -> uuid.UUID: """Accept a UUID **or** a report code, and resolve it to an id. Resolution is scoped to the caller's own assignments, so a code the caller does not own answers "not found" rather than confirming that somebody else's report exists. The subsequent ``self_service.load_my_report`` still re-checks ownership — this is a lookup convenience, not the authorization. """ value = (raw or "").strip() if not value: raise ValidationError("report_id is required") try: return uuid.UUID(value) except (ValueError, AttributeError): pass code = _normalize_code(value) if code is None: raise ValidationError( f"'{raw}' is neither a report id nor a report code " "(expected a UUID or BB-YYYY-NNN)", ) found = ( await db.execute( select(BountyReport.id).where( BountyReport.code == code, BountyReport.assignee_person_id == person_id, BountyReport.deleted_at.is_(None), ), ) ).scalar_one_or_none() if found is None: raise NotFoundError( f"no vulnerability report {code} is assigned to you", ) return found def parse_optional_date(raw: str | None, *, field: str) -> date | None: """``"2026-09-30"`` → date. Empty/None → None; anything else raises.""" if raw is None: return None value = raw.strip() if not value: return None try: return date.fromisoformat(value) except ValueError as exc: raise ValidationError( f"{field} must be a date in YYYY-MM-DD form, got '{raw}'", ) from exc