Fix DB issue 2s

This commit is contained in:
Urtzi Alfaro
2025-09-30 21:58:10 +02:00
parent 147893015e
commit 7cc4b957a5
77 changed files with 4385 additions and 1211 deletions

View File

@@ -1,4 +1,10 @@
# services/inventory/Dockerfile
# Inventory Dockerfile
# Add this stage at the top of each service Dockerfile
FROM python:3.11-slim AS shared
WORKDIR /shared
COPY shared/ /shared/
# Then your main service stage
FROM python:3.11-slim
WORKDIR /app
@@ -7,34 +13,34 @@ WORKDIR /app
RUN apt-get update && apt-get install -y \
gcc \
g++ \
curl \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements and install Python dependencies
# Copy requirements
COPY services/inventory/requirements.txt .
# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy shared modules first
COPY shared/ /app/shared/
# Copy shared libraries from the shared stage
COPY --from=shared /shared /app/shared
# Copy application code
COPY services/inventory/app/ /app/app/
# Copy migrations and alembic config
COPY services/inventory/migrations/ /app/migrations/
COPY services/inventory/alembic.ini /app/alembic.ini
COPY services/inventory/ .
# Copy scripts directory
COPY scripts/ /app/scripts/
# Set Python path to include shared modules
ENV PYTHONPATH=/app
# Add shared libraries to Python path
ENV PYTHONPATH="/app:/app/shared:${PYTHONPATH:-}"
# Expose port
EXPOSE 8000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD python -c "import requests; requests.get('http://localhost:8000/health', timeout=5)" || exit 1
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
# Run the application
CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
# Run application
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

View File

@@ -22,7 +22,7 @@ from app.api.food_safety import router as food_safety_router
class InventoryService(StandardFastAPIService):
"""Inventory Service with standardized setup"""
expected_migration_version = "001_initial_inventory"
expected_migration_version = "00001"
async def on_startup(self, app):
"""Custom startup logic including migration verification"""

View File

@@ -219,7 +219,7 @@ class BusinessModelInsights(BaseModel):
production_scale_indicator: str
# Recommendations
model_specific_recommendations: List[str]
business_model_specific_recommendations: List[str]
optimization_opportunities: List[str]
class Config:
@@ -247,4 +247,4 @@ class AlertsFilter(BaseModel):
date_from: Optional[datetime] = None
date_to: Optional[datetime] = None
assigned_to: Optional[UUID] = None
unresolved_only: bool = True
unresolved_only: bool = True

View File

@@ -181,7 +181,7 @@ class DashboardService:
seasonal_variation=operational_patterns["seasonal_variation"],
bulk_purchasing_indicator=operational_patterns["bulk_indicator"],
production_scale_indicator=operational_patterns["scale_indicator"],
model_specific_recommendations=recommendations["specific"],
business_model_specific_recommendations=recommendations["specific"],
optimization_opportunities=recommendations["optimization"]
)
@@ -1141,4 +1141,4 @@ class DashboardService:
"movement_velocity": [],
"delivery_reliability": Decimal("0"),
"quality_consistency": Decimal("0")
}
}

View File

@@ -0,0 +1,492 @@
"""Initial schema for inventory service
Revision ID: 0000001
Revises:
Create Date: 2025-09-30 18:00:00.0000
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '0001'
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
# Create ingredients table
op.create_table('ingredients',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('tenant_id', sa.UUID(), nullable=False),
sa.Column('name', sa.String(255), nullable=False),
sa.Column('sku', sa.String(100), nullable=True),
sa.Column('barcode', sa.String(50), nullable=True),
sa.Column('product_type', sa.Enum('INGREDIENT', 'FINISHED_PRODUCT', name='producttype'), nullable=False),
sa.Column('ingredient_category', sa.Enum('FLOUR', 'YEAST', 'DAIRY', 'EGGS', 'SUGAR', 'FATS', 'SALT', 'SPICES', 'ADDITIVES', 'PACKAGING', 'CLEANING', 'OTHER', name='ingredientcategory'), nullable=True),
sa.Column('product_category', sa.Enum('BREAD', 'CROISSANTS', 'PASTRIES', 'CAKES', 'COOKIES', 'MUFFINS', 'SANDWICHES', 'SEASONAL', 'BEVERAGES', 'OTHER_PRODUCTS', name='productcategory'), nullable=True),
sa.Column('subcategory', sa.String(100), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('brand', sa.String(100), nullable=True),
sa.Column('unit_of_measure', sa.Enum('KILOGRAMS', 'GRAMS', 'LITERS', 'MILLILITERS', 'UNITS', 'PIECES', 'PACKAGES', 'BAGS', 'BOXES', name='unitofmeasure'), nullable=False),
sa.Column('package_size', sa.Float(), nullable=True),
sa.Column('average_cost', sa.Numeric(10, 2), nullable=True),
sa.Column('last_purchase_price', sa.Numeric(10, 2), nullable=True),
sa.Column('standard_cost', sa.Numeric(10, 2), nullable=True),
sa.Column('low_stock_threshold', sa.Float(), nullable=False),
sa.Column('reorder_point', sa.Float(), nullable=False),
sa.Column('reorder_quantity', sa.Float(), nullable=False),
sa.Column('max_stock_level', sa.Float(), nullable=True),
sa.Column('shelf_life_days', sa.Integer(), nullable=True),
sa.Column('display_life_hours', sa.Integer(), nullable=True),
sa.Column('best_before_hours', sa.Integer(), nullable=True),
sa.Column('storage_instructions', sa.Text(), nullable=True),
sa.Column('central_baker_product_code', sa.String(100), nullable=True),
sa.Column('delivery_days', sa.String(20), nullable=True),
sa.Column('minimum_order_quantity', sa.Float(), nullable=True),
sa.Column('pack_size', sa.Integer(), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=True),
sa.Column('is_perishable', sa.Boolean(), nullable=True),
sa.Column('allergen_info', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('nutritional_info', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('timezone(\'utc\', now())'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('timezone(\'utc\', now())'), nullable=True),
sa.Column('created_by', sa.UUID(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_ingredients_tenant_id'), 'ingredients', ['tenant_id'], unique=False)
op.create_index(op.f('ix_ingredients_name'), 'ingredients', ['name'], unique=False)
op.create_index(op.f('ix_ingredients_sku'), 'ingredients', ['sku'], unique=False)
op.create_index(op.f('ix_ingredients_barcode'), 'ingredients', ['barcode'], unique=False)
op.create_index(op.f('ix_ingredients_product_type'), 'ingredients', ['product_type'], unique=False)
op.create_index(op.f('ix_ingredients_ingredient_category'), 'ingredients', ['ingredient_category'], unique=False)
op.create_index(op.f('ix_ingredients_product_category'), 'ingredients', ['product_category'], unique=False)
op.create_index('idx_ingredients_tenant_name', 'ingredients', ['tenant_id', 'name'], unique=True)
op.create_index('idx_ingredients_tenant_sku', 'ingredients', ['tenant_id', 'sku'], unique=False)
op.create_index('idx_ingredients_barcode', 'ingredients', ['barcode'], unique=False)
op.create_index('idx_ingredients_product_type', 'ingredients', ['tenant_id', 'product_type'], unique=False)
op.create_index('idx_ingredients_ingredient_category', 'ingredients', ['tenant_id', 'ingredient_category'], unique=False)
op.create_index('idx_ingredients_product_category', 'ingredients', ['tenant_id', 'product_category'], unique=False)
op.create_index('idx_ingredients_stock_levels', 'ingredients', ['tenant_id', 'low_stock_threshold', 'reorder_point'], unique=False)
# Create stock table
op.create_table('stock',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('tenant_id', sa.UUID(), nullable=False),
sa.Column('ingredient_id', sa.UUID(), nullable=False),
sa.Column('supplier_id', sa.UUID(), nullable=True),
sa.Column('batch_number', sa.String(100), nullable=True),
sa.Column('lot_number', sa.String(100), nullable=True),
sa.Column('supplier_batch_ref', sa.String(100), nullable=True),
sa.Column('production_stage', sa.Enum('raw_ingredient', 'par_baked', 'fully_baked', 'prepared_dough', 'frozen_product', name='productionstage'), nullable=False),
sa.Column('transformation_reference', sa.String(100), nullable=True),
sa.Column('current_quantity', sa.Float(), nullable=False),
sa.Column('reserved_quantity', sa.Float(), nullable=False),
sa.Column('available_quantity', sa.Float(), nullable=False),
sa.Column('received_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('expiration_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('best_before_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('original_expiration_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('transformation_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('final_expiration_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('unit_cost', sa.Numeric(10, 2), nullable=True),
sa.Column('total_cost', sa.Numeric(10, 2), nullable=True),
sa.Column('storage_location', sa.String(100), nullable=True),
sa.Column('warehouse_zone', sa.String(50), nullable=True),
sa.Column('shelf_position', sa.String(50), nullable=True),
sa.Column('requires_refrigeration', sa.Boolean(), nullable=True),
sa.Column('requires_freezing', sa.Boolean(), nullable=True),
sa.Column('storage_temperature_min', sa.Float(), nullable=True),
sa.Column('storage_temperature_max', sa.Float(), nullable=True),
sa.Column('storage_humidity_max', sa.Float(), nullable=True),
sa.Column('shelf_life_days', sa.Integer(), nullable=True),
sa.Column('storage_instructions', sa.Text(), nullable=True),
sa.Column('is_available', sa.Boolean(), nullable=True),
sa.Column('is_expired', sa.Boolean(), nullable=True),
sa.Column('quality_status', sa.String(20), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('timezone(\'utc\', now())'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('timezone(\'utc\', now())'), nullable=True),
sa.ForeignKeyConstraint(['ingredient_id'], ['ingredients.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_stock_tenant_id'), 'stock', ['tenant_id'], unique=False)
op.create_index(op.f('ix_stock_ingredient_id'), 'stock', ['ingredient_id'], unique=False)
op.create_index(op.f('ix_stock_supplier_id'), 'stock', ['supplier_id'], unique=False)
op.create_index(op.f('ix_stock_batch_number'), 'stock', ['batch_number'], unique=False)
op.create_index(op.f('ix_stock_lot_number'), 'stock', ['lot_number'], unique=False)
op.create_index(op.f('ix_stock_transformation_reference'), 'stock', ['transformation_reference'], unique=False)
op.create_index(op.f('ix_stock_expiration_date'), 'stock', ['expiration_date'], unique=False)
op.create_index(op.f('ix_stock_is_expired'), 'stock', ['is_expired'], unique=False)
op.create_index('idx_stock_tenant_ingredient', 'stock', ['tenant_id', 'ingredient_id'], unique=False)
op.create_index('idx_stock_expiration', 'stock', ['tenant_id', 'expiration_date', 'is_available'], unique=False)
op.create_index('idx_stock_batch', 'stock', ['tenant_id', 'batch_number'], unique=False)
op.create_index('idx_stock_low_levels', 'stock', ['tenant_id', 'current_quantity', 'is_available'], unique=False)
op.create_index('idx_stock_quality', 'stock', ['tenant_id', 'quality_status', 'is_available'], unique=False)
op.create_index('idx_stock_production_stage', 'stock', ['tenant_id', 'production_stage', 'is_available'], unique=False)
op.create_index('idx_stock_transformation', 'stock', ['tenant_id', 'transformation_reference'], unique=False)
op.create_index('idx_stock_final_expiration', 'stock', ['tenant_id', 'final_expiration_date', 'is_available'], unique=False)
# Create stock_movements table
op.create_table('stock_movements',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('tenant_id', sa.UUID(), nullable=False),
sa.Column('ingredient_id', sa.UUID(), nullable=False),
sa.Column('stock_id', sa.UUID(), nullable=True),
sa.Column('movement_type', sa.Enum('PURCHASE', 'PRODUCTION_USE', 'ADJUSTMENT', 'WASTE', 'TRANSFER', 'RETURN', 'INITIAL_STOCK', name='stockmovementtype'), nullable=False),
sa.Column('quantity', sa.Float(), nullable=False),
sa.Column('unit_cost', sa.Numeric(10, 2), nullable=True),
sa.Column('total_cost', sa.Numeric(10, 2), nullable=True),
sa.Column('quantity_before', sa.Float(), nullable=True),
sa.Column('quantity_after', sa.Float(), nullable=True),
sa.Column('reference_number', sa.String(100), nullable=True),
sa.Column('supplier_id', sa.UUID(), nullable=True),
sa.Column('notes', sa.Text(), nullable=True),
sa.Column('reason_code', sa.String(50), nullable=True),
sa.Column('movement_date', sa.DateTime(timezone=True), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('timezone(\'utc\', now())'), nullable=True),
sa.Column('created_by', sa.UUID(), nullable=True),
sa.ForeignKeyConstraint(['ingredient_id'], ['ingredients.id'], ),
sa.ForeignKeyConstraint(['stock_id'], ['stock.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_stock_movements_tenant_id'), 'stock_movements', ['tenant_id'], unique=False)
op.create_index(op.f('ix_stock_movements_ingredient_id'), 'stock_movements', ['ingredient_id'], unique=False)
op.create_index(op.f('ix_stock_movements_stock_id'), 'stock_movements', ['stock_id'], unique=False)
op.create_index(op.f('ix_stock_movements_movement_type'), 'stock_movements', ['movement_type'], unique=False)
op.create_index(op.f('ix_stock_movements_reference_number'), 'stock_movements', ['reference_number'], unique=False)
op.create_index(op.f('ix_stock_movements_supplier_id'), 'stock_movements', ['supplier_id'], unique=False)
op.create_index(op.f('ix_stock_movements_movement_date'), 'stock_movements', ['movement_date'], unique=False)
op.create_index('idx_movements_tenant_date', 'stock_movements', ['tenant_id', 'movement_date'], unique=False)
op.create_index('idx_movements_tenant_ingredient', 'stock_movements', ['tenant_id', 'ingredient_id', 'movement_date'], unique=False)
op.create_index('idx_movements_type', 'stock_movements', ['tenant_id', 'movement_type', 'movement_date'], unique=False)
op.create_index('idx_movements_reference', 'stock_movements', ['reference_number'], unique=False)
op.create_index('idx_movements_supplier', 'stock_movements', ['supplier_id', 'movement_date'], unique=False)
# Create product_transformations table
op.create_table('product_transformations',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('tenant_id', sa.UUID(), nullable=False),
sa.Column('transformation_reference', sa.String(100), nullable=False),
sa.Column('source_ingredient_id', sa.UUID(), nullable=False),
sa.Column('target_ingredient_id', sa.UUID(), nullable=False),
sa.Column('source_stage', sa.Enum('raw_ingredient', 'par_baked', 'fully_baked', 'prepared_dough', 'frozen_product', name='productionstage'), nullable=False),
sa.Column('target_stage', sa.Enum('raw_ingredient', 'par_baked', 'fully_baked', 'prepared_dough', 'frozen_product', name='productionstage'), nullable=False),
sa.Column('source_quantity', sa.Float(), nullable=False),
sa.Column('target_quantity', sa.Float(), nullable=False),
sa.Column('conversion_ratio', sa.Float(), nullable=False),
sa.Column('expiration_calculation_method', sa.String(50), nullable=False),
sa.Column('expiration_days_offset', sa.Integer(), nullable=True),
sa.Column('transformation_date', sa.DateTime(timezone=True), nullable=False),
sa.Column('process_notes', sa.Text(), nullable=True),
sa.Column('performed_by', sa.UUID(), nullable=True),
sa.Column('source_batch_numbers', sa.Text(), nullable=True),
sa.Column('target_batch_number', sa.String(100), nullable=True),
sa.Column('is_completed', sa.Boolean(), nullable=True),
sa.Column('is_reversed', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('timezone(\'utc\', now())'), nullable=True),
sa.Column('created_by', sa.UUID(), nullable=True),
sa.ForeignKeyConstraint(['source_ingredient_id'], ['ingredients.id'], ),
sa.ForeignKeyConstraint(['target_ingredient_id'], ['ingredients.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_product_transformations_tenant_id'), 'product_transformations', ['tenant_id'], unique=False)
op.create_index(op.f('ix_product_transformations_transformation_reference'), 'product_transformations', ['transformation_reference'], unique=False)
op.create_index(op.f('ix_product_transformations_source_ingredient_id'), 'product_transformations', ['source_ingredient_id'], unique=False)
op.create_index(op.f('ix_product_transformations_target_ingredient_id'), 'product_transformations', ['target_ingredient_id'], unique=False)
op.create_index('idx_transformations_tenant_date', 'product_transformations', ['tenant_id', 'transformation_date'], unique=False)
op.create_index('idx_transformations_reference', 'product_transformations', ['transformation_reference'], unique=False)
op.create_index('idx_transformations_source', 'product_transformations', ['tenant_id', 'source_ingredient_id'], unique=False)
op.create_index('idx_transformations_target', 'product_transformations', ['tenant_id', 'target_ingredient_id'], unique=False)
op.create_index('idx_transformations_stages', 'product_transformations', ['source_stage', 'target_stage'], unique=False)
# Create stock_alerts table
op.create_table('stock_alerts',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('tenant_id', sa.UUID(), nullable=False),
sa.Column('ingredient_id', sa.UUID(), nullable=False),
sa.Column('stock_id', sa.UUID(), nullable=True),
sa.Column('alert_type', sa.String(50), nullable=False),
sa.Column('severity', sa.String(20), nullable=False),
sa.Column('title', sa.String(255), nullable=False),
sa.Column('message', sa.Text(), nullable=False),
sa.Column('current_quantity', sa.Float(), nullable=True),
sa.Column('threshold_value', sa.Float(), nullable=True),
sa.Column('expiration_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=True),
sa.Column('is_acknowledged', sa.Boolean(), nullable=True),
sa.Column('acknowledged_by', sa.UUID(), nullable=True),
sa.Column('acknowledged_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('is_resolved', sa.Boolean(), nullable=True),
sa.Column('resolved_by', sa.UUID(), nullable=True),
sa.Column('resolved_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('resolution_notes', sa.Text(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('timezone(\'utc\', now())'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('timezone(\'utc\', now())'), nullable=True),
sa.ForeignKeyConstraint(['ingredient_id'], ['ingredients.id'], ),
sa.ForeignKeyConstraint(['stock_id'], ['stock.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_stock_alerts_tenant_id'), 'stock_alerts', ['tenant_id'], unique=False)
op.create_index(op.f('ix_stock_alerts_ingredient_id'), 'stock_alerts', ['ingredient_id'], unique=False)
op.create_index(op.f('ix_stock_alerts_stock_id'), 'stock_alerts', ['stock_id'], unique=False)
op.create_index(op.f('ix_stock_alerts_alert_type'), 'stock_alerts', ['alert_type'], unique=False)
op.create_index(op.f('ix_stock_alerts_is_resolved'), 'stock_alerts', ['is_resolved'], unique=False)
op.create_index(op.f('ix_stock_alerts_is_active'), 'stock_alerts', ['is_active'], unique=False)
op.create_index('idx_alerts_tenant_active', 'stock_alerts', ['tenant_id', 'is_active', 'created_at'], unique=False)
op.create_index('idx_alerts_type_severity', 'stock_alerts', ['alert_type', 'severity', 'is_active'], unique=False)
op.create_index('idx_alerts_ingredient', 'stock_alerts', ['ingredient_id', 'is_active'], unique=False)
op.create_index('idx_alerts_unresolved', 'stock_alerts', ['tenant_id', 'is_resolved', 'is_active'], unique=False)
# Create food_safety_compliance table
op.create_table('food_safety_compliance',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('tenant_id', sa.UUID(), nullable=False),
sa.Column('ingredient_id', sa.UUID(), nullable=False),
sa.Column('standard', sa.Enum('HACCP', 'FDA', 'USDA', 'FSMA', 'SQF', 'BRC', 'IFS', 'ISO22000', 'ORGANIC', 'NON_GMO', 'ALLERGEN_FREE', 'KOSHER', 'HALAL', name='foodsafetystandard'), nullable=False),
sa.Column('compliance_status', sa.Enum('COMPLIANT', 'NON_COMPLIANT', 'PENDING_REVIEW', 'EXPIRED', 'WARNING', name='compliancestatus'), nullable=False),
sa.Column('certification_number', sa.String(100), nullable=True),
sa.Column('certifying_body', sa.String(200), nullable=True),
sa.Column('certification_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('expiration_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('requirements', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('compliance_notes', sa.Text(), nullable=True),
sa.Column('documentation_url', sa.String(500), nullable=True),
sa.Column('last_audit_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('next_audit_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('auditor_name', sa.String(200), nullable=True),
sa.Column('audit_score', sa.Float(), nullable=True),
sa.Column('risk_level', sa.String(20), nullable=False),
sa.Column('risk_factors', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('mitigation_measures', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=False),
sa.Column('requires_monitoring', sa.Boolean(), nullable=False),
sa.Column('monitoring_frequency_days', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('timezone(\'utc\', now())'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('timezone(\'utc\', now())'), nullable=True),
sa.Column('created_by', sa.UUID(), nullable=True),
sa.Column('updated_by', sa.UUID(), nullable=True),
sa.ForeignKeyConstraint(['ingredient_id'], ['ingredients.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_food_safety_compliance_tenant_id'), 'food_safety_compliance', ['tenant_id'], unique=False)
op.create_index(op.f('ix_food_safety_compliance_ingredient_id'), 'food_safety_compliance', ['ingredient_id'], unique=False)
op.create_index(op.f('ix_food_safety_compliance_standard'), 'food_safety_compliance', ['standard'], unique=False)
op.create_index(op.f('ix_food_safety_compliance_expiration_date'), 'food_safety_compliance', ['expiration_date'], unique=False)
op.create_index(op.f('ix_food_safety_compliance_next_audit_date'), 'food_safety_compliance', ['next_audit_date'], unique=False)
# Create temperature_logs table
op.create_table('temperature_logs',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('tenant_id', sa.UUID(), nullable=False),
sa.Column('storage_location', sa.String(100), nullable=False),
sa.Column('warehouse_zone', sa.String(50), nullable=True),
sa.Column('equipment_id', sa.String(100), nullable=True),
sa.Column('temperature_celsius', sa.Float(), nullable=False),
sa.Column('humidity_percentage', sa.Float(), nullable=True),
sa.Column('target_temperature_min', sa.Float(), nullable=True),
sa.Column('target_temperature_max', sa.Float(), nullable=True),
sa.Column('is_within_range', sa.Boolean(), nullable=False),
sa.Column('alert_triggered', sa.Boolean(), nullable=False),
sa.Column('deviation_minutes', sa.Integer(), nullable=True),
sa.Column('measurement_method', sa.String(50), nullable=False),
sa.Column('device_id', sa.String(100), nullable=True),
sa.Column('calibration_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('recorded_at', sa.DateTime(timezone=True), server_default=sa.text('timezone(\'utc\', now())'), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('timezone(\'utc\', now())'), nullable=False),
sa.Column('recorded_by', sa.UUID(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_temperature_logs_tenant_id'), 'temperature_logs', ['tenant_id'], unique=False)
op.create_index(op.f('ix_temperature_logs_storage_location'), 'temperature_logs', ['storage_location'], unique=False)
op.create_index(op.f('ix_temperature_logs_equipment_id'), 'temperature_logs', ['equipment_id'], unique=False)
op.create_index(op.f('ix_temperature_logs_recorded_at'), 'temperature_logs', ['recorded_at'], unique=False)
# Create food_safety_alerts table
op.create_table('food_safety_alerts',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('tenant_id', sa.UUID(), nullable=False),
sa.Column('alert_code', sa.String(50), nullable=False),
sa.Column('alert_type', sa.Enum('TEMPERATURE_VIOLATION', 'EXPIRATION_WARNING', 'EXPIRED_PRODUCT', 'CONTAMINATION_RISK', 'ALLERGEN_CROSS_CONTAMINATION', 'STORAGE_VIOLATION', 'QUALITY_DEGRADATION', 'RECALL_NOTICE', 'CERTIFICATION_EXPIRY', 'SUPPLIER_COMPLIANCE_ISSUE', name='foodsafetyalerttype'), nullable=False),
sa.Column('severity', sa.String(20), nullable=False),
sa.Column('risk_level', sa.String(20), nullable=False),
sa.Column('source_entity_type', sa.String(50), nullable=False),
sa.Column('source_entity_id', sa.UUID(), nullable=False),
sa.Column('ingredient_id', sa.UUID(), nullable=True),
sa.Column('stock_id', sa.UUID(), nullable=True),
sa.Column('title', sa.String(200), nullable=False),
sa.Column('description', sa.Text(), nullable=False),
sa.Column('detailed_message', sa.Text(), nullable=True),
sa.Column('regulatory_requirement', sa.String(100), nullable=True),
sa.Column('compliance_standard', sa.Enum('HACCP', 'FDA', 'USDA', 'FSMA', 'SQF', 'BRC', 'IFS', 'ISO22000', 'ORGANIC', 'NON_GMO', 'ALLERGEN_FREE', 'KOSHER', 'HALAL', name='foodsafetystandard'), nullable=True),
sa.Column('regulatory_action_required', sa.Boolean(), nullable=False),
sa.Column('trigger_condition', sa.String(200), nullable=True),
sa.Column('threshold_value', sa.Numeric(15, 4), nullable=True),
sa.Column('actual_value', sa.Numeric(15, 4), nullable=True),
sa.Column('alert_data', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('environmental_factors', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('affected_products', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('public_health_risk', sa.Boolean(), nullable=False),
sa.Column('business_impact', sa.Text(), nullable=True),
sa.Column('estimated_loss', sa.Numeric(12, 2), nullable=True),
sa.Column('status', sa.String(50), nullable=False),
sa.Column('alert_state', sa.String(50), nullable=False),
sa.Column('immediate_actions_taken', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('investigation_notes', sa.Text(), nullable=True),
sa.Column('resolution_action', sa.String(200), nullable=True),
sa.Column('resolution_notes', sa.Text(), nullable=True),
sa.Column('corrective_actions', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('preventive_measures', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('first_occurred_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('last_occurred_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('acknowledged_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('resolved_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('escalation_deadline', sa.DateTime(timezone=True), nullable=True),
sa.Column('occurrence_count', sa.Integer(), nullable=False),
sa.Column('is_recurring', sa.Boolean(), nullable=False),
sa.Column('recurrence_pattern', sa.String(100), nullable=True),
sa.Column('assigned_to', sa.UUID(), nullable=True),
sa.Column('assigned_role', sa.String(50), nullable=True),
sa.Column('escalated_to', sa.UUID(), nullable=True),
sa.Column('escalation_level', sa.Integer(), nullable=False),
sa.Column('notification_sent', sa.Boolean(), nullable=False),
sa.Column('notification_methods', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('notification_recipients', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('regulatory_notification_required', sa.Boolean(), nullable=False),
sa.Column('regulatory_notification_sent', sa.Boolean(), nullable=False),
sa.Column('documentation', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('audit_trail', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('external_reference', sa.String(100), nullable=True),
sa.Column('detection_time', sa.DateTime(timezone=True), nullable=True),
sa.Column('response_time_minutes', sa.Integer(), nullable=True),
sa.Column('resolution_time_minutes', sa.Integer(), nullable=True),
sa.Column('alert_accuracy', sa.Boolean(), nullable=True),
sa.Column('false_positive', sa.Boolean(), nullable=False),
sa.Column('feedback_notes', sa.Text(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('timezone(\'utc\', now())'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('timezone(\'utc\', now())'), nullable=False),
sa.Column('created_by', sa.UUID(), nullable=True),
sa.Column('updated_by', sa.UUID(), nullable=True),
sa.ForeignKeyConstraint(['ingredient_id'], ['ingredients.id'], ),
sa.ForeignKeyConstraint(['stock_id'], ['stock.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_food_safety_alerts_tenant_id'), 'food_safety_alerts', ['tenant_id'], unique=False)
op.create_index(op.f('ix_food_safety_alerts_alert_code'), 'food_safety_alerts', ['alert_code'], unique=False)
op.create_index(op.f('ix_food_safety_alerts_alert_type'), 'food_safety_alerts', ['alert_type'], unique=False)
op.create_index(op.f('ix_food_safety_alerts_severity'), 'food_safety_alerts', ['severity'], unique=False)
op.create_index(op.f('ix_food_safety_alerts_source_entity_id'), 'food_safety_alerts', ['source_entity_id'], unique=False)
op.create_index(op.f('ix_food_safety_alerts_ingredient_id'), 'food_safety_alerts', ['ingredient_id'], unique=False)
op.create_index(op.f('ix_food_safety_alerts_stock_id'), 'food_safety_alerts', ['stock_id'], unique=False)
op.create_index(op.f('ix_food_safety_alerts_first_occurred_at'), 'food_safety_alerts', ['first_occurred_at'], unique=False)
op.create_index(op.f('ix_food_safety_alerts_status'), 'food_safety_alerts', ['status'], unique=False)
op.create_index(op.f('ix_food_safety_alerts_assigned_to'), 'food_safety_alerts', ['assigned_to'], unique=False)
op.create_index(op.f('ix_food_safety_alerts_escalated_to'), 'food_safety_alerts', ['escalated_to'], unique=False)
def downgrade() -> None:
# Drop food_safety_alerts table
op.drop_index(op.f('ix_food_safety_alerts_escalated_to'), table_name='food_safety_alerts')
op.drop_index(op.f('ix_food_safety_alerts_assigned_to'), table_name='food_safety_alerts')
op.drop_index(op.f('ix_food_safety_alerts_status'), table_name='food_safety_alerts')
op.drop_index(op.f('ix_food_safety_alerts_first_occurred_at'), table_name='food_safety_alerts')
op.drop_index(op.f('ix_food_safety_alerts_stock_id'), table_name='food_safety_alerts')
op.drop_index(op.f('ix_food_safety_alerts_ingredient_id'), table_name='food_safety_alerts')
op.drop_index(op.f('ix_food_safety_alerts_source_entity_id'), table_name='food_safety_alerts')
op.drop_index(op.f('ix_food_safety_alerts_severity'), table_name='food_safety_alerts')
op.drop_index(op.f('ix_food_safety_alerts_alert_type'), table_name='food_safety_alerts')
op.drop_index(op.f('ix_food_safety_alerts_alert_code'), table_name='food_safety_alerts')
op.drop_index(op.f('ix_food_safety_alerts_tenant_id'), table_name='food_safety_alerts')
op.drop_table('food_safety_alerts')
# Drop temperature_logs table
op.drop_index(op.f('ix_temperature_logs_recorded_at'), table_name='temperature_logs')
op.drop_index(op.f('ix_temperature_logs_equipment_id'), table_name='temperature_logs')
op.drop_index(op.f('ix_temperature_logs_storage_location'), table_name='temperature_logs')
op.drop_index(op.f('ix_temperature_logs_tenant_id'), table_name='temperature_logs')
op.drop_table('temperature_logs')
# Drop food_safety_compliance table
op.drop_index(op.f('ix_food_safety_compliance_next_audit_date'), table_name='food_safety_compliance')
op.drop_index(op.f('ix_food_safety_compliance_expiration_date'), table_name='food_safety_compliance')
op.drop_index(op.f('ix_food_safety_compliance_standard'), table_name='food_safety_compliance')
op.drop_index(op.f('ix_food_safety_compliance_ingredient_id'), table_name='food_safety_compliance')
op.drop_index(op.f('ix_food_safety_compliance_tenant_id'), table_name='food_safety_compliance')
op.drop_table('food_safety_compliance')
# Drop stock_alerts table
op.drop_index('idx_alerts_unresolved', table_name='stock_alerts')
op.drop_index('idx_alerts_ingredient', table_name='stock_alerts')
op.drop_index('idx_alerts_type_severity', table_name='stock_alerts')
op.drop_index('idx_alerts_tenant_active', table_name='stock_alerts')
op.drop_index(op.f('ix_stock_alerts_is_active'), table_name='stock_alerts')
op.drop_index(op.f('ix_stock_alerts_is_resolved'), table_name='stock_alerts')
op.drop_index(op.f('ix_stock_alerts_alert_type'), table_name='stock_alerts')
op.drop_index(op.f('ix_stock_alerts_stock_id'), table_name='stock_alerts')
op.drop_index(op.f('ix_stock_alerts_ingredient_id'), table_name='stock_alerts')
op.drop_index(op.f('ix_stock_alerts_tenant_id'), table_name='stock_alerts')
op.drop_table('stock_alerts')
# Drop product_transformations table
op.drop_index('idx_transformations_stages', table_name='product_transformations')
op.drop_index('idx_transformations_target', table_name='product_transformations')
op.drop_index('idx_transformations_source', table_name='product_transformations')
op.drop_index('idx_transformations_reference', table_name='product_transformations')
op.drop_index('idx_transformations_tenant_date', table_name='product_transformations')
op.drop_index(op.f('ix_product_transformations_target_ingredient_id'), table_name='product_transformations')
op.drop_index(op.f('ix_product_transformations_source_ingredient_id'), table_name='product_transformations')
op.drop_index(op.f('ix_product_transformations_transformation_reference'), table_name='product_transformations')
op.drop_index(op.f('ix_product_transformations_tenant_id'), table_name='product_transformations')
op.drop_table('product_transformations')
# Drop stock_movements table
op.drop_index('idx_movements_supplier', table_name='stock_movements')
op.drop_index('idx_movements_reference', table_name='stock_movements')
op.drop_index('idx_movements_type', table_name='stock_movements')
op.drop_index('idx_movements_tenant_ingredient', table_name='stock_movements')
op.drop_index('idx_movements_tenant_date', table_name='stock_movements')
op.drop_index(op.f('ix_stock_movements_movement_date'), table_name='stock_movements')
op.drop_index(op.f('ix_stock_movements_supplier_id'), table_name='stock_movements')
op.drop_index(op.f('ix_stock_movements_reference_number'), table_name='stock_movements')
op.drop_index(op.f('ix_stock_movements_movement_type'), table_name='stock_movements')
op.drop_index(op.f('ix_stock_movements_stock_id'), table_name='stock_movements')
op.drop_index(op.f('ix_stock_movements_ingredient_id'), table_name='stock_movements')
op.drop_index(op.f('ix_stock_movements_tenant_id'), table_name='stock_movements')
op.drop_table('stock_movements')
# Drop stock table
op.drop_index('idx_stock_final_expiration', table_name='stock')
op.drop_index('idx_stock_transformation', table_name='stock')
op.drop_index('idx_stock_production_stage', table_name='stock')
op.drop_index('idx_stock_quality', table_name='stock')
op.drop_index('idx_stock_low_levels', table_name='stock')
op.drop_index('idx_stock_batch', table_name='stock')
op.drop_index('idx_stock_expiration', table_name='stock')
op.drop_index('idx_stock_tenant_ingredient', table_name='stock')
op.drop_index(op.f('ix_stock_is_expired'), table_name='stock')
op.drop_index(op.f('ix_stock_expiration_date'), table_name='stock')
op.drop_index(op.f('ix_stock_transformation_reference'), table_name='stock')
op.drop_index(op.f('ix_stock_lot_number'), table_name='stock')
op.drop_index(op.f('ix_stock_batch_number'), table_name='stock')
op.drop_index(op.f('ix_stock_supplier_id'), table_name='stock')
op.drop_index(op.f('ix_stock_ingredient_id'), table_name='stock')
op.drop_index(op.f('ix_stock_tenant_id'), table_name='stock')
op.drop_table('stock')
# Drop ingredients table
op.drop_index('idx_ingredients_stock_levels', table_name='ingredients')
op.drop_index('idx_ingredients_product_category', table_name='ingredients')
op.drop_index('idx_ingredients_ingredient_category', table_name='ingredients')
op.drop_index('idx_ingredients_product_type', table_name='ingredients')
op.drop_index('idx_ingredients_barcode', table_name='ingredients')
op.drop_index('idx_ingredients_tenant_sku', table_name='ingredients')
op.drop_index('idx_ingredients_tenant_name', table_name='ingredients')
op.drop_index(op.f('ix_ingredients_product_category'), table_name='ingredients')
op.drop_index(op.f('ix_ingredients_ingredient_category'), table_name='ingredients')
op.drop_index(op.f('ix_ingredients_product_type'), table_name='ingredients')
op.drop_index(op.f('ix_ingredients_barcode'), table_name='ingredients')
op.drop_index(op.f('ix_ingredients_sku'), table_name='ingredients')
op.drop_index(op.f('ix_ingredients_name'), table_name='ingredients')
op.drop_index(op.f('ix_ingredients_tenant_id'), table_name='ingredients')
op.drop_table('ingredients')