demo seed change 7

This commit is contained in:
Urtzi Alfaro
2025-12-15 13:39:33 +01:00
parent 46bd4f77b6
commit 5642b5a0c0
14 changed files with 5653 additions and 780 deletions

View File

@@ -388,6 +388,7 @@ async def clone_demo_data(
quality_score=batch_data.get('quality_score'),
waste_quantity=batch_data.get('waste_quantity'),
defect_quantity=batch_data.get('defect_quantity'),
waste_defect_type=batch_data.get('waste_defect_type'),
equipment_used=batch_data.get('equipment_used'),
staff_assigned=batch_data.get('staff_assigned'),
station_id=batch_data.get('station_id'),
@@ -395,6 +396,7 @@ async def clone_demo_data(
forecast_id=batch_data.get('forecast_id'),
is_rush_order=batch_data.get('is_rush_order', False),
is_special_recipe=batch_data.get('is_special_recipe', False),
is_ai_assisted=batch_data.get('is_ai_assisted', False),
production_notes=batch_data.get('production_notes'),
quality_notes=batch_data.get('quality_notes'),
delay_reason=batch_data.get('delay_reason'),

View File

@@ -0,0 +1,293 @@
"""
Production Service - Sustainability API
Exposes production-specific sustainability metrics following microservices principles
Each service owns its domain data
"""
from datetime import datetime, timedelta
from typing import Optional
from uuid import UUID
from fastapi import APIRouter, Depends, Path, Query, Request
import structlog
from shared.auth.decorators import get_current_user_dep
from app.services.production_service import ProductionService
from shared.routing import RouteBuilder
logger = structlog.get_logger()
# Create route builder for consistent URL structure
route_builder = RouteBuilder('production')
router = APIRouter(tags=["production-sustainability"])
def get_production_service(request: Request) -> ProductionService:
"""Dependency injection for production service"""
from app.core.database import database_manager
from app.core.config import settings
notification_service = getattr(request.app.state, 'notification_service', None)
return ProductionService(database_manager, settings, notification_service)
@router.get(
"/api/v1/tenants/{tenant_id}/production/sustainability/waste-metrics",
response_model=dict,
summary="Get production waste metrics",
description="""
Returns production-specific waste metrics for sustainability tracking.
This endpoint is part of the microservices architecture where each service
owns its domain data. Frontend aggregates data from multiple services.
Metrics include:
- Total production waste from batches (waste_quantity + defect_quantity)
- Production volumes (planned vs actual)
- Waste breakdown by defect type
- AI-assisted batch tracking
"""
)
async def get_production_waste_metrics(
tenant_id: UUID = Path(..., description="Tenant ID"),
start_date: Optional[datetime] = Query(None, description="Start date for metrics (default: 30 days ago)"),
end_date: Optional[datetime] = Query(None, description="End date for metrics (default: now)"),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""
Get production waste metrics for sustainability dashboard
Returns production-specific metrics that frontend will aggregate with
inventory metrics for complete sustainability picture.
"""
try:
# Set default dates
if not end_date:
end_date = datetime.now()
if not start_date:
start_date = end_date - timedelta(days=30)
# Get waste analytics from production service
waste_data = await production_service.get_waste_analytics(
tenant_id=tenant_id,
start_date=start_date,
end_date=end_date
)
# Enrich with metadata
response = {
**waste_data,
"service": "production",
"period": {
"start_date": start_date.isoformat(),
"end_date": end_date.isoformat(),
"days": (end_date - start_date).days
},
"metadata": {
"data_source": "production_batches",
"calculation_method": "SUM(waste_quantity + defect_quantity)",
"filters_applied": {
"status": ["COMPLETED", "QUALITY_CHECK"],
"date_range": f"{start_date.date()} to {end_date.date()}"
}
}
}
logger.info(
"Production waste metrics retrieved",
tenant_id=str(tenant_id),
total_waste_kg=waste_data.get('total_production_waste', 0),
period_days=(end_date - start_date).days,
user_id=current_user.get('user_id')
)
return response
except Exception as e:
logger.error(
"Error getting production waste metrics",
tenant_id=str(tenant_id),
error=str(e)
)
raise
@router.get(
"/api/v1/tenants/{tenant_id}/production/sustainability/baseline",
response_model=dict,
summary="Get production baseline metrics",
description="""
Returns baseline production metrics from the first 90 days of operation.
Used by frontend to calculate SDG 12.3 compliance (waste reduction targets).
If tenant has less than 90 days of data, returns industry average baseline.
"""
)
async def get_production_baseline(
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""
Get baseline production metrics for SDG compliance calculations
Frontend uses this to calculate:
- Waste reduction percentage vs baseline
- Progress toward SDG 12.3 targets
- Grant eligibility based on improvement
"""
try:
baseline_data = await production_service.get_baseline_metrics(tenant_id)
# Add metadata
response = {
**baseline_data,
"service": "production",
"metadata": {
"baseline_period_days": 90,
"calculation_method": "First 90 days of production data",
"fallback": "Industry average (25%) if insufficient data"
}
}
logger.info(
"Production baseline metrics retrieved",
tenant_id=str(tenant_id),
has_baseline=baseline_data.get('has_baseline', False),
baseline_waste_pct=baseline_data.get('waste_percentage'),
user_id=current_user.get('user_id')
)
return response
except Exception as e:
logger.error(
"Error getting production baseline",
tenant_id=str(tenant_id),
error=str(e)
)
raise
@router.get(
"/api/v1/tenants/{tenant_id}/production/sustainability/ai-impact",
response_model=dict,
summary="Get AI waste reduction impact",
description="""
Analyzes the impact of AI-assisted production on waste reduction.
Compares waste rates between:
- AI-assisted batches (with is_ai_assisted=true)
- Manual batches (is_ai_assisted=false)
Shows ROI of AI features for sustainability.
"""
)
async def get_ai_waste_impact(
tenant_id: UUID = Path(..., description="Tenant ID"),
start_date: Optional[datetime] = Query(None, description="Start date (default: 30 days ago)"),
end_date: Optional[datetime] = Query(None, description="End date (default: now)"),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""
Get AI impact on waste reduction
Frontend uses this to showcase:
- Value proposition of AI features
- Waste avoided through AI assistance
- Financial ROI of AI investment
"""
try:
# Set default dates
if not end_date:
end_date = datetime.now()
if not start_date:
start_date = end_date - timedelta(days=30)
# Get AI impact analytics (we'll implement this)
ai_impact = await production_service.get_ai_waste_impact(
tenant_id=tenant_id,
start_date=start_date,
end_date=end_date
)
logger.info(
"AI waste impact retrieved",
tenant_id=str(tenant_id),
ai_waste_reduction_pct=ai_impact.get('waste_reduction_percentage'),
user_id=current_user.get('user_id')
)
return ai_impact
except Exception as e:
logger.error(
"Error getting AI waste impact",
tenant_id=str(tenant_id),
error=str(e)
)
raise
@router.get(
"/api/v1/tenants/{tenant_id}/production/sustainability/summary",
response_model=dict,
summary="Get production sustainability summary",
description="""
Quick summary endpoint combining all production sustainability metrics.
Useful for dashboard widgets that need overview data without multiple calls.
"""
)
async def get_production_sustainability_summary(
tenant_id: UUID = Path(..., description="Tenant ID"),
days: int = Query(30, ge=7, le=365, description="Number of days to analyze"),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""
Get comprehensive production sustainability summary
Combines waste metrics, baseline, and AI impact in one response.
Optimized for dashboard widgets.
"""
try:
end_date = datetime.now()
start_date = end_date - timedelta(days=days)
# Get all metrics in parallel (within service)
waste_data = await production_service.get_waste_analytics(tenant_id, start_date, end_date)
baseline_data = await production_service.get_baseline_metrics(tenant_id)
# Try to get AI impact (may not be available for all tenants)
try:
ai_impact = await production_service.get_ai_waste_impact(tenant_id, start_date, end_date)
except:
ai_impact = {"available": False}
summary = {
"service": "production",
"period_days": days,
"waste_metrics": waste_data,
"baseline": baseline_data,
"ai_impact": ai_impact,
"last_updated": datetime.now().isoformat()
}
logger.info(
"Production sustainability summary retrieved",
tenant_id=str(tenant_id),
period_days=days,
user_id=current_user.get('user_id')
)
return summary
except Exception as e:
logger.error(
"Error getting production sustainability summary",
tenant_id=str(tenant_id),
error=str(e)
)
raise

View File

@@ -30,7 +30,8 @@ from app.api import (
production_orders_operations, # Tenant deletion endpoints
audit,
ml_insights, # ML insights endpoint
batch
batch,
sustainability # Sustainability metrics endpoints
)
from app.api.internal_alert_trigger import router as internal_alert_trigger_router
@@ -214,6 +215,7 @@ service.add_router(production_schedules.router)
service.add_router(production_operations.router)
service.add_router(production_dashboard.router)
service.add_router(analytics.router)
service.add_router(sustainability.router) # Sustainability metrics endpoints
service.add_router(internal_demo.router, tags=["internal-demo"])
service.add_router(ml_insights.router) # ML insights endpoint
service.add_router(ml_insights.internal_router) # Internal ML insights endpoint for demo cloning

View File

@@ -1858,6 +1858,124 @@ class ProductionService:
)
raise
async def get_ai_waste_impact(
self,
tenant_id: UUID,
start_date: datetime,
end_date: datetime
) -> Dict[str, Any]:
"""
Get AI impact on waste reduction
Compares waste rates between AI-assisted and manual batches
to demonstrate ROI of AI features for sustainability.
"""
try:
async with self.database_manager.get_session() as session:
from app.repositories.production_batch_repository import ProductionBatchRepository
from sqlalchemy import text
batch_repo = ProductionBatchRepository(session)
# Query for AI vs manual batch comparison
query = text("""
SELECT
-- AI-assisted batches
COUNT(CASE WHEN is_ai_assisted = true THEN 1 END) as ai_batches,
COALESCE(SUM(CASE WHEN is_ai_assisted = true THEN planned_quantity ELSE 0 END), 0) as ai_planned,
COALESCE(SUM(CASE WHEN is_ai_assisted = true THEN actual_quantity ELSE 0 END), 0) as ai_actual,
COALESCE(SUM(CASE WHEN is_ai_assisted = true THEN waste_quantity ELSE 0 END), 0) as ai_waste,
COALESCE(SUM(CASE WHEN is_ai_assisted = true THEN defect_quantity ELSE 0 END), 0) as ai_defects,
-- Manual batches
COUNT(CASE WHEN is_ai_assisted = false THEN 1 END) as manual_batches,
COALESCE(SUM(CASE WHEN is_ai_assisted = false THEN planned_quantity ELSE 0 END), 0) as manual_planned,
COALESCE(SUM(CASE WHEN is_ai_assisted = false THEN actual_quantity ELSE 0 END), 0) as manual_actual,
COALESCE(SUM(CASE WHEN is_ai_assisted = false THEN waste_quantity ELSE 0 END), 0) as manual_waste,
COALESCE(SUM(CASE WHEN is_ai_assisted = false THEN defect_quantity ELSE 0 END), 0) as manual_defects
FROM production_batches
WHERE tenant_id = :tenant_id
AND created_at BETWEEN :start_date AND :end_date
AND status IN ('COMPLETED', 'QUALITY_CHECK')
""")
result = await session.execute(
query,
{
'tenant_id': tenant_id,
'start_date': start_date,
'end_date': end_date
}
)
row = result.fetchone()
# Calculate waste percentages
ai_total_waste = float(row.ai_waste or 0) + float(row.ai_defects or 0)
manual_total_waste = float(row.manual_waste or 0) + float(row.manual_defects or 0)
ai_waste_pct = (ai_total_waste / float(row.ai_planned)) * 100 if row.ai_planned > 0 else 0
manual_waste_pct = (manual_total_waste / float(row.manual_planned)) * 100 if row.manual_planned > 0 else 0
# Calculate reduction
waste_reduction_pct = 0
if manual_waste_pct > 0:
waste_reduction_pct = ((manual_waste_pct - ai_waste_pct) / manual_waste_pct) * 100
# Calculate waste avoided
if manual_waste_pct > 0 and row.ai_planned > 0:
waste_avoided_kg = (float(row.ai_planned) * (manual_waste_pct / 100)) - ai_total_waste
else:
waste_avoided_kg = 0
# Financial impact (€3.50/kg average waste cost)
waste_cost_avoided = waste_avoided_kg * 3.50
ai_impact_data = {
'ai_batches': {
'count': int(row.ai_batches or 0),
'production_kg': float(row.ai_planned or 0),
'waste_kg': ai_total_waste,
'waste_percentage': round(ai_waste_pct, 2)
},
'manual_batches': {
'count': int(row.manual_batches or 0),
'production_kg': float(row.manual_planned or 0),
'waste_kg': manual_total_waste,
'waste_percentage': round(manual_waste_pct, 2)
},
'impact': {
'waste_reduction_percentage': round(waste_reduction_pct, 1),
'waste_avoided_kg': round(waste_avoided_kg, 2),
'cost_savings_eur': round(waste_cost_avoided, 2),
'annual_projection_eur': round(waste_cost_avoided * 12, 2)
},
'adoption': {
'ai_adoption_rate': round((int(row.ai_batches or 0) / (int(row.ai_batches or 0) + int(row.manual_batches or 1))) * 100, 1),
'recommendation': 'increase_ai_usage' if waste_reduction_pct > 10 else 'monitor'
},
'period': {
'start_date': start_date.isoformat(),
'end_date': end_date.isoformat()
}
}
logger.info(
"AI waste impact calculated",
tenant_id=str(tenant_id),
waste_reduction_pct=waste_reduction_pct,
waste_avoided_kg=waste_avoided_kg
)
return ai_impact_data
except Exception as e:
logger.error(
"Error calculating AI waste impact",
tenant_id=str(tenant_id),
error=str(e)
)
raise
# ================================================================
# NEW: ORCHESTRATOR INTEGRATION
# ================================================================