fix(build): add pg and @types/pg dependencies, add master courier fallbacks and compile live tracking (v2.4.1)

This commit is contained in:
root
2026-10-04 09:04:31 +00:00
parent be3eb4fcd9
commit caa1a0c81e
19 changed files with 1261 additions and 186 deletions
+314
View File
@@ -0,0 +1,314 @@
import axios from "axios";
// Google Maps Platform Skill Attribution & Governance
// Attribution ID: gmp_git_agentskills_v1
const GMP_ATTRIBUTION_ID = "gmp_git_agentskills_v1";
export class GoogleMapsAdapter {
apiKey;
defaultHub = {
// Dhaka Central Logistics Hub (Tejgaon / Mirpur axis)
lat: 23.7925,
lng: 90.4078,
name: "Aura Central Sorting Hub, Dhaka",
};
constructor(apiKey = "") {
this.apiKey = apiKey || process.env.GOOGLE_MAPS_API_KEY || "";
}
isConfigured() {
return Boolean(this.apiKey);
}
/**
* Validate and Geocode Bangladesh Address using Google Maps Geocoding API
*/
async validateAndGeocode(req) {
const fullQuery = [req.address, req.thana, req.district, "Bangladesh"]
.filter(Boolean)
.join(", ");
if (!this.apiKey) {
return this.fallbackAddressParse(req);
}
try {
const url = `https://maps.googleapis.com/maps/api/geocode/json`;
const res = await axios.get(url, {
params: {
address: fullQuery,
components: "country:BD",
language: "bn,en",
key: this.apiKey,
},
timeout: 8000,
});
const data = res.data;
if (data.status !== "OK" || !data.results || !data.results.length) {
return this.fallbackAddressParse(req);
}
const result = data.results[0];
const components = result.address_components || [];
const location = result.geometry?.location || { lat: 23.8103, lng: 90.4125 };
const locType = result.geometry?.location_type || "APPROXIMATE";
let thana = "";
let district = "";
let division = "";
for (const c of components) {
const types = c.types || [];
if (types.includes("sublocality") || types.includes("sublocality_level_1") || types.includes("neighborhood")) {
thana = c.long_name;
}
else if (types.includes("administrative_area_level_2")) {
district = c.long_name;
}
else if (types.includes("administrative_area_level_1")) {
division = c.long_name;
}
}
let confidence = 50;
if (locType === "ROOFTOP")
confidence = 98;
else if (locType === "RANGE_INTERPOLATED")
confidence = 85;
else if (locType === "GEOMETRIC_CENTER")
confidence = 70;
return {
success: true,
formatted_address: result.formatted_address,
thana: thana || req.thana || "",
district: district || req.district || "Dhaka",
division: division || "Dhaka Division",
coordinates: {
lat: location.lat,
lng: location.lng,
},
location_type: locType,
is_deliverable: confidence >= 60,
confidence_score: confidence,
place_id: result.place_id,
};
}
catch (err) {
return this.fallbackAddressParse(req);
}
}
/**
* Compute Road Distance & Classify Delivery Zone via Google Routes API
*/
async calculateDeliveryZoneAndFee(req) {
let destLat = req.destination_coords?.lat;
let destLng = req.destination_coords?.lng;
if (!destLat || !destLng) {
const geo = await this.validateAndGeocode({ address: req.recipient_address });
destLat = geo.coordinates.lat;
destLng = geo.coordinates.lng;
}
let roadDistanceKm = 0;
let routeSummary = "";
if (this.apiKey && destLat && destLng) {
try {
const routesUrl = "https://routes.googleapis.com/directions/v2:computeRoutes";
const routesRes = await axios.post(routesUrl, {
origin: { location: { latLng: { latitude: this.defaultHub.lat, longitude: this.defaultHub.lng } } },
destination: { location: { latLng: { latitude: destLat, longitude: destLng } } },
travelMode: "DRIVE",
}, {
headers: {
"Content-Type": "application/json",
"X-Goog-Api-Key": this.apiKey,
"X-Goog-FieldMask": "routes.distanceMeters,routes.duration,routes.description",
},
timeout: 8000,
});
const route = routesRes.data?.routes?.[0];
if (route && route.distanceMeters) {
roadDistanceKm = Math.round((route.distanceMeters / 1000) * 10) / 10;
routeSummary = route.description || "";
}
}
catch (routesErr) {
roadDistanceKm = this.haversineDistance(this.defaultHub.lat, this.defaultHub.lng, destLat, destLng);
}
}
else if (destLat && destLng) {
roadDistanceKm = this.haversineDistance(this.defaultHub.lat, this.defaultHub.lng, destLat, destLng);
}
const addr = req.recipient_address.toLowerCase();
const isSubDhakaArea = /savar|gazipur|keraniganj|narayanganj|সাভার|গাজীপুর|কেরানীগঞ্জ|নারায়ণগঞ্জ/i.test(addr);
let zone = "outside_dhaka";
let zoneTitle = "ঢাকার বাইরে (সারা বাংলাদেশ)";
let fee = 150;
let hours = "৭২–৯৬ ঘণ্টা";
if (roadDistanceKm > 0 && roadDistanceKm <= 22 && !isSubDhakaArea) {
zone = "inside_dhaka";
zoneTitle = "ঢাকার ভেতরে (মেট্রোপলিটন)";
fee = 80;
hours = "২৪–৪৮ ঘণ্টা (সরাসরি হোম ডেলিভারি)";
}
else if ((roadDistanceKm > 22 && roadDistanceKm <= 45) || isSubDhakaArea) {
zone = "sub_dhaka";
zoneTitle = "সাব-ঢাকা (আশপাশের অঞ্চল)";
fee = 100;
hours = "৪৮–৭২ ঘণ্টা";
}
return {
success: true,
zone,
zone_title: zoneTitle,
road_distance_km: roadDistanceKm,
estimated_delivery_hours: hours,
standard_delivery_fee: fee,
cod_charge_percentage: 1.0,
route_summary: routeSummary || `Hub to Destination (~${roadDistanceKm} km)`,
};
}
/**
* Enterprise Multi-Carrier Rate Comparison (Steadfast vs Pathao vs RedX vs Paperfly)
*/
async compareRates(req) {
const zoneInfo = await this.calculateDeliveryZoneAndFee({
recipient_address: req.recipient_address,
weight_kg: req.weight_kg || 0.5,
});
const weight = req.weight_kg || 0.5;
const cod = req.cod_amount || 0;
const extraKg = Math.max(0, Math.ceil(weight - 1.0));
// Base rates per carrier based on real BD logistics market tariffs
let sfBase = 70, ptBase = 60, rxBase = 80, pfBase = 80;
let sfExtra = 20, ptExtra = 25, rxExtra = 20, pfExtra = 25;
let sfHours = "২৪–৪৮ ঘণ্টা", ptHours = "১২–২৪ ঘণ্টা (Same-Day)", rxHours = "২৪–৪৮ ঘণ্টা", pfHours = "২৪–৪৮ ঘণ্টা";
if (zoneInfo.zone === "sub_dhaka") {
sfBase = 100;
ptBase = 110;
rxBase = 110;
pfBase = 110;
sfHours = "৪৮–৭২ ঘণ্টা";
ptHours = "২৪–৪৮ ঘণ্টা";
rxHours = "৪৮–৭২ ঘণ্টা";
pfHours = "৪৮–৭২ ঘণ্টা";
}
else if (zoneInfo.zone === "outside_dhaka") {
sfBase = 140;
ptBase = 150;
rxBase = 160;
pfBase = 170;
sfHours = "৭২–৯৬ ঘণ্টা";
ptHours = "৪৮–৭২ ঘণ্টা";
rxHours = "৭২–৯৬ ঘণ্টা";
pfHours = "৭২–৯৬ ঘণ্টা";
}
const sfCod = Math.round(cod * 0.01);
const ptCod = Math.round(cod * 0.01);
const rxCod = Math.round(cod * 0.01);
const pfCod = Math.round(cod * 0.015);
const sfTotal = sfBase + (extraKg * sfExtra) + sfCod;
const ptTotal = ptBase + (extraKg * ptExtra) + ptCod;
const rxTotal = rxBase + (extraKg * rxExtra) + rxCod;
const pfTotal = pfBase + (extraKg * pfExtra) + pfCod;
const carrierQuotes = [
{
courier: "steadfast",
name: "Steadfast Courier",
base: sfBase,
extra: extraKg * sfExtra,
cod: sfCod,
total: sfTotal,
hours: sfHours,
notes: "৬৪ জেলা ও সকল থানায় সরাসরি ক্যাশ অন ডেলিভারি হাব নেটওয়ার্ক।",
},
{
courier: "pathao",
name: "Pathao Express",
base: ptBase,
extra: extraKg * ptExtra,
cod: ptCod,
total: ptTotal,
hours: ptHours,
notes: "ঢাকা ও প্রধান মেট্রোপলিটন সিটিতে দ্রুততম অন-ডিমান্ড রাইডার পিকআপ।",
},
{
courier: "redx",
name: "RedX Delivery",
base: rxBase,
extra: extraKg * rxExtra,
cod: rxCod,
total: rxTotal,
hours: rxHours,
notes: "শক্তিশালী সাব-ডিস্ট্রিক্ট হাব ও বৃহৎ পার্সেল হ্যান্ডলিং নেটওয়ার্ক।",
},
{
courier: "paperfly",
name: "Paperfly Go",
base: pfBase,
extra: extraKg * pfExtra,
cod: pfCod,
total: pfTotal,
hours: pfHours,
notes: "প্রত্যন্ত ইউনিয়ন ও ডোরস্টেপ ডেলিভারি কভারেজ।",
},
];
let recommended = "steadfast";
let reason = "সর্বনিম্ন খরচ ও ৬৪ জেলায় ৯৯.৮% সফল ডেলিভারি নেটওয়ার্ক";
if (zoneInfo.zone === "inside_dhaka" && req.priority === "fastest") {
recommended = "pathao";
reason = "ঢাকা মেট্রোপলিটনে দ্রুততম সেম-ডে এক্সপ্রেস ডেলিভারি";
}
else if (req.priority === "cheapest") {
const sorted = [...carrierQuotes].sort((a, b) => a.total - b.total);
recommended = sorted[0].courier;
reason = `${sorted[0].name} এই রুটে সর্বনিম্ন খরচে ডেলিভারি প্রদান করে (৳${sorted[0].total})।`;
}
else if (zoneInfo.zone === "inside_dhaka") {
recommended = ptTotal <= sfTotal ? "pathao" : "steadfast";
reason = "ঢাকা মেট্রোপলিটনে সাশ্রয়ী ডেলিভারি ও দ্রুত পিকআপ";
}
else {
recommended = "steadfast";
reason = "ঢাকার বাইরে সারা বাংলাদেশের ৬৪ জেলায় সর্বোচ্চ কভারেজ ও সর্বনিম্ন রিটার্ন রেট";
}
const quotes = carrierQuotes.map((cq) => ({
courier: cq.courier,
courier_name: cq.name,
base_delivery_fee: cq.base,
weight_surcharge: cq.extra,
cod_charge: cq.cod,
total_cost: cq.total,
estimated_hours: cq.hours,
coverage_notes: cq.notes,
is_recommended: cq.courier === recommended,
}));
return {
success: true,
zone: zoneInfo.zone,
zone_title: zoneInfo.zone_title,
road_distance_km: zoneInfo.road_distance_km,
weight_kg: weight,
cod_amount: cod,
recommended_courier: recommended,
recommendation_reason: reason,
quotes,
};
}
fallbackAddressParse(req) {
const raw = (req.address + " " + (req.district || "")).toLowerCase();
const isDhaka = /dhaka|ঢাকা/i.test(raw);
return {
success: true,
formatted_address: req.address,
thana: req.thana || "",
district: req.district || (isDhaka ? "Dhaka" : "Outside Dhaka"),
division: isDhaka ? "Dhaka" : "",
coordinates: isDhaka ? { lat: 23.8103, lng: 90.4125 } : { lat: 22.3569, lng: 91.7832 },
location_type: "APPROXIMATE",
is_deliverable: true,
confidence_score: 65,
};
}
haversineDistance(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 Math.round(R * c * 1.2 * 10) / 10;
}
}