# services/recipes/app/api/production.py """ API endpoints for production management """ from fastapi import APIRouter, Depends, HTTPException, Header, Query from sqlalchemy.orm import Session from typing import List, Optional from uuid import UUID from datetime import date, datetime import logging from ..core.database import get_db from ..services.production_service import ProductionService from ..schemas.production import ( ProductionBatchCreate, ProductionBatchUpdate, ProductionBatchResponse, ProductionBatchSearchRequest, ProductionScheduleCreate, ProductionScheduleUpdate, ProductionScheduleResponse, ProductionStatisticsResponse, StartProductionRequest, CompleteProductionRequest ) logger = logging.getLogger(__name__) router = APIRouter() def get_tenant_id(x_tenant_id: str = Header(...)) -> UUID: """Extract tenant ID from header""" try: return UUID(x_tenant_id) except ValueError: raise HTTPException(status_code=400, detail="Invalid tenant ID format") def get_user_id(x_user_id: str = Header(...)) -> UUID: """Extract user ID from header""" try: return UUID(x_user_id) except ValueError: raise HTTPException(status_code=400, detail="Invalid user ID format") # Production Batch Endpoints @router.post("/batches", response_model=ProductionBatchResponse) async def create_production_batch( batch_data: ProductionBatchCreate, tenant_id: UUID = Depends(get_tenant_id), user_id: UUID = Depends(get_user_id), db: Session = Depends(get_db) ): """Create a new production batch""" try: production_service = ProductionService(db) batch_dict = batch_data.dict() batch_dict["tenant_id"] = tenant_id result = await production_service.create_production_batch(batch_dict, user_id) if not result["success"]: raise HTTPException(status_code=400, detail=result["error"]) return ProductionBatchResponse(**result["data"]) except HTTPException: raise except Exception as e: logger.error(f"Error creating production batch: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.get("/batches/{batch_id}", response_model=ProductionBatchResponse) async def get_production_batch( batch_id: UUID, tenant_id: UUID = Depends(get_tenant_id), db: Session = Depends(get_db) ): """Get production batch by ID with consumptions""" try: production_service = ProductionService(db) batch = production_service.get_production_batch_with_consumptions(batch_id) if not batch: raise HTTPException(status_code=404, detail="Production batch not found") # Verify tenant ownership if batch["tenant_id"] != str(tenant_id): raise HTTPException(status_code=403, detail="Access denied") return ProductionBatchResponse(**batch) except HTTPException: raise except Exception as e: logger.error(f"Error getting production batch {batch_id}: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.put("/batches/{batch_id}", response_model=ProductionBatchResponse) async def update_production_batch( batch_id: UUID, batch_data: ProductionBatchUpdate, tenant_id: UUID = Depends(get_tenant_id), user_id: UUID = Depends(get_user_id), db: Session = Depends(get_db) ): """Update an existing production batch""" try: production_service = ProductionService(db) # Check if batch exists and belongs to tenant existing_batch = production_service.get_production_batch_with_consumptions(batch_id) if not existing_batch: raise HTTPException(status_code=404, detail="Production batch not found") if existing_batch["tenant_id"] != str(tenant_id): raise HTTPException(status_code=403, detail="Access denied") batch_dict = batch_data.dict(exclude_unset=True) result = await production_service.update_production_batch(batch_id, batch_dict, user_id) if not result["success"]: raise HTTPException(status_code=400, detail=result["error"]) return ProductionBatchResponse(**result["data"]) except HTTPException: raise except Exception as e: logger.error(f"Error updating production batch {batch_id}: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.delete("/batches/{batch_id}") async def delete_production_batch( batch_id: UUID, tenant_id: UUID = Depends(get_tenant_id), db: Session = Depends(get_db) ): """Delete a production batch""" try: production_service = ProductionService(db) # Check if batch exists and belongs to tenant existing_batch = production_service.get_production_batch_with_consumptions(batch_id) if not existing_batch: raise HTTPException(status_code=404, detail="Production batch not found") if existing_batch["tenant_id"] != str(tenant_id): raise HTTPException(status_code=403, detail="Access denied") success = production_service.production_repo.delete(batch_id) if not success: raise HTTPException(status_code=404, detail="Production batch not found") return {"message": "Production batch deleted successfully"} except HTTPException: raise except Exception as e: logger.error(f"Error deleting production batch {batch_id}: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.get("/batches", response_model=List[ProductionBatchResponse]) async def search_production_batches( tenant_id: UUID = Depends(get_tenant_id), search_term: Optional[str] = Query(None), status: Optional[str] = Query(None), priority: Optional[str] = Query(None), start_date: Optional[date] = Query(None), end_date: Optional[date] = Query(None), recipe_id: Optional[UUID] = Query(None), limit: int = Query(100, ge=1, le=1000), offset: int = Query(0, ge=0), db: Session = Depends(get_db) ): """Search production batches with filters""" try: production_service = ProductionService(db) batches = production_service.search_production_batches( tenant_id=tenant_id, search_term=search_term, status=status, priority=priority, start_date=start_date, end_date=end_date, recipe_id=recipe_id, limit=limit, offset=offset ) return [ProductionBatchResponse(**batch) for batch in batches] except Exception as e: logger.error(f"Error searching production batches: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.post("/batches/{batch_id}/start", response_model=ProductionBatchResponse) async def start_production_batch( batch_id: UUID, start_data: StartProductionRequest, tenant_id: UUID = Depends(get_tenant_id), user_id: UUID = Depends(get_user_id), db: Session = Depends(get_db) ): """Start production batch and record ingredient consumptions""" try: production_service = ProductionService(db) # Check if batch exists and belongs to tenant existing_batch = production_service.get_production_batch_with_consumptions(batch_id) if not existing_batch: raise HTTPException(status_code=404, detail="Production batch not found") if existing_batch["tenant_id"] != str(tenant_id): raise HTTPException(status_code=403, detail="Access denied") consumptions_list = [cons.dict() for cons in start_data.ingredient_consumptions] result = await production_service.start_production_batch( batch_id, consumptions_list, start_data.staff_member or user_id, start_data.production_notes ) if not result["success"]: raise HTTPException(status_code=400, detail=result["error"]) return ProductionBatchResponse(**result["data"]) except HTTPException: raise except Exception as e: logger.error(f"Error starting production batch {batch_id}: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.post("/batches/{batch_id}/complete", response_model=ProductionBatchResponse) async def complete_production_batch( batch_id: UUID, complete_data: CompleteProductionRequest, tenant_id: UUID = Depends(get_tenant_id), user_id: UUID = Depends(get_user_id), db: Session = Depends(get_db) ): """Complete production batch and add finished products to inventory""" try: production_service = ProductionService(db) # Check if batch exists and belongs to tenant existing_batch = production_service.get_production_batch_with_consumptions(batch_id) if not existing_batch: raise HTTPException(status_code=404, detail="Production batch not found") if existing_batch["tenant_id"] != str(tenant_id): raise HTTPException(status_code=403, detail="Access denied") completion_data = complete_data.dict() result = await production_service.complete_production_batch( batch_id, completion_data, complete_data.staff_member or user_id ) if not result["success"]: raise HTTPException(status_code=400, detail=result["error"]) return ProductionBatchResponse(**result["data"]) except HTTPException: raise except Exception as e: logger.error(f"Error completing production batch {batch_id}: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.get("/batches/active/list", response_model=List[ProductionBatchResponse]) async def get_active_production_batches( tenant_id: UUID = Depends(get_tenant_id), db: Session = Depends(get_db) ): """Get all active production batches""" try: production_service = ProductionService(db) batches = production_service.get_active_production_batches(tenant_id) return [ProductionBatchResponse(**batch) for batch in batches] except Exception as e: logger.error(f"Error getting active production batches: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.get("/statistics/dashboard", response_model=ProductionStatisticsResponse) async def get_production_statistics( tenant_id: UUID = Depends(get_tenant_id), start_date: Optional[date] = Query(None), end_date: Optional[date] = Query(None), db: Session = Depends(get_db) ): """Get production statistics for dashboard""" try: production_service = ProductionService(db) stats = production_service.get_production_statistics(tenant_id, start_date, end_date) return ProductionStatisticsResponse(**stats) except Exception as e: logger.error(f"Error getting production statistics: {e}") raise HTTPException(status_code=500, detail="Internal server error") # Production Schedule Endpoints @router.post("/schedules", response_model=ProductionScheduleResponse) async def create_production_schedule( schedule_data: ProductionScheduleCreate, tenant_id: UUID = Depends(get_tenant_id), user_id: UUID = Depends(get_user_id), db: Session = Depends(get_db) ): """Create a new production schedule""" try: production_service = ProductionService(db) schedule_dict = schedule_data.dict() schedule_dict["tenant_id"] = tenant_id schedule_dict["created_by"] = user_id result = production_service.create_production_schedule(schedule_dict) if not result["success"]: raise HTTPException(status_code=400, detail=result["error"]) return ProductionScheduleResponse(**result["data"]) except HTTPException: raise except Exception as e: logger.error(f"Error creating production schedule: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.get("/schedules/{schedule_id}", response_model=ProductionScheduleResponse) async def get_production_schedule( schedule_id: UUID, tenant_id: UUID = Depends(get_tenant_id), db: Session = Depends(get_db) ): """Get production schedule by ID""" try: production_service = ProductionService(db) schedule = production_service.get_production_schedule(schedule_id) if not schedule: raise HTTPException(status_code=404, detail="Production schedule not found") # Verify tenant ownership if schedule["tenant_id"] != str(tenant_id): raise HTTPException(status_code=403, detail="Access denied") return ProductionScheduleResponse(**schedule) except HTTPException: raise except Exception as e: logger.error(f"Error getting production schedule {schedule_id}: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.get("/schedules/date/{schedule_date}", response_model=ProductionScheduleResponse) async def get_production_schedule_by_date( schedule_date: date, tenant_id: UUID = Depends(get_tenant_id), db: Session = Depends(get_db) ): """Get production schedule for specific date""" try: production_service = ProductionService(db) schedule = production_service.get_production_schedule_by_date(tenant_id, schedule_date) if not schedule: raise HTTPException(status_code=404, detail="Production schedule not found for this date") return ProductionScheduleResponse(**schedule) except HTTPException: raise except Exception as e: logger.error(f"Error getting production schedule for {schedule_date}: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.get("/schedules", response_model=List[ProductionScheduleResponse]) async def get_production_schedules( tenant_id: UUID = Depends(get_tenant_id), start_date: Optional[date] = Query(None), end_date: Optional[date] = Query(None), published_only: bool = Query(False), db: Session = Depends(get_db) ): """Get production schedules within date range""" try: production_service = ProductionService(db) if published_only: schedules = production_service.get_published_schedules(tenant_id, start_date, end_date) else: schedules = production_service.get_production_schedules_range(tenant_id, start_date, end_date) return [ProductionScheduleResponse(**schedule) for schedule in schedules] except Exception as e: logger.error(f"Error getting production schedules: {e}") raise HTTPException(status_code=500, detail="Internal server error")