Improve the frontend and repository layer

This commit is contained in:
Urtzi Alfaro
2025-10-23 07:44:54 +02:00
parent 8d30172483
commit 07c33fa578
112 changed files with 14726 additions and 2733 deletions

View File

@@ -18,6 +18,7 @@ from shared.alerts.base_service import BaseAlertService, AlertServiceMixin
from shared.alerts.templates import format_item_message
from app.repositories.stock_repository import StockRepository
from app.repositories.stock_movement_repository import StockMovementRepository
from app.repositories.inventory_alert_repository import InventoryAlertRepository
logger = structlog.get_logger()
@@ -90,54 +91,20 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
"""Batch check all stock levels for critical shortages (alerts)"""
try:
self._checks_performed += 1
query = """
WITH stock_analysis AS (
SELECT
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 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.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 = :tenant_id 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
CASE status
WHEN 'critical' THEN 1
WHEN 'low' THEN 2
WHEN 'overstock' THEN 3
END,
shortage_amount DESC
"""
tenants = await self.get_active_tenants()
for tenant_id in tenants:
try:
# Add timeout to prevent hanging connections
# Add timeout to prevent hanging connections
async with asyncio.timeout(30): # 30 second timeout
async with self.db_manager.get_background_session() as session:
result = await session.execute(text(query), {"tenant_id": tenant_id})
issues = result.fetchall()
# Use repository for stock analysis
alert_repo = InventoryAlertRepository(session)
issues = await alert_repo.get_stock_issues(tenant_id)
for issue in issues:
# Convert SQLAlchemy Row to dictionary for easier access
issue_dict = dict(issue._mapping) if hasattr(issue, '_mapping') else dict(issue)
await self._process_stock_issue(tenant_id, issue_dict)
await self._process_stock_issue(tenant_id, issue)
except Exception as e:
logger.error("Error checking stock for tenant",
@@ -230,39 +197,24 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
"""Check for products approaching expiry (alerts)"""
try:
self._checks_performed += 1
query = """
SELECT
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
"""
tenants = await self.get_active_tenants()
# Add timeout to prevent hanging connections
async with asyncio.timeout(30): # 30 second timeout
async with self.db_manager.get_background_session() as session:
result = await session.execute(text(query))
expiring_items = result.fetchall()
# Group by tenant
by_tenant = {}
for item in expiring_items:
# Convert SQLAlchemy Row to dictionary for easier access
item_dict = dict(item._mapping) if hasattr(item, '_mapping') else dict(item)
tenant_id = item_dict['tenant_id']
if tenant_id not in by_tenant:
by_tenant[tenant_id] = []
by_tenant[tenant_id].append(item_dict)
for tenant_id, items in by_tenant.items():
await self._process_expiring_items(tenant_id, items)
alert_repo = InventoryAlertRepository(session)
for tenant_id in tenants:
try:
# Get expiring products for this tenant
items = await alert_repo.get_expiring_products(tenant_id, days_threshold=7)
if items:
await self._process_expiring_items(tenant_id, items)
except Exception as e:
logger.error("Error checking expiring products for tenant",
tenant_id=str(tenant_id),
error=str(e))
except Exception as e:
logger.error("Expiry check failed", error=str(e))
@@ -334,31 +286,23 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
"""Check for temperature breaches (alerts)"""
try:
self._checks_performed += 1
query = """
SELECT
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
"""
tenants = await self.get_active_tenants()
# Add timeout to prevent hanging connections
async with asyncio.timeout(30): # 30 second timeout
async with self.db_manager.get_background_session() as session:
result = await session.execute(text(query))
breaches = result.fetchall()
for breach in breaches:
# Convert SQLAlchemy Row to dictionary for easier access
breach_dict = dict(breach._mapping) if hasattr(breach, '_mapping') else dict(breach)
await self._process_temperature_breach(breach_dict)
alert_repo = InventoryAlertRepository(session)
for tenant_id in tenants:
try:
breaches = await alert_repo.get_temperature_breaches(tenant_id, hours_back=24)
for breach in breaches:
await self._process_temperature_breach(breach)
except Exception as e:
logger.error("Error checking temperature breaches for tenant",
tenant_id=str(tenant_id),
error=str(e))
except Exception as e:
logger.error("Temperature check failed", error=str(e))
@@ -405,10 +349,8 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
# Add timeout to prevent hanging connections
async with asyncio.timeout(10): # 10 second timeout for simple update
async with self.db_manager.get_background_session() as session:
await session.execute(
text("UPDATE temperature_logs SET alert_triggered = true WHERE id = :id"),
{"id": breach['id']}
)
alert_repo = InventoryAlertRepository(session)
await alert_repo.mark_temperature_alert_triggered(breach['id'])
except Exception as e:
logger.error("Error processing temperature breach",
@@ -458,20 +400,17 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
"""
tenants = await self.get_active_tenants()
for tenant_id in tenants:
try:
from sqlalchemy import text
# Add timeout to prevent hanging connections
async with asyncio.timeout(30): # 30 second timeout
async with self.db_manager.get_background_session() as session:
result = await session.execute(text(query), {"tenant_id": tenant_id})
recommendations = result.fetchall()
for rec in recommendations:
# Convert SQLAlchemy Row to dictionary for easier access
rec_dict = dict(rec._mapping) if hasattr(rec, '_mapping') else dict(rec)
await self._generate_stock_recommendation(tenant_id, rec_dict)
# Add timeout to prevent hanging connections
async with asyncio.timeout(30): # 30 second timeout
async with self.db_manager.get_background_session() as session:
alert_repo = InventoryAlertRepository(session)
for tenant_id in tenants:
try:
recommendations = await alert_repo.get_reorder_recommendations(tenant_id)
for rec in recommendations:
await self._generate_stock_recommendation(tenant_id, rec)
except Exception as e:
logger.error("Error generating recommendations for tenant",
@@ -559,20 +498,17 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
"""
tenants = await self.get_active_tenants()
for tenant_id in tenants:
try:
from sqlalchemy import text
# Add timeout to prevent hanging connections
async with asyncio.timeout(30): # 30 second timeout
async with self.db_manager.get_background_session() as session:
result = await session.execute(text(query), {"tenant_id": tenant_id})
waste_data = result.fetchall()
for waste in waste_data:
# Convert SQLAlchemy Row to dictionary for easier access
waste_dict = dict(waste._mapping) if hasattr(waste, '_mapping') else dict(waste)
await self._generate_waste_recommendation(tenant_id, waste_dict)
# Add timeout to prevent hanging connections
async with asyncio.timeout(30): # 30 second timeout
async with self.db_manager.get_background_session() as session:
alert_repo = InventoryAlertRepository(session)
for tenant_id in tenants:
try:
waste_data = await alert_repo.get_waste_opportunities(tenant_id)
for waste in waste_data:
await self._generate_waste_recommendation(tenant_id, waste)
except Exception as e:
logger.error("Error generating waste recommendations",
@@ -738,21 +674,11 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
async def get_active_tenants(self) -> List[UUID]:
"""Get list of active tenant IDs from ingredients table (inventory service specific)"""
try:
query = text("SELECT DISTINCT tenant_id FROM ingredients WHERE is_active = true")
# Add timeout to prevent hanging connections
async with asyncio.timeout(10): # 10 second timeout
async with self.db_manager.get_background_session() as session:
result = await session.execute(query)
# Handle PostgreSQL UUID objects properly
tenant_ids = []
for row in result.fetchall():
tenant_id = row.tenant_id
# Convert to UUID if it's not already
if isinstance(tenant_id, UUID):
tenant_ids.append(tenant_id)
else:
tenant_ids.append(UUID(str(tenant_id)))
return tenant_ids
alert_repo = InventoryAlertRepository(session)
return await alert_repo.get_active_tenant_ids()
except Exception as e:
logger.error("Error fetching active tenants from ingredients", error=str(e))
return []
@@ -760,27 +686,15 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
async def get_stock_after_order(self, ingredient_id: str, order_quantity: float) -> Optional[Dict[str, Any]]:
"""Get stock information after hypothetical order"""
try:
query = """
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) - :order_quantity) as remaining
FROM ingredients i
LEFT JOIN stock s ON s.ingredient_id = i.id AND s.is_available = true
WHERE i.id = :ingredient_id
GROUP BY i.id, i.name, i.low_stock_threshold
"""
# Add timeout to prevent hanging connections
async with asyncio.timeout(10): # 10 second timeout
async with self.db_manager.get_background_session() as session:
result = await session.execute(text(query), {"ingredient_id": ingredient_id, "order_quantity": order_quantity})
row = result.fetchone()
return dict(row) if row else None
alert_repo = InventoryAlertRepository(session)
return await alert_repo.get_stock_after_order(ingredient_id, order_quantity)
except Exception as e:
logger.error("Error getting stock after order",
ingredient_id=ingredient_id,
logger.error("Error getting stock after order",
ingredient_id=ingredient_id,
error=str(e))
return None