Add more services

This commit is contained in:
Urtzi Alfaro
2025-08-21 20:28:14 +02:00
parent d6fd53e461
commit c6dd6fd1de
85 changed files with 17842 additions and 1828 deletions

View File

@@ -0,0 +1,6 @@
# ================================================================
# services/production/app/api/__init__.py
# ================================================================
"""
API routes and endpoints for production service
"""

View File

@@ -0,0 +1,462 @@
# ================================================================
# services/production/app/api/production.py
# ================================================================
"""
Production API endpoints
"""
from fastapi import APIRouter, Depends, HTTPException, Path, Query
from typing import Optional, List
from datetime import date, datetime
from uuid import UUID
import structlog
from shared.auth.decorators import get_current_user_dep, get_current_tenant_id_dep
from app.core.database import get_db
from app.services.production_service import ProductionService
from app.services.production_alert_service import ProductionAlertService
from app.schemas.production import (
ProductionBatchCreate, ProductionBatchUpdate, ProductionBatchStatusUpdate,
ProductionBatchResponse, ProductionBatchListResponse,
DailyProductionRequirements, ProductionDashboardSummary, ProductionMetrics,
ProductionAlertResponse, ProductionAlertListResponse
)
from app.core.config import settings
logger = structlog.get_logger()
router = APIRouter(tags=["production"])
def get_production_service() -> ProductionService:
"""Dependency injection for production service"""
from app.core.database import database_manager
return ProductionService(database_manager, settings)
def get_production_alert_service() -> ProductionAlertService:
"""Dependency injection for production alert service"""
from app.core.database import database_manager
return ProductionAlertService(database_manager, settings)
# ================================================================
# DASHBOARD ENDPOINTS
# ================================================================
@router.get("/tenants/{tenant_id}/production/dashboard-summary", response_model=ProductionDashboardSummary)
async def get_dashboard_summary(
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""Get production dashboard summary using shared auth"""
try:
# Verify tenant access using shared auth pattern
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
summary = await production_service.get_dashboard_summary(tenant_id)
logger.info("Retrieved production dashboard summary",
tenant_id=str(tenant_id), user_id=current_user.get("user_id"))
return summary
except Exception as e:
logger.error("Error getting production dashboard summary",
error=str(e), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to get dashboard summary")
@router.get("/tenants/{tenant_id}/production/daily-requirements", response_model=DailyProductionRequirements)
async def get_daily_requirements(
tenant_id: UUID = Path(...),
date: Optional[date] = Query(None, description="Target date for production requirements"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""Get daily production requirements"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
target_date = date or datetime.now().date()
requirements = await production_service.calculate_daily_requirements(tenant_id, target_date)
logger.info("Retrieved daily production requirements",
tenant_id=str(tenant_id), date=target_date.isoformat())
return requirements
except Exception as e:
logger.error("Error getting daily production requirements",
error=str(e), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to get daily requirements")
@router.get("/tenants/{tenant_id}/production/requirements", response_model=dict)
async def get_production_requirements(
tenant_id: UUID = Path(...),
date: Optional[date] = Query(None, description="Target date for production requirements"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""Get production requirements for procurement planning"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
target_date = date or datetime.now().date()
requirements = await production_service.get_production_requirements(tenant_id, target_date)
logger.info("Retrieved production requirements for procurement",
tenant_id=str(tenant_id), date=target_date.isoformat())
return requirements
except Exception as e:
logger.error("Error getting production requirements",
error=str(e), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to get production requirements")
# ================================================================
# PRODUCTION BATCH ENDPOINTS
# ================================================================
@router.post("/tenants/{tenant_id}/production/batches", response_model=ProductionBatchResponse)
async def create_production_batch(
batch_data: ProductionBatchCreate,
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""Create a new production batch"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
batch = await production_service.create_production_batch(tenant_id, batch_data)
logger.info("Created production batch",
batch_id=str(batch.id), tenant_id=str(tenant_id))
return ProductionBatchResponse.model_validate(batch)
except ValueError as e:
logger.warning("Invalid batch data", error=str(e), tenant_id=str(tenant_id))
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error("Error creating production batch",
error=str(e), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to create production batch")
@router.get("/tenants/{tenant_id}/production/batches/active", response_model=ProductionBatchListResponse)
async def get_active_batches(
tenant_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
db=Depends(get_db)
):
"""Get currently active production batches"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
from app.repositories.production_batch_repository import ProductionBatchRepository
batch_repo = ProductionBatchRepository(db)
batches = await batch_repo.get_active_batches(str(tenant_id))
batch_responses = [ProductionBatchResponse.model_validate(batch) for batch in batches]
logger.info("Retrieved active production batches",
count=len(batches), tenant_id=str(tenant_id))
return ProductionBatchListResponse(
batches=batch_responses,
total_count=len(batches),
page=1,
page_size=len(batches)
)
except Exception as e:
logger.error("Error getting active batches",
error=str(e), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to get active batches")
@router.get("/tenants/{tenant_id}/production/batches/{batch_id}", response_model=ProductionBatchResponse)
async def get_batch_details(
tenant_id: UUID = Path(...),
batch_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
db=Depends(get_db)
):
"""Get detailed information about a production batch"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
from app.repositories.production_batch_repository import ProductionBatchRepository
batch_repo = ProductionBatchRepository(db)
batch = await batch_repo.get(batch_id)
if not batch or str(batch.tenant_id) != str(tenant_id):
raise HTTPException(status_code=404, detail="Production batch not found")
logger.info("Retrieved production batch details",
batch_id=str(batch_id), tenant_id=str(tenant_id))
return ProductionBatchResponse.model_validate(batch)
except HTTPException:
raise
except Exception as e:
logger.error("Error getting batch details",
error=str(e), batch_id=str(batch_id), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to get batch details")
@router.put("/tenants/{tenant_id}/production/batches/{batch_id}/status", response_model=ProductionBatchResponse)
async def update_batch_status(
status_update: ProductionBatchStatusUpdate,
tenant_id: UUID = Path(...),
batch_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""Update production batch status"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
batch = await production_service.update_batch_status(tenant_id, batch_id, status_update)
logger.info("Updated production batch status",
batch_id=str(batch_id),
new_status=status_update.status.value,
tenant_id=str(tenant_id))
return ProductionBatchResponse.model_validate(batch)
except ValueError as e:
logger.warning("Invalid status update", error=str(e), batch_id=str(batch_id))
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error("Error updating batch status",
error=str(e), batch_id=str(batch_id), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to update batch status")
# ================================================================
# PRODUCTION SCHEDULE ENDPOINTS
# ================================================================
@router.get("/tenants/{tenant_id}/production/schedule", response_model=dict)
async def get_production_schedule(
tenant_id: UUID = Path(...),
start_date: Optional[date] = Query(None, description="Start date for schedule"),
end_date: Optional[date] = Query(None, description="End date for schedule"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
db=Depends(get_db)
):
"""Get production schedule for a date range"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
# Default to next 7 days if no dates provided
if not start_date:
start_date = datetime.now().date()
if not end_date:
end_date = start_date + timedelta(days=7)
from app.repositories.production_schedule_repository import ProductionScheduleRepository
schedule_repo = ProductionScheduleRepository(db)
schedules = await schedule_repo.get_schedules_by_date_range(
str(tenant_id), start_date, end_date
)
schedule_data = {
"start_date": start_date.isoformat(),
"end_date": end_date.isoformat(),
"schedules": [
{
"id": str(schedule.id),
"date": schedule.schedule_date.isoformat(),
"shift_start": schedule.shift_start.isoformat(),
"shift_end": schedule.shift_end.isoformat(),
"capacity_utilization": schedule.utilization_percentage,
"batches_planned": schedule.total_batches_planned,
"is_finalized": schedule.is_finalized
}
for schedule in schedules
],
"total_schedules": len(schedules)
}
logger.info("Retrieved production schedule",
tenant_id=str(tenant_id),
start_date=start_date.isoformat(),
end_date=end_date.isoformat(),
schedules_count=len(schedules))
return schedule_data
except Exception as e:
logger.error("Error getting production schedule",
error=str(e), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to get production schedule")
# ================================================================
# ALERTS ENDPOINTS
# ================================================================
@router.get("/tenants/{tenant_id}/production/alerts", response_model=ProductionAlertListResponse)
async def get_production_alerts(
tenant_id: UUID = Path(...),
active_only: bool = Query(True, description="Return only active alerts"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
alert_service: ProductionAlertService = Depends(get_production_alert_service)
):
"""Get production-related alerts"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
if active_only:
alerts = await alert_service.get_active_alerts(tenant_id)
else:
# Get all alerts (would need additional repo method)
alerts = await alert_service.get_active_alerts(tenant_id)
alert_responses = [ProductionAlertResponse.model_validate(alert) for alert in alerts]
logger.info("Retrieved production alerts",
count=len(alerts), tenant_id=str(tenant_id))
return ProductionAlertListResponse(
alerts=alert_responses,
total_count=len(alerts),
page=1,
page_size=len(alerts)
)
except Exception as e:
logger.error("Error getting production alerts",
error=str(e), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to get production alerts")
@router.post("/tenants/{tenant_id}/production/alerts/{alert_id}/acknowledge", response_model=ProductionAlertResponse)
async def acknowledge_alert(
tenant_id: UUID = Path(...),
alert_id: UUID = Path(...),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
alert_service: ProductionAlertService = Depends(get_production_alert_service)
):
"""Acknowledge a production-related alert"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
acknowledged_by = current_user.get("email", "unknown_user")
alert = await alert_service.acknowledge_alert(tenant_id, alert_id, acknowledged_by)
logger.info("Acknowledged production alert",
alert_id=str(alert_id),
acknowledged_by=acknowledged_by,
tenant_id=str(tenant_id))
return ProductionAlertResponse.model_validate(alert)
except Exception as e:
logger.error("Error acknowledging production alert",
error=str(e), alert_id=str(alert_id), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to acknowledge alert")
# ================================================================
# CAPACITY MANAGEMENT ENDPOINTS
# ================================================================
@router.get("/tenants/{tenant_id}/production/capacity/status", response_model=dict)
async def get_capacity_status(
tenant_id: UUID = Path(...),
date: Optional[date] = Query(None, description="Date for capacity status"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
db=Depends(get_db)
):
"""Get production capacity status for a specific date"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
target_date = date or datetime.now().date()
from app.repositories.production_capacity_repository import ProductionCapacityRepository
capacity_repo = ProductionCapacityRepository(db)
capacity_summary = await capacity_repo.get_capacity_utilization_summary(
str(tenant_id), target_date, target_date
)
logger.info("Retrieved capacity status",
tenant_id=str(tenant_id), date=target_date.isoformat())
return capacity_summary
except Exception as e:
logger.error("Error getting capacity status",
error=str(e), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to get capacity status")
# ================================================================
# METRICS AND ANALYTICS ENDPOINTS
# ================================================================
@router.get("/tenants/{tenant_id}/production/metrics/yield", response_model=dict)
async def get_yield_metrics(
tenant_id: UUID = Path(...),
start_date: date = Query(..., description="Start date for metrics"),
end_date: date = Query(..., description="End date for metrics"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: dict = Depends(get_current_user_dep),
db=Depends(get_db)
):
"""Get production yield metrics for analysis"""
try:
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
from app.repositories.production_batch_repository import ProductionBatchRepository
batch_repo = ProductionBatchRepository(db)
metrics = await batch_repo.get_production_metrics(str(tenant_id), start_date, end_date)
logger.info("Retrieved yield metrics",
tenant_id=str(tenant_id),
start_date=start_date.isoformat(),
end_date=end_date.isoformat())
return metrics
except Exception as e:
logger.error("Error getting yield metrics",
error=str(e), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to get yield metrics")