| const prisma = require('../config/database'); |
| const axios = require('axios'); |
| const Joi = require('joi'); |
| const { AI_SERVICE_URL } = process.env; |
|
|
| |
| |
| |
| const createShipment = async (req, res) => { |
| try { |
| |
| 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; |
|
|
| |
| const distanceKm = calculateDistance( |
| { lat: pickupLat, lng: pickupLng }, |
| { lat: dropLat, lng: dropLng } |
| ); |
|
|
| |
| 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'); |
| |
| 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), |
| } |
| } |
| }; |
| } |
|
|
| |
| 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, |
| }, |
| 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', |
| }); |
| } |
| }; |
|
|
| |
| |
| |
| 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', |
| }); |
| } |
| }; |
|
|
| |
| |
| |
| 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', |
| }); |
| } |
|
|
| |
| 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', |
| }); |
| } |
| }; |
|
|
| |
| |
| |
| 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', |
| }); |
| } |
|
|
| |
| if (shipment.shipperId !== req.user.id) { |
| return res.status(403).json({ |
| success: false, |
| message: 'Access denied', |
| }); |
| } |
|
|
| |
| 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', |
| }); |
| } |
| }; |
|
|
| |
| |
| |
| 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', |
| }); |
| } |
| }; |
|
|
| |
| |
| |
| 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', |
| }); |
| } |
|
|
| |
| const truck = await prisma.truck.findFirst({ |
| where: { driverId }, |
| }); |
|
|
| if (!truck) { |
| return res.status(400).json({ |
| success: false, |
| message: 'No truck assigned to driver', |
| }); |
| } |
|
|
| |
| const result = await prisma.$transaction(async (tx) => { |
| |
| const updatedShipment = await tx.shipment.update({ |
| where: { id }, |
| data: { status: 'ASSIGNED' }, |
| }); |
|
|
| |
| 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', |
| }); |
| } |
| }; |
|
|
| |
| const calculateDistance = (point1, point2) => { |
| const R = 6371; |
| 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, |
| }; |