369 lines
18 KiB
Python
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,
|
|
} |