# services/inventory/app/api/food_safety_compliance.py """ Food Safety Compliance API - ATOMIC CRUD operations on FoodSafetyCompliance model """ from typing import List, Optional from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query, Path, status from sqlalchemy.ext.asyncio import AsyncSession import structlog from shared.auth.decorators import get_current_user_dep from shared.auth.access_control import require_user_role from shared.routing import RouteBuilder from app.core.database import get_db from app.services.food_safety_service import FoodSafetyService from app.models import AuditLog from app.schemas.food_safety import ( FoodSafetyComplianceCreate, FoodSafetyComplianceUpdate, FoodSafetyComplianceResponse ) logger = structlog.get_logger() route_builder = RouteBuilder('inventory') router = APIRouter(tags=["food-safety-compliance"]) async def get_food_safety_service() -> FoodSafetyService: """Get food safety service instance""" return FoodSafetyService() @router.post( route_builder.build_base_route("food-safety/compliance"), response_model=FoodSafetyComplianceResponse, status_code=status.HTTP_201_CREATED ) @require_user_role(['admin', 'owner', 'member']) async def create_compliance_record( compliance_data: FoodSafetyComplianceCreate, tenant_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), food_safety_service: FoodSafetyService = Depends(get_food_safety_service), db: AsyncSession = Depends(get_db) ): """Create a new food safety compliance record""" try: compliance_data.tenant_id = tenant_id compliance = await food_safety_service.create_compliance_record( db, compliance_data, user_id=UUID(current_user["user_id"]) ) logger.info("Compliance record created", compliance_id=str(compliance.id), standard=compliance.standard) return compliance except ValueError as e: logger.warning("Invalid compliance data", error=str(e)) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e) ) except Exception as e: logger.error("Error creating compliance record", error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to create compliance record" ) @router.get( route_builder.build_base_route("food-safety/compliance"), response_model=List[FoodSafetyComplianceResponse] ) async def get_compliance_records( tenant_id: UUID = Path(...), ingredient_id: Optional[UUID] = Query(None, description="Filter by ingredient ID"), standard: Optional[str] = Query(None, description="Filter by compliance standard"), status_filter: Optional[str] = Query(None, description="Filter by compliance status"), skip: int = Query(0, ge=0, description="Number of records to skip"), limit: int = Query(100, ge=1, le=1000, description="Number of records to return"), current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """Get compliance records with filtering""" try: filters = {} if ingredient_id: filters["ingredient_id"] = ingredient_id if standard: filters["standard"] = standard if status_filter: filters["compliance_status"] = status_filter query = """ SELECT * FROM food_safety_compliance WHERE tenant_id = :tenant_id AND is_active = true """ params = {"tenant_id": tenant_id} if filters: for key, value in filters.items(): query += f" AND {key} = :{key}" params[key] = value query += " ORDER BY created_at DESC LIMIT :limit OFFSET :skip" params.update({"limit": limit, "skip": skip}) result = await db.execute(query, params) records = result.fetchall() return [ FoodSafetyComplianceResponse(**dict(record)) for record in records ] except Exception as e: logger.error("Error getting compliance records", error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve compliance records" ) @router.get( route_builder.build_resource_detail_route("food-safety/compliance", "compliance_id"), response_model=FoodSafetyComplianceResponse ) async def get_compliance_record( compliance_id: UUID = Path(...), tenant_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """Get specific compliance record""" try: query = "SELECT * FROM food_safety_compliance WHERE id = :compliance_id AND tenant_id = :tenant_id" result = await db.execute(query, {"compliance_id": compliance_id, "tenant_id": tenant_id}) record = result.fetchone() if not record: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Compliance record not found" ) return FoodSafetyComplianceResponse(**dict(record)) except HTTPException: raise except Exception as e: logger.error("Error getting compliance record", error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve compliance record" ) @router.put( route_builder.build_resource_detail_route("food-safety/compliance", "compliance_id"), response_model=FoodSafetyComplianceResponse ) @require_user_role(['admin', 'owner', 'member']) async def update_compliance_record( compliance_data: FoodSafetyComplianceUpdate, tenant_id: UUID = Path(...), compliance_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), food_safety_service: FoodSafetyService = Depends(get_food_safety_service), db: AsyncSession = Depends(get_db) ): """Update an existing compliance record""" try: compliance = await food_safety_service.update_compliance_record( db, compliance_id, tenant_id, compliance_data, user_id=UUID(current_user["user_id"]) ) if not compliance: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Compliance record not found" ) logger.info("Compliance record updated", compliance_id=str(compliance.id)) return compliance except HTTPException: raise except Exception as e: logger.error("Error updating compliance record", compliance_id=str(compliance_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to update compliance record" ) @router.delete( route_builder.build_resource_detail_route("food-safety/compliance", "compliance_id"), status_code=status.HTTP_403_FORBIDDEN ) @require_user_role(['admin', 'owner']) async def delete_compliance_record( compliance_id: UUID = Path(...), tenant_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """ Compliance records CANNOT be deleted for regulatory compliance. Use the archive endpoint to mark records as inactive. """ raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail={ "error": "compliance_records_cannot_be_deleted", "message": "Compliance records cannot be deleted for regulatory compliance. Use PUT /food-safety/compliance/{id}/archive to archive records instead.", "reason": "Food safety compliance records must be retained for regulatory audits", "alternative_endpoint": f"/api/v1/tenants/{tenant_id}/inventory/food-safety/compliance/{compliance_id}/archive" } ) @router.put( route_builder.build_nested_resource_route("food-safety/compliance", "compliance_id", "archive"), response_model=dict ) @require_user_role(['admin', 'owner']) async def archive_compliance_record( compliance_id: UUID = Path(...), tenant_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """Archive (soft delete) compliance record - marks as inactive but retains for audit""" try: query = """ UPDATE food_safety_compliance SET is_active = false, updated_at = NOW(), updated_by = :user_id WHERE id = :compliance_id AND tenant_id = :tenant_id """ result = await db.execute(query, { "compliance_id": compliance_id, "tenant_id": tenant_id, "user_id": UUID(current_user["user_id"]) }) if result.rowcount == 0: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Compliance record not found" ) await db.commit() # Log audit event for archiving compliance record try: from shared.security import create_audit_logger, AuditSeverity, AuditAction audit_logger = create_audit_logger("inventory-service", AuditLog) await audit_logger.log_event( db_session=db, tenant_id=str(tenant_id), user_id=current_user["user_id"], action="archive", resource_type="compliance_record", resource_id=str(compliance_id), severity=AuditSeverity.HIGH.value, description=f"Archived compliance record (retained for regulatory compliance)", endpoint=f"/food-safety/compliance/{compliance_id}/archive", method="PUT" ) except Exception as audit_error: logger.warning("Failed to log audit event", error=str(audit_error)) return { "message": "Compliance record archived successfully", "compliance_id": str(compliance_id), "archived": True, "note": "Record retained for regulatory compliance audits" } except HTTPException: raise except Exception as e: logger.error("Error archiving compliance record", error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to archive compliance record" )