Start integrating the onboarding flow with backend 12

This commit is contained in:
Urtzi Alfaro
2025-09-07 17:25:30 +02:00
parent 9005286ada
commit b73f3b4993
32 changed files with 3172 additions and 3087 deletions

View File

@@ -25,9 +25,9 @@ ENV PYTHONPATH=/app
# Expose port
EXPOSE 8000
# Health check
# 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
CMD python -c "import requests; requests.get('http://localhost:8000/health/', timeout=5)" || exit 1
# Run the application
CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

View File

@@ -79,22 +79,25 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
query = """
WITH stock_analysis AS (
SELECT
i.*,
COALESCE(p.scheduled_quantity, 0) as tomorrow_needed,
COALESCE(s.avg_daily_usage, 0) as avg_daily_usage,
COALESCE(s.lead_time_days, 7) as lead_time_days,
i.id, i.name, i.tenant_id,
COALESCE(SUM(s.current_quantity), 0) as current_stock,
i.low_stock_threshold as minimum_stock,
i.max_stock_level as maximum_stock,
i.reorder_point,
0 as tomorrow_needed,
0 as avg_daily_usage,
7 as lead_time_days,
CASE
WHEN i.current_stock < i.minimum_stock THEN 'critical'
WHEN i.current_stock < i.minimum_stock * 1.2 THEN 'low'
WHEN i.current_stock > i.maximum_stock THEN 'overstock'
WHEN COALESCE(SUM(s.current_quantity), 0) < i.low_stock_threshold THEN 'critical'
WHEN COALESCE(SUM(s.current_quantity), 0) < i.low_stock_threshold * 1.2 THEN 'low'
WHEN i.max_stock_level IS NOT NULL AND COALESCE(SUM(s.current_quantity), 0) > i.max_stock_level THEN 'overstock'
ELSE 'normal'
END as status,
GREATEST(0, i.minimum_stock - i.current_stock) as shortage_amount
FROM inventory_items i
LEFT JOIN production_schedule p ON p.ingredient_id = i.id
AND p.date = CURRENT_DATE + INTERVAL '1 day'
LEFT JOIN supplier_items s ON s.ingredient_id = i.id
WHERE i.tenant_id = $1 AND i.active = true
GREATEST(0, i.low_stock_threshold - COALESCE(SUM(s.current_quantity), 0)) as shortage_amount
FROM ingredients i
LEFT JOIN stock s ON s.ingredient_id = i.id AND s.is_available = true
WHERE i.tenant_id = $1 AND i.is_active = true
GROUP BY i.id, i.name, i.tenant_id, i.low_stock_threshold, i.max_stock_level, i.reorder_point
)
SELECT * FROM stock_analysis WHERE status != 'normal'
ORDER BY
@@ -212,15 +215,16 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
query = """
SELECT
i.id, i.name, i.current_stock, i.tenant_id,
b.id as batch_id, b.expiry_date, b.quantity,
EXTRACT(days FROM (b.expiry_date - CURRENT_DATE)) as days_to_expiry
FROM inventory_items i
JOIN inventory_batches b ON b.ingredient_id = i.id
WHERE b.expiry_date <= CURRENT_DATE + INTERVAL '7 days'
AND b.quantity > 0
AND b.status = 'active'
ORDER BY b.expiry_date ASC
i.id, i.name, i.tenant_id,
s.id as stock_id, s.expiration_date, s.current_quantity,
EXTRACT(days FROM (s.expiration_date - CURRENT_DATE)) as days_to_expiry
FROM ingredients i
JOIN stock s ON s.ingredient_id = i.id
WHERE s.expiration_date <= CURRENT_DATE + INTERVAL '7 days'
AND s.current_quantity > 0
AND s.is_available = true
AND s.expiration_date IS NOT NULL
ORDER BY s.expiration_date ASC
"""
from sqlalchemy import text
@@ -275,8 +279,8 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
{
'id': str(item['id']),
'name': item['name'],
'batch_id': str(item['batch_id']),
'quantity': float(item['quantity']),
'stock_id': str(item['stock_id']),
'quantity': float(item['current_quantity']),
'days_expired': abs(item['days_to_expiry'])
} for item in expired
]
@@ -294,9 +298,9 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
'actions': ['Usar inmediatamente', 'Promoción especial', 'Revisar recetas', 'Documentar'],
'metadata': {
'ingredient_id': str(item['id']),
'batch_id': str(item['batch_id']),
'stock_id': str(item['stock_id']),
'days_to_expiry': item['days_to_expiry'],
'quantity': float(item['quantity'])
'quantity': float(item['current_quantity'])
}
}, item_type='alert')
@@ -312,14 +316,16 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
query = """
SELECT
t.id, t.sensor_id, t.location, t.temperature,
t.max_threshold, t.tenant_id,
EXTRACT(minutes FROM (NOW() - t.first_breach_time)) as breach_duration_minutes
FROM temperature_readings t
WHERE t.temperature > t.max_threshold
AND t.breach_duration_minutes >= 30 -- Only after 30 minutes
AND t.last_alert_sent < NOW() - INTERVAL '15 minutes' -- Avoid spam
ORDER BY t.temperature DESC, t.breach_duration_minutes DESC
t.id, t.equipment_id as sensor_id, t.storage_location as location,
t.temperature_celsius as temperature,
t.target_temperature_max as max_threshold, t.tenant_id,
COALESCE(t.deviation_minutes, 0) as breach_duration_minutes
FROM temperature_logs t
WHERE t.temperature_celsius > COALESCE(t.target_temperature_max, 25)
AND NOT t.is_within_range
AND COALESCE(t.deviation_minutes, 0) >= 30 -- Only after 30 minutes
AND (t.recorded_at < NOW() - INTERVAL '15 minutes' OR t.alert_triggered = false) -- Avoid spam
ORDER BY t.temperature_celsius DESC, t.deviation_minutes DESC
"""
from sqlalchemy import text
@@ -371,11 +377,14 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
}
}, item_type='alert')
# Update last alert sent time to avoid spam
await self.db_manager.execute(
"UPDATE temperature_readings SET last_alert_sent = NOW() WHERE id = $1",
breach['id']
)
# Update alert triggered flag to avoid spam
from sqlalchemy import text
async with self.db_manager.get_session() as session:
await session.execute(
text("UPDATE temperature_logs SET alert_triggered = true WHERE id = :id"),
{"id": breach['id']}
)
await session.commit()
except Exception as e:
logger.error("Error processing temperature breach",
@@ -391,24 +400,27 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
query = """
WITH usage_analysis AS (
SELECT
i.id, i.name, i.tenant_id, i.minimum_stock, i.maximum_stock,
i.current_stock,
AVG(sm.quantity) FILTER (WHERE sm.movement_type = 'out'
i.id, i.name, i.tenant_id,
i.low_stock_threshold as minimum_stock,
i.max_stock_level as maximum_stock,
COALESCE(SUM(s.current_quantity), 0) as current_stock,
AVG(sm.quantity) FILTER (WHERE sm.movement_type = 'production_use'
AND sm.created_at > CURRENT_DATE - INTERVAL '30 days') as avg_daily_usage,
COUNT(sm.id) FILTER (WHERE sm.movement_type = 'out'
COUNT(sm.id) FILTER (WHERE sm.movement_type = 'production_use'
AND sm.created_at > CURRENT_DATE - INTERVAL '30 days') as usage_days,
MAX(sm.created_at) FILTER (WHERE sm.movement_type = 'out') as last_used
FROM inventory_items i
MAX(sm.created_at) FILTER (WHERE sm.movement_type = 'production_use') as last_used
FROM ingredients i
LEFT JOIN stock s ON s.ingredient_id = i.id AND s.is_available = true
LEFT JOIN stock_movements sm ON sm.ingredient_id = i.id
WHERE i.active = true AND i.tenant_id = $1
GROUP BY i.id
HAVING COUNT(sm.id) FILTER (WHERE sm.movement_type = 'out'
AND sm.created_at > CURRENT_DATE - INTERVAL '30 days') >= 5
WHERE i.is_active = true AND i.tenant_id = $1
GROUP BY i.id, i.name, i.tenant_id, i.low_stock_threshold, i.max_stock_level
HAVING COUNT(sm.id) FILTER (WHERE sm.movement_type = 'production_use'
AND sm.created_at > CURRENT_DATE - INTERVAL '30 days') >= 3
),
recommendations AS (
SELECT *,
CASE
WHEN avg_daily_usage * 7 > maximum_stock THEN 'increase_max'
WHEN avg_daily_usage * 7 > maximum_stock AND maximum_stock IS NOT NULL THEN 'increase_max'
WHEN avg_daily_usage * 3 < minimum_stock THEN 'decrease_min'
WHEN current_stock / NULLIF(avg_daily_usage, 0) > 14 THEN 'reduce_stock'
WHEN avg_daily_usage > 0 AND minimum_stock / avg_daily_usage < 3 THEN 'increase_min'
@@ -500,20 +512,21 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
async def generate_waste_reduction_recommendations(self):
"""Generate waste reduction recommendations"""
try:
# Analyze waste patterns
# Analyze waste patterns from stock movements
query = """
SELECT
i.id, i.name, i.tenant_id,
SUM(w.quantity) as total_waste_30d,
COUNT(w.id) as waste_incidents,
AVG(w.quantity) as avg_waste_per_incident,
w.waste_reason
FROM inventory_items i
JOIN waste_logs w ON w.ingredient_id = i.id
WHERE w.created_at > CURRENT_DATE - INTERVAL '30 days'
SUM(sm.quantity) as total_waste_30d,
COUNT(sm.id) as waste_incidents,
AVG(sm.quantity) as avg_waste_per_incident,
COALESCE(sm.reason_code, 'unknown') as waste_reason
FROM ingredients i
JOIN stock_movements sm ON sm.ingredient_id = i.id
WHERE sm.movement_type = 'waste'
AND sm.created_at > CURRENT_DATE - INTERVAL '30 days'
AND i.tenant_id = $1
GROUP BY i.id, w.waste_reason
HAVING SUM(w.quantity) > 5 -- More than 5kg wasted
GROUP BY i.id, i.name, i.tenant_id, sm.reason_code
HAVING SUM(sm.quantity) > 5 -- More than 5kg wasted
ORDER BY total_waste_30d DESC
"""
@@ -694,10 +707,14 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
"""Get stock information after hypothetical order"""
try:
query = """
SELECT id, name, current_stock, minimum_stock,
(current_stock - $2) as remaining
FROM inventory_items
WHERE id = $1
SELECT i.id, i.name,
COALESCE(SUM(s.current_quantity), 0) as current_stock,
i.low_stock_threshold as minimum_stock,
(COALESCE(SUM(s.current_quantity), 0) - $2) as remaining
FROM ingredients i
LEFT JOIN stock s ON s.ingredient_id = i.id AND s.is_available = true
WHERE i.id = $1
GROUP BY i.id, i.name, i.low_stock_threshold
"""
result = await self.db_manager.fetchrow(query, ingredient_id, order_quantity)