|
|
|
@ -1929,46 +1929,135 @@ exports.getSuppliersForPlanSearch = async (req, reply) => {
|
|
|
|
|
type_of_water,
|
|
|
|
|
capacity: requestedCapacityStr,
|
|
|
|
|
quantity: requestedQuantityStr,
|
|
|
|
|
// frequency, start_date, end_date are provided by UI but not used for filtering now
|
|
|
|
|
frequency, start_date, end_date, // currently not used to filter suppliers
|
|
|
|
|
|
|
|
|
|
// new filters
|
|
|
|
|
radius_from, radius_to,
|
|
|
|
|
rating_from, rating_to,
|
|
|
|
|
price_from, price_to,
|
|
|
|
|
pump
|
|
|
|
|
} = req.body;
|
|
|
|
|
|
|
|
|
|
// helpers inside function (per your preference)
|
|
|
|
|
const parseCapacity = (v) => parseFloat((v || "0").toString().replace(/,/g, "")) || 0;
|
|
|
|
|
// ---- helpers (kept inside as you prefer) ----
|
|
|
|
|
const parseFloatSafe = (v) => {
|
|
|
|
|
const n = parseFloat((v ?? "").toString().replace(/,/g, ""));
|
|
|
|
|
return Number.isFinite(n) ? n : NaN;
|
|
|
|
|
};
|
|
|
|
|
const parseIntSafe = (v) => {
|
|
|
|
|
const n = parseInt((v ?? "").toString().replace(/,/g, ""), 10);
|
|
|
|
|
return Number.isFinite(n) ? n : NaN;
|
|
|
|
|
};
|
|
|
|
|
const isValid = (n) => Number.isFinite(n);
|
|
|
|
|
const inRange = (n, from, to) =>
|
|
|
|
|
(!isValid(from) || n >= from) && (!isValid(to) || n <= to);
|
|
|
|
|
|
|
|
|
|
const normalizePump = (val) => {
|
|
|
|
|
if (val == null) return undefined;
|
|
|
|
|
const s = String(val).trim().toLowerCase();
|
|
|
|
|
if (["1","true","yes","y"].includes(s)) return true;
|
|
|
|
|
if (["0","false","no","n"].includes(s)) return false;
|
|
|
|
|
return undefined; // ignore if unknown
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const requestedCapacity = parseCapacity(requestedCapacityStr);
|
|
|
|
|
const requestedQuantity = parseInt((requestedQuantityStr || "0").toString(), 10) || 0;
|
|
|
|
|
const parseLatLng = (raw) => {
|
|
|
|
|
// supports: "17.38,78.49" | {lat: 17.38, lng: 78.49} | [17.38, 78.49]
|
|
|
|
|
if (!raw) return null;
|
|
|
|
|
try {
|
|
|
|
|
if (typeof raw === "string") {
|
|
|
|
|
const parts = raw.split(",").map(x => parseFloat(x.trim()));
|
|
|
|
|
if (parts.length === 2 && parts.every(Number.isFinite)) return { lat: parts[0], lng: parts[1] };
|
|
|
|
|
// try JSON
|
|
|
|
|
const j = JSON.parse(raw);
|
|
|
|
|
return parseLatLng(j);
|
|
|
|
|
}
|
|
|
|
|
if (Array.isArray(raw) && raw.length === 2) {
|
|
|
|
|
const [lat, lng] = raw.map(Number);
|
|
|
|
|
if (Number.isFinite(lat) && Number.isFinite(lng)) return { lat, lng };
|
|
|
|
|
}
|
|
|
|
|
if (typeof raw === "object" && raw !== null) {
|
|
|
|
|
const lat = parseFloat(raw.lat ?? raw.latitude);
|
|
|
|
|
const lng = parseFloat(raw.lng ?? raw.lon ?? raw.longitude);
|
|
|
|
|
if (Number.isFinite(lat) && Number.isFinite(lng)) return { lat, lng };
|
|
|
|
|
}
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
return null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const haversineKm = (a, b) => {
|
|
|
|
|
const R = 6371;
|
|
|
|
|
const dLat = (b.lat - a.lat) * Math.PI / 180;
|
|
|
|
|
const dLng = (b.lng - a.lng) * Math.PI / 180;
|
|
|
|
|
const s1 = Math.sin(dLat/2) ** 2;
|
|
|
|
|
const s2 = Math.cos(a.lat*Math.PI/180) * Math.cos(b.lat*Math.PI/180) * Math.sin(dLng/2) ** 2;
|
|
|
|
|
return 2 * R * Math.asin(Math.sqrt(s1 + s2));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getSupplierRating = (s) => {
|
|
|
|
|
// adapt to whatever field you actually store
|
|
|
|
|
const cands = [s.rating, s.avgRating, s.averageRating, s.overallRating];
|
|
|
|
|
const n = cands.find(x => Number.isFinite(Number(x)));
|
|
|
|
|
return Number(n ?? NaN);
|
|
|
|
|
};
|
|
|
|
|
// ---- end helpers ----
|
|
|
|
|
|
|
|
|
|
// parse inputs
|
|
|
|
|
const requestedCapacity = parseFloatSafe(requestedCapacityStr) || 0;
|
|
|
|
|
const requestedQuantity = parseIntSafe(requestedQuantityStr) || 0;
|
|
|
|
|
const totalRequiredCapacity = requestedCapacity * requestedQuantity;
|
|
|
|
|
|
|
|
|
|
const priceFrom = parseIntSafe(price_from);
|
|
|
|
|
const priceTo = parseIntSafe(price_to);
|
|
|
|
|
const ratingFrom = parseFloatSafe(rating_from);
|
|
|
|
|
const ratingTo = parseFloatSafe(rating_to);
|
|
|
|
|
const radiusFrom = parseFloatSafe(radius_from);
|
|
|
|
|
const radiusTo = parseFloatSafe(radius_to);
|
|
|
|
|
const pumpWanted = normalizePump(pump);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// favorites
|
|
|
|
|
const customer = await User.findOne({ customerId }, { favorate_suppliers: 1 }).lean();
|
|
|
|
|
// favorites + customer coords (for radius)
|
|
|
|
|
const customer = await User.findOne({ customerId }, { favorate_suppliers: 1, googleLocation: 1, location: 1 }).lean();
|
|
|
|
|
const favoriteSet = new Set(customer?.favorate_suppliers || []);
|
|
|
|
|
const customerCoords =
|
|
|
|
|
parseLatLng(customer?.googleLocation) ||
|
|
|
|
|
parseLatLng(customer?.location);
|
|
|
|
|
|
|
|
|
|
// Tanker filter: ONLY by type_of_water (NO booked tanker exclusion, NO price/radius/rating)
|
|
|
|
|
// 1) Tankers base query: by type_of_water (+ pump if requested)
|
|
|
|
|
const tankerQuery = {};
|
|
|
|
|
if (type_of_water && type_of_water.trim() !== "") {
|
|
|
|
|
tankerQuery.typeofwater = type_of_water;
|
|
|
|
|
if (type_of_water?.trim()) tankerQuery.typeofwater = type_of_water.trim();
|
|
|
|
|
if (pumpWanted !== undefined) {
|
|
|
|
|
// try to match common representations
|
|
|
|
|
tankerQuery.$or = [
|
|
|
|
|
{ pump: pumpWanted ? { $in: [true, "1", "yes", "true", 1, "Y", "y"] } : { $in: [false, "0", "no", "false", 0, "N", "n"] } },
|
|
|
|
|
{ pumpAvailable: pumpWanted } // if you store as boolean
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let tankers = await Tanker.find(tankerQuery).lean();
|
|
|
|
|
|
|
|
|
|
// 2) Price range on tanker.price
|
|
|
|
|
if (isValid(priceFrom) || isValid(priceTo)) {
|
|
|
|
|
tankers = tankers.filter(t => {
|
|
|
|
|
const p = parseIntSafe(t.price);
|
|
|
|
|
return isValid(p) && inRange(p, priceFrom, priceTo);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
const tankers = await Tanker.find(tankerQuery).lean();
|
|
|
|
|
|
|
|
|
|
// Group tankers by supplier
|
|
|
|
|
// 3) Group by supplier
|
|
|
|
|
const supplierTankerMap = {};
|
|
|
|
|
for (const t of tankers) {
|
|
|
|
|
if (!supplierTankerMap[t.supplierId]) supplierTankerMap[t.supplierId] = [];
|
|
|
|
|
supplierTankerMap[t.supplierId].push(t);
|
|
|
|
|
if (!t?.supplierId) continue;
|
|
|
|
|
(supplierTankerMap[t.supplierId] ||= []).push(t);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Capacity check per supplier (capacity * quantity)
|
|
|
|
|
const qualified = [];
|
|
|
|
|
// 4) Capacity qualification
|
|
|
|
|
let qualified = [];
|
|
|
|
|
for (const [supplierId, supplierTankers] of Object.entries(supplierTankerMap)) {
|
|
|
|
|
const totalAvail = supplierTankers.reduce((sum, tt) => sum + parseCapacity(tt.capacity), 0);
|
|
|
|
|
if (requestedCapacity > 0 && requestedQuantity > 0) {
|
|
|
|
|
if (totalAvail < totalRequiredCapacity) continue;
|
|
|
|
|
}
|
|
|
|
|
const totalAvail = supplierTankers.reduce((sum, tt) => sum + (parseFloatSafe(tt.capacity) || 0), 0);
|
|
|
|
|
if (requestedCapacity > 0 && requestedQuantity > 0 && totalAvail < totalRequiredCapacity) continue;
|
|
|
|
|
qualified.push({ supplierId, tankers: supplierTankers });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fetch suppliers + connection flags
|
|
|
|
|
// 5) Fetch suppliers for remaining filters (rating & radius) + flags
|
|
|
|
|
const supplierIds = qualified.map(q => q.supplierId);
|
|
|
|
|
const [suppliersData, acceptedReqs] = await Promise.all([
|
|
|
|
|
Supplier.find({ supplierId: { $in: supplierIds } }).lean(),
|
|
|
|
@ -1977,31 +2066,59 @@ exports.getSuppliersForPlanSearch = async (req, reply) => {
|
|
|
|
|
{ supplierId: 1, _id: 0 }
|
|
|
|
|
).lean()
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Build quick lookup
|
|
|
|
|
const supplierById = new Map(suppliersData.map(s => [s.supplierId, s]));
|
|
|
|
|
const connectedSet = new Set(acceptedReqs.map(r => r.supplierId));
|
|
|
|
|
|
|
|
|
|
// Optional: check if any (single-day) requested booking exists with that supplier
|
|
|
|
|
// 6) Apply rating & radius filters on suppliers
|
|
|
|
|
if (isValid(ratingFrom) || isValid(ratingTo) || (isValid(radiusFrom) || isValid(radiusTo))) {
|
|
|
|
|
qualified = qualified.filter(q => {
|
|
|
|
|
const s = supplierById.get(q.supplierId);
|
|
|
|
|
if (!s) return false;
|
|
|
|
|
|
|
|
|
|
// rating
|
|
|
|
|
if (isValid(ratingFrom) || isValid(ratingTo)) {
|
|
|
|
|
const r = getSupplierRating(s);
|
|
|
|
|
if (!isValid(r) || !inRange(r, ratingFrom, ratingTo)) return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// radius (requires coords on both sides)
|
|
|
|
|
if ((isValid(radiusFrom) || isValid(radiusTo)) && customerCoords) {
|
|
|
|
|
const supCoords =
|
|
|
|
|
parseLatLng(s.googleLocation) ||
|
|
|
|
|
parseLatLng(s.location) ||
|
|
|
|
|
parseLatLng(s.addressLocation);
|
|
|
|
|
if (!supCoords) return false;
|
|
|
|
|
const distKm = haversineKm(customerCoords, supCoords);
|
|
|
|
|
if (!inRange(distKm, radiusFrom, radiusTo)) return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 7) Build response with flags + optional 'requestedBooking' flag
|
|
|
|
|
const suppliers = [];
|
|
|
|
|
for (const q of qualified) {
|
|
|
|
|
const supplierData = supplierById.get(q.supplierId);
|
|
|
|
|
const friendRequestAccepted = connectedSet.has(q.supplierId);
|
|
|
|
|
const s = supplierById.get(q.supplierId);
|
|
|
|
|
if (!s) continue;
|
|
|
|
|
|
|
|
|
|
const isConnected = connectedSet.has(q.supplierId);
|
|
|
|
|
const isFavorite = favoriteSet.has(q.supplierId);
|
|
|
|
|
|
|
|
|
|
// If you want to expose a hint that user has already sent a single-day request earlier
|
|
|
|
|
const requestedBookingRecord = await RequestedBooking.findOne({
|
|
|
|
|
customerId,
|
|
|
|
|
"requested_suppliers.supplierId": q.supplierId
|
|
|
|
|
}, { time: 1 }).lean();
|
|
|
|
|
|
|
|
|
|
const requestedBooking = requestedBookingRecord
|
|
|
|
|
? { status: true, time: requestedBookingRecord.time }
|
|
|
|
|
: { status: false };
|
|
|
|
|
|
|
|
|
|
suppliers.push({
|
|
|
|
|
supplier: supplierData,
|
|
|
|
|
supplier: s,
|
|
|
|
|
tankers: q.tankers,
|
|
|
|
|
isConnected: friendRequestAccepted,
|
|
|
|
|
isConnected,
|
|
|
|
|
isFavorite,
|
|
|
|
|
requestedBooking
|
|
|
|
|
requestedBooking: requestedBookingRecord ? { status: true, time: requestedBookingRecord.time } : { status: false }
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -2014,8 +2131,7 @@ exports.getSuppliersForPlanSearch = async (req, reply) => {
|
|
|
|
|
error: err.message
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// controllers/validationHandler.js (add below the previous handler)
|
|
|
|
|
|
|
|
|
|