Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 30s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 17s
- Ruff: 144 auto-fixes (unused imports, == None → is None), F821/F811/F841 manuell - CVEs: python-multipart>=0.0.22, weasyprint>=68.0, pillow>=12.1.1, npm audit fix (0 vulns) - TS: 5 tote Drafting-Engine-Dateien entfernt, allowed-facts/sanitizer/StepHeader/context fixes - Tests: +104 (ISMS 58, Evidence 18, VVT 14, Generation 14) → 1449 passed - Refactoring: collect_ci_evidence (F→A), row_to_response (E→A), extract_requirements (E→A) - Dead Code: pca-platform, 7 Go-Handler, dsr_api.py, duplicate Schemas entfernt Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
638 lines
22 KiB
Python
638 lines
22 KiB
Python
"""
|
|
FastAPI routes for Audit Sessions & Sign-off functionality.
|
|
|
|
Sprint 3 Phase 3: Auditor-Verbesserungen
|
|
|
|
Endpoints:
|
|
- /audit/sessions: Manage audit sessions
|
|
- /audit/checklist: Audit checklist with sign-off
|
|
"""
|
|
|
|
import logging
|
|
from datetime import datetime
|
|
from typing import Optional, List
|
|
from uuid import uuid4
|
|
import hashlib
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import func
|
|
|
|
from classroom_engine.database import get_db
|
|
|
|
from ..db.models import (
|
|
AuditSessionDB, AuditSignOffDB, AuditResultEnum, AuditSessionStatusEnum,
|
|
RequirementDB, RegulationDB, ControlMappingDB
|
|
)
|
|
from .schemas import (
|
|
CreateAuditSessionRequest, AuditSessionResponse, AuditSessionSummary, AuditSessionDetailResponse,
|
|
SignOffRequest, SignOffResponse,
|
|
AuditChecklistItem, AuditChecklistResponse, AuditStatistics,
|
|
PaginationMeta,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/audit", tags=["compliance-audit"])
|
|
|
|
|
|
# ============================================================================
|
|
# Audit Sessions
|
|
# ============================================================================
|
|
|
|
@router.post("/sessions", response_model=AuditSessionResponse)
|
|
async def create_audit_session(
|
|
request: CreateAuditSessionRequest,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
Create a new audit session for structured compliance reviews.
|
|
|
|
An audit session groups requirements for systematic review by an auditor.
|
|
"""
|
|
# Get total requirements count based on filters
|
|
query = db.query(RequirementDB)
|
|
if request.regulation_codes:
|
|
reg_ids = db.query(RegulationDB.id).filter(
|
|
RegulationDB.code.in_(request.regulation_codes)
|
|
).all()
|
|
reg_ids = [r[0] for r in reg_ids]
|
|
query = query.filter(RequirementDB.regulation_id.in_(reg_ids))
|
|
|
|
total_items = query.count()
|
|
|
|
# Create the session
|
|
session = AuditSessionDB(
|
|
id=str(uuid4()),
|
|
name=request.name,
|
|
description=request.description,
|
|
auditor_name=request.auditor_name,
|
|
auditor_email=request.auditor_email,
|
|
auditor_organization=request.auditor_organization,
|
|
status=AuditSessionStatusEnum.DRAFT,
|
|
regulation_ids=request.regulation_codes,
|
|
total_items=total_items,
|
|
completed_items=0,
|
|
compliant_count=0,
|
|
non_compliant_count=0,
|
|
)
|
|
|
|
db.add(session)
|
|
db.commit()
|
|
db.refresh(session)
|
|
|
|
return AuditSessionResponse(
|
|
id=session.id,
|
|
name=session.name,
|
|
description=session.description,
|
|
auditor_name=session.auditor_name,
|
|
auditor_email=session.auditor_email,
|
|
auditor_organization=session.auditor_organization,
|
|
status=session.status.value,
|
|
regulation_ids=session.regulation_ids,
|
|
total_items=session.total_items,
|
|
completed_items=session.completed_items,
|
|
compliant_count=session.compliant_count,
|
|
non_compliant_count=session.non_compliant_count,
|
|
completion_percentage=session.completion_percentage,
|
|
created_at=session.created_at,
|
|
started_at=session.started_at,
|
|
completed_at=session.completed_at,
|
|
)
|
|
|
|
|
|
@router.get("/sessions", response_model=List[AuditSessionSummary])
|
|
async def list_audit_sessions(
|
|
status: Optional[str] = None,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
List all audit sessions, optionally filtered by status.
|
|
"""
|
|
query = db.query(AuditSessionDB)
|
|
|
|
if status:
|
|
try:
|
|
status_enum = AuditSessionStatusEnum(status)
|
|
query = query.filter(AuditSessionDB.status == status_enum)
|
|
except ValueError:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Invalid status: {status}. Valid values: draft, in_progress, completed, archived"
|
|
)
|
|
|
|
sessions = query.order_by(AuditSessionDB.created_at.desc()).all()
|
|
|
|
return [
|
|
AuditSessionSummary(
|
|
id=s.id,
|
|
name=s.name,
|
|
auditor_name=s.auditor_name,
|
|
status=s.status.value,
|
|
total_items=s.total_items,
|
|
completed_items=s.completed_items,
|
|
completion_percentage=s.completion_percentage,
|
|
created_at=s.created_at,
|
|
started_at=s.started_at,
|
|
completed_at=s.completed_at,
|
|
)
|
|
for s in sessions
|
|
]
|
|
|
|
|
|
@router.get("/sessions/{session_id}", response_model=AuditSessionDetailResponse)
|
|
async def get_audit_session(
|
|
session_id: str,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
Get detailed information about a specific audit session.
|
|
"""
|
|
session = db.query(AuditSessionDB).filter(AuditSessionDB.id == session_id).first()
|
|
if not session:
|
|
raise HTTPException(status_code=404, detail=f"Audit session {session_id} not found")
|
|
|
|
# Get sign-off statistics
|
|
signoffs = db.query(AuditSignOffDB).filter(AuditSignOffDB.session_id == session_id).all()
|
|
|
|
stats = AuditStatistics(
|
|
total=session.total_items,
|
|
compliant=sum(1 for s in signoffs if s.result == AuditResultEnum.COMPLIANT),
|
|
compliant_with_notes=sum(1 for s in signoffs if s.result == AuditResultEnum.COMPLIANT_WITH_NOTES),
|
|
non_compliant=sum(1 for s in signoffs if s.result == AuditResultEnum.NON_COMPLIANT),
|
|
not_applicable=sum(1 for s in signoffs if s.result == AuditResultEnum.NOT_APPLICABLE),
|
|
pending=session.total_items - len(signoffs),
|
|
completion_percentage=session.completion_percentage,
|
|
)
|
|
|
|
return AuditSessionDetailResponse(
|
|
id=session.id,
|
|
name=session.name,
|
|
description=session.description,
|
|
auditor_name=session.auditor_name,
|
|
auditor_email=session.auditor_email,
|
|
auditor_organization=session.auditor_organization,
|
|
status=session.status.value,
|
|
regulation_ids=session.regulation_ids,
|
|
total_items=session.total_items,
|
|
completed_items=session.completed_items,
|
|
compliant_count=session.compliant_count,
|
|
non_compliant_count=session.non_compliant_count,
|
|
completion_percentage=session.completion_percentage,
|
|
created_at=session.created_at,
|
|
started_at=session.started_at,
|
|
completed_at=session.completed_at,
|
|
statistics=stats,
|
|
)
|
|
|
|
|
|
@router.put("/sessions/{session_id}/start")
|
|
async def start_audit_session(
|
|
session_id: str,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
Start an audit session (change status from draft to in_progress).
|
|
"""
|
|
session = db.query(AuditSessionDB).filter(AuditSessionDB.id == session_id).first()
|
|
if not session:
|
|
raise HTTPException(status_code=404, detail=f"Audit session {session_id} not found")
|
|
|
|
if session.status != AuditSessionStatusEnum.DRAFT:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Session cannot be started. Current status: {session.status.value}"
|
|
)
|
|
|
|
session.status = AuditSessionStatusEnum.IN_PROGRESS
|
|
session.started_at = datetime.utcnow()
|
|
db.commit()
|
|
|
|
return {"success": True, "message": "Audit session started", "status": "in_progress"}
|
|
|
|
|
|
@router.put("/sessions/{session_id}/complete")
|
|
async def complete_audit_session(
|
|
session_id: str,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
Complete an audit session (change status from in_progress to completed).
|
|
"""
|
|
session = db.query(AuditSessionDB).filter(AuditSessionDB.id == session_id).first()
|
|
if not session:
|
|
raise HTTPException(status_code=404, detail=f"Audit session {session_id} not found")
|
|
|
|
if session.status != AuditSessionStatusEnum.IN_PROGRESS:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Session cannot be completed. Current status: {session.status.value}"
|
|
)
|
|
|
|
session.status = AuditSessionStatusEnum.COMPLETED
|
|
session.completed_at = datetime.utcnow()
|
|
db.commit()
|
|
|
|
return {"success": True, "message": "Audit session completed", "status": "completed"}
|
|
|
|
|
|
@router.put("/sessions/{session_id}/archive")
|
|
async def archive_audit_session(
|
|
session_id: str,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
Archive a completed audit session.
|
|
"""
|
|
session = db.query(AuditSessionDB).filter(AuditSessionDB.id == session_id).first()
|
|
if not session:
|
|
raise HTTPException(status_code=404, detail=f"Audit session {session_id} not found")
|
|
|
|
if session.status != AuditSessionStatusEnum.COMPLETED:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Only completed sessions can be archived. Current status: {session.status.value}"
|
|
)
|
|
|
|
session.status = AuditSessionStatusEnum.ARCHIVED
|
|
db.commit()
|
|
|
|
return {"success": True, "message": "Audit session archived", "status": "archived"}
|
|
|
|
|
|
@router.delete("/sessions/{session_id}")
|
|
async def delete_audit_session(
|
|
session_id: str,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
Delete an audit session and all its sign-offs.
|
|
|
|
Only draft sessions can be deleted.
|
|
"""
|
|
session = db.query(AuditSessionDB).filter(AuditSessionDB.id == session_id).first()
|
|
if not session:
|
|
raise HTTPException(status_code=404, detail=f"Audit session {session_id} not found")
|
|
|
|
if session.status not in [AuditSessionStatusEnum.DRAFT, AuditSessionStatusEnum.ARCHIVED]:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Cannot delete session with status: {session.status.value}. Archive it first."
|
|
)
|
|
|
|
# Delete all sign-offs first (cascade should handle this, but be explicit)
|
|
db.query(AuditSignOffDB).filter(AuditSignOffDB.session_id == session_id).delete()
|
|
|
|
# Delete the session
|
|
db.delete(session)
|
|
db.commit()
|
|
|
|
return {"success": True, "message": f"Audit session {session_id} deleted"}
|
|
|
|
|
|
# ============================================================================
|
|
# Audit Checklist & Sign-off
|
|
# ============================================================================
|
|
|
|
@router.get("/checklist/{session_id}", response_model=AuditChecklistResponse)
|
|
async def get_audit_checklist(
|
|
session_id: str,
|
|
page: int = Query(1, ge=1),
|
|
page_size: int = Query(50, ge=1, le=200),
|
|
status_filter: Optional[str] = None,
|
|
regulation_filter: Optional[str] = None,
|
|
search: Optional[str] = None,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
Get the audit checklist for a session with pagination.
|
|
|
|
Returns requirements with their current sign-off status.
|
|
"""
|
|
# Get the session
|
|
session = db.query(AuditSessionDB).filter(AuditSessionDB.id == session_id).first()
|
|
if not session:
|
|
raise HTTPException(status_code=404, detail=f"Audit session {session_id} not found")
|
|
|
|
# Build base query for requirements
|
|
query = db.query(RequirementDB).join(RegulationDB)
|
|
|
|
# Apply session's regulation filter
|
|
if session.regulation_ids:
|
|
query = query.filter(RegulationDB.code.in_(session.regulation_ids))
|
|
|
|
# Apply additional filters
|
|
if regulation_filter:
|
|
query = query.filter(RegulationDB.code == regulation_filter)
|
|
|
|
if search:
|
|
search_term = f"%{search}%"
|
|
query = query.filter(
|
|
(RequirementDB.title.ilike(search_term)) |
|
|
(RequirementDB.article.ilike(search_term)) |
|
|
(RequirementDB.description.ilike(search_term))
|
|
)
|
|
|
|
# Get total count before pagination
|
|
total_count = query.count()
|
|
|
|
# Apply pagination
|
|
requirements = (
|
|
query
|
|
.order_by(RegulationDB.code, RequirementDB.article)
|
|
.offset((page - 1) * page_size)
|
|
.limit(page_size)
|
|
.all()
|
|
)
|
|
|
|
# Get existing sign-offs for these requirements
|
|
req_ids = [r.id for r in requirements]
|
|
signoffs = (
|
|
db.query(AuditSignOffDB)
|
|
.filter(AuditSignOffDB.session_id == session_id)
|
|
.filter(AuditSignOffDB.requirement_id.in_(req_ids))
|
|
.all()
|
|
)
|
|
signoff_map = {s.requirement_id: s for s in signoffs}
|
|
|
|
# Get control mappings counts
|
|
mapping_counts = (
|
|
db.query(ControlMappingDB.requirement_id, func.count(ControlMappingDB.id))
|
|
.filter(ControlMappingDB.requirement_id.in_(req_ids))
|
|
.group_by(ControlMappingDB.requirement_id)
|
|
.all()
|
|
)
|
|
mapping_count_map = dict(mapping_counts)
|
|
|
|
# Build checklist items
|
|
items = []
|
|
for req in requirements:
|
|
signoff = signoff_map.get(req.id)
|
|
|
|
# Apply status filter if specified
|
|
if status_filter:
|
|
if status_filter == "pending" and signoff is not None:
|
|
continue
|
|
elif status_filter != "pending" and (signoff is None or signoff.result.value != status_filter):
|
|
continue
|
|
|
|
item = AuditChecklistItem(
|
|
requirement_id=req.id,
|
|
regulation_code=req.regulation.code,
|
|
article=req.article,
|
|
paragraph=req.paragraph,
|
|
title=req.title,
|
|
description=req.description,
|
|
current_result=signoff.result.value if signoff else "pending",
|
|
notes=signoff.notes if signoff else None,
|
|
is_signed=signoff.signature_hash is not None if signoff else False,
|
|
signed_at=signoff.signed_at if signoff else None,
|
|
signed_by=signoff.signed_by if signoff else None,
|
|
evidence_count=0, # TODO: Add evidence count
|
|
controls_mapped=mapping_count_map.get(req.id, 0),
|
|
implementation_status=req.implementation_status,
|
|
priority=req.priority,
|
|
)
|
|
items.append(item)
|
|
|
|
# Calculate statistics
|
|
all_signoffs = db.query(AuditSignOffDB).filter(AuditSignOffDB.session_id == session_id).all()
|
|
stats = AuditStatistics(
|
|
total=session.total_items,
|
|
compliant=sum(1 for s in all_signoffs if s.result == AuditResultEnum.COMPLIANT),
|
|
compliant_with_notes=sum(1 for s in all_signoffs if s.result == AuditResultEnum.COMPLIANT_WITH_NOTES),
|
|
non_compliant=sum(1 for s in all_signoffs if s.result == AuditResultEnum.NON_COMPLIANT),
|
|
not_applicable=sum(1 for s in all_signoffs if s.result == AuditResultEnum.NOT_APPLICABLE),
|
|
pending=session.total_items - len(all_signoffs),
|
|
completion_percentage=session.completion_percentage,
|
|
)
|
|
|
|
return AuditChecklistResponse(
|
|
session=AuditSessionSummary(
|
|
id=session.id,
|
|
name=session.name,
|
|
auditor_name=session.auditor_name,
|
|
status=session.status.value,
|
|
total_items=session.total_items,
|
|
completed_items=session.completed_items,
|
|
completion_percentage=session.completion_percentage,
|
|
created_at=session.created_at,
|
|
started_at=session.started_at,
|
|
completed_at=session.completed_at,
|
|
),
|
|
items=items,
|
|
pagination=PaginationMeta(
|
|
page=page,
|
|
page_size=page_size,
|
|
total=total_count,
|
|
total_pages=(total_count + page_size - 1) // page_size,
|
|
),
|
|
statistics=stats,
|
|
)
|
|
|
|
|
|
@router.put("/checklist/{session_id}/items/{requirement_id}/sign-off", response_model=SignOffResponse)
|
|
async def sign_off_item(
|
|
session_id: str,
|
|
requirement_id: str,
|
|
request: SignOffRequest,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
Sign off on a specific requirement in an audit session.
|
|
|
|
If sign=True, creates a digital signature (SHA-256 hash).
|
|
"""
|
|
# Validate session exists and is in progress
|
|
session = db.query(AuditSessionDB).filter(AuditSessionDB.id == session_id).first()
|
|
if not session:
|
|
raise HTTPException(status_code=404, detail=f"Audit session {session_id} not found")
|
|
|
|
if session.status not in [AuditSessionStatusEnum.DRAFT, AuditSessionStatusEnum.IN_PROGRESS]:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Cannot sign off items in session with status: {session.status.value}"
|
|
)
|
|
|
|
# Validate requirement exists
|
|
requirement = db.query(RequirementDB).filter(RequirementDB.id == requirement_id).first()
|
|
if not requirement:
|
|
raise HTTPException(status_code=404, detail=f"Requirement {requirement_id} not found")
|
|
|
|
# Map string result to enum
|
|
try:
|
|
result_enum = AuditResultEnum(request.result)
|
|
except ValueError:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Invalid result: {request.result}. Valid values: compliant, compliant_notes, non_compliant, not_applicable, pending"
|
|
)
|
|
|
|
# Check if sign-off already exists
|
|
signoff = (
|
|
db.query(AuditSignOffDB)
|
|
.filter(AuditSignOffDB.session_id == session_id)
|
|
.filter(AuditSignOffDB.requirement_id == requirement_id)
|
|
.first()
|
|
)
|
|
|
|
was_new = signoff is None
|
|
old_result = signoff.result if signoff else None
|
|
|
|
if signoff:
|
|
# Update existing sign-off
|
|
signoff.result = result_enum
|
|
signoff.notes = request.notes
|
|
signoff.updated_at = datetime.utcnow()
|
|
else:
|
|
# Create new sign-off
|
|
signoff = AuditSignOffDB(
|
|
id=str(uuid4()),
|
|
session_id=session_id,
|
|
requirement_id=requirement_id,
|
|
result=result_enum,
|
|
notes=request.notes,
|
|
)
|
|
db.add(signoff)
|
|
|
|
# Create digital signature if requested
|
|
signature = None
|
|
if request.sign:
|
|
timestamp = datetime.utcnow().isoformat()
|
|
data = f"{result_enum.value}|{requirement_id}|{session.auditor_name}|{timestamp}"
|
|
signature = hashlib.sha256(data.encode()).hexdigest()
|
|
signoff.signature_hash = signature
|
|
signoff.signed_at = datetime.utcnow()
|
|
signoff.signed_by = session.auditor_name
|
|
|
|
# Update session statistics
|
|
if was_new:
|
|
session.completed_items += 1
|
|
|
|
# Update compliant/non-compliant counts
|
|
if old_result != result_enum:
|
|
if old_result == AuditResultEnum.COMPLIANT or old_result == AuditResultEnum.COMPLIANT_WITH_NOTES:
|
|
session.compliant_count = max(0, session.compliant_count - 1)
|
|
elif old_result == AuditResultEnum.NON_COMPLIANT:
|
|
session.non_compliant_count = max(0, session.non_compliant_count - 1)
|
|
|
|
if result_enum == AuditResultEnum.COMPLIANT or result_enum == AuditResultEnum.COMPLIANT_WITH_NOTES:
|
|
session.compliant_count += 1
|
|
elif result_enum == AuditResultEnum.NON_COMPLIANT:
|
|
session.non_compliant_count += 1
|
|
|
|
# Auto-start session if this is the first sign-off
|
|
if session.status == AuditSessionStatusEnum.DRAFT:
|
|
session.status = AuditSessionStatusEnum.IN_PROGRESS
|
|
session.started_at = datetime.utcnow()
|
|
|
|
db.commit()
|
|
db.refresh(signoff)
|
|
|
|
return SignOffResponse(
|
|
id=signoff.id,
|
|
session_id=signoff.session_id,
|
|
requirement_id=signoff.requirement_id,
|
|
result=signoff.result.value,
|
|
notes=signoff.notes,
|
|
is_signed=signoff.signature_hash is not None,
|
|
signature_hash=signoff.signature_hash,
|
|
signed_at=signoff.signed_at,
|
|
signed_by=signoff.signed_by,
|
|
created_at=signoff.created_at,
|
|
updated_at=signoff.updated_at,
|
|
)
|
|
|
|
|
|
@router.get("/checklist/{session_id}/items/{requirement_id}", response_model=SignOffResponse)
|
|
async def get_sign_off(
|
|
session_id: str,
|
|
requirement_id: str,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
Get the current sign-off status for a specific requirement.
|
|
"""
|
|
signoff = (
|
|
db.query(AuditSignOffDB)
|
|
.filter(AuditSignOffDB.session_id == session_id)
|
|
.filter(AuditSignOffDB.requirement_id == requirement_id)
|
|
.first()
|
|
)
|
|
|
|
if not signoff:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail=f"No sign-off found for requirement {requirement_id} in session {session_id}"
|
|
)
|
|
|
|
return SignOffResponse(
|
|
id=signoff.id,
|
|
session_id=signoff.session_id,
|
|
requirement_id=signoff.requirement_id,
|
|
result=signoff.result.value,
|
|
notes=signoff.notes,
|
|
is_signed=signoff.signature_hash is not None,
|
|
signature_hash=signoff.signature_hash,
|
|
signed_at=signoff.signed_at,
|
|
signed_by=signoff.signed_by,
|
|
created_at=signoff.created_at,
|
|
updated_at=signoff.updated_at,
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# PDF Report Generation
|
|
# ============================================================================
|
|
|
|
@router.get("/sessions/{session_id}/report/pdf")
|
|
async def generate_audit_pdf_report(
|
|
session_id: str,
|
|
language: str = Query("de", regex="^(de|en)$"),
|
|
include_signatures: bool = Query(True),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
Generate a PDF report for an audit session.
|
|
|
|
Parameters:
|
|
- session_id: The audit session ID
|
|
- language: Output language ('de' or 'en'), default 'de'
|
|
- include_signatures: Include digital signature verification section
|
|
|
|
Returns:
|
|
- PDF file as streaming response
|
|
"""
|
|
from fastapi.responses import StreamingResponse
|
|
import io
|
|
from ..services.audit_pdf_generator import AuditPDFGenerator
|
|
|
|
# Validate session exists
|
|
session = db.query(AuditSessionDB).filter(AuditSessionDB.id == session_id).first()
|
|
if not session:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail=f"Audit session {session_id} not found"
|
|
)
|
|
|
|
try:
|
|
generator = AuditPDFGenerator(db)
|
|
pdf_bytes, filename = generator.generate(
|
|
session_id=session_id,
|
|
language=language,
|
|
include_signatures=include_signatures,
|
|
)
|
|
|
|
return StreamingResponse(
|
|
io.BytesIO(pdf_bytes),
|
|
media_type="application/pdf",
|
|
headers={
|
|
"Content-Disposition": f"attachment; filename={filename}",
|
|
}
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Failed to generate PDF report: {e}")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=f"Failed to generate PDF report: {str(e)}"
|
|
)
|