FairRelay / ops /backend-dm /controllers /routeController.js
MouleeswaranM's picture
Upload folder using huggingface_hub
fcf8749 verified
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;