Files
bakery-ia/services/production/app/api/production_schedules.py

224 lines
8.7 KiB
Python
Raw Normal View History

2025-10-06 15:27:01 +02:00
# services/production/app/api/production_schedules.py
"""
Production Schedules API - ATOMIC CRUD operations on ProductionSchedule model
"""
2025-12-13 23:57:54 +01:00
from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request
2025-10-06 15:27:01 +02:00
from typing import Optional
from datetime import date, datetime, timedelta
from uuid import UUID
import structlog
from shared.auth.decorators import get_current_user_dep
from shared.auth.access_control import require_user_role
2025-10-06 15:27:01 +02:00
from shared.routing import RouteBuilder
from shared.security import create_audit_logger, AuditSeverity, AuditAction
2025-10-06 15:27:01 +02:00
from app.core.database import get_db
from app.services.production_service import ProductionService
2025-10-29 06:58:05 +01:00
from app.models import AuditLog
2025-10-06 15:27:01 +02:00
from app.schemas.production import (
ProductionScheduleCreate,
ProductionScheduleUpdate,
ProductionScheduleResponse
)
from app.core.config import settings
logger = structlog.get_logger()
route_builder = RouteBuilder('production')
router = APIRouter(tags=["production-schedules"])
2025-10-29 06:58:05 +01:00
# Initialize audit logger with the production service's AuditLog model
audit_logger = create_audit_logger("production-service", AuditLog)
2025-10-06 15:27:01 +02:00
2025-12-13 23:57:54 +01:00
def get_production_service(request: Request) -> ProductionService:
2025-10-06 15:27:01 +02:00
"""Dependency injection for production service"""
from app.core.database import database_manager
2025-12-13 23:57:54 +01:00
notification_service = getattr(request.app.state, 'notification_service', None)
return ProductionService(database_manager, settings, notification_service)
2025-10-06 15:27:01 +02:00
@router.get(
route_builder.build_base_route("schedules"),
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_user: dict = Depends(get_current_user_dep),
db=Depends(get_db)
):
"""Get production schedule for a date range"""
try:
# 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")
@router.get(
route_builder.build_resource_detail_route("schedules", "schedule_id"),
response_model=ProductionScheduleResponse
)
async def get_production_schedule_details(
tenant_id: UUID = Path(...),
schedule_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep),
db=Depends(get_db)
):
"""Retrieve full schedule details including assignments"""
try:
from app.repositories.production_schedule_repository import ProductionScheduleRepository
schedule_repo = ProductionScheduleRepository(db)
schedule = await schedule_repo.get(schedule_id)
if not schedule or str(schedule.tenant_id) != str(tenant_id):
raise HTTPException(status_code=404, detail="Production schedule not found")
logger.info("Retrieved production schedule details",
schedule_id=str(schedule_id), tenant_id=str(tenant_id))
return ProductionScheduleResponse.model_validate(schedule)
except HTTPException:
raise
except Exception as e:
logger.error("Error getting production schedule details",
error=str(e), schedule_id=str(schedule_id), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to get production schedule details")
@router.post(
route_builder.build_base_route("schedules"),
response_model=ProductionScheduleResponse
)
@require_user_role(['admin', 'owner'])
2025-10-06 15:27:01 +02:00
async def create_production_schedule(
schedule_data: ProductionScheduleCreate,
tenant_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""Generate or manually create a daily/shift schedule (Admin+ only)"""
2025-10-06 15:27:01 +02:00
try:
schedule = await production_service.create_production_schedule(tenant_id, schedule_data)
logger.info("Created production schedule",
schedule_id=str(schedule.id), tenant_id=str(tenant_id))
return ProductionScheduleResponse.model_validate(schedule)
except ValueError as e:
logger.warning("Invalid schedule 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 schedule",
error=str(e), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to create production schedule")
@router.put(
route_builder.build_resource_detail_route("schedules", "schedule_id"),
response_model=ProductionScheduleResponse
)
@require_user_role(['admin', 'owner'])
2025-10-06 15:27:01 +02:00
async def update_production_schedule(
schedule_update: ProductionScheduleUpdate,
tenant_id: UUID = Path(...),
schedule_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""Edit schedule before finalizing (Admin+ only)"""
2025-10-06 15:27:01 +02:00
try:
schedule = await production_service.update_production_schedule(tenant_id, schedule_id, schedule_update)
logger.info("Updated production schedule",
schedule_id=str(schedule_id), tenant_id=str(tenant_id))
return ProductionScheduleResponse.model_validate(schedule)
except ValueError as e:
logger.warning("Invalid schedule update", error=str(e), schedule_id=str(schedule_id))
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error("Error updating production schedule",
error=str(e), schedule_id=str(schedule_id), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to update production schedule")
@router.delete(
route_builder.build_resource_detail_route("schedules", "schedule_id")
)
async def delete_production_schedule(
tenant_id: UUID = Path(...),
schedule_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep),
db=Depends(get_db)
):
"""Delete a production schedule (if not finalized)"""
try:
from app.repositories.production_schedule_repository import ProductionScheduleRepository
schedule_repo = ProductionScheduleRepository(db)
schedule = await schedule_repo.get(schedule_id)
if not schedule or str(schedule.tenant_id) != str(tenant_id):
raise HTTPException(status_code=404, detail="Production schedule not found")
if schedule.is_finalized:
raise HTTPException(status_code=400, detail="Cannot delete finalized schedule")
await schedule_repo.delete(schedule_id)
logger.info("Deleted production schedule",
schedule_id=str(schedule_id), tenant_id=str(tenant_id))
return {"message": "Production schedule deleted successfully"}
except HTTPException:
raise
except Exception as e:
logger.error("Error deleting production schedule",
error=str(e), schedule_id=str(schedule_id), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to delete production schedule")