FairRelay / ops /backend-dm /controllers /shipmentController.js
MouleeswaranM's picture
Upload folder using huggingface_hub
fcf8749 verified
const prisma = require('../config/database');
const axios = require('axios');
const Joi = require('joi');
const { AI_SERVICE_URL } = process.env;
/**
* POST /api/shipments/create - Create shipment (calls XGBoost)
*/
const createShipment = async (req, res) => {
try {
// Validation
const schema = Joi.object({
pickupLat: Joi.number().required(),
pickupLng: Joi.number().required(),
pickupLocation: Joi.string().max(200).required(),
dropLat: Joi.number().required(),
dropLng: Joi.number().required(),
dropLocation: Joi.string().max(200).required(),
cargoType: Joi.string().valid(
'Electronics', 'Industrial Machinery', 'Textiles',
'Automotive Parts', 'FMCG Products', 'Pharmaceuticals',
'Steel & Metal', 'Agricultural Products', 'Furniture'
).required(),
cargoWeight: Joi.number().min(0.1).max(50).required(),
specialInstructions: Joi.string().max(500).optional(),
priority: Joi.string().valid('LOW', 'MEDIUM', 'HIGH').default('LOW'),
});
const { error } = schema.validate(req.body);
if (error) {
return res.status(400).json({
success: false,
message: error.details[0].message,
});
}
const {
pickupLat, pickupLng, pickupLocation,
dropLat, dropLng, dropLocation,
cargoType, cargoWeight, specialInstructions, priority
} = req.body;
// Calculate distance (Haversine formula)
const distanceKm = calculateDistance(
{ lat: pickupLat, lng: pickupLng },
{ lat: dropLat, lng: dropLng }
);
// Call XGBoost AI Pricing Service
let aiPriceResponse;
try {
aiPriceResponse = await axios.post(`${AI_SERVICE_URL}/pricing/predict`, {
distance_km: distanceKm,
cargo_weight_tonnes: cargoWeight,
cargo_type: cargoType,
pickup_city: extractCity(pickupLocation),
drop_city: extractCity(dropLocation),
time_of_day: new Date().getHours() < 18 ? 'day' : 'night',
day_of_week: ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'][new Date().getDay()],
fuel_price: 98.5,
traffic_level: 'medium',
urgency: priority.toLowerCase(),
}, { timeout: 5000 });
} catch (aiError) {
console.log('❌ AI service unavailable, using fallback pricing');
// Fallback pricing formula
aiPriceResponse = {
data: {
predicted_price: Math.round((distanceKm * 18 + cargoWeight * 100) / 10) * 10,
confidence: 0.75,
price_breakdown: {
base_rate: Math.round(distanceKm * 15),
distance_premium: Math.round(distanceKm * 2),
weight_premium: Math.round(cargoWeight * 80),
}
}
};
}
// Create shipment
const shipment = await prisma.shipment.create({
data: {
shipperId: req.user.id,
pickupLocation,
pickupLat,
pickupLng,
dropLocation,
dropLat,
dropLng,
cargoType,
cargoWeight,
specialInstructions: specialInstructions || null,
estimatedPrice: aiPriceResponse.data.predicted_price,
status: 'PENDING',
priority,
isMarketplaceLoad: Math.random() > 0.7, // 30% chance
},
include: {
shipper: {
select: { name: true, phone: true }
}
}
});
res.status(201).json({
success: true,
message: 'Shipment created successfully',
data: {
id: shipment.id,
status: shipment.status,
estimatedPrice: shipment.estimatedPrice,
aiConfidence: aiPriceResponse.data.confidence,
distanceKm,
priceBreakdown: aiPriceResponse.data.price_breakdown,
shipment,
},
});
} catch (error) {
console.error('Create shipment error:', error);
res.status(500).json({
success: false,
message: 'Failed to create shipment',
});
}
};
/**
* GET /api/shipments/my-shipments - List shipper's shipments
*/
const getMyShipments = async (req, res) => {
try {
const shipments = await prisma.shipment.findMany({
where: { shipperId: req.user.id },
orderBy: { createdAt: 'desc' },
include: {
dispatcher: {
select: { name: true }
},
delivery: {
include: {
driver: {
select: { name: true, rating: true, phone: true }
},
truck: {
select: { licensePlate: true, model: true }
}
}
}
},
take: 20,
});
res.status(200).json({
success: true,
data: shipments,
count: shipments.length,
});
} catch (error) {
console.error('Get shipments error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch shipments',
});
}
};
/**
* GET /api/shipments/:id - Get single shipment
*/
const getShipment = async (req, res) => {
try {
const { id } = req.params;
const shipment = await prisma.shipment.findUnique({
where: { id },
include: {
shipper: {
select: { name: true, phone: true }
},
dispatcher: {
select: { name: true }
},
delivery: {
include: {
driver: {
select: { name: true, rating: true, phone: true }
},
truck: {
select: { licensePlate: true, model: true }
}
}
}
},
});
if (!shipment) {
return res.status(404).json({
success: false,
message: 'Shipment not found',
});
}
// Only shipper or dispatcher can view
if (shipment.shipperId !== req.user.id && req.user.role !== 'DISPATCHER') {
return res.status(403).json({
success: false,
message: 'Access denied',
});
}
res.status(200).json({
success: true,
data: shipment,
});
} catch (error) {
console.error('Get shipment error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch shipment',
});
}
};
/**
* PUT /api/shipments/:id/cancel - Cancel pending shipment
*/
const cancelShipment = async (req, res) => {
try {
const { id } = req.params;
const shipment = await prisma.shipment.findUnique({
where: { id },
});
if (!shipment) {
return res.status(404).json({
success: false,
message: 'Shipment not found',
});
}
// Only shipper can cancel their own shipment
if (shipment.shipperId !== req.user.id) {
return res.status(403).json({
success: false,
message: 'Access denied',
});
}
// Only cancel PENDING shipments
if (!['PENDING', 'AWAITING_DISPATCHER'].includes(shipment.status)) {
return res.status(400).json({
success: false,
message: 'Only pending shipments can be cancelled',
});
}
await prisma.shipment.update({
where: { id },
data: { status: 'CANCELLED' },
});
res.status(200).json({
success: true,
message: 'Shipment cancelled successfully',
});
} catch (error) {
console.error('Cancel shipment error:', error);
res.status(500).json({
success: false,
message: 'Failed to cancel shipment',
});
}
};
/**
* GET /api/shipments/pending - Get all pending shipments for drivers
*/
const getPendingShipments = async (req, res) => {
try {
const shipments = await prisma.shipment.findMany({
where: {
status: 'PENDING',
},
orderBy: { createdAt: 'desc' },
include: {
shipper: {
select: { name: true, phone: true }
}
},
take: 20,
});
res.status(200).json({
success: true,
data: shipments,
count: shipments.length,
});
} catch (error) {
console.error('Get pending shipments error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch pending shipments',
});
}
};
/**
* POST /api/shipments/:id/accept - Driver accepts a shipment
*/
const acceptShipment = async (req, res) => {
try {
const { id } = req.params;
const driverId = req.user.id;
const shipment = await prisma.shipment.findUnique({
where: { id },
});
if (!shipment) {
return res.status(404).json({
success: false,
message: 'Shipment not found',
});
}
if (shipment.status !== 'PENDING') {
return res.status(400).json({
success: false,
message: 'Shipment already assigned',
});
}
// Get driver's truck
const truck = await prisma.truck.findFirst({
where: { driverId },
});
if (!truck) {
return res.status(400).json({
success: false,
message: 'No truck assigned to driver',
});
}
// Create delivery and update shipment in transaction
const result = await prisma.$transaction(async (tx) => {
// Update shipment status
const updatedShipment = await tx.shipment.update({
where: { id },
data: { status: 'ASSIGNED' },
});
// Create delivery record
const delivery = await tx.delivery.create({
data: {
driverId,
truckId: truck.id,
shipmentId: id,
pickupLocation: shipment.pickupLocation,
pickupLat: shipment.pickupLat,
pickupLng: shipment.pickupLng,
dropLocation: shipment.dropLocation,
dropLat: shipment.dropLat,
dropLng: shipment.dropLng,
cargoType: shipment.cargoType,
cargoWeight: shipment.cargoWeight,
status: 'ALLOCATED',
estimatedPrice: shipment.estimatedPrice || 0,
},
});
return { shipment: updatedShipment, delivery };
});
res.status(200).json({
success: true,
message: 'Shipment accepted! Check your deliveries.',
data: result,
});
} catch (error) {
console.error('Accept shipment error:', error);
res.status(500).json({
success: false,
message: 'Failed to accept shipment',
});
}
};
// Helper functions
const calculateDistance = (point1, point2) => {
const R = 6371; // Earth's radius in km
const dLat = (point2.lat - point1.lat) * Math.PI / 180;
const dLng = (point2.lng - point1.lng) * Math.PI / 180;
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(point1.lat * Math.PI / 180) * Math.cos(point2.lat * Math.PI / 180) *
Math.sin(dLng / 2) * Math.sin(dLng / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return Math.round(R * c);
};
const extractCity = (location) => {
const cities = ['Mumbai', 'Delhi', 'Bangalore', 'Pune', 'Hyderabad', 'Chennai'];
const city = cities.find(city => location.includes(city));
return city || 'Other';
};
module.exports = {
createShipment,
getMyShipments,
getShipment,
cancelShipment,
getPendingShipments,
acceptShipment,
};