""" FastAPI routes for Compliance Scope persistence. Stores the tenant's scope decision (frameworks, regulations, industry context) in sdk_states.state->compliance_scope as JSONB. Endpoints: - GET /v1/compliance-scope?tenant_id=... → returns scope or 404 - POST /v1/compliance-scope → UPSERT scope (idempotent) """ import json import logging from typing import Any, Optional from fastapi import APIRouter, HTTPException, Header from pydantic import BaseModel from database import SessionLocal logger = logging.getLogger(__name__) router = APIRouter(prefix="/v1/compliance-scope", tags=["compliance-scope"]) # ============================================================================= # REQUEST / RESPONSE MODELS # ============================================================================= class ComplianceScopeRequest(BaseModel): """Scope selection submitted by the frontend wizard.""" scope: dict[str, Any] tenant_id: Optional[str] = None class ComplianceScopeResponse(BaseModel): """Persisted scope object returned to the frontend.""" tenant_id: str scope: dict[str, Any] updated_at: str created_at: str # ============================================================================= # HELPERS # ============================================================================= def _get_tid( x_tenant_id: Optional[str], query_tenant_id: str, ) -> str: return x_tenant_id or query_tenant_id or "default" def _row_to_response(row) -> ComplianceScopeResponse: """Convert a DB row (tenant_id, scope, created_at, updated_at) to response.""" return ComplianceScopeResponse( tenant_id=row[0], scope=row[1] if isinstance(row[1], dict) else {}, created_at=str(row[2]), updated_at=str(row[3]), ) # ============================================================================= # ROUTES # ============================================================================= @router.get("", response_model=ComplianceScopeResponse) async def get_compliance_scope( tenant_id: str = "default", x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID"), ): """Return the persisted compliance scope for a tenant, or 404 if not set.""" tid = _get_tid(x_tenant_id, tenant_id) db = SessionLocal() try: row = db.execute( """SELECT tenant_id, state->'compliance_scope' AS scope, created_at, updated_at FROM sdk_states WHERE tenant_id = :tid AND state ? 'compliance_scope'""", {"tid": tid}, ).fetchone() if not row or row[1] is None: raise HTTPException(status_code=404, detail="Compliance scope not found") return _row_to_response(row) finally: db.close() @router.post("", response_model=ComplianceScopeResponse) async def upsert_compliance_scope( body: ComplianceScopeRequest, tenant_id: str = "default", x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID"), ): """Create or update the compliance scope for a tenant (UPSERT).""" tid = _get_tid(x_tenant_id, body.tenant_id or tenant_id) scope_json = json.dumps(body.scope) db = SessionLocal() try: db.execute( """INSERT INTO sdk_states (tenant_id, state) VALUES (:tid, jsonb_build_object('compliance_scope', :scope::jsonb)) ON CONFLICT (tenant_id) DO UPDATE SET state = sdk_states.state || jsonb_build_object('compliance_scope', :scope::jsonb), updated_at = NOW()""", {"tid": tid, "scope": scope_json}, ) db.commit() row = db.execute( """SELECT tenant_id, state->'compliance_scope' AS scope, created_at, updated_at FROM sdk_states WHERE tenant_id = :tid""", {"tid": tid}, ).fetchone() return _row_to_response(row) except Exception as e: db.rollback() logger.error(f"Failed to upsert compliance scope: {e}") raise HTTPException(status_code=500, detail="Failed to save compliance scope") finally: db.close()