""" API Routes for Distribution Service """ from fastapi import APIRouter, Depends, HTTPException, Query, Header from typing import List, Optional, Dict, Any from datetime import date, timedelta import structlog import os from app.api.dependencies import get_distribution_service from shared.auth.tenant_access import verify_tenant_permission_dep from app.core.config import settings logger = structlog.get_logger() async def verify_internal_api_key(x_internal_api_key: str = Header(None)): """Verify internal API key for service-to-service communication""" required_key = settings.INTERNAL_API_KEY if x_internal_api_key != required_key: logger.warning("Unauthorized internal API access attempted") raise HTTPException(status_code=403, detail="Invalid internal API key") return True router = APIRouter() @router.post("/tenants/{tenant_id}/distribution/plans/generate") async def generate_daily_distribution_plan( tenant_id: str, target_date: date = Query(..., description="Date for which to generate distribution plan"), vehicle_capacity_kg: float = Query(1000.0, description="Vehicle capacity in kg"), distribution_service: object = Depends(get_distribution_service), verified_tenant: str = Depends(verify_tenant_permission_dep) ): """ Generate daily distribution plan for internal transfers **Enterprise Tier Feature**: Distribution and routing require Enterprise subscription. """ try: # Validate subscription tier for distribution features from shared.subscription.plans import PlanFeatures from shared.clients import get_tenant_client tenant_client = get_tenant_client(config=settings, service_name="distribution-service") subscription = await tenant_client.get_tenant_subscription(tenant_id) if not subscription: raise HTTPException( status_code=403, detail="No active subscription found. Distribution routing requires Enterprise tier." ) # Check if tier has distribution feature (enterprise only) tier = subscription.get("plan", "starter") if not PlanFeatures.has_feature(tier, "distribution_management"): raise HTTPException( status_code=403, detail=f"Distribution routing requires Enterprise tier. Current tier: {tier}" ) result = await distribution_service.generate_daily_distribution_plan( parent_tenant_id=tenant_id, target_date=target_date, vehicle_capacity_kg=vehicle_capacity_kg ) return result except HTTPException: raise except Exception as e: logger.error("Error generating distribution plan", error=str(e), exc_info=True) raise HTTPException(status_code=500, detail=f"Failed to generate distribution plan: {str(e)}") @router.get("/tenants/{tenant_id}/distribution/routes") async def get_delivery_routes( tenant_id: str, date_from: Optional[date] = Query(None, description="Start date for route filtering"), date_to: Optional[date] = Query(None, description="End date for route filtering"), status: Optional[str] = Query(None, description="Filter by route status"), distribution_service: object = Depends(get_distribution_service), verified_tenant: str = Depends(verify_tenant_permission_dep) ): """ Get delivery routes with optional filtering """ try: # If no date range specified, default to today if not date_from and not date_to: date_from = date.today() date_to = date.today() elif not date_to: date_to = date_from routes = [] current_date = date_from while current_date <= date_to: daily_routes = await distribution_service.get_delivery_routes_for_date(tenant_id, current_date) routes.extend(daily_routes) current_date = current_date + timedelta(days=1) if status: routes = [r for r in routes if r.get('status') == status] return {"routes": routes} except Exception as e: logger.error("Error getting delivery routes", error=str(e), exc_info=True) raise HTTPException(status_code=500, detail=f"Failed to get delivery routes: {str(e)}") @router.get("/tenants/{tenant_id}/distribution/shipments") async def get_shipments( tenant_id: str, date_from: Optional[date] = Query(None, description="Start date for shipment filtering"), date_to: Optional[date] = Query(None, description="End date for shipment filtering"), status: Optional[str] = Query(None, description="Filter by shipment status"), distribution_service: object = Depends(get_distribution_service), verified_tenant: str = Depends(verify_tenant_permission_dep) ): """ Get shipments with optional filtering """ try: # If no date range specified, default to today if not date_from and not date_to: date_from = date.today() date_to = date.today() elif not date_to: date_to = date_from shipments = [] current_date = date_from while current_date <= date_to: daily_shipments = await distribution_service.get_shipments_for_date(tenant_id, current_date) shipments.extend(daily_shipments) current_date = current_date + timedelta(days=1) if status: shipments = [s for s in shipments if s.get('status') == status] return {"shipments": shipments} except Exception as e: logger.error("Error getting shipments", error=str(e), exc_info=True) raise HTTPException(status_code=500, detail=f"Failed to get shipments: {str(e)}") @router.put("/tenants/{tenant_id}/distribution/shipments/{shipment_id}/status") async def update_shipment_status( tenant_id: str, shipment_id: str, status_update: dict, # Should be a Pydantic model in production distribution_service: object = Depends(get_distribution_service), verified_tenant: str = Depends(verify_tenant_permission_dep) ): """ Update shipment status """ try: new_status = status_update.get('status') if not new_status: raise HTTPException(status_code=400, detail="Status is required") user_id = "temp_user" # Would come from auth context result = await distribution_service.update_shipment_status( shipment_id=shipment_id, new_status=new_status, user_id=user_id, metadata=status_update.get('metadata') ) return result except Exception as e: logger.error("Error updating shipment status", error=str(e), exc_info=True) raise HTTPException(status_code=500, detail=f"Failed to update shipment status: {str(e)}") @router.post("/tenants/{tenant_id}/distribution/shipments/{shipment_id}/delivery-proof") async def upload_delivery_proof( tenant_id: str, shipment_id: str, delivery_proof: dict, # Should be a Pydantic model in production distribution_service: object = Depends(get_distribution_service), verified_tenant: str = Depends(verify_tenant_permission_dep) ): """ Upload delivery proof (signature, photo, etc.) """ try: # Implementation would handle signature/photo upload # This is a placeholder until proper models are created raise HTTPException(status_code=501, detail="Delivery proof upload endpoint not yet implemented") except Exception as e: logger.error("Error uploading delivery proof", error=str(e), exc_info=True) raise HTTPException(status_code=500, detail=f"Failed to upload delivery proof: {str(e)}") @router.get("/tenants/{tenant_id}/distribution/routes/{route_id}") async def get_route_detail( tenant_id: str, route_id: str, distribution_service: object = Depends(get_distribution_service), verified_tenant: str = Depends(verify_tenant_permission_dep) ): """ Get delivery route details """ try: # Implementation would fetch detailed route information # For now, return a simple response routes = await distribution_service.get_delivery_routes_for_date(tenant_id, date.today()) route = next((r for r in routes if r.get('id') == route_id), None) if not route: raise HTTPException(status_code=404, detail="Route not found") return route except HTTPException: raise except Exception as e: logger.error("Error getting route detail", error=str(e), exc_info=True) raise HTTPException(status_code=500, detail=f"Failed to get route detail: {str(e)}")