""" FastAPI routes for Company Profile CRUD with audit logging. Endpoints: - GET /v1/company-profile: Get company profile for a tenant - POST /v1/company-profile: Create or update company profile - GET /v1/company-profile/audit: Get audit log for a tenant """ import json import logging import uuid from typing import Optional from fastapi import APIRouter, HTTPException, Header from pydantic import BaseModel from database import SessionLocal logger = logging.getLogger(__name__) router = APIRouter(prefix="/v1/company-profile", tags=["company-profile"]) # ============================================================================= # REQUEST/RESPONSE MODELS # ============================================================================= class CompanyProfileRequest(BaseModel): company_name: str = "" legal_form: str = "GmbH" industry: str = "" founded_year: Optional[int] = None business_model: str = "B2B" offerings: list[str] = [] company_size: str = "small" employee_count: str = "1-9" annual_revenue: str = "< 2 Mio" headquarters_country: str = "DE" headquarters_city: str = "" has_international_locations: bool = False international_countries: list[str] = [] target_markets: list[str] = ["DE"] primary_jurisdiction: str = "DE" is_data_controller: bool = True is_data_processor: bool = False uses_ai: bool = False ai_use_cases: list[str] = [] dpo_name: Optional[str] = None dpo_email: Optional[str] = None legal_contact_name: Optional[str] = None legal_contact_email: Optional[str] = None machine_builder: Optional[dict] = None is_complete: bool = False class CompanyProfileResponse(BaseModel): id: str tenant_id: str company_name: str legal_form: str industry: str founded_year: Optional[int] business_model: str offerings: list[str] company_size: str employee_count: str annual_revenue: str headquarters_country: str headquarters_city: str has_international_locations: bool international_countries: list[str] target_markets: list[str] primary_jurisdiction: str is_data_controller: bool is_data_processor: bool uses_ai: bool ai_use_cases: list[str] dpo_name: Optional[str] dpo_email: Optional[str] legal_contact_name: Optional[str] legal_contact_email: Optional[str] machine_builder: Optional[dict] is_complete: bool completed_at: Optional[str] created_at: str updated_at: str class AuditEntryResponse(BaseModel): id: str action: str changed_fields: Optional[dict] changed_by: Optional[str] created_at: str class AuditListResponse(BaseModel): entries: list[AuditEntryResponse] total: int # ============================================================================= # HELPERS # ============================================================================= def row_to_response(row) -> CompanyProfileResponse: """Convert a DB row to response model.""" return CompanyProfileResponse( id=str(row[0]), tenant_id=row[1], company_name=row[2] or "", legal_form=row[3] or "GmbH", industry=row[4] or "", founded_year=row[5], business_model=row[6] or "B2B", offerings=row[7] if isinstance(row[7], list) else [], company_size=row[8] or "small", employee_count=row[9] or "1-9", annual_revenue=row[10] or "< 2 Mio", headquarters_country=row[11] or "DE", headquarters_city=row[12] or "", has_international_locations=row[13] or False, international_countries=row[14] if isinstance(row[14], list) else [], target_markets=row[15] if isinstance(row[15], list) else ["DE"], primary_jurisdiction=row[16] or "DE", is_data_controller=row[17] if row[17] is not None else True, is_data_processor=row[18] or False, uses_ai=row[19] or False, ai_use_cases=row[20] if isinstance(row[20], list) else [], dpo_name=row[21], dpo_email=row[22], legal_contact_name=row[23], legal_contact_email=row[24], machine_builder=row[25] if isinstance(row[25], dict) else None, is_complete=row[26] or False, completed_at=str(row[27]) if row[27] else None, created_at=str(row[28]), updated_at=str(row[29]), ) def log_audit(db, tenant_id: str, action: str, changed_fields: Optional[dict], changed_by: Optional[str]): """Write an audit log entry.""" try: db.execute( """INSERT INTO compliance_company_profile_audit (tenant_id, action, changed_fields, changed_by) VALUES (:tenant_id, :action, :fields::jsonb, :changed_by)""", { "tenant_id": tenant_id, "action": action, "fields": json.dumps(changed_fields) if changed_fields else None, "changed_by": changed_by, }, ) except Exception as e: logger.warning(f"Failed to write audit log: {e}") # ============================================================================= # ROUTES # ============================================================================= @router.get("", response_model=CompanyProfileResponse) async def get_company_profile( tenant_id: str = "default", x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID"), ): """Get company profile for a tenant.""" tid = x_tenant_id or tenant_id db = SessionLocal() try: result = db.execute( """SELECT id, tenant_id, company_name, legal_form, industry, founded_year, business_model, offerings, company_size, employee_count, annual_revenue, headquarters_country, headquarters_city, has_international_locations, international_countries, target_markets, primary_jurisdiction, is_data_controller, is_data_processor, uses_ai, ai_use_cases, dpo_name, dpo_email, legal_contact_name, legal_contact_email, machine_builder, is_complete, completed_at, created_at, updated_at FROM compliance_company_profiles WHERE tenant_id = :tenant_id""", {"tenant_id": tid}, ) row = result.fetchone() if not row: raise HTTPException(status_code=404, detail="Company profile not found") return row_to_response(row) finally: db.close() @router.post("", response_model=CompanyProfileResponse) async def upsert_company_profile( profile: CompanyProfileRequest, tenant_id: str = "default", x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID"), ): """Create or update company profile (upsert).""" tid = x_tenant_id or tenant_id db = SessionLocal() try: # Check if profile exists existing = db.execute( "SELECT id FROM compliance_company_profiles WHERE tenant_id = :tid", {"tid": tid}, ).fetchone() action = "update" if existing else "create" completed_at_clause = ", completed_at = NOW()" if profile.is_complete else ", completed_at = NULL" db.execute( f"""INSERT INTO compliance_company_profiles (tenant_id, company_name, legal_form, industry, founded_year, business_model, offerings, company_size, employee_count, annual_revenue, headquarters_country, headquarters_city, has_international_locations, international_countries, target_markets, primary_jurisdiction, is_data_controller, is_data_processor, uses_ai, ai_use_cases, dpo_name, dpo_email, legal_contact_name, legal_contact_email, machine_builder, is_complete) VALUES (:tid, :company_name, :legal_form, :industry, :founded_year, :business_model, :offerings::jsonb, :company_size, :employee_count, :annual_revenue, :hq_country, :hq_city, :has_intl, :intl_countries::jsonb, :target_markets::jsonb, :jurisdiction, :is_controller, :is_processor, :uses_ai, :ai_use_cases::jsonb, :dpo_name, :dpo_email, :legal_name, :legal_email, :machine_builder::jsonb, :is_complete) ON CONFLICT (tenant_id) DO UPDATE SET company_name = EXCLUDED.company_name, legal_form = EXCLUDED.legal_form, industry = EXCLUDED.industry, founded_year = EXCLUDED.founded_year, business_model = EXCLUDED.business_model, offerings = EXCLUDED.offerings, company_size = EXCLUDED.company_size, employee_count = EXCLUDED.employee_count, annual_revenue = EXCLUDED.annual_revenue, headquarters_country = EXCLUDED.headquarters_country, headquarters_city = EXCLUDED.headquarters_city, has_international_locations = EXCLUDED.has_international_locations, international_countries = EXCLUDED.international_countries, target_markets = EXCLUDED.target_markets, primary_jurisdiction = EXCLUDED.primary_jurisdiction, is_data_controller = EXCLUDED.is_data_controller, is_data_processor = EXCLUDED.is_data_processor, uses_ai = EXCLUDED.uses_ai, ai_use_cases = EXCLUDED.ai_use_cases, dpo_name = EXCLUDED.dpo_name, dpo_email = EXCLUDED.dpo_email, legal_contact_name = EXCLUDED.legal_contact_name, legal_contact_email = EXCLUDED.legal_contact_email, machine_builder = EXCLUDED.machine_builder, is_complete = EXCLUDED.is_complete, updated_at = NOW() {completed_at_clause}""", { "tid": tid, "company_name": profile.company_name, "legal_form": profile.legal_form, "industry": profile.industry, "founded_year": profile.founded_year, "business_model": profile.business_model, "offerings": json.dumps(profile.offerings), "company_size": profile.company_size, "employee_count": profile.employee_count, "annual_revenue": profile.annual_revenue, "hq_country": profile.headquarters_country, "hq_city": profile.headquarters_city, "has_intl": profile.has_international_locations, "intl_countries": json.dumps(profile.international_countries), "target_markets": json.dumps(profile.target_markets), "jurisdiction": profile.primary_jurisdiction, "is_controller": profile.is_data_controller, "is_processor": profile.is_data_processor, "uses_ai": profile.uses_ai, "ai_use_cases": json.dumps(profile.ai_use_cases), "dpo_name": profile.dpo_name, "dpo_email": profile.dpo_email, "legal_name": profile.legal_contact_name, "legal_email": profile.legal_contact_email, "machine_builder": json.dumps(profile.machine_builder) if profile.machine_builder else None, "is_complete": profile.is_complete, }, ) # Audit log log_audit(db, tid, action, profile.model_dump(), None) db.commit() # Fetch and return result = db.execute( """SELECT id, tenant_id, company_name, legal_form, industry, founded_year, business_model, offerings, company_size, employee_count, annual_revenue, headquarters_country, headquarters_city, has_international_locations, international_countries, target_markets, primary_jurisdiction, is_data_controller, is_data_processor, uses_ai, ai_use_cases, dpo_name, dpo_email, legal_contact_name, legal_contact_email, machine_builder, is_complete, completed_at, created_at, updated_at FROM compliance_company_profiles WHERE tenant_id = :tid""", {"tid": tid}, ) row = result.fetchone() return row_to_response(row) except Exception as e: db.rollback() logger.error(f"Failed to upsert company profile: {e}") raise HTTPException(status_code=500, detail="Failed to save company profile") finally: db.close() @router.delete("", status_code=200) async def delete_company_profile( tenant_id: str = "default", x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID"), ): """Delete company profile for a tenant (DSGVO Recht auf Loeschung, Art. 17).""" tid = x_tenant_id or tenant_id db = SessionLocal() try: existing = db.execute( "SELECT id FROM compliance_company_profiles WHERE tenant_id = :tid", {"tid": tid}, ).fetchone() if not existing: raise HTTPException(status_code=404, detail="Company profile not found") db.execute( "DELETE FROM compliance_company_profiles WHERE tenant_id = :tid", {"tid": tid}, ) log_audit(db, tid, "delete", None, None) db.commit() return {"success": True, "message": "Company profile deleted"} except HTTPException: raise except Exception as e: db.rollback() logger.error(f"Failed to delete company profile: {e}") raise HTTPException(status_code=500, detail="Failed to delete company profile") finally: db.close() @router.get("/audit", response_model=AuditListResponse) async def get_audit_log( tenant_id: str = "default", x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID"), ): """Get audit log for company profile changes.""" tid = x_tenant_id or tenant_id db = SessionLocal() try: result = db.execute( """SELECT id, action, changed_fields, changed_by, created_at FROM compliance_company_profile_audit WHERE tenant_id = :tid ORDER BY created_at DESC LIMIT 100""", {"tid": tid}, ) rows = result.fetchall() entries = [ AuditEntryResponse( id=str(r[0]), action=r[1], changed_fields=r[2] if isinstance(r[2], dict) else None, changed_by=r[3], created_at=str(r[4]), ) for r in rows ] return AuditListResponse(entries=entries, total=len(entries)) finally: db.close()