""" FastAPI routes for Vendor Compliance — Auftragsverarbeitung (Art. 28 DSGVO). Endpoints: Vendors (7): GET /vendor-compliance/vendors — Liste + Filter GET /vendor-compliance/vendors/stats — Statistiken GET /vendor-compliance/vendors/{id} — Detail POST /vendor-compliance/vendors — Erstellen PUT /vendor-compliance/vendors/{id} — Update DELETE /vendor-compliance/vendors/{id} — Loeschen PATCH /vendor-compliance/vendors/{id}/status — Status aendern Contracts (5): GET /vendor-compliance/contracts — Liste GET /vendor-compliance/contracts/{id} — Detail POST /vendor-compliance/contracts — Erstellen PUT /vendor-compliance/contracts/{id} — Update DELETE /vendor-compliance/contracts/{id} — Loeschen Findings (5): GET /vendor-compliance/findings — Liste GET /vendor-compliance/findings/{id} — Detail POST /vendor-compliance/findings — Erstellen PUT /vendor-compliance/findings/{id} — Update DELETE /vendor-compliance/findings/{id} — Loeschen Control Instances (5): GET /vendor-compliance/control-instances — Liste GET /vendor-compliance/control-instances/{id} — Detail POST /vendor-compliance/control-instances — Erstellen PUT /vendor-compliance/control-instances/{id} — Update DELETE /vendor-compliance/control-instances/{id} — Loeschen Controls Library (3): GET /vendor-compliance/controls — Alle Controls POST /vendor-compliance/controls — Erstellen DELETE /vendor-compliance/controls/{id} — Loeschen Export Stubs (3): POST /vendor-compliance/export — 501 GET /vendor-compliance/export/{id} — 501 GET /vendor-compliance/export/{id}/download — 501 Phase 1 Step 4 refactor: handlers delegate to VendorService, ContractService, FindingService, ControlInstanceService, and ControlsLibraryService. Module-level helpers re-exported for legacy test imports. """ import logging from typing import Optional from fastapi import APIRouter, Depends, Query from sqlalchemy.orm import Session from classroom_engine.database import get_db from compliance.api._http_errors import translate_domain_errors from compliance.services.vendor_compliance_service import ( DEFAULT_TENANT_ID, # noqa: F401 — re-export VendorService, _get, # noqa: F401 — re-export _now_iso, _ok, # noqa: F401 — re-export _parse_json, # noqa: F401 — re-export _to_camel, # noqa: F401 — re-export _to_snake, # noqa: F401 — re-export _ts, # noqa: F401 — re-export _vendor_to_response, # noqa: F401 — re-export _VENDOR_CAMEL_TO_SNAKE, # noqa: F401 — re-export _VENDOR_SNAKE_TO_CAMEL, # noqa: F401 — re-export ) from compliance.services.vendor_compliance_sub_service import ( ContractService, _contract_to_response, # noqa: F401 — re-export _control_instance_to_response, # noqa: F401 — re-export _finding_to_response, # noqa: F401 — re-export ) from compliance.services.vendor_compliance_extra_service import ( ControlInstanceService, ControlsLibraryService, FindingService, ) logger = logging.getLogger(__name__) router = APIRouter(prefix="/vendor-compliance", tags=["vendor-compliance"]) # --------------------------------------------------------------------------- # Service factories # --------------------------------------------------------------------------- def _vendor_svc(db: Session = Depends(get_db)) -> VendorService: return VendorService(db) def _contract_svc(db: Session = Depends(get_db)) -> ContractService: return ContractService(db) def _finding_svc(db: Session = Depends(get_db)) -> FindingService: return FindingService(db) def _ci_svc(db: Session = Depends(get_db)) -> ControlInstanceService: return ControlInstanceService(db) def _ctrl_svc(db: Session = Depends(get_db)) -> ControlsLibraryService: return ControlsLibraryService(db) # ============================================================================ # Vendors # ============================================================================ @router.get("/vendors/stats") def get_vendor_stats( tenant_id: Optional[str] = Query(None), svc: VendorService = Depends(_vendor_svc), ): with translate_domain_errors(): return svc.get_stats(tenant_id) @router.get("/vendors") def list_vendors( tenant_id: Optional[str] = Query(None), status: Optional[str] = Query(None), risk_level: Optional[str] = Query(None, alias="riskLevel"), search: Optional[str] = Query(None), skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=500), svc: VendorService = Depends(_vendor_svc), ): with translate_domain_errors(): return svc.list_vendors(tenant_id, status, risk_level, search, skip, limit) @router.get("/vendors/{vendor_id}") def get_vendor( vendor_id: str, svc: VendorService = Depends(_vendor_svc), ): with translate_domain_errors(): return svc.get_vendor(vendor_id) @router.post("/vendors", status_code=201) def create_vendor( body: dict = {}, svc: VendorService = Depends(_vendor_svc), ): with translate_domain_errors(): return svc.create_vendor(body) @router.put("/vendors/{vendor_id}") def update_vendor( vendor_id: str, body: dict = {}, svc: VendorService = Depends(_vendor_svc), ): with translate_domain_errors(): return svc.update_vendor(vendor_id, body) @router.delete("/vendors/{vendor_id}") def delete_vendor( vendor_id: str, svc: VendorService = Depends(_vendor_svc), ): with translate_domain_errors(): return svc.delete_vendor(vendor_id) @router.patch("/vendors/{vendor_id}/status") def patch_vendor_status( vendor_id: str, body: dict = {}, svc: VendorService = Depends(_vendor_svc), ): with translate_domain_errors(): return svc.patch_status(vendor_id, body) # ============================================================================ # Contracts # ============================================================================ @router.get("/contracts") def list_contracts( tenant_id: Optional[str] = Query(None), vendor_id: Optional[str] = Query(None), status: Optional[str] = Query(None), skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=500), svc: ContractService = Depends(_contract_svc), ): with translate_domain_errors(): return svc.list_contracts(tenant_id, vendor_id, status, skip, limit) @router.get("/contracts/{contract_id}") def get_contract( contract_id: str, svc: ContractService = Depends(_contract_svc), ): with translate_domain_errors(): return svc.get_contract(contract_id) @router.post("/contracts", status_code=201) def create_contract( body: dict = {}, svc: ContractService = Depends(_contract_svc), ): with translate_domain_errors(): return svc.create_contract(body) @router.put("/contracts/{contract_id}") def update_contract( contract_id: str, body: dict = {}, svc: ContractService = Depends(_contract_svc), ): with translate_domain_errors(): return svc.update_contract(contract_id, body) @router.delete("/contracts/{contract_id}") def delete_contract( contract_id: str, svc: ContractService = Depends(_contract_svc), ): with translate_domain_errors(): return svc.delete_contract(contract_id) # ============================================================================ # Findings # ============================================================================ @router.get("/findings") def list_findings( tenant_id: Optional[str] = Query(None), vendor_id: Optional[str] = Query(None), severity: Optional[str] = Query(None), status: Optional[str] = Query(None), skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=500), svc: FindingService = Depends(_finding_svc), ): with translate_domain_errors(): return svc.list_findings(tenant_id, vendor_id, severity, status, skip, limit) @router.get("/findings/{finding_id}") def get_finding( finding_id: str, svc: FindingService = Depends(_finding_svc), ): with translate_domain_errors(): return svc.get_finding(finding_id) @router.post("/findings", status_code=201) def create_finding( body: dict = {}, svc: FindingService = Depends(_finding_svc), ): with translate_domain_errors(): return svc.create_finding(body) @router.put("/findings/{finding_id}") def update_finding( finding_id: str, body: dict = {}, svc: FindingService = Depends(_finding_svc), ): with translate_domain_errors(): return svc.update_finding(finding_id, body) @router.delete("/findings/{finding_id}") def delete_finding( finding_id: str, svc: FindingService = Depends(_finding_svc), ): with translate_domain_errors(): return svc.delete_finding(finding_id) # ============================================================================ # Control Instances # ============================================================================ @router.get("/control-instances") def list_control_instances( tenant_id: Optional[str] = Query(None), vendor_id: Optional[str] = Query(None), skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=500), svc: ControlInstanceService = Depends(_ci_svc), ): with translate_domain_errors(): return svc.list_instances(tenant_id, vendor_id, skip, limit) @router.get("/control-instances/{instance_id}") def get_control_instance( instance_id: str, svc: ControlInstanceService = Depends(_ci_svc), ): with translate_domain_errors(): return svc.get_instance(instance_id) @router.post("/control-instances", status_code=201) def create_control_instance( body: dict = {}, svc: ControlInstanceService = Depends(_ci_svc), ): with translate_domain_errors(): return svc.create_instance(body) @router.put("/control-instances/{instance_id}") def update_control_instance( instance_id: str, body: dict = {}, svc: ControlInstanceService = Depends(_ci_svc), ): with translate_domain_errors(): return svc.update_instance(instance_id, body) @router.delete("/control-instances/{instance_id}") def delete_control_instance( instance_id: str, svc: ControlInstanceService = Depends(_ci_svc), ): with translate_domain_errors(): return svc.delete_instance(instance_id) # ============================================================================ # Controls Library # ============================================================================ @router.get("/controls") def list_controls( tenant_id: Optional[str] = Query(None), domain: Optional[str] = Query(None), svc: ControlsLibraryService = Depends(_ctrl_svc), ): with translate_domain_errors(): return svc.list_controls(tenant_id, domain) @router.post("/controls", status_code=201) def create_control( body: dict = {}, svc: ControlsLibraryService = Depends(_ctrl_svc), ): with translate_domain_errors(): return svc.create_control(body) @router.delete("/controls/{control_id}") def delete_control( control_id: str, svc: ControlsLibraryService = Depends(_ctrl_svc), ): with translate_domain_errors(): return svc.delete_control(control_id) # ============================================================================ # Export Stubs (501 Not Implemented) # ============================================================================ @router.post("/export", status_code=501) def export_report(): return {"success": False, "error": "Export not implemented yet", "timestamp": _now_iso()} @router.get("/export/{report_id}", status_code=501) def get_export(report_id: str): return {"success": False, "error": "Export not implemented yet", "timestamp": _now_iso()} @router.get("/export/{report_id}/download", status_code=501) def download_export(report_id: str): return {"success": False, "error": "Export not implemented yet", "timestamp": _now_iso()}