Download ops/backend-dm/controllers/routeController.js from MouleeswaranM/FairRelay: direct link, hf CLI and curl.
- Browser
- Download file 21.9 kB
-
https://huggingface.co/MouleeswaranM/FairRelay/resolve/main/ops/backend-dm/controllers/routeController.js
- Command line
-
hf download hf://MouleeswaranM/FairRelay/ops/backend-dm/controllers/routeController.js
-
curl -L -o routeController.js https://huggingface.co/MouleeswaranM/FairRelay/resolve/main/ops/backend-dm/controllers/routeController.js
21.9 kB
| const prisma = require('../config/database'); | |
| /** | |
| * Helper to calculate haversine distance (simplistic) | |
| */ | |
| function getDistance(lat1, lon1, lat2, lon2) { | |
| const R = 6371; | |
| const dLat = (lat2 - lat1) * Math.PI / 180; | |
| const dLon = (lon2 - lon1) * Math.PI / 180; | |
| const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + | |
| Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * | |
| Math.sin(dLon / 2) * Math.sin(dLon / 2); | |
| const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); | |
| return R * c; | |
| } | |
| /** | |
| * POST /api/routes/create-and-assign | |
| * Create and assign optimized route with driver relay logic | |
| */ | |
| exports.createAndAssignRoute = async (req, res) => { | |
| try { | |
| const { | |
| courierCompanyId, | |
| truckNumber, | |
| driverNumber, | |
| deliveryIds, | |
| totalDistance, | |
| totalDuration, | |
| estimatedStartTime, | |
| estimatedEndTime, | |
| baselineDistance, | |
| carbonSaved, | |
| emptyMilesSaved | |
| } = req.body; | |
| // Validate required fields | |
| if (!courierCompanyId || !truckNumber || !driverNumber || !deliveryIds || !totalDistance) { | |
| return res.status(400).json({ | |
| success: false, | |
| message: 'Missing required fields: courierCompanyId, truckId, driverId, deliveryIds, totalDistance' | |
| }); | |
| } | |
| if (!Array.isArray(deliveryIds) || deliveryIds.length === 0) { | |
| return res.status(400).json({ | |
| success: false, | |
| message: 'deliveryIds must be a non-empty array' | |
| }); | |
| } | |
| // 1. COMPANY-LEVEL ISOLATION: Validate all entities belong to same company | |
| const [driver, truck] = await Promise.all([ | |
| prisma.user.findUnique({ | |
| where: { phone: driverNumber }, | |
| select: { | |
| id: true, | |
| courierCompanyId: true, | |
| totalDistanceKm: true, | |
| role: true | |
| } | |
| }), | |
| prisma.truck.findUnique({ | |
| where: { licensePlate: truckNumber }, | |
| select: { | |
| id: true, | |
| courierCompanyId: true, | |
| maxWeight: true, | |
| maxVolume: true, | |
| currentWeight: true, | |
| currentVolume: true, | |
| isAvailable: true | |
| } | |
| }) | |
| ]); | |
| if (!driver) { | |
| return res.status(404).json({ | |
| success: false, | |
| message: 'Driver not found' | |
| }); | |
| } | |
| if (!truck) { | |
| return res.status(404).json({ | |
| success: false, | |
| message: 'Truck not found' | |
| }); | |
| } | |
| if (driver.role !== 'DRIVER') { | |
| return res.status(400).json({ | |
| success: false, | |
| message: 'Specified user is not a driver' | |
| }); | |
| } | |
| // Company isolation check | |
| if (driver.courierCompanyId !== courierCompanyId || truck.courierCompanyId !== courierCompanyId) { | |
| return res.status(403).json({ | |
| success: false, | |
| message: 'Driver and/or Truck does not belong to the specified courier company' | |
| }); | |
| } | |
| if (!truck.isAvailable) { | |
| return res.status(400).json({ | |
| success: false, | |
| message: 'Truck is not available for assignment' | |
| }); | |
| } | |
| // 2. DRIVER RELAY LOGIC: Distance-based assignment validation | |
| const isLongDistance = totalDistance > 300; | |
| const driverHistoricalDistance = driver.totalDistanceKm || 0; | |
| const HIGH_WORKLOAD_THRESHOLD = 500; // km | |
| if (isLongDistance && driverHistoricalDistance < HIGH_WORKLOAD_THRESHOLD) { | |
| return res.status(400).json({ | |
| success: false, | |
| message: `Long-distance route (${totalDistance}km) requires experienced driver with high historical distance (>${HIGH_WORKLOAD_THRESHOLD}km). Driver has only ${driverHistoricalDistance}km.` | |
| }); | |
| } | |
| if (!isLongDistance && driverHistoricalDistance >= HIGH_WORKLOAD_THRESHOLD) { | |
| return res.status(400).json({ | |
| success: false, | |
| message: `Short-distance route (${totalDistance}km) should be assigned to driver with lower workload. Driver has ${driverHistoricalDistance}km historical distance.` | |
| }); | |
| } | |
| // 3. Fetch and validate deliveries | |
| const deliveries = await prisma.delivery.findMany({ | |
| where: { | |
| id: { in: deliveryIds }, | |
| courierCompanyId | |
| }, | |
| orderBy: { pickupTime: 'asc' } | |
| }); | |
| if (deliveries.length !== deliveryIds.length) { | |
| return res.status(404).json({ | |
| success: false, | |
| message: 'One or more deliveries not found or do not belong to the specified company' | |
| }); | |
| } | |
| // Check if any delivery is already allocated | |
| const allocatedDeliveries = deliveries.filter(d => d.status !== 'PENDING'); | |
| if (allocatedDeliveries.length > 0) { | |
| return res.status(400).json({ | |
| success: false, | |
| message: `Deliveries already allocated: ${allocatedDeliveries.map(d => d.packageId).join(', ')}` | |
| }); | |
| } | |
| // 4. Calculate total weight and volume | |
| const totalWeight = deliveries.reduce((sum, d) => sum + (d.cargoWeight || 0), 0); | |
| const totalVolume = deliveries.reduce((sum, d) => sum + (d.cargoVolumeLtrs || 0), 0); | |
| // Check truck capacity | |
| const availableWeight = truck.maxWeight - truck.currentWeight; | |
| const availableVolume = truck.maxVolume - truck.currentVolume; | |
| if (totalWeight > availableWeight) { | |
| return res.status(400).json({ | |
| success: false, | |
| message: `Truck capacity exceeded. Required: ${totalWeight}kg, Available: ${availableWeight}kg` | |
| }); | |
| } | |
| if (totalVolume > availableVolume) { | |
| return res.status(400).json({ | |
| success: false, | |
| message: `Truck volume exceeded. Required: ${totalVolume}L, Available: ${availableVolume}L` | |
| }); | |
| } | |
| // 5. MULTI-STOP ARCHITECTURE: Generate waypoints | |
| const waypoints = []; | |
| deliveries.forEach(delivery => { | |
| // Add pickup waypoint | |
| waypoints.push({ | |
| type: 'PICKUP', | |
| location: delivery.pickupLocation, | |
| lat: delivery.pickupLat, | |
| lng: delivery.pickupLng, | |
| deliveryId: delivery.id, | |
| packageId: delivery.packageId | |
| }); | |
| // Add drop waypoint | |
| waypoints.push({ | |
| type: 'DROP', | |
| location: delivery.dropLocation, | |
| lat: delivery.dropLat, | |
| lng: delivery.dropLng, | |
| deliveryId: delivery.id, | |
| packageId: delivery.packageId | |
| }); | |
| }); | |
| // 6. DATABASE INTEGRITY: Use transaction for atomic operations | |
| const result = await prisma.$transaction(async (tx) => { | |
| // Create optimized route | |
| const route = await tx.optimizedRoute.create({ | |
| data: { | |
| courierCompanyId, | |
| truckId, | |
| driverId, | |
| totalDistance, | |
| totalDuration: totalDuration || 0, | |
| estimatedStartTime: estimatedStartTime ? new Date(estimatedStartTime) : new Date(), | |
| estimatedEndTime: estimatedEndTime ? new Date(estimatedEndTime) : new Date(Date.now() + (totalDuration || 60) * 60000), | |
| totalPackages: deliveries.length, | |
| totalWeight, | |
| totalVolume, | |
| utilizationPercent: truck.maxWeight ? ((totalWeight / truck.maxWeight) * 100) : 0, | |
| baselineDistance: baselineDistance || totalDistance, | |
| carbonSaved: carbonSaved || 0, | |
| emptyMilesSaved: emptyMilesSaved || 0, | |
| waypoints: waypoints, | |
| isTSPOptimized: true, | |
| status: 'ALLOCATED' | |
| }, | |
| include: { | |
| _count: { | |
| select: { deliveries: true } | |
| } | |
| } | |
| }); | |
| // Update all deliveries | |
| await tx.delivery.updateMany({ | |
| where: { id: { in: deliveryIds } }, | |
| data: { | |
| optimizedRouteId: route.id, | |
| truckId, | |
| driverId, | |
| status: 'ALLOCATED' | |
| } | |
| }); | |
| // Update truck capacity and availability | |
| await tx.truck.update({ | |
| where: { id: truckId }, | |
| data: { | |
| currentWeight: { increment: totalWeight }, | |
| currentVolume: { increment: totalVolume }, | |
| isAvailable: false | |
| } | |
| }); | |
| // Update driver's total distance (for future relay logic) | |
| await tx.user.update({ | |
| where: { id: driverId }, | |
| data: { | |
| totalDistanceKm: { increment: totalDistance } | |
| } | |
| }); | |
| return route; | |
| }); | |
| res.status(201).json({ | |
| success: true, | |
| message: `Route created and assigned successfully with ${deliveries.length} deliveries`, | |
| data: { | |
| route: result | |
| } | |
| }); | |
| } catch (error) { | |
| console.error('Create and assign route error:', error); | |
| res.status(500).json({ | |
| success: false, | |
| message: 'Failed to create and assign route', | |
| error: error.message | |
| }); | |
| } | |
| }; | |
| /** | |
| * POST /api/routes/allocate | |
| * Triggers TSP/Greedy allocation for pending deliveries | |
| */ | |
| exports.allocateRoutes = async (req, res) => { | |
| try { | |
| const { courierCompanyId } = req.body; | |
| if (!courierCompanyId) { | |
| return res.status(400).json({ success: false, message: 'courierCompanyId is required' }); | |
| } | |
| // 1. Fetch pending deliveries for this company | |
| const pendingDeliveries = await prisma.delivery.findMany({ | |
| where: { | |
| courierCompanyId, | |
| status: 'PENDING' | |
| }, | |
| orderBy: { timeWindowStart: 'asc' } | |
| }); | |
| if (pendingDeliveries.length === 0) { | |
| return res.status(200).json({ success: true, message: 'No pending deliveries to allocate' }); | |
| } | |
| // 2. Fetch available trucks for this company | |
| const availableTrucks = await prisma.truck.findMany({ | |
| where: { | |
| courierCompanyId, | |
| isAvailable: true, | |
| registrationStatus: 'APPROVED' | |
| }, | |
| include: { owner: true } | |
| }); | |
| if (availableTrucks.length === 0) { | |
| return res.status(400).json({ success: false, message: 'No available trucks for allocation' }); | |
| } | |
| // 3. Greedy Allocation Logic | |
| // For simplicity, we loop through trucks and fill them up with deliveries | |
| const allocations = []; | |
| let deliveryIndex = 0; | |
| for (const truck of availableTrucks) { | |
| if (deliveryIndex >= pendingDeliveries.length) break; | |
| let currentWeight = 0; | |
| let currentVolume = 0; | |
| const routeDeliveries = []; | |
| while (deliveryIndex < pendingDeliveries.length) { | |
| const delivery = pendingDeliveries[deliveryIndex]; | |
| // Check capacity | |
| const fitsWeight = (currentWeight + delivery.cargoWeight) <= (truck.maxWeight || Infinity); | |
| const fitsVolume = (currentVolume + delivery.cargoVolumeLtrs) <= (truck.maxVolume || Infinity); | |
| if (fitsWeight && fitsVolume) { | |
| routeDeliveries.push(delivery); | |
| currentWeight += delivery.cargoWeight; | |
| currentVolume += delivery.cargoVolumeLtrs; | |
| deliveryIndex++; | |
| } else { | |
| // Truck full | |
| break; | |
| } | |
| } | |
| if (routeDeliveries.length > 0) { | |
| allocations.push({ | |
| truck, | |
| deliveries: routeDeliveries, | |
| totalWeight: currentWeight, | |
| totalVolume: currentVolume | |
| }); | |
| } | |
| } | |
| // 4. Create OptimizedRoute records and update deliveries | |
| const results = await prisma.$transaction(async (tx) => { | |
| const createdRoutes = []; | |
| for (const alloc of allocations) { | |
| const { truck, deliveries, totalWeight, totalVolume } = alloc; | |
| // Create Route | |
| const route = await tx.optimizedRoute.create({ | |
| data: { | |
| courierCompanyId, | |
| truckId: truck.id, | |
| driverId: truck.ownerId, | |
| totalDistance: 0, // Placeholder | |
| totalDuration: 60, // Placeholder | |
| estimatedStartTime: new Date(), | |
| estimatedEndTime: new Date(Date.now() + 3600000), | |
| totalPackages: deliveries.length, | |
| totalWeight, | |
| totalVolume, | |
| utilizationPercent: truck.maxWeight ? (totalWeight / truck.maxWeight) * 100 : 0, | |
| baselineDistance: 0, | |
| carbonSaved: 0, | |
| emptyMilesSaved: 0, | |
| status: 'ALLOCATED' | |
| } | |
| }); | |
| // Update Deliveries | |
| await tx.delivery.updateMany({ | |
| where: { id: { in: deliveries.map(d => d.id) } }, | |
| data: { | |
| truckId: truck.id, | |
| driverId: truck.ownerId, | |
| optimizedRouteId: route.id, | |
| status: 'ALLOCATED' | |
| } | |
| }); | |
| // Update Truck availability | |
| await tx.truck.update({ | |
| where: { id: truck.id }, | |
| data: { isAvailable: false } | |
| }); | |
| createdRoutes.push(route); | |
| } | |
| return createdRoutes; | |
| }); | |
| res.status(201).json({ | |
| success: true, | |
| message: `Allocated ${allocations.length} routes for ${pendingDeliveries.length} packages`, | |
| data: results | |
| }); | |
| } catch (err) { | |
| console.error('Allocation Error:', err); | |
| res.status(500).json({ success: false, message: 'Internal server error during allocation', error: err.message }); | |
| } | |
| }; | |
| /** | |
| * POST /api/routes/assign-multi-stop | |
| * Resolve identifiers, match deliveries, and assign multi-stop route | |
| */ | |
| exports.assignMultiStopRoute = async (req, res) => { | |
| try { | |
| const { | |
| driverPhone, | |
| truckLicensePlate, | |
| courierCompanyId, | |
| totalDistance, | |
| checkpoints | |
| } = req.body; | |
| // 1. Basic Validation | |
| if (!driverPhone || !truckLicensePlate || !courierCompanyId || !totalDistance || !Array.isArray(checkpoints)) { | |
| return res.status(400).json({ | |
| success: false, | |
| message: 'Missing required fields: driverPhone, truckLicensePlate, courierCompanyId, totalDistance, checkpoints' | |
| }); | |
| } | |
| // 2. Asset Identification (Lookup Phase) | |
| const [driver, truck] = await Promise.all([ | |
| prisma.user.findUnique({ | |
| where: { phone: driverPhone }, | |
| select: { id: true, role: true, courierCompanyId: true, totalDistanceKm: true } | |
| }), | |
| prisma.truck.findUnique({ | |
| where: { licensePlate: truckLicensePlate }, | |
| select: { id: true, courierCompanyId: true, maxWeight: true, maxVolume: true, currentWeight: true, currentVolume: true } | |
| }) | |
| ]); | |
| if (!driver || driver.role !== 'DRIVER' || driver.courierCompanyId !== courierCompanyId) { | |
| return res.status(404).json({ | |
| success: false, | |
| message: 'Driver not found or does not belong to the specified company' | |
| }); | |
| } | |
| if (!truck || truck.courierCompanyId !== courierCompanyId) { | |
| return res.status(404).json({ | |
| success: false, | |
| message: 'Truck not found or does not belong to the specified company' | |
| }); | |
| } | |
| // 3. Delivery Resolution (Matching Phase) | |
| const deliveryIds = []; | |
| const deliveriesToLink = []; | |
| for (const cp of checkpoints) { | |
| const delivery = await prisma.delivery.findFirst({ | |
| where: { | |
| pickupLocation: cp.pickupLocation, | |
| dropLocation: cp.dropLocation, | |
| status: 'PENDING', | |
| // Relaxed constraints: find the delivery even if company or truck is not yet set correctly | |
| OR: [ | |
| { courierCompanyId: courierCompanyId }, | |
| { courierCompanyId: null } | |
| ] | |
| } | |
| }); | |
| if (!delivery) { | |
| return res.status(400).json({ | |
| success: false, | |
| message: `Checkpoint mismatch: No PENDING delivery found for truck ${truckLicensePlate} from "${cp.pickupLocation}" to "${cp.dropLocation}"` | |
| }); | |
| } | |
| deliveryIds.push(delivery.id); | |
| deliveriesToLink.push(delivery); | |
| } | |
| // 4. Enforce Driver Relay Logic | |
| const HIGH_WORKLOAD_THRESHOLD = 500; // km | |
| const isLongDistance = totalDistance > 300; | |
| const driverHistory = driver.totalDistanceKm || 0; | |
| if (isLongDistance && driverHistory < HIGH_WORKLOAD_THRESHOLD) { | |
| return res.status(400).json({ | |
| success: false, | |
| message: `Workload Balance Error: Long-distance mission (${totalDistance}km) requires experienced driver (>${HIGH_WORKLOAD_THRESHOLD}km history). Driver has ${driverHistory}km.` | |
| }); | |
| } | |
| if (!isLongDistance && driverHistory >= HIGH_WORKLOAD_THRESHOLD) { | |
| return res.status(400).json({ | |
| success: false, | |
| message: `Workload Balance Error: Short-haul mission (${totalDistance}km) should be assigned to driver with less history. Driver has ${driverHistory}km.` | |
| }); | |
| } | |
| // 5. Atomic Transaction | |
| const totalWeight = deliveriesToLink.reduce((sum, d) => sum + (d.cargoWeight || 0), 0); | |
| const totalVolume = deliveriesToLink.reduce((sum, d) => sum + (d.cargoVolumeLtrs || 0), 0); | |
| // Generate Waypoints | |
| const waypoints = []; | |
| deliveriesToLink.forEach(d => { | |
| waypoints.push({ type: 'PICKUP', location: d.pickupLocation, lat: d.pickupLat, lng: d.pickupLng, deliveryId: d.id }); | |
| waypoints.push({ type: 'DROP', location: d.dropLocation, lat: d.dropLat, lng: d.dropLng, deliveryId: d.id }); | |
| }); | |
| const result = await prisma.$transaction(async (tx) => { | |
| const route = await tx.optimizedRoute.create({ | |
| data: { | |
| courierCompanyId, | |
| truckId: truck.id, | |
| driverId: driver.id, | |
| totalDistance, | |
| totalPackages: deliveriesToLink.length, | |
| totalWeight, | |
| totalVolume, | |
| utilizationPercent: truck.maxWeight ? (totalWeight / truck.maxWeight) * 100 : 0, | |
| baselineDistance: totalDistance, | |
| carbonSaved: 0, | |
| emptyMilesSaved: 0, | |
| waypoints: waypoints, | |
| estimatedStartTime: new Date(), | |
| estimatedEndTime: new Date(Date.now() + 3600000), // Default 1hr | |
| status: 'ALLOCATED' | |
| } | |
| }); | |
| await tx.delivery.updateMany({ | |
| where: { id: { in: deliveryIds } }, | |
| data: { | |
| optimizedRouteId: route.id, | |
| driverId: driver.id, | |
| status: 'ALLOCATED' | |
| } | |
| }); | |
| // Update driver total distance | |
| await tx.user.update({ | |
| where: { id: driver.id }, | |
| data: { totalDistanceKm: { increment: totalDistance } } | |
| }); | |
| // Update truck (mark not available) | |
| await tx.truck.update({ | |
| where: { id: truck.id }, | |
| data: { isAvailable: false } | |
| }); | |
| return route; | |
| }); | |
| res.status(201).json({ | |
| success: true, | |
| message: 'Multi-stop route created successfully', | |
| data: { | |
| route: result, | |
| checkpointsLinked: checkpoints.length | |
| } | |
| }); | |
| } catch (error) { | |
| console.error('Multi-stop assignment error:', error); | |
| res.status(500).json({ | |
| success: false, | |
| message: 'Internal server error during multi-stop assignment', | |
| error: error.message | |
| }); | |
| } | |
| }; | |
| module.exports = exports; | |