Files
bakery-ia/services/inventory/app/models/food_safety.py
2025-08-21 20:28:14 +02:00

369 lines
18 KiB
Python

# ================================================================
# 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,
}