Imporve enterprise

This commit is contained in:
Urtzi Alfaro
2025-12-17 20:50:22 +01:00
parent e3ef47b879
commit f8591639a7
28 changed files with 6802 additions and 258 deletions

View File

@@ -0,0 +1,341 @@
"""
VRP Optimization API Endpoints
Endpoints for VRP optimization and metrics retrieval
"""
from fastapi import APIRouter, Depends, HTTPException, Query
from typing import List, Dict, Any, Optional
from pydantic import BaseModel, Field
import structlog
from app.services.vrp_optimization_service import VRPOptimizationService
from app.services.distribution_service import DistributionService
from shared.auth.tenant_access import verify_tenant_permission_dep
from app.core.config import settings
logger = structlog.get_logger()
router = APIRouter()
# Pydantic models for request/response
class VRPOptimizationRequest(BaseModel):
algorithm_version: str = Field(default="v2.1", description="VRP algorithm version to use")
constraints: Optional[Dict[str, Any]] = Field(
None,
description="Optimization constraints: max_route_duration, max_route_distance, etc."
)
class VRPOptimizationResponse(BaseModel):
success: bool
route_id: str
optimization_savings: Dict[str, Any]
vrp_algorithm_version: str
vrp_optimization_timestamp: str
vrp_constraints_satisfied: bool
vrp_objective_value: float
class RouteOptimizationMetrics(BaseModel):
route_id: str
route_number: str
route_date: str
vrp_optimization_savings: Optional[Dict[str, Any]]
vrp_algorithm_version: Optional[str]
vrp_optimization_timestamp: Optional[str]
vrp_constraints_satisfied: Optional[bool]
vrp_objective_value: Optional[float]
total_distance_km: Optional[float]
estimated_duration_minutes: Optional[int]
class NetworkOptimizationSummary(BaseModel):
total_routes: int
optimized_routes: int
total_distance_saved_km: float
total_time_saved_minutes: float
total_fuel_saved_liters: float
total_co2_saved_kg: float
total_cost_saved_eur: float
optimization_rate: float
average_savings_per_route: Optional[Dict[str, Any]]
class OptimizationHistoryItem(BaseModel):
optimization_id: str
route_id: str
timestamp: str
algorithm_version: str
distance_saved_km: float
time_saved_minutes: float
fuel_saved_liters: float
co2_saved_kg: float
cost_saved_eur: float
constraints_satisfied: bool
async def get_vrp_optimization_service() -> VRPOptimizationService:
"""Dependency injection for VRPOptimizationService"""
from app.core.database import database_manager
from app.services.distribution_service import DistributionService as BusinessDistributionService
from app.repositories.delivery_route_repository import DeliveryRouteRepository
from app.repositories.shipment_repository import ShipmentRepository
from app.repositories.delivery_schedule_repository import DeliveryScheduleRepository
from shared.clients.tenant_client import TenantServiceClient
from shared.clients.inventory_client import InventoryServiceClient
from shared.clients.procurement_client import ProcurementServiceClient
from app.services.routing_optimizer import RoutingOptimizer
# Create the business distribution service with proper dependencies
route_repository = DeliveryRouteRepository(database_manager.get_session())
shipment_repository = ShipmentRepository(database_manager.get_session())
schedule_repository = DeliveryScheduleRepository(database_manager.get_session())
# Create client instances (these will be initialized with proper config)
tenant_client = TenantServiceClient()
inventory_client = InventoryServiceClient()
procurement_client = ProcurementServiceClient()
routing_optimizer = RoutingOptimizer()
distribution_service = BusinessDistributionService(
route_repository=route_repository,
shipment_repository=shipment_repository,
schedule_repository=schedule_repository,
procurement_client=procurement_client,
tenant_client=tenant_client,
inventory_client=inventory_client,
routing_optimizer=routing_optimizer
)
return VRPOptimizationService(distribution_service, database_manager)
@router.post("/tenants/{tenant_id}/routes/{route_id}/optimize",
response_model=VRPOptimizationResponse,
summary="Optimize delivery route with VRP")
async def optimize_route_with_vrp(
tenant_id: str,
route_id: str,
optimization_request: VRPOptimizationRequest,
vrp_service: VRPOptimizationService = Depends(get_vrp_optimization_service),
verified_tenant: str = Depends(verify_tenant_permission_dep)
):
"""
Optimize a delivery route using VRP algorithm
This endpoint applies VRP optimization to a specific delivery route and stores
the optimization metrics for analysis and reporting.
"""
try:
result = await vrp_service.optimize_route_with_vrp(
route_id=route_id,
algorithm_version=optimization_request.algorithm_version,
constraints=optimization_request.constraints
)
if not result.get('success'):
raise HTTPException(status_code=500, detail="Optimization failed")
return VRPOptimizationResponse(
success=True,
route_id=result['route_id'],
optimization_savings=result['optimization_savings'],
vrp_algorithm_version=result['optimization_savings'].get('algorithm_version', optimization_request.algorithm_version),
vrp_optimization_timestamp=result['optimization_savings'].get('timestamp', datetime.now().isoformat()),
vrp_constraints_satisfied=result['optimization_savings'].get('constraints_satisfied', True),
vrp_objective_value=result['optimization_savings'].get('objective_value', 0.0)
)
except Exception as e:
logger.error("VRP optimization failed", tenant_id=tenant_id, route_id=route_id, error=str(e))
raise HTTPException(status_code=500, detail=f"VRP optimization failed: {str(e)}")
@router.get("/tenants/{tenant_id}/routes/{route_id}/optimization-metrics",
response_model=RouteOptimizationMetrics,
summary="Get VRP optimization metrics for route")
async def get_route_optimization_metrics(
tenant_id: str,
route_id: str,
vrp_service: VRPOptimizationService = Depends(get_vrp_optimization_service),
verified_tenant: str = Depends(verify_tenant_permission_dep)
):
"""
Get VRP optimization metrics for a specific route
Retrieves stored optimization metrics including savings, algorithm version,
and constraint satisfaction status.
"""
try:
metrics = await vrp_service.get_route_optimization_metrics(route_id)
return RouteOptimizationMetrics(**metrics)
except Exception as e:
logger.error("Failed to get route optimization metrics", tenant_id=tenant_id, route_id=route_id, error=str(e))
raise HTTPException(status_code=500, detail=f"Failed to get optimization metrics: {str(e)}")
@router.get("/tenants/{tenant_id}/vrp/optimization-summary",
response_model=NetworkOptimizationSummary,
summary="Get network-wide VRP optimization summary")
async def get_network_optimization_summary(
tenant_id: str,
vrp_service: VRPOptimizationService = Depends(get_vrp_optimization_service),
verified_tenant: str = Depends(verify_tenant_permission_dep)
):
"""
Get aggregated VRP optimization metrics across all routes
Provides network-wide summary of optimization benefits including
total savings, optimization rate, and average improvements.
"""
try:
summary = await vrp_service.get_network_optimization_summary(tenant_id)
return NetworkOptimizationSummary(**summary)
except Exception as e:
logger.error("Failed to get network optimization summary", tenant_id=tenant_id, error=str(e))
raise HTTPException(status_code=500, detail=f"Failed to get optimization summary: {str(e)}")
@router.post("/tenants/{tenant_id}/vrp/batch-optimize",
summary="Batch optimize multiple routes")
async def batch_optimize_routes(
tenant_id: str,
route_ids: List[str] = Query(..., description="List of route IDs to optimize"),
algorithm_version: str = Query("v2.1", description="VRP algorithm version"),
vrp_service: VRPOptimizationService = Depends(get_vrp_optimization_service),
verified_tenant: str = Depends(verify_tenant_permission_dep)
):
"""
Batch optimize multiple delivery routes with VRP
Applies VRP optimization to multiple routes in a single request.
"""
try:
result = await vrp_service.batch_optimize_routes(tenant_id, route_ids)
return {
'success': True,
'total_routes_processed': result['total_routes_processed'],
'successful_optimizations': result['successful_optimizations'],
'failed_optimizations': result['failed_optimizations'],
'results': result['results']
}
except Exception as e:
logger.error("Batch optimization failed", tenant_id=tenant_id, error=str(e))
raise HTTPException(status_code=500, detail=f"Batch optimization failed: {str(e)}")
@router.get("/tenants/{tenant_id}/routes/{route_id}/optimization-history",
response_model=List[OptimizationHistoryItem],
summary="Get optimization history for route")
async def get_optimization_history(
tenant_id: str,
route_id: str,
limit: int = Query(10, description="Maximum number of historical records to return"),
vrp_service: VRPOptimizationService = Depends(get_vrp_optimization_service),
verified_tenant: str = Depends(verify_tenant_permission_dep)
):
"""
Get historical optimization records for a route
Retrieves past optimization runs and their results for analysis.
"""
try:
history = await vrp_service.get_optimization_history(route_id, limit)
return [OptimizationHistoryItem(**item) for item in history]
except Exception as e:
logger.error("Failed to get optimization history", tenant_id=tenant_id, route_id=route_id, error=str(e))
raise HTTPException(status_code=500, detail=f"Failed to get optimization history: {str(e)}")
@router.get("/tenants/{tenant_id}/vrp/constraints/validate",
summary="Validate VRP constraints")
async def validate_vrp_constraints(
tenant_id: str,
route_id: str,
max_route_duration: Optional[int] = Query(None, description="Maximum route duration in minutes"),
max_route_distance: Optional[float] = Query(None, description="Maximum route distance in km"),
vrp_service: VRPOptimizationService = Depends(get_vrp_optimization_service),
verified_tenant: str = Depends(verify_tenant_permission_dep)
):
"""
Validate VRP constraints against a route
Checks if a route satisfies specified VRP constraints.
"""
try:
from app.services.vrp_optimization_service import VRPConstraintValidator
# Get route data
route = await vrp_service.distribution_service.get_delivery_route(route_id)
if not route:
raise HTTPException(status_code=404, detail="Route not found")
# Build constraints dict
constraints = {}
if max_route_duration is not None:
constraints['max_route_duration'] = max_route_duration
if max_route_distance is not None:
constraints['max_route_distance'] = max_route_distance
# Validate constraints
validation_result = VRPConstraintValidator.validate_constraints(route, constraints)
return {
'success': True,
'all_constraints_satisfied': validation_result['all_satisfied'],
'constraint_violations': validation_result['constraint_violations']
}
except Exception as e:
logger.error("Failed to validate VRP constraints", tenant_id=tenant_id, route_id=route_id, error=str(e))
raise HTTPException(status_code=500, detail=f"Failed to validate constraints: {str(e)}")
@router.post("/tenants/{tenant_id}/vrp/simulate",
summary="Simulate VRP optimization")
async def simulate_vrp_optimization(
tenant_id: str,
route_id: str,
vrp_service: VRPOptimizationService = Depends(get_vrp_optimization_service),
verified_tenant: str = Depends(verify_tenant_permission_dep)
):
"""
Simulate VRP optimization without saving results
Useful for testing and previewing optimization results.
"""
try:
from app.services.vrp_optimization_service import VRPOptimizationSimulator
# Get route data
route = await vrp_service.distribution_service.get_delivery_route(route_id)
if not route:
raise HTTPException(status_code=404, detail="Route not found")
# Simulate optimization
simulation_result = VRPOptimizationSimulator.simulate_optimization(route)
return {
'success': True,
'original_route': simulation_result['original_route'],
'optimized_route': simulation_result['optimized_route'],
'optimization_savings': simulation_result['optimization_savings'],
'algorithm_version': simulation_result['algorithm_version'],
'constraints_satisfied': simulation_result['constraints_satisfied'],
'objective_value': simulation_result['objective_value']
}
except Exception as e:
logger.error("VRP simulation failed", tenant_id=tenant_id, route_id=route_id, error=str(e))
raise HTTPException(status_code=500, detail=f"VRP simulation failed: {str(e)}")
# Import datetime at runtime to avoid circular imports
from datetime import datetime

View File

@@ -9,6 +9,7 @@ from app.core.database import database_manager
from app.api.routes import router as distribution_router
from app.api.shipments import router as shipments_router
from app.api.internal_demo import router as internal_demo_router
from app.api.vrp_optimization import router as vrp_optimization_router
from shared.service_base import StandardFastAPIService
@@ -122,4 +123,5 @@ service.setup_standard_endpoints()
# Note: Routes now use RouteBuilder which includes full paths, so no prefix needed
service.add_router(distribution_router, tags=["distribution"])
service.add_router(shipments_router, tags=["shipments"])
service.add_router(internal_demo_router, tags=["internal-demo"])
service.add_router(internal_demo_router, tags=["internal-demo"])
service.add_router(vrp_optimization_router, tags=["vrp-optimization"])

View File

@@ -58,6 +58,13 @@ class DeliveryRoute(Base):
total_distance_km = Column(Float, nullable=True)
estimated_duration_minutes = Column(Integer, nullable=True)
# VRP Optimization metrics (Phase 2 enhancement)
vrp_optimization_savings = Column(JSONB, nullable=True) # {"distance_saved_km": 12.5, "time_saved_minutes": 25, "fuel_saved_liters": 8.2, "co2_saved_kg": 15.4, "cost_saved_eur": 12.50}
vrp_algorithm_version = Column(String(50), nullable=True) # Version of VRP algorithm used
vrp_optimization_timestamp = Column(DateTime(timezone=True), nullable=True) # When optimization was performed
vrp_constraints_satisfied = Column(Boolean, nullable=True) # Whether all constraints were satisfied
vrp_objective_value = Column(Float, nullable=True) # Objective function value from optimization
# Route details
route_sequence = Column(JSONB, nullable=True) # Ordered array of stops with timing: [{"stop_number": 1, "location_id": "...", "estimated_arrival": "...", "actual_arrival": "..."}]
notes = Column(Text, nullable=True)

View File

@@ -231,4 +231,82 @@ class DeliveryRouteRepository:
await self.db_session.commit()
deleted_count = result.rowcount
return deleted_count
return deleted_count
async def update_route_vrp_metrics(self, route_id: str, vrp_metrics: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""
Update VRP optimization metrics for a route
"""
stmt = select(DeliveryRoute).where(DeliveryRoute.id == route_id)
result = await self.db_session.execute(stmt)
route = result.scalar_one_or_none()
if not route:
return None
# Update VRP metrics fields
route.vrp_optimization_savings = vrp_metrics.get('vrp_optimization_savings')
route.vrp_algorithm_version = vrp_metrics.get('vrp_algorithm_version')
route.vrp_optimization_timestamp = vrp_metrics.get('vrp_optimization_timestamp')
route.vrp_constraints_satisfied = vrp_metrics.get('vrp_constraints_satisfied')
route.vrp_objective_value = vrp_metrics.get('vrp_objective_value')
await self.db_session.commit()
await self.db_session.refresh(route)
return {
'id': str(route.id),
'vrp_optimization_savings': route.vrp_optimization_savings,
'vrp_algorithm_version': route.vrp_algorithm_version,
'vrp_optimization_timestamp': route.vrp_optimization_timestamp,
'vrp_constraints_satisfied': route.vrp_constraints_satisfied,
'vrp_objective_value': route.vrp_objective_value
}
async def get_routes_by_tenant(self, tenant_id: str, limit: int = None, offset: int = None, order_by: str = None) -> List[Dict[str, Any]]:
"""
Get all routes for a specific tenant with pagination and ordering
"""
stmt = select(DeliveryRoute).where(DeliveryRoute.tenant_id == tenant_id)
# Apply ordering if specified
if order_by:
if 'vrp_optimization_timestamp' in order_by:
if 'DESC' in order_by:
stmt = stmt.order_by(DeliveryRoute.vrp_optimization_timestamp.desc())
else:
stmt = stmt.order_by(DeliveryRoute.vrp_optimization_timestamp.asc())
elif 'route_date' in order_by:
if 'DESC' in order_by:
stmt = stmt.order_by(DeliveryRoute.route_date.desc())
else:
stmt = stmt.order_by(DeliveryRoute.route_date.asc())
# Apply pagination if specified
if limit is not None:
stmt = stmt.limit(limit)
if offset is not None:
stmt = stmt.offset(offset)
result = await self.db_session.execute(stmt)
routes = result.scalars().all()
return [{
'id': str(route.id),
'tenant_id': str(route.tenant_id),
'route_number': route.route_number,
'route_date': route.route_date,
'vehicle_id': route.vehicle_id,
'driver_id': route.driver_id,
'total_distance_km': route.total_distance_km,
'estimated_duration_minutes': route.estimated_duration_minutes,
'route_sequence': route.route_sequence,
'status': route.status.value if hasattr(route.status, 'value') else route.status,
'created_at': route.created_at,
'updated_at': route.updated_at,
'vrp_optimization_savings': route.vrp_optimization_savings,
'vrp_algorithm_version': route.vrp_algorithm_version,
'vrp_optimization_timestamp': route.vrp_optimization_timestamp,
'vrp_constraints_satisfied': route.vrp_constraints_satisfied,
'vrp_objective_value': route.vrp_objective_value
} for route in routes]

View File

@@ -302,4 +302,23 @@ class DistributionService:
except Exception as e:
logger.error(f"Error creating delivery schedule: {e}")
raise
raise
# VRP Optimization Service Methods
async def get_route_by_id(self, route_id: str) -> Optional[Dict[str, Any]]:
"""
Get a specific delivery route by ID
"""
return await self.route_repository.get_route_by_id(route_id)
async def update_route_vrp_metrics(self, route_id: str, vrp_metrics: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""
Update VRP optimization metrics for a route
"""
return await self.route_repository.update_route_vrp_metrics(route_id, vrp_metrics)
async def get_routes_by_tenant(self, tenant_id: str, limit: int = None, offset: int = None, order_by: str = None) -> List[Dict[str, Any]]:
"""
Get all routes for a specific tenant with pagination and ordering
"""
return await self.route_repository.get_routes_by_tenant(tenant_id, limit, offset, order_by)

View File

@@ -0,0 +1,357 @@
"""
VRP Optimization Service
Business logic for VRP optimization and metrics management
"""
from typing import List, Dict, Any, Optional
from datetime import datetime
import structlog
from sqlalchemy.ext.asyncio import AsyncSession
from app.repositories.delivery_route_repository import DeliveryRouteRepository
from app.services.routing_optimizer import RoutingOptimizer
from app.core.database import get_db
logger = structlog.get_logger()
class VRPOptimizationService:
"""
Service for VRP optimization operations
"""
def __init__(self, distribution_service: "DistributionService", database_manager: Any):
"""
Initialize VRP optimization service
Args:
distribution_service: Distribution service instance
database_manager: Database manager for session management
"""
self.distribution_service = distribution_service
self.database_manager = database_manager
self.routing_optimizer = RoutingOptimizer()
async def optimize_route(
self,
tenant_id: str,
route_id: str,
optimization_params: Dict[str, Any]
) -> Dict[str, Any]:
"""
Optimize a specific delivery route using VRP
Args:
tenant_id: Tenant ID
route_id: Route ID to optimize
optimization_params: Optimization parameters
Returns:
Optimization result with metrics
"""
try:
# Get the current route using distribution service
route = await self.distribution_service.get_route_by_id(route_id)
if not route:
raise ValueError(f"Route {route_id} not found")
# Extract deliveries from route sequence
deliveries = self._extract_deliveries_from_route(route)
# Perform VRP optimization
depot_location = optimization_params.get('depot_location', (0.0, 0.0))
vehicle_capacity = optimization_params.get('vehicle_capacity_kg', 1000.0)
time_limit = optimization_params.get('time_limit_seconds', 30.0)
optimization_result = await self.routing_optimizer.optimize_daily_routes(
deliveries=deliveries,
depot_location=depot_location,
vehicle_capacity_kg=vehicle_capacity,
time_limit_seconds=time_limit
)
# Update route with optimization metrics
vrp_metrics = {
'vrp_optimization_savings': {
'distance_saved_km': optimization_result.get('distance_savings_km', 0.0),
'time_saved_minutes': optimization_result.get('time_savings_minutes', 0.0),
'cost_saved': optimization_result.get('cost_savings', 0.0)
},
'vrp_algorithm_version': 'or-tools-v1.0',
'vrp_optimization_timestamp': datetime.utcnow(),
'vrp_constraints_satisfied': optimization_result.get('constraints_satisfied', True),
'vrp_objective_value': optimization_result.get('objective_value', 0.0)
}
# Update the route with VRP metrics using distribution service
await self.distribution_service.update_route_vrp_metrics(route_id, vrp_metrics)
return {
'success': True,
'route_id': route_id,
'optimization_metrics': vrp_metrics,
'optimized_route': optimization_result.get('optimized_route', [])
}
except Exception as e:
logger.error("vrp_optimization_failed", error=str(e), route_id=route_id)
raise
def _extract_deliveries_from_route(self, route: Any) -> List[Dict[str, Any]]:
"""
Extract deliveries from route sequence
Args:
route: Delivery route object
Returns:
List of delivery dictionaries
"""
deliveries = []
route_sequence = route.route_sequence or []
for stop in route_sequence:
deliveries.append({
'id': stop.get('id', ''),
'location': (stop.get('lat', 0.0), stop.get('lng', 0.0)),
'weight_kg': stop.get('weight_kg', 0.0),
'time_window': stop.get('time_window')
})
return deliveries
async def get_route_optimization_metrics(
self,
tenant_id: str,
route_id: str
) -> Dict[str, Any]:
"""
Get VRP optimization metrics for a specific route
Args:
tenant_id: Tenant ID
route_id: Route ID
Returns:
VRP optimization metrics
"""
route = await self.route_repository.get_route_by_id(route_id)
if not route:
raise ValueError(f"Route {route_id} not found")
return {
'vrp_optimization_savings': route.vrp_optimization_savings,
'vrp_algorithm_version': route.vrp_algorithm_version,
'vrp_optimization_timestamp': route.vrp_optimization_timestamp,
'vrp_constraints_satisfied': route.vrp_constraints_satisfied,
'vrp_objective_value': route.vrp_objective_value
}
async def get_network_optimization_summary(
self,
tenant_id: str
) -> Dict[str, Any]:
"""
Get VRP optimization summary across all routes for a tenant
Args:
tenant_id: Tenant ID
Returns:
Network optimization summary
"""
routes = await self.route_repository.get_routes_by_tenant(tenant_id)
total_optimized = 0
total_distance_saved = 0.0
total_time_saved = 0.0
total_cost_saved = 0.0
for route in routes:
if route.vrp_optimization_timestamp:
total_optimized += 1
savings = route.vrp_optimization_savings or {}
total_distance_saved += savings.get('distance_saved_km', 0.0)
total_time_saved += savings.get('time_saved_minutes', 0.0)
total_cost_saved += savings.get('cost_saved', 0.0)
return {
'total_routes': len(routes),
'total_optimized_routes': total_optimized,
'optimization_rate': total_optimized / len(routes) if routes else 0.0,
'total_distance_saved_km': total_distance_saved,
'total_time_saved_minutes': total_time_saved,
'total_cost_saved': total_cost_saved,
'average_savings_per_route': {
'distance_km': total_distance_saved / total_optimized if total_optimized > 0 else 0.0,
'time_minutes': total_time_saved / total_optimized if total_optimized > 0 else 0.0,
'cost': total_cost_saved / total_optimized if total_optimized > 0 else 0.0
}
}
async def batch_optimize_routes(
self,
tenant_id: str,
route_ids: List[str],
optimization_params: Dict[str, Any]
) -> Dict[str, Any]:
"""
Batch optimize multiple routes
Args:
tenant_id: Tenant ID
route_ids: List of route IDs to optimize
optimization_params: Optimization parameters
Returns:
Batch optimization results
"""
results = []
for route_id in route_ids:
try:
result = await self.optimize_route(tenant_id, route_id, optimization_params)
results.append({
'route_id': route_id,
'success': True,
'metrics': result['optimization_metrics']
})
except Exception as e:
results.append({
'route_id': route_id,
'success': False,
'error': str(e)
})
return {
'total_routes': len(route_ids),
'successful_optimizations': sum(1 for r in results if r['success']),
'failed_optimizations': sum(1 for r in results if not r['success']),
'results': results
}
async def validate_optimization_constraints(
self,
tenant_id: str,
route_id: str
) -> Dict[str, Any]:
"""
Validate VRP optimization constraints for a route
Args:
tenant_id: Tenant ID
route_id: Route ID
Returns:
Constraint validation results
"""
route = await self.route_repository.get_route_by_id(route_id)
if not route:
raise ValueError(f"Route {route_id} not found")
# Check if route has been optimized
if not route.vrp_optimization_timestamp:
return {
'route_id': route_id,
'is_optimized': False,
'constraints_valid': False,
'message': 'Route has not been optimized yet'
}
# Validate constraints
constraints_valid = route.vrp_constraints_satisfied or False
return {
'route_id': route_id,
'is_optimized': True,
'constraints_valid': constraints_valid,
'vrp_algorithm_version': route.vrp_algorithm_version,
'optimization_timestamp': route.vrp_optimization_timestamp
}
async def get_optimization_history(
self,
tenant_id: str,
limit: int = 50,
offset: int = 0
) -> Dict[str, Any]:
"""
Get VRP optimization history for a tenant
Args:
tenant_id: Tenant ID
limit: Maximum number of records to return
offset: Pagination offset
Returns:
Optimization history
"""
routes = await self.route_repository.get_routes_by_tenant(
tenant_id,
limit=limit,
offset=offset,
order_by='vrp_optimization_timestamp DESC'
)
history = []
for route in routes:
if route.vrp_optimization_timestamp:
history.append({
'route_id': str(route.id),
'route_number': route.route_number,
'optimization_timestamp': route.vrp_optimization_timestamp,
'algorithm_version': route.vrp_algorithm_version,
'constraints_satisfied': route.vrp_constraints_satisfied,
'objective_value': route.vrp_objective_value,
'savings': route.vrp_optimization_savings
})
return {
'total_records': len(history),
'history': history
}
async def simulate_optimization(
self,
tenant_id: str,
route_data: Dict[str, Any]
) -> Dict[str, Any]:
"""
Simulate VRP optimization without saving results
Args:
tenant_id: Tenant ID
route_data: Route data for simulation
Returns:
Simulation results
"""
try:
deliveries = route_data.get('deliveries', [])
depot_location = route_data.get('depot_location', (0.0, 0.0))
vehicle_capacity = route_data.get('vehicle_capacity_kg', 1000.0)
time_limit = route_data.get('time_limit_seconds', 30.0)
simulation_result = await self.routing_optimizer.optimize_daily_routes(
deliveries=deliveries,
depot_location=depot_location,
vehicle_capacity_kg=vehicle_capacity,
time_limit_seconds=time_limit
)
return {
'success': True,
'simulation_results': simulation_result,
'estimated_savings': {
'distance_km': simulation_result.get('distance_savings_km', 0.0),
'time_minutes': simulation_result.get('time_savings_minutes', 0.0),
'cost': simulation_result.get('cost_savings', 0.0)
}
}
except Exception as e:
logger.error("vrp_simulation_failed", error=str(e))
return {
'success': False,
'error': str(e)
}

View File

@@ -41,6 +41,12 @@ def upgrade():
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('created_by', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('updated_by', postgresql.UUID(as_uuid=True), nullable=False),
# VRP Optimization Metrics
sa.Column('vrp_optimization_savings', sa.JSON(), nullable=True),
sa.Column('vrp_algorithm_version', sa.String(length=50), nullable=True),
sa.Column('vrp_optimization_timestamp', sa.DateTime(timezone=True), nullable=True),
sa.Column('vrp_constraints_satisfied', sa.Boolean(), nullable=True),
sa.Column('vrp_objective_value', sa.Float(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('route_number')
)
@@ -53,6 +59,8 @@ def upgrade():
op.create_index('ix_delivery_routes_driver_id', 'delivery_routes', ['driver_id'])
op.create_index('ix_delivery_routes_tenant_date', 'delivery_routes', ['tenant_id', 'route_date'])
op.create_index('ix_delivery_routes_date_tenant_status', 'delivery_routes', ['route_date', 'tenant_id', 'status'])
# VRP Optimization Index
op.create_index('ix_delivery_routes_vrp_optimization', 'delivery_routes', ['vrp_optimization_timestamp'], unique=False)
# Create shipments table
@@ -156,6 +164,7 @@ def downgrade():
op.drop_table('shipments')
# Drop indexes for delivery_routes
op.drop_index('ix_delivery_routes_vrp_optimization', table_name='delivery_routes')
op.drop_index('ix_delivery_routes_date_tenant_status', table_name='delivery_routes')
op.drop_index('ix_delivery_routes_tenant_date', table_name='delivery_routes')
op.drop_index('ix_delivery_routes_driver_id', table_name='delivery_routes')