Initial commit - production deployment
This commit is contained in:
369
services/inventory/app/models/food_safety.py
Normal file
369
services/inventory/app/models/food_safety.py
Normal file
@@ -0,0 +1,369 @@
|
||||
# ================================================================
|
||||
# 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,
|
||||
}
|
||||
Reference in New Issue
Block a user