# ================================================================ # services/inventory/app/models/food_safety.py # ================================================================ """ Food safety and compliance models for Inventory Service """ import uuid import enum from datetime import datetime from typing import Dict, Any, Optional from sqlalchemy import Column, String, DateTime, Float, Integer, Text, Boolean, Numeric, ForeignKey, Enum as SQLEnum from sqlalchemy.dialects.postgresql import UUID, JSONB from sqlalchemy.orm import relationship from sqlalchemy.sql import func from shared.database.base import Base class FoodSafetyStandard(enum.Enum): """Food safety standards and certifications""" HACCP = "haccp" FDA = "fda" USDA = "usda" FSMA = "fsma" SQF = "sqf" BRC = "brc" IFS = "ifs" ISO22000 = "iso22000" ORGANIC = "organic" NON_GMO = "non_gmo" ALLERGEN_FREE = "allergen_free" KOSHER = "kosher" HALAL = "halal" class ComplianceStatus(enum.Enum): """Compliance status for food safety requirements""" COMPLIANT = "compliant" NON_COMPLIANT = "non_compliant" PENDING_REVIEW = "pending_review" EXPIRED = "expired" WARNING = "warning" class FoodSafetyAlertType(enum.Enum): """Types of food safety alerts""" TEMPERATURE_VIOLATION = "temperature_violation" EXPIRATION_WARNING = "expiration_warning" EXPIRED_PRODUCT = "expired_product" CONTAMINATION_RISK = "contamination_risk" ALLERGEN_CROSS_CONTAMINATION = "allergen_cross_contamination" STORAGE_VIOLATION = "storage_violation" QUALITY_DEGRADATION = "quality_degradation" RECALL_NOTICE = "recall_notice" CERTIFICATION_EXPIRY = "certification_expiry" SUPPLIER_COMPLIANCE_ISSUE = "supplier_compliance_issue" class FoodSafetyCompliance(Base): """Food safety compliance tracking for ingredients and products""" __tablename__ = "food_safety_compliance" # Primary identification id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True) ingredient_id = Column(UUID(as_uuid=True), ForeignKey("ingredients.id"), nullable=False, index=True) # Compliance standard standard = Column(SQLEnum(FoodSafetyStandard), nullable=False, index=True) compliance_status = Column(SQLEnum(ComplianceStatus), nullable=False, default=ComplianceStatus.PENDING_REVIEW) # Certification details certification_number = Column(String(100), nullable=True) certifying_body = Column(String(200), nullable=True) certification_date = Column(DateTime(timezone=True), nullable=True) expiration_date = Column(DateTime(timezone=True), nullable=True, index=True) # Compliance requirements requirements = Column(JSONB, nullable=True) # Specific requirements for this standard compliance_notes = Column(Text, nullable=True) documentation_url = Column(String(500), nullable=True) # Audit information last_audit_date = Column(DateTime(timezone=True), nullable=True) next_audit_date = Column(DateTime(timezone=True), nullable=True, index=True) auditor_name = Column(String(200), nullable=True) audit_score = Column(Float, nullable=True) # 0-100 score # Risk assessment risk_level = Column(String(20), nullable=False, default="medium") # low, medium, high, critical risk_factors = Column(JSONB, nullable=True) # List of identified risk factors mitigation_measures = Column(JSONB, nullable=True) # Implemented mitigation measures # Status tracking is_active = Column(Boolean, nullable=False, default=True) requires_monitoring = Column(Boolean, nullable=False, default=True) monitoring_frequency_days = Column(Integer, nullable=True) # How often to check compliance # Audit fields created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False) created_by = Column(UUID(as_uuid=True), nullable=True) updated_by = Column(UUID(as_uuid=True), nullable=True) def to_dict(self) -> Dict[str, Any]: """Convert model to dictionary for API responses""" return { 'id': str(self.id), 'tenant_id': str(self.tenant_id), 'ingredient_id': str(self.ingredient_id), 'standard': self.standard.value if self.standard else None, 'compliance_status': self.compliance_status.value if self.compliance_status else None, 'certification_number': self.certification_number, 'certifying_body': self.certifying_body, 'certification_date': self.certification_date.isoformat() if self.certification_date else None, 'expiration_date': self.expiration_date.isoformat() if self.expiration_date else None, 'requirements': self.requirements, 'compliance_notes': self.compliance_notes, 'documentation_url': self.documentation_url, 'last_audit_date': self.last_audit_date.isoformat() if self.last_audit_date else None, 'next_audit_date': self.next_audit_date.isoformat() if self.next_audit_date else None, 'auditor_name': self.auditor_name, 'audit_score': self.audit_score, 'risk_level': self.risk_level, 'risk_factors': self.risk_factors, 'mitigation_measures': self.mitigation_measures, 'is_active': self.is_active, 'requires_monitoring': self.requires_monitoring, 'monitoring_frequency_days': self.monitoring_frequency_days, 'created_at': self.created_at.isoformat() if self.created_at else None, 'updated_at': self.updated_at.isoformat() if self.updated_at else None, 'created_by': str(self.created_by) if self.created_by else None, 'updated_by': str(self.updated_by) if self.updated_by else None, } class TemperatureLog(Base): """Temperature monitoring logs for storage areas""" __tablename__ = "temperature_logs" # Primary identification id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True) # Location information storage_location = Column(String(100), nullable=False, index=True) warehouse_zone = Column(String(50), nullable=True) equipment_id = Column(String(100), nullable=True) # Freezer/refrigerator ID # Temperature readings temperature_celsius = Column(Float, nullable=False) humidity_percentage = Column(Float, nullable=True) target_temperature_min = Column(Float, nullable=True) target_temperature_max = Column(Float, nullable=True) # Status and alerts is_within_range = Column(Boolean, nullable=False, default=True) alert_triggered = Column(Boolean, nullable=False, default=False) deviation_minutes = Column(Integer, nullable=True) # How long outside range # Measurement details measurement_method = Column(String(50), nullable=False, default="manual") # manual, automatic, sensor device_id = Column(String(100), nullable=True) calibration_date = Column(DateTime(timezone=True), nullable=True) # Timestamp recorded_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False, index=True) # Audit fields created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) recorded_by = Column(UUID(as_uuid=True), nullable=True) def to_dict(self) -> Dict[str, Any]: """Convert model to dictionary for API responses""" return { 'id': str(self.id), 'tenant_id': str(self.tenant_id), 'storage_location': self.storage_location, 'warehouse_zone': self.warehouse_zone, 'equipment_id': self.equipment_id, 'temperature_celsius': self.temperature_celsius, 'humidity_percentage': self.humidity_percentage, 'target_temperature_min': self.target_temperature_min, 'target_temperature_max': self.target_temperature_max, 'is_within_range': self.is_within_range, 'alert_triggered': self.alert_triggered, 'deviation_minutes': self.deviation_minutes, 'measurement_method': self.measurement_method, 'device_id': self.device_id, 'calibration_date': self.calibration_date.isoformat() if self.calibration_date else None, 'recorded_at': self.recorded_at.isoformat() if self.recorded_at else None, 'created_at': self.created_at.isoformat() if self.created_at else None, 'recorded_by': str(self.recorded_by) if self.recorded_by else None, } class FoodSafetyAlert(Base): """Food safety alerts and notifications""" __tablename__ = "food_safety_alerts" # Primary identification id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True) alert_code = Column(String(50), nullable=False, index=True) # Alert classification alert_type = Column(SQLEnum(FoodSafetyAlertType), nullable=False, index=True) severity = Column(String(20), nullable=False, default="medium", index=True) # low, medium, high, critical risk_level = Column(String(20), nullable=False, default="medium") # Source information source_entity_type = Column(String(50), nullable=False) # ingredient, stock, temperature_log, compliance source_entity_id = Column(UUID(as_uuid=True), nullable=False, index=True) ingredient_id = Column(UUID(as_uuid=True), ForeignKey("ingredients.id"), nullable=True, index=True) stock_id = Column(UUID(as_uuid=True), ForeignKey("stock.id"), nullable=True, index=True) # Alert content title = Column(String(200), nullable=False) description = Column(Text, nullable=False) detailed_message = Column(Text, nullable=True) # Regulatory and compliance context regulatory_requirement = Column(String(100), nullable=True) compliance_standard = Column(SQLEnum(FoodSafetyStandard), nullable=True) regulatory_action_required = Column(Boolean, nullable=False, default=False) # Alert conditions and triggers trigger_condition = Column(String(200), nullable=True) threshold_value = Column(Numeric(15, 4), nullable=True) actual_value = Column(Numeric(15, 4), nullable=True) # Context data alert_data = Column(JSONB, nullable=True) # Additional context-specific data environmental_factors = Column(JSONB, nullable=True) # Temperature, humidity, etc. affected_products = Column(JSONB, nullable=True) # List of affected product IDs # Risk assessment public_health_risk = Column(Boolean, nullable=False, default=False) business_impact = Column(Text, nullable=True) estimated_loss = Column(Numeric(12, 2), nullable=True) # Alert status and lifecycle status = Column(String(50), nullable=False, default="active", index=True) # Status values: active, acknowledged, investigating, resolved, dismissed, escalated alert_state = Column(String(50), nullable=False, default="new") # new, escalated, recurring # Response and resolution immediate_actions_taken = Column(JSONB, nullable=True) # Actions taken immediately investigation_notes = Column(Text, nullable=True) resolution_action = Column(String(200), nullable=True) resolution_notes = Column(Text, nullable=True) corrective_actions = Column(JSONB, nullable=True) # List of corrective actions preventive_measures = Column(JSONB, nullable=True) # Preventive measures implemented # Timing and escalation first_occurred_at = Column(DateTime(timezone=True), nullable=False, index=True) last_occurred_at = Column(DateTime(timezone=True), nullable=False) acknowledged_at = Column(DateTime(timezone=True), nullable=True) resolved_at = Column(DateTime(timezone=True), nullable=True) escalation_deadline = Column(DateTime(timezone=True), nullable=True) # Occurrence tracking occurrence_count = Column(Integer, nullable=False, default=1) is_recurring = Column(Boolean, nullable=False, default=False) recurrence_pattern = Column(String(100), nullable=True) # Responsibility and assignment assigned_to = Column(UUID(as_uuid=True), nullable=True) assigned_role = Column(String(50), nullable=True) # food_safety_manager, quality_assurance, etc. escalated_to = Column(UUID(as_uuid=True), nullable=True) escalation_level = Column(Integer, nullable=False, default=0) # Notification tracking notification_sent = Column(Boolean, nullable=False, default=False) notification_methods = Column(JSONB, nullable=True) # [email, sms, whatsapp, dashboard] notification_recipients = Column(JSONB, nullable=True) # List of recipients regulatory_notification_required = Column(Boolean, nullable=False, default=False) regulatory_notification_sent = Column(Boolean, nullable=False, default=False) # Documentation and audit trail documentation = Column(JSONB, nullable=True) # Links to documentation, photos, etc. audit_trail = Column(JSONB, nullable=True) # Changes and actions taken external_reference = Column(String(100), nullable=True) # External system reference # Performance tracking detection_time = Column(DateTime(timezone=True), nullable=True) # When issue was detected response_time_minutes = Column(Integer, nullable=True) # Time to acknowledge resolution_time_minutes = Column(Integer, nullable=True) # Time to resolve # Quality and feedback alert_accuracy = Column(Boolean, nullable=True) # Was this a valid alert? false_positive = Column(Boolean, nullable=False, default=False) feedback_notes = Column(Text, nullable=True) # Audit fields created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False) created_by = Column(UUID(as_uuid=True), nullable=True) updated_by = Column(UUID(as_uuid=True), nullable=True) def to_dict(self) -> Dict[str, Any]: """Convert model to dictionary for API responses""" return { 'id': str(self.id), 'tenant_id': str(self.tenant_id), 'alert_code': self.alert_code, 'alert_type': self.alert_type.value if self.alert_type else None, 'severity': self.severity, 'risk_level': self.risk_level, 'source_entity_type': self.source_entity_type, 'source_entity_id': str(self.source_entity_id), 'ingredient_id': str(self.ingredient_id) if self.ingredient_id else None, 'stock_id': str(self.stock_id) if self.stock_id else None, 'title': self.title, 'description': self.description, 'detailed_message': self.detailed_message, 'regulatory_requirement': self.regulatory_requirement, 'compliance_standard': self.compliance_standard.value if self.compliance_standard else None, 'regulatory_action_required': self.regulatory_action_required, 'trigger_condition': self.trigger_condition, 'threshold_value': float(self.threshold_value) if self.threshold_value else None, 'actual_value': float(self.actual_value) if self.actual_value else None, 'alert_data': self.alert_data, 'environmental_factors': self.environmental_factors, 'affected_products': self.affected_products, 'public_health_risk': self.public_health_risk, 'business_impact': self.business_impact, 'estimated_loss': float(self.estimated_loss) if self.estimated_loss else None, 'status': self.status, 'alert_state': self.alert_state, 'immediate_actions_taken': self.immediate_actions_taken, 'investigation_notes': self.investigation_notes, 'resolution_action': self.resolution_action, 'resolution_notes': self.resolution_notes, 'corrective_actions': self.corrective_actions, 'preventive_measures': self.preventive_measures, 'first_occurred_at': self.first_occurred_at.isoformat() if self.first_occurred_at else None, 'last_occurred_at': self.last_occurred_at.isoformat() if self.last_occurred_at else None, 'acknowledged_at': self.acknowledged_at.isoformat() if self.acknowledged_at else None, 'resolved_at': self.resolved_at.isoformat() if self.resolved_at else None, 'escalation_deadline': self.escalation_deadline.isoformat() if self.escalation_deadline else None, 'occurrence_count': self.occurrence_count, 'is_recurring': self.is_recurring, 'recurrence_pattern': self.recurrence_pattern, 'assigned_to': str(self.assigned_to) if self.assigned_to else None, 'assigned_role': self.assigned_role, 'escalated_to': str(self.escalated_to) if self.escalated_to else None, 'escalation_level': self.escalation_level, 'notification_sent': self.notification_sent, 'notification_methods': self.notification_methods, 'notification_recipients': self.notification_recipients, 'regulatory_notification_required': self.regulatory_notification_required, 'regulatory_notification_sent': self.regulatory_notification_sent, 'documentation': self.documentation, 'audit_trail': self.audit_trail, 'external_reference': self.external_reference, 'detection_time': self.detection_time.isoformat() if self.detection_time else None, 'response_time_minutes': self.response_time_minutes, 'resolution_time_minutes': self.resolution_time_minutes, 'alert_accuracy': self.alert_accuracy, 'false_positive': self.false_positive, 'feedback_notes': self.feedback_notes, 'created_at': self.created_at.isoformat() if self.created_at else None, 'updated_at': self.updated_at.isoformat() if self.updated_at else None, 'created_by': str(self.created_by) if self.created_by else None, 'updated_by': str(self.updated_by) if self.updated_by else None, }