""" FastAPI routes for Service Module Registry. Endpoints: - /modules: Module listing and management - /modules/overview: Module compliance overview - /modules/{module_id}: Module details - /modules/seed: Seed modules from data - /modules/{module_id}/regulations: Add regulation mapping """ import logging from typing import Optional from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from classroom_engine.database import get_db from ..db import RegulationRepository from .schemas import ( ServiceModuleResponse, ServiceModuleListResponse, ServiceModuleDetailResponse, ModuleRegulationMappingCreate, ModuleRegulationMappingResponse, ModuleSeedRequest, ModuleSeedResponse, ModuleComplianceOverview, ) logger = logging.getLogger(__name__) router = APIRouter(tags=["compliance-modules"]) # ============================================================================ # Service Module Registry # ============================================================================ @router.get("/modules", response_model=ServiceModuleListResponse) async def list_modules( service_type: Optional[str] = None, criticality: Optional[str] = None, processes_pii: Optional[bool] = None, ai_components: Optional[bool] = None, db: Session = Depends(get_db), ): """List all service modules with optional filters.""" from ..db.repository import ServiceModuleRepository repo = ServiceModuleRepository(db) modules = repo.get_all( service_type=service_type, criticality=criticality, processes_pii=processes_pii, ai_components=ai_components, ) # Count regulations and risks for each module results = [] for m in modules: reg_count = len(m.regulation_mappings) if m.regulation_mappings else 0 risk_count = len(m.module_risks) if m.module_risks else 0 results.append(ServiceModuleResponse( id=m.id, name=m.name, display_name=m.display_name, description=m.description, service_type=m.service_type.value if m.service_type else None, port=m.port, technology_stack=m.technology_stack or [], repository_path=m.repository_path, docker_image=m.docker_image, data_categories=m.data_categories or [], processes_pii=m.processes_pii, processes_health_data=m.processes_health_data, ai_components=m.ai_components, criticality=m.criticality, owner_team=m.owner_team, owner_contact=m.owner_contact, is_active=m.is_active, compliance_score=m.compliance_score, last_compliance_check=m.last_compliance_check, created_at=m.created_at, updated_at=m.updated_at, regulation_count=reg_count, risk_count=risk_count, )) return ServiceModuleListResponse(modules=results, total=len(results)) @router.get("/modules/overview", response_model=ModuleComplianceOverview) async def get_modules_overview(db: Session = Depends(get_db)): """Get overview statistics for all modules.""" from ..db.repository import ServiceModuleRepository repo = ServiceModuleRepository(db) overview = repo.get_overview() return ModuleComplianceOverview(**overview) @router.get("/modules/{module_id}", response_model=ServiceModuleDetailResponse) async def get_module(module_id: str, db: Session = Depends(get_db)): """Get a specific module with its regulations and risks.""" from ..db.repository import ServiceModuleRepository repo = ServiceModuleRepository(db) module = repo.get_with_regulations(module_id) if not module: # Try by name module = repo.get_by_name(module_id) if module: module = repo.get_with_regulations(module.id) if not module: raise HTTPException(status_code=404, detail=f"Module {module_id} not found") # Build regulation list regulations = [] for mapping in (module.regulation_mappings or []): reg = mapping.regulation if reg: regulations.append({ "code": reg.code, "name": reg.name, "relevance_level": mapping.relevance_level.value if mapping.relevance_level else "medium", "notes": mapping.notes, }) # Build risk list risks = [] for mr in (module.module_risks or []): risk = mr.risk if risk: risks.append({ "risk_id": risk.risk_id, "title": risk.title, "inherent_risk": risk.inherent_risk.value if risk.inherent_risk else None, "module_risk_level": mr.module_risk_level.value if mr.module_risk_level else None, }) return ServiceModuleDetailResponse( id=module.id, name=module.name, display_name=module.display_name, description=module.description, service_type=module.service_type.value if module.service_type else None, port=module.port, technology_stack=module.technology_stack or [], repository_path=module.repository_path, docker_image=module.docker_image, data_categories=module.data_categories or [], processes_pii=module.processes_pii, processes_health_data=module.processes_health_data, ai_components=module.ai_components, criticality=module.criticality, owner_team=module.owner_team, owner_contact=module.owner_contact, is_active=module.is_active, compliance_score=module.compliance_score, last_compliance_check=module.last_compliance_check, created_at=module.created_at, updated_at=module.updated_at, regulation_count=len(regulations), risk_count=len(risks), regulations=regulations, risks=risks, ) @router.post("/modules/seed", response_model=ModuleSeedResponse) async def seed_modules( request: ModuleSeedRequest, db: Session = Depends(get_db), ): """Seed service modules from predefined data.""" from classroom_engine.database import engine from ..db.models import ServiceModuleDB, ModuleRegulationMappingDB, ModuleRiskDB from ..db.repository import ServiceModuleRepository from ..data.service_modules import BREAKPILOT_SERVICES try: # Ensure tables exist ServiceModuleDB.__table__.create(engine, checkfirst=True) ModuleRegulationMappingDB.__table__.create(engine, checkfirst=True) ModuleRiskDB.__table__.create(engine, checkfirst=True) repo = ServiceModuleRepository(db) result = repo.seed_from_data(BREAKPILOT_SERVICES, force=request.force) return ModuleSeedResponse( success=True, message=f"Seeded {result['modules_created']} modules with {result['mappings_created']} regulation mappings", modules_created=result["modules_created"], mappings_created=result["mappings_created"], ) except Exception as e: logger.error(f"Module seeding failed: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/modules/{module_id}/regulations", response_model=ModuleRegulationMappingResponse) async def add_module_regulation( module_id: str, mapping: ModuleRegulationMappingCreate, db: Session = Depends(get_db), ): """Add a regulation mapping to a module.""" from ..db.repository import ServiceModuleRepository repo = ServiceModuleRepository(db) module = repo.get_by_id(module_id) if not module: module = repo.get_by_name(module_id) if not module: raise HTTPException(status_code=404, detail=f"Module {module_id} not found") # Verify regulation exists reg_repo = RegulationRepository(db) regulation = reg_repo.get_by_id(mapping.regulation_id) if not regulation: regulation = reg_repo.get_by_code(mapping.regulation_id) if not regulation: raise HTTPException(status_code=404, detail=f"Regulation {mapping.regulation_id} not found") try: new_mapping = repo.add_regulation_mapping( module_id=module.id, regulation_id=regulation.id, relevance_level=mapping.relevance_level, notes=mapping.notes, applicable_articles=mapping.applicable_articles, ) return ModuleRegulationMappingResponse( id=new_mapping.id, module_id=new_mapping.module_id, regulation_id=new_mapping.regulation_id, relevance_level=new_mapping.relevance_level.value if new_mapping.relevance_level else "medium", notes=new_mapping.notes, applicable_articles=new_mapping.applicable_articles, module_name=module.name, regulation_code=regulation.code, regulation_name=regulation.name, created_at=new_mapping.created_at, ) except Exception as e: logger.error(f"Failed to add regulation mapping: {e}") raise HTTPException(status_code=500, detail=str(e))