Files
bakery-ia/services/distribution/app/api/routes.py
2025-11-30 09:12:40 +01:00

226 lines
8.5 KiB
Python

"""
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)}")