""" Audit Session & Sign-Off models — Sprint 3 Phase 3. Extracted from compliance/db/models.py as the first worked example of the Phase 1 model split. The classes are re-exported from compliance.db.models for backwards compatibility, so existing imports continue to work unchanged. Tables: - compliance_audit_sessions: Structured compliance audit sessions - compliance_audit_signoffs: Per-requirement sign-offs with digital signatures DO NOT change __tablename__, column names, or relationship strings — the database schema is frozen. """ import uuid import enum from datetime import datetime, timezone from sqlalchemy import ( Column, String, Text, Integer, DateTime, ForeignKey, Enum, JSON, Index, ) from sqlalchemy.orm import relationship from classroom_engine.database import Base # ============================================================================ # ENUMS # ============================================================================ class AuditResultEnum(str, enum.Enum): """Result of an audit sign-off for a requirement.""" COMPLIANT = "compliant" # Fully compliant COMPLIANT_WITH_NOTES = "compliant_notes" # Compliant with observations NON_COMPLIANT = "non_compliant" # Not compliant - remediation required NOT_APPLICABLE = "not_applicable" # Not applicable to this audit PENDING = "pending" # Not yet reviewed class AuditSessionStatusEnum(str, enum.Enum): """Status of an audit session.""" DRAFT = "draft" # Session created, not started IN_PROGRESS = "in_progress" # Audit in progress COMPLETED = "completed" # All items reviewed ARCHIVED = "archived" # Historical record # ============================================================================ # MODELS # ============================================================================ class AuditSessionDB(Base): """ Audit session for structured compliance reviews. Enables auditors to: - Create named audit sessions (e.g., "Q1 2026 GDPR Audit") - Track progress through requirements - Sign off individual items with digital signatures - Generate audit reports """ __tablename__ = 'compliance_audit_sessions' id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) name = Column(String(200), nullable=False) # e.g., "Q1 2026 Compliance Audit" description = Column(Text) # Auditor information auditor_name = Column(String(100), nullable=False) # e.g., "Dr. Thomas Müller" auditor_email = Column(String(200)) auditor_organization = Column(String(200)) # External auditor company # Session scope status = Column(Enum(AuditSessionStatusEnum), default=AuditSessionStatusEnum.DRAFT) regulation_ids = Column(JSON) # Filter: ["GDPR", "AIACT"] or null for all # Progress tracking total_items = Column(Integer, default=0) completed_items = Column(Integer, default=0) compliant_count = Column(Integer, default=0) non_compliant_count = Column(Integer, default=0) # Timestamps created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) started_at = Column(DateTime) # When audit began completed_at = Column(DateTime) # When audit finished updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)) # Relationships signoffs = relationship("AuditSignOffDB", back_populates="session", cascade="all, delete-orphan") __table_args__ = ( Index('ix_audit_session_status', 'status'), Index('ix_audit_session_auditor', 'auditor_name'), ) def __repr__(self): return f"" @property def completion_percentage(self) -> float: """Calculate completion percentage.""" if self.total_items == 0: return 0.0 return round((self.completed_items / self.total_items) * 100, 1) class AuditSignOffDB(Base): """ Individual sign-off for a requirement within an audit session. Features: - Records audit result (compliant, non-compliant, etc.) - Stores auditor notes and observations - Creates digital signature (SHA-256 hash) for tamper evidence """ __tablename__ = 'compliance_audit_signoffs' id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) session_id = Column(String(36), ForeignKey('compliance_audit_sessions.id'), nullable=False, index=True) requirement_id = Column(String(36), ForeignKey('compliance_requirements.id'), nullable=False, index=True) # Audit result result = Column(Enum(AuditResultEnum), default=AuditResultEnum.PENDING) notes = Column(Text) # Auditor observations # Evidence references for this sign-off evidence_ids = Column(JSON) # List of evidence IDs reviewed # Digital signature (SHA-256 hash of result + auditor + timestamp) signature_hash = Column(String(64)) # SHA-256 hex string signed_at = Column(DateTime) signed_by = Column(String(100)) # Auditor name at time of signing # Timestamps created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)) # Relationships session = relationship("AuditSessionDB", back_populates="signoffs") requirement = relationship("RequirementDB") __table_args__ = ( Index('ix_signoff_session_requirement', 'session_id', 'requirement_id', unique=True), Index('ix_signoff_result', 'result'), ) def __repr__(self): return f"" def create_signature(self, auditor_name: str) -> str: """ Create a digital signature for this sign-off. Returns SHA-256 hash of: result + requirement_id + auditor_name + timestamp """ import hashlib timestamp = datetime.now(timezone.utc).isoformat() data = f"{self.result.value}|{self.requirement_id}|{auditor_name}|{timestamp}" signature = hashlib.sha256(data.encode()).hexdigest() self.signature_hash = signature self.signed_at = datetime.now(timezone.utc) self.signed_by = auditor_name return signature __all__ = [ "AuditResultEnum", "AuditSessionStatusEnum", "AuditSessionDB", "AuditSignOffDB", ]