FairRelay / ops /backend-dm /controllers /synergyController.js
MouleeswaranM's picture
Upload folder using huggingface_hub
fcf8749 verified
const prisma = require('../config/database');
/**
* Phase 1: Continuous Proximity & Detection (moved to services/synergyMonitor.js)
*/
/**
* Phase 2: Dispatcher Orchestration - Accept Synergy
* Dispatcher approves the opportunity and initiates handshake
*/
async function dispatcherAcceptSynergy(req, res) {
try {
const { opportunityId, dispatcherId } = req.body;
const io = req.app.get('io');
// Validate opportunity
const opportunity = await prisma.absorptionOpportunity.findUnique({
where: { id: opportunityId },
include: {
route1: {
include: {
truck: { include: { owner: true } },
deliveries: true
}
},
route2: {
include: {
truck: { include: { owner: true } },
deliveries: true
}
},
nearestHub: true
}
});
if (!opportunity) {
return res.status(404).json({
success: false,
message: 'Opportunity not found'
});
}
if (opportunity.status !== 'PENDING') {
return res.status(400).json({
success: false,
message: 'Opportunity already processed'
});
}
// Update opportunity status
await prisma.absorptionOpportunity.update({
where: { id: opportunityId },
data: {
status: 'BOTH_ACCEPTED',
acceptedByRoute1At: new Date(),
acceptedByRoute2At: new Date()
}
});
// Create AbsorptionTransfer record
const transfer = await prisma.absorptionTransfer.create({
data: {
absorptionOpportunityId: opportunityId,
exporterDriverId: opportunity.route2.truck.owner.id, // Truck B exports
importerDriverId: opportunity.route1.truck.owner.id, // Truck A imports
hubId: opportunity.nearestHubId,
deliveryIdsToTransfer: opportunity.eligibleDeliveryIds,
spaceAvailableExporter: opportunity.truck2SpaceAvailable,
spaceAvailableImporter: opportunity.truck1SpaceAvailable,
distanceSavedKm: opportunity.totalDistanceSaved,
carbonSavedKg: opportunity.potentialCarbonSaved,
status: 'PENDING'
}
});
// Emit HANDSHAKE_REQUIRED to both drivers
const handshakeData = {
transferId: transfer.id,
hubLat: opportunity.nearestHub.latitude,
hubLng: opportunity.nearestHub.longitude,
hubName: opportunity.nearestHub.name,
hubAddress: opportunity.nearestHub.address
};
// Emit to Truck A driver (Importer)
io.to(`driver_${opportunity.route1.truck.owner.id}`).emit('HANDSHAKE_REQUIRED', {
...handshakeData,
role: 'IMPORTER',
counterpartTruck: opportunity.route2.truck.licensePlate,
counterpartDriver: opportunity.route2.truck.owner.name
});
// Emit to Truck B driver (Exporter)
io.to(`driver_${opportunity.route2.truck.owner.id}`).emit('HANDSHAKE_REQUIRED', {
...handshakeData,
role: 'EXPORTER',
counterpartTruck: opportunity.route1.truck.licensePlate,
counterpartDriver: opportunity.route1.truck.owner.name
});
res.status(200).json({
success: true,
message: 'Synergy accepted, drivers notified',
data: {
transferId: transfer.id,
hubCoordinates: {
lat: opportunity.nearestHub.latitude,
lng: opportunity.nearestHub.longitude
}
}
});
} catch (error) {
console.error('Dispatcher accept error:', error);
res.status(500).json({
success: false,
message: error.message || 'Failed to accept synergy'
});
}
}
/**
* Phase 3: Generate QR Code (Exporter - Truck B)
*/
async function generateQRCode(req, res) {
try {
const { transferId, truckId } = req.body;
const userId = req.user.id;
// Validate transfer
const transfer = await prisma.absorptionTransfer.findUnique({
where: { id: transferId },
include: {
exporterDriver: true,
absorptionOpportunity: {
include: {
route2: {
include: {
deliveries: {
select: {
id: true,
packageId: true,
cargoType: true,
cargoWeight: true,
cargoVolumeLtrs: true,
pickupLocation: true,
dropLocation: true
}
}
}
}
}
}
}
});
if (!transfer) {
return res.status(404).json({
success: false,
message: 'Transfer not found'
});
}
// Verify user is the exporter
if (transfer.exporterDriverId !== userId) {
return res.status(403).json({
success: false,
message: 'Only exporter can generate QR code'
});
}
// Get deliveries to transfer
const deliveryIds = transfer.deliveryIdsToTransfer.split(',');
const deliveries = transfer.absorptionOpportunity.route2.deliveries.filter(
d => deliveryIds.includes(d.id)
);
// Calculate totals
const totalWeight = deliveries.reduce((sum, d) => sum + d.cargoWeight, 0);
const totalVolume = deliveries.reduce((sum, d) => sum + d.cargoVolumeLtrs, 0);
// Generate QR payload
const qrPayload = {
transferId: transfer.id,
deliveryIds: deliveries.map(d => d.id),
totalWeight,
totalVolume,
packageDetails: deliveries.map(d => ({
packageId: d.packageId,
cargoType: d.cargoType,
weight: d.cargoWeight,
volume: d.cargoVolumeLtrs,
from: d.pickupLocation,
to: d.dropLocation
})),
timestamp: new Date().toISOString()
};
const qrData = JSON.stringify(qrPayload);
// Store QR data in transfer
await prisma.absorptionTransfer.update({
where: { id: transferId },
data: { qrCodeData: qrData }
});
res.status(200).json({
success: true,
message: 'QR code generated',
data: { qrData }
});
} catch (error) {
console.error('QR generation error:', error);
res.status(500).json({
success: false,
message: error.message || 'Failed to generate QR code'
});
}
}
/**
* Phase 3: Verify QR Code (Importer - Truck A)
*/
async function verifyQR(req, res) {
try {
const { transferId, qrData, currentLat, currentLng } = req.body;
const userId = req.user.id;
const io = req.app.get('io');
// Validate transfer
const transfer = await prisma.absorptionTransfer.findUnique({
where: { id: transferId },
include: {
importerDriver: true,
hub: true
}
});
if (!transfer) {
return res.status(404).json({
success: false,
message: 'Transfer not found'
});
}
// Verify user is the importer
if (transfer.importerDriverId !== userId) {
return res.status(403).json({
success: false,
message: 'Only importer can verify QR code'
});
}
// Parse and validate QR data
let scannedData;
try {
scannedData = JSON.parse(qrData);
} catch (e) {
return res.status(400).json({
success: false,
message: 'Invalid QR code format'
});
}
// Verify transfer ID matches
if (scannedData.transferId !== transferId) {
return res.status(400).json({
success: false,
message: 'QR code does not match this transfer'
});
}
// Verify delivery IDs match
const expectedIds = transfer.deliveryIdsToTransfer.split(',').sort();
const scannedIds = scannedData.deliveryIds.sort();
if (JSON.stringify(expectedIds) !== JSON.stringify(scannedIds)) {
return res.status(400).json({
success: false,
message: 'Delivery IDs mismatch'
});
}
// Generate checklist data
const checklistData = scannedData.packageDetails.map(pkg => ({
packageId: pkg.packageId,
cargoType: pkg.cargoType,
weight: pkg.weight,
volume: pkg.volume,
from: pkg.from,
to: pkg.to,
verified: false
}));
// Update transfer status
await prisma.absorptionTransfer.update({
where: { id: transferId },
data: {
status: 'QR_SCANNED',
qrCodeScanned: true,
scannedAt: new Date(),
checklistData: checklistData
}
});
// Emit QR_SCANNED event to dispatcher
io.emit('QR_SCANNED', {
transferId,
location: { lat: currentLat, lng: currentLng },
timestamp: new Date().toISOString(),
importerDriver: transfer.importerDriver.name
});
res.status(200).json({
success: true,
message: 'QR code verified successfully',
data: { checklistData }
});
} catch (error) {
console.error('QR verification error:', error);
res.status(500).json({
success: false,
message: error.message || 'Failed to verify QR code'
});
}
}
/**
* Phase 4: Complete Handover
*/
async function completeHandover(req, res) {
try {
const { transferId, photos, checklistData } = req.body;
const userId = req.user.id;
const io = req.app.get('io');
// Validate transfer
const transfer = await prisma.absorptionTransfer.findUnique({
where: { id: transferId },
include: {
importerDriver: true,
exporterDriver: true,
absorptionOpportunity: {
include: {
route1: { include: { truck: true } },
route2: { include: { truck: true } }
}
}
}
});
if (!transfer) {
return res.status(404).json({
success: false,
message: 'Transfer not found'
});
}
// Verify user is the importer
if (transfer.importerDriverId !== userId) {
return res.status(403).json({
success: false,
message: 'Only importer can complete handover'
});
}
// Verify QR was scanned
if (transfer.status !== 'QR_SCANNED' && transfer.status !== 'CHECKLIST_VERIFIED') {
return res.status(400).json({
success: false,
message: 'QR code must be scanned first'
});
}
const deliveryIds = transfer.deliveryIdsToTransfer.split(',');
const truckA = transfer.absorptionOpportunity.route1.truck;
const truckB = transfer.absorptionOpportunity.route2.truck;
// Execute handover in transaction
await prisma.$transaction(async (tx) => {
// 1. Update photos
await tx.absorptionTransfer.update({
where: { id: transferId },
data: {
photos: photos || [],
checklistData: checklistData,
status: 'COMPLETED',
completedAt: new Date()
}
});
// 2. Get deliveries to calculate weight/volume
const deliveries = await tx.delivery.findMany({
where: { id: { in: deliveryIds } }
});
const totalWeight = deliveries.reduce((sum, d) => sum + d.cargoWeight, 0);
const totalVolume = deliveries.reduce((sum, d) => sum + d.cargoVolumeLtrs, 0);
// 3. Update E-Way Bills
await tx.eWayBill.updateMany({
where: {
driverId: transfer.exporterDriverId,
status: 'ACTIVE'
},
data: {
vehicleNo: truckA.licensePlate,
driverId: transfer.importerDriverId,
status: 'TRANSFERRED'
}
});
// 4. Reassign deliveries from Truck B to Truck A
await tx.delivery.updateMany({
where: { id: { in: deliveryIds } },
data: {
truckId: truckA.id,
driverId: transfer.importerDriverId,
status: 'ABSORPTION_TRANSFERRED'
}
});
// 5. Update Truck A (add weight/volume)
await tx.truck.update({
where: { id: truckA.id },
data: {
currentWeight: { increment: totalWeight },
currentVolume: { increment: totalVolume }
}
});
// 6. Update Truck B (reduce weight/volume)
await tx.truck.update({
where: { id: truckB.id },
data: {
currentWeight: { decrement: totalWeight },
currentVolume: { decrement: totalVolume }
}
});
// 7. Update opportunity status
await tx.absorptionOpportunity.update({
where: { id: transfer.absorptionOpportunityId },
data: { status: 'COMPLETED' }
});
});
// Emit TRANSFER_COMPLETED event
io.emit('TRANSFER_COMPLETED', {
transferId,
deliveryIds,
newTruckId: truckA.id,
newTruckPlate: truckA.licensePlate,
importerDriver: transfer.importerDriver.name,
exporterDriver: transfer.exporterDriver.name,
timestamp: new Date().toISOString()
});
// Notify both drivers
io.to(`driver_${transfer.importerDriverId}`).emit('TRANSFER_COMPLETED', {
message: 'Handover completed successfully',
role: 'IMPORTER'
});
io.to(`driver_${transfer.exporterDriverId}`).emit('TRANSFER_COMPLETED', {
message: 'Handover completed successfully',
role: 'EXPORTER'
});
res.status(200).json({
success: true,
message: 'Handover completed successfully',
data: {
updatedDeliveries: deliveryIds.length,
updatedEWayBills: await prisma.eWayBill.count({
where: {
driverId: transfer.importerDriverId,
status: 'TRANSFERRED'
}
})
}
});
} catch (error) {
console.error('Handover completion error:', error);
res.status(500).json({
success: false,
message: error.message || 'Failed to complete handover'
});
}
}
/**
* Legacy functions (kept for backward compatibility)
*/
async function searchSynergy(req, res) {
try {
const { truckId } = req.body;
const truck = await prisma.truck.findUnique({ where: { id: truckId } });
if (!truck) {
return res.status(404).json({ message: 'Truck not found' });
}
res.status(200).json({
success: true,
message: 'Manual search deprecated. Use continuous monitoring service.'
});
} catch (error) {
res.status(500).json({ error: error.message });
}
}
async function acceptSynergy(req, res) {
// DEPRECATED: Driver acceptance is no longer allowed
// Only dispatcher can accept synergy opportunities
try {
return res.status(403).json({
success: false,
message: 'Driver acceptance not allowed. Synergy opportunities are approved by dispatcher only. Please wait for dispatcher approval.',
deprecated: true
});
} catch (error) {
res.status(500).json({ error: error.message });
}
}
/**
* Phase 2: Dispatcher Orchestration - Reject Synergy
* Dispatcher rejects the opportunity
*/
async function dispatcherRejectSynergy(req, res) {
try {
const { opportunityId, dispatcherId, reason } = req.body;
const io = req.app.get('io');
// Validate opportunity
const opportunity = await prisma.absorptionOpportunity.findUnique({
where: { id: opportunityId },
include: {
route1: {
include: {
truck: { include: { owner: true } }
}
},
route2: {
include: {
truck: { include: { owner: true } }
}
}
}
});
if (!opportunity) {
return res.status(404).json({
success: false,
message: 'Opportunity not found'
});
}
if (opportunity.status !== 'PENDING') {
return res.status(400).json({
success: false,
message: 'Opportunity already processed'
});
}
// Update opportunity status to REJECTED
await prisma.absorptionOpportunity.update({
where: { id: opportunityId },
data: {
status: 'REJECTED',
rejectedAt: new Date(),
rejectionReason: reason || 'Rejected by dispatcher'
}
});
// Notify both drivers
const rejectionData = {
opportunityId,
reason: reason || 'Rejected by dispatcher',
timestamp: new Date().toISOString()
};
io.to(`driver_${opportunity.route1.truck.owner.id}`).emit('SYNERGY_REJECTED', rejectionData);
io.to(`driver_${opportunity.route2.truck.owner.id}`).emit('SYNERGY_REJECTED', rejectionData);
res.status(200).json({
success: true,
message: 'Synergy opportunity rejected',
data: {
opportunityId,
status: 'REJECTED'
}
});
} catch (error) {
console.error('Dispatcher reject error:', error);
res.status(500).json({
success: false,
message: error.message || 'Failed to reject synergy'
});
}
}
async function handleHandshake(req, res) {
try {
res.status(200).json({
success: true,
message: 'Use new QR-based handshake flow'
});
} catch (error) {
res.status(500).json({ error: error.message });
}
}
module.exports = {
// Phase 2-4 functions
dispatcherAcceptSynergy,
dispatcherRejectSynergy,
generateQRCode,
verifyQR,
completeHandover,
// Legacy functions (deprecated)
searchSynergy,
acceptSynergy,
handleHandshake
};