diff --git a/dist/adapters/google_maps.js b/dist/adapters/google_maps.js index d8ca291..f9b7fee 100644 --- a/dist/adapters/google_maps.js +++ b/dist/adapters/google_maps.js @@ -120,6 +120,9 @@ export class GoogleMapsAdapter { roadDistanceKm = Math.round((route.distanceMeters / 1000) * 10) / 10; routeSummary = route.description || ""; } + else { + roadDistanceKm = this.haversineDistance(this.defaultHub.lat, this.defaultHub.lng, destLat, destLng); + } } catch (routesErr) { roadDistanceKm = this.haversineDistance(this.defaultHub.lat, this.defaultHub.lng, destLat, destLng); @@ -284,19 +287,102 @@ export class GoogleMapsAdapter { quotes, }; } - fallbackAddressParse(req) { - const raw = (req.address + " " + (req.district || "")).toLowerCase(); + async fallbackAddressParse(req) { + const fullQuery = [req.address, req.thana, req.district, "Bangladesh"].filter(Boolean).join(", "); + // Tier 2: Live OpenStreetMap Nominatim Geocoding (Zero-Billing Fallback) + try { + const nomRes = await axios.get("https://nominatim.openstreetmap.org/search", { + params: { + q: fullQuery, + countrycodes: "bd", + format: "json", + addressdetails: 1, + limit: 1, + }, + headers: { "User-Agent": "AuraCourierMCP/2.5 (https://courier.auraajenticai.cloud)" }, + timeout: 5500, + }); + if (Array.isArray(nomRes.data) && nomRes.data.length > 0) { + const hit = nomRes.data[0]; + const addr = hit.address || {}; + const lat = parseFloat(hit.lat); + const lng = parseFloat(hit.lon); + const thana = addr.suburb || addr.neighbourhood || addr.town || addr.municipality || addr.city_district || req.thana || ""; + const district = (addr.county || addr.city || addr.state_district || req.district || "Dhaka").replace(/\s*District$/i, ""); + const division = addr.state || "Dhaka Division"; + return { + success: true, + formatted_address: hit.display_name || fullQuery, + thana, + district, + division, + coordinates: { lat, lng }, + location_type: "GEOMETRIC_CENTER", + is_deliverable: true, + confidence_score: 88, + place_id: `osm_${hit.place_id || Date.now()}`, + }; + } + } + catch (_) { + // Proceed to Tier 3 Spatial Gazetteer + } + // Tier 3: Bangladesh 64-District & Dhaka Thana Spatial Gazetteer + const raw = [req.address, req.thana, req.district].filter(Boolean).join(" ").toLowerCase(); + const gazetteer = [ + { rx: /mirpur|মিরপুর|pallabi|পল্লবী|kazipara|shewrapara|কাজীপাড়া|শেওড়াপাড়া/, thana: "Mirpur", district: "Dhaka", division: "Dhaka Division", lat: 23.8042, lng: 90.3667 }, + { rx: /uttara|উত্তরা|airport|বিমানবন্দর|sector|সেক্টর|dakshin khan|uttar khan/, thana: "Uttara", district: "Dhaka", division: "Dhaka Division", lat: 23.8759, lng: 90.3795 }, + { rx: /dhanmondi|ধানমন্ডি|kalabagan|কলাবাগান|jigatola|জিগাতলা|shankar/, thana: "Dhanmondi", district: "Dhaka", division: "Dhaka Division", lat: 23.7461, lng: 90.3742 }, + { rx: /gulshan|গুলশান|banani|বনানী|baridhara|বারিধারা|niketan|নিকেতন/, thana: "Gulshan", district: "Dhaka", division: "Dhaka Division", lat: 23.7925, lng: 90.4078 }, + { rx: /mohammadpur|মোহাম্মদপুর|adabor|আদাবর|lalmatia|লালমাটিয়া|bosila|বসিলা/, thana: "Mohammadpur", district: "Dhaka", division: "Dhaka Division", lat: 23.7662, lng: 90.3589 }, + { rx: /badda|বাড্ডা|rampura|রামপুরা|banasree|বনশ্রী|aftabnagar|আফতাবনগর|hatirjheel/, thana: "Badda", district: "Dhaka", division: "Dhaka Division", lat: 23.7805, lng: 90.4267 }, + { rx: /bashundhara|বসুন্ধরা|vatara|ভাটারা|kuril|কুড়িল|nikunja|নিকুঞ্জ|khilkhet|খিলক্ষেত/, thana: "Vatara", district: "Dhaka", division: "Dhaka Division", lat: 23.8191, lng: 90.4526 }, + { rx: /motijheel|মতিঝিল|paltan|পল্টন|kakrail|কাকরাইল|shantinagar|শান্তিনগর|malibagh|মালিবাগ|mogbazar|মগবাজার/, thana: "Motijheel", district: "Dhaka", division: "Dhaka Division", lat: 23.7330, lng: 90.4172 }, + { rx: /shahbag|শাহবাগ|hatirpool|হাতিরপুল|motaleb|মোতালেব|eleph|এলিফ্যান্ট|new market|নিউ মার্কেট|farmgate|ফার্মগেট|tejgaon|তেজগাঁও|karwan|কারওয়ান/, thana: "Shahbagh", district: "Dhaka", division: "Dhaka Division", lat: 23.7415, lng: 90.3918 }, + { rx: /jatrabari|যাত্রাবাড়ী|demra|ডেমরা|kadamtali|কদমতলী|shanir akhra|শনির আখড়া|wari|ওয়ারী|old dhaka|পুরান ঢাকা|lalbagh|লালবাগ|kotwali/, thana: "Jatrabari", district: "Dhaka", division: "Dhaka Division", lat: 23.7104, lng: 90.4348 }, + { rx: /keraniganj|কেরানীগঞ্জ|ati bazar|আটি বাজার|atibazar|hasnabad/, thana: "Keraniganj", district: "Dhaka", division: "Dhaka Division", lat: 23.6942, lng: 90.3475 }, + { rx: /savar|সাভার|ashulia|আশুলিয়া|hemayetpur|হেমায়েতপুর|dhamrai|ধামরাই/, thana: "Savar", district: "Dhaka", division: "Dhaka Division", lat: 23.8583, lng: 90.2667 }, + { rx: /gazipur|গাজীপুর|tongi|টঙ্গী|board bazar|বোর্ড বাজার|joydebpur|জয়দেবপুর|konabari/, thana: "Tongi", district: "Gazipur", division: "Dhaka Division", lat: 23.9889, lng: 90.3950 }, + { rx: /narayanganj|নারায়ণগঞ্জ|siddhirganj|সিদ্ধিরগঞ্জ|fatullah|ফতুল্লা|chashara|চাষাড়া|sonargaon|rupganj/, thana: "Narayanganj Sadar", district: "Narayanganj", division: "Dhaka Division", lat: 23.6238, lng: 90.5000 }, + { rx: /chattogram|chittagong|চট্টগ্রাম|agrabad|আগ্রাবাদ|nasirabad|নাসিরাবাদ|halishahar|হালিশহর|panchlaish|gec|জিইসি|kotwali/, thana: "Panchlaish", district: "Chattogram", division: "Chattogram Division", lat: 22.3569, lng: 91.7832 }, + { rx: /cox.?s?s*bazar|কক্সবাজার|chakaria|teknaf|ukhiya/, thana: "Cox's Bazar Sadar", district: "Cox's Bazar", division: "Chattogram Division", lat: 21.4272, lng: 92.0058 }, + { rx: /cumilla|comilla|কুমিল্লা|kandirpar|কান্দিরপাড়|laksam|daudkandi/, thana: "Cumilla Sadar", district: "Cumilla", division: "Chattogram Division", lat: 23.4607, lng: 91.1809 }, + { rx: /feni|ফেনী|noakhali|নোয়াখালী|maijdee|চৌমুহনী|lakshmipur|লক্ষ্মীপুর|chandpur|চাঁদপুর|brahmanbaria|ব্রাহ্মণবাড়িয়া/, thana: "Sadar", district: "Noakhali", division: "Chattogram Division", lat: 22.8724, lng: 91.0973 }, + { rx: /sylhet|সিলেট|zindabazar|জিন্দাবাজার|amberkhana|আম্বরখানা|upashahar|উপশহর|moulvibazar|মৌলভীবাজার|sreemangal|habiganj|হবিগঞ্জ|sunamganj|সুনামগঞ্জ/, thana: "Sylhet Sadar", district: "Sylhet", division: "Sylhet Division", lat: 24.8949, lng: 91.8687 }, + { rx: /rajshahi|রাজশাহী|boalia|বোয়ালিয়া|shaheb bazar|সাহেব বাজার|motihar|নাটোর|natore|naogaon|নওগাঁ|chapainawabganj|চাঁপাইনবাবগঞ্জ|pabna|পাবনা|sirajganj|সিরাজগঞ্জ/, thana: "Boalia", district: "Rajshahi", division: "Rajshahi Division", lat: 24.3745, lng: 88.6042 }, + { rx: /bogura|bogra|বগুড়া|sherpur|শেরপুর|joypurhat|জয়পুরহাট/, thana: "Bogura Sadar", district: "Bogura", division: "Rajshahi Division", lat: 24.8481, lng: 89.3730 }, + { rx: /khulna|খুলনা|sonadanga|সোনাডাঙ্গা|khalishpur|খালিশপুর|daulatpur|jashore|jessore|যশোর|kushtia|কুষ্টিয়া|satkhira|সাতক্ষীরা|bagerhat|বাগেরহাট|jhenaidah|ঝিনাইদহ/, thana: "Sonadanga", district: "Khulna", division: "Khulna Division", lat: 22.8456, lng: 89.5403 }, + { rx: /barishal|barisal|বরিশাল|patukhali|পটুয়াখালী|bhola|ভোলা|pirojpur|পিরোজপুর|jhalokati|barguna/, thana: "Barishal Sadar", district: "Barishal", division: "Barishal Division", lat: 22.7010, lng: 90.3535 }, + { rx: /rangpur|রংপুর|dinajpur|দিনাজপুর|thakurgaon|ঠাকুরগাঁও|panchagarh|পঞ্চগড়|nilphamari|নীলফামারী|saidpur|সৈয়দপুর|kurigram|কুড়িগ্রাম|gaibandha|গাইবান্ধা|lalmonirhat/, thana: "Rangpur Sadar", district: "Rangpur", division: "Rangpur Division", lat: 25.7439, lng: 89.2752 }, + { rx: /mymensingh|ময়মনসিংহ|jamalpur|জামালপুর|netrokona|নেত্রকোনা|tangail|টাঙ্গাইল|kishoreganj|কিশোরগঞ্জ|faridpur|ফরিদপুর|madaripur|মাদারীপুর|gopalganj|গোপালগঞ্জ|shariatpur|শরীয়তপুর|rajbari|রাজবাড়ী|manikganj|মানিকগঞ্জ|munshiganj|মুন্সিগঞ্জ|narsingdi|নরসিংদী/, thana: "Mymensingh Sadar", district: "Mymensingh", division: "Mymensingh Division", lat: 24.7471, lng: 90.4203 }, + ]; + for (const g of gazetteer) { + if (g.rx.test(raw)) { + return { + success: true, + formatted_address: `${req.address}, ${req.thana || g.thana}, ${req.district || g.district}, Bangladesh`, + thana: req.thana || g.thana, + district: req.district || g.district, + division: g.division, + coordinates: { lat: g.lat, lng: g.lng }, + location_type: "GEOMETRIC_CENTER", + is_deliverable: true, + confidence_score: 84, + place_id: `bd_gazetteer_${g.district.toLowerCase().replace(/\s+/g, "_")}`, + }; + } + } const isDhaka = /dhaka|ঢাকা/i.test(raw); return { success: true, formatted_address: req.address, - thana: req.thana || "", + thana: req.thana || (isDhaka ? "Dhaka Metro" : "Sadar"), district: req.district || (isDhaka ? "Dhaka" : "Outside Dhaka"), - division: isDhaka ? "Dhaka" : "", - coordinates: isDhaka ? { lat: 23.8103, lng: 90.4125 } : { lat: 22.3569, lng: 91.7832 }, + division: isDhaka ? "Dhaka Division" : "Bangladesh", + coordinates: isDhaka ? { lat: 23.8103, lng: 90.4125 } : { lat: 24.0000, lng: 90.0000 }, location_type: "APPROXIMATE", is_deliverable: true, - confidence_score: 65, + confidence_score: 72, }; } haversineDistance(lat1, lon1, lat2, lon2) { diff --git a/dist/http.js b/dist/http.js index 1f3c840..3f6143f 100644 --- a/dist/http.js +++ b/dist/http.js @@ -50,6 +50,17 @@ function keysFromRequest(req) { } const app = express(); app.use(express.json({ limit: "1mb" })); +// Robust Malformed JSON Syntax Error Guard +app.use((err, req, res, next) => { + if (err && (err.type === "entity.parse.failed" || err.status === 400)) { + return res.status(400).json({ + error: "Malformed JSON payload in request body", + details: err.message, + timestamp: new Date().toISOString() + }); + } + next(err); +}); // Allow browser-based and cross-origin MCP clients. app.use((req, res, next) => { res.header("Access-Control-Allow-Origin", "*"); diff --git a/dist/registry.d.ts b/dist/registry.d.ts index 66ca2b8..7ce22fa 100644 --- a/dist/registry.d.ts +++ b/dist/registry.d.ts @@ -12,6 +12,7 @@ export declare class CourierRegistry { }[]; getAdapter(name: SupportedCourier): CourierAdapter; getGoogleMaps(): GoogleMapsAdapter; + resolveSmartAutoCourierOrder(address: string, priority?: string): SupportedCourier[]; createParcel(req: ParcelCreateRequest): Promise; trackParcel(trackingCode: string, courierName?: SupportedCourier): Promise; getBalance(courierName: SupportedCourier): Promise; diff --git a/dist/registry.js b/dist/registry.js index cbf554b..0847930 100644 --- a/dist/registry.js +++ b/dist/registry.js @@ -40,28 +40,51 @@ export class CourierRegistry { getGoogleMaps() { return this.googleMaps; } - async createParcel(req) { - let courierName = "steadfast"; - if (req.courier && req.courier !== "auto") { - courierName = req.courier; + resolveSmartAutoCourierOrder(address, priority = "balanced") { + const addr = String(address || "").toLowerCase(); + const isSubDhaka = /savar|ashulia|keraniganj|gajipur|gazipur|tongi|narayanganj|siddhirganj|fatullah|dohar|nawabganj|সাভার|আশুলিয়া|কেরানীগঞ্জ|গাজীপুর|টঙ্গী|নারায়ণগঞ্জ/.test(addr); + const isDhakaMetro = !isSubDhaka && (/dhaka|ঢাকা|gulshan|banani|dhanmondi|uttara|mirpur|mohammadpur|motijheel|badda|rampura|bashundhara|tejgaon|khilgaon|malibagh|mogbazar|farmgate|shahbag|hatirpool|lalmatia|wari|jatrabari|গুলশান|বনানী|ধানমন্ডি|উত্তরা|মিরপুর|মোহাম্মদপুর|মতিঝিল|বাড্ডা|রামপুরা|বসুন্ধরা|তেজগাঁও|খিলগাঁও|মালিবাগ|মগবাজার|ফার্মগেট|শাহবাগ|হাতিরপুল|ওয়ারী|যাত্রাবাড়ী/.test(addr)); + let idealOrder; + if (isDhakaMetro) { + idealOrder = priority === "cheapest" + ? ["pathao", "redx", "paperfly", "steadfast"] + : ["pathao", "redx", "steadfast", "paperfly"]; + } + else if (isSubDhaka) { + idealOrder = priority === "fastest" + ? ["pathao", "steadfast", "redx", "paperfly"] + : ["paperfly", "steadfast", "pathao", "redx"]; } else { - // Smart routing: inside Dhaka metro with Pathao configured prefers Pathao, else Steadfast - const addr = req.recipient_address.toLowerCase(); - if (addr.includes("dhaka") && - (addr.includes("gulshan") || - addr.includes("banani") || - addr.includes("dhanmondi") || - addr.includes("uttara") || - addr.includes("mirpur"))) { - courierName = this.adapters.get("pathao")?.isConfigured() ? "pathao" : "steadfast"; + idealOrder = priority === "cheapest" + ? ["paperfly", "pathao", "steadfast", "redx"] + : ["steadfast", "paperfly", "pathao", "redx"]; + } + const configured = idealOrder.filter((c) => this.adapters.get(c)?.isConfigured()); + return configured.length > 0 ? configured : idealOrder; + } + async createParcel(req) { + const rawChoice = String(req.courier || "auto").toLowerCase(); + const isAuto = rawChoice === "auto" || rawChoice.startsWith("auto_"); + const priority = rawChoice === "auto_fastest" ? "fastest" : rawChoice === "auto_cheapest" ? "cheapest" : "balanced"; + const candidates = isAuto + ? this.resolveSmartAutoCourierOrder(req.recipient_address, priority) + : [rawChoice]; + let lastErr = null; + for (const courierName of candidates) { + try { + const adapter = this.getAdapter(courierName); + const res = await adapter.createParcel({ ...req, courier: courierName }); + if (res && res.success !== false && (res.tracking_code || res.consignment_id)) { + return res; + } + lastErr = new Error(res?.message || res?.error || `${courierName} rejected parcel`); } - else { - courierName = "steadfast"; + catch (err) { + lastErr = err; } } - const adapter = this.getAdapter(courierName); - return await adapter.createParcel(req); + throw lastErr || new Error("All configured courier gateways failed to book parcel."); } async trackParcel(trackingCode, courierName) { if (courierName) { @@ -95,6 +118,7 @@ export class CourierRegistry { const cid = req.consignment_id; const name = req.customer_name || "গ্রাহক"; const courier = req.courier; + const phoneTag = req.customer_phone ? ` [Customer Phone: ${req.customer_phone}]` : ""; let status = "CUSTOMER_REENGAGED"; let actionTaken = ""; let customerMsg = ""; @@ -138,7 +162,7 @@ export class CourierRegistry { resolution_status: status, action_taken: actionTaken, customer_whatsapp_message: customerMsg, - carrier_instruction: carrierInstr, + carrier_instruction: carrierInstr + phoneTag, timestamp: new Date().toISOString(), }; } diff --git a/src/adapters/google_maps.ts b/src/adapters/google_maps.ts index f767431..694503e 100644 --- a/src/adapters/google_maps.ts +++ b/src/adapters/google_maps.ts @@ -145,6 +145,8 @@ export class GoogleMapsAdapter { if (route && route.distanceMeters) { roadDistanceKm = Math.round((route.distanceMeters / 1000) * 10) / 10; routeSummary = route.description || ""; + } else { + roadDistanceKm = this.haversineDistance(this.defaultHub.lat, this.defaultHub.lng, destLat, destLng); } } catch (routesErr) { roadDistanceKm = this.haversineDistance(this.defaultHub.lat, this.defaultHub.lng, destLat, destLng); @@ -316,20 +318,105 @@ export class GoogleMapsAdapter { }; } - private fallbackAddressParse(req: AddressValidationRequest): AddressValidationResponse { - const raw = (req.address + " " + (req.district || "")).toLowerCase(); - const isDhaka = /dhaka|ঢাকা/i.test(raw); + private async fallbackAddressParse(req: AddressValidationRequest): Promise { + const fullQuery = [req.address, req.thana, req.district, "Bangladesh"].filter(Boolean).join(", "); + // Tier 2: Live OpenStreetMap Nominatim Geocoding (Zero-Billing Fallback) + try { + const nomRes = await axios.get("https://nominatim.openstreetmap.org/search", { + params: { + q: fullQuery, + countrycodes: "bd", + format: "json", + addressdetails: 1, + limit: 1, + }, + headers: { "User-Agent": "AuraCourierMCP/2.5 (https://courier.auraajenticai.cloud)" }, + timeout: 5500, + }); + if (Array.isArray(nomRes.data) && nomRes.data.length > 0) { + const hit = nomRes.data[0]; + const addr = hit.address || {}; + const lat = parseFloat(hit.lat); + const lng = parseFloat(hit.lon); + const thana = addr.suburb || addr.neighbourhood || addr.town || addr.municipality || addr.city_district || req.thana || ""; + const district = (addr.county || addr.city || addr.state_district || req.district || "Dhaka").replace(/\s*District$/i, ""); + const division = addr.state || "Dhaka Division"; + return { + success: true, + formatted_address: hit.display_name || fullQuery, + thana, + district, + division, + coordinates: { lat, lng }, + location_type: "GEOMETRIC_CENTER", + is_deliverable: true, + confidence_score: 88, + place_id: `osm_${hit.place_id || Date.now()}`, + }; + } + } catch (_) { + // Proceed to Tier 3 Spatial Gazetteer + } + + // Tier 3: Bangladesh 64-District & Dhaka Thana Spatial Gazetteer + const raw = [req.address, req.thana, req.district].filter(Boolean).join(" ").toLowerCase(); + const gazetteer: Array<{ rx: RegExp; thana: string; district: string; division: string; lat: number; lng: number }> = [ + { rx: /mirpur|মিরপুর|pallabi|পল্লবী|kazipara|shewrapara|কাজীপাড়া|শেওড়াপাড়া/, thana: "Mirpur", district: "Dhaka", division: "Dhaka Division", lat: 23.8042, lng: 90.3667 }, + { rx: /uttara|উত্তরা|airport|বিমানবন্দর|sector|সেক্টর|dakshin khan|uttar khan/, thana: "Uttara", district: "Dhaka", division: "Dhaka Division", lat: 23.8759, lng: 90.3795 }, + { rx: /dhanmondi|ধানমন্ডি|kalabagan|কলাবাগান|jigatola|জিগাতলা|shankar/, thana: "Dhanmondi", district: "Dhaka", division: "Dhaka Division", lat: 23.7461, lng: 90.3742 }, + { rx: /gulshan|গুলশান|banani|বনানী|baridhara|বারিধারা|niketan|নিকেতন/, thana: "Gulshan", district: "Dhaka", division: "Dhaka Division", lat: 23.7925, lng: 90.4078 }, + { rx: /mohammadpur|মোহাম্মদপুর|adabor|আদাবর|lalmatia|লালমাটিয়া|bosila|বসিলা/, thana: "Mohammadpur", district: "Dhaka", division: "Dhaka Division", lat: 23.7662, lng: 90.3589 }, + { rx: /badda|বাড্ডা|rampura|রামপুরা|banasree|বনশ্রী|aftabnagar|আফতাবনগর|hatirjheel/, thana: "Badda", district: "Dhaka", division: "Dhaka Division", lat: 23.7805, lng: 90.4267 }, + { rx: /bashundhara|বসুন্ধরা|vatara|ভাটারা|kuril|কুড়িল|nikunja|নিকুঞ্জ|khilkhet|খিলক্ষেত/, thana: "Vatara", district: "Dhaka", division: "Dhaka Division", lat: 23.8191, lng: 90.4526 }, + { rx: /motijheel|মতিঝিল|paltan|পল্টন|kakrail|কাকরাইল|shantinagar|শান্তিনগর|malibagh|মালিবাগ|mogbazar|মগবাজার/, thana: "Motijheel", district: "Dhaka", division: "Dhaka Division", lat: 23.7330, lng: 90.4172 }, + { rx: /shahbag|শাহবাগ|hatirpool|হাতিরপুল|motaleb|মোতালেব|eleph|এলিফ্যান্ট|new market|নিউ মার্কেট|farmgate|ফার্মগেট|tejgaon|তেজগাঁও|karwan|কারওয়ান/, thana: "Shahbagh", district: "Dhaka", division: "Dhaka Division", lat: 23.7415, lng: 90.3918 }, + { rx: /jatrabari|যাত্রাবাড়ী|demra|ডেমরা|kadamtali|কদমতলী|shanir akhra|শনির আখড়া|wari|ওয়ারী|old dhaka|পুরান ঢাকা|lalbagh|লালবাগ|kotwali/, thana: "Jatrabari", district: "Dhaka", division: "Dhaka Division", lat: 23.7104, lng: 90.4348 }, + { rx: /keraniganj|কেরানীগঞ্জ|ati bazar|আটি বাজার|atibazar|hasnabad/, thana: "Keraniganj", district: "Dhaka", division: "Dhaka Division", lat: 23.6942, lng: 90.3475 }, + { rx: /savar|সাভার|ashulia|আশুলিয়া|hemayetpur|হেমায়েতপুর|dhamrai|ধামরাই/, thana: "Savar", district: "Dhaka", division: "Dhaka Division", lat: 23.8583, lng: 90.2667 }, + { rx: /gazipur|গাজীপুর|tongi|টঙ্গী|board bazar|বোর্ড বাজার|joydebpur|জয়দেবপুর|konabari/, thana: "Tongi", district: "Gazipur", division: "Dhaka Division", lat: 23.9889, lng: 90.3950 }, + { rx: /narayanganj|নারায়ণগঞ্জ|siddhirganj|সিদ্ধিরগঞ্জ|fatullah|ফতুল্লা|chashara|চাষাড়া|sonargaon|rupganj/, thana: "Narayanganj Sadar", district: "Narayanganj", division: "Dhaka Division", lat: 23.6238, lng: 90.5000 }, + { rx: /chattogram|chittagong|চট্টগ্রাম|agrabad|আগ্রাবাদ|nasirabad|নাসিরাবাদ|halishahar|হালিশহর|panchlaish|gec|জিইসি|kotwali/, thana: "Panchlaish", district: "Chattogram", division: "Chattogram Division", lat: 22.3569, lng: 91.7832 }, + { rx: /cox.?s?s*bazar|কক্সবাজার|chakaria|teknaf|ukhiya/, thana: "Cox's Bazar Sadar", district: "Cox's Bazar", division: "Chattogram Division", lat: 21.4272, lng: 92.0058 }, + { rx: /cumilla|comilla|কুমিল্লা|kandirpar|কান্দিরপাড়|laksam|daudkandi/, thana: "Cumilla Sadar", district: "Cumilla", division: "Chattogram Division", lat: 23.4607, lng: 91.1809 }, + { rx: /feni|ফেনী|noakhali|নোয়াখালী|maijdee|চৌমুহনী|lakshmipur|লক্ষ্মীপুর|chandpur|চাঁদপুর|brahmanbaria|ব্রাহ্মণবাড়িয়া/, thana: "Sadar", district: "Noakhali", division: "Chattogram Division", lat: 22.8724, lng: 91.0973 }, + { rx: /sylhet|সিলেট|zindabazar|জিন্দাবাজার|amberkhana|আম্বরখানা|upashahar|উপশহর|moulvibazar|মৌলভীবাজার|sreemangal|habiganj|হবিগঞ্জ|sunamganj|সুনামগঞ্জ/, thana: "Sylhet Sadar", district: "Sylhet", division: "Sylhet Division", lat: 24.8949, lng: 91.8687 }, + { rx: /rajshahi|রাজশাহী|boalia|বোয়ালিয়া|shaheb bazar|সাহেব বাজার|motihar|নাটোর|natore|naogaon|নওগাঁ|chapainawabganj|চাঁপাইনবাবগঞ্জ|pabna|পাবনা|sirajganj|সিরাজগঞ্জ/, thana: "Boalia", district: "Rajshahi", division: "Rajshahi Division", lat: 24.3745, lng: 88.6042 }, + { rx: /bogura|bogra|বগুড়া|sherpur|শেরপুর|joypurhat|জয়পুরহাট/, thana: "Bogura Sadar", district: "Bogura", division: "Rajshahi Division", lat: 24.8481, lng: 89.3730 }, + { rx: /khulna|খুলনা|sonadanga|সোনাডাঙ্গা|khalishpur|খালিশপুর|daulatpur|jashore|jessore|যশোর|kushtia|কুষ্টিয়া|satkhira|সাতক্ষীরা|bagerhat|বাগেরহাট|jhenaidah|ঝিনাইদহ/, thana: "Sonadanga", district: "Khulna", division: "Khulna Division", lat: 22.8456, lng: 89.5403 }, + { rx: /barishal|barisal|বরিশাল|patukhali|পটুয়াখালী|bhola|ভোলা|pirojpur|পিরোজপুর|jhalokati|barguna/, thana: "Barishal Sadar", district: "Barishal", division: "Barishal Division", lat: 22.7010, lng: 90.3535 }, + { rx: /rangpur|রংপুর|dinajpur|দিনাজপুর|thakurgaon|ঠাকুরগাঁও|panchagarh|পঞ্চগড়|nilphamari|নীলফামারী|saidpur|সৈয়দপুর|kurigram|কুড়িগ্রাম|gaibandha|গাইবান্ধা|lalmonirhat/, thana: "Rangpur Sadar", district: "Rangpur", division: "Rangpur Division", lat: 25.7439, lng: 89.2752 }, + { rx: /mymensingh|ময়মনসিংহ|jamalpur|জামালপুর|netrokona|নেত্রকোনা|tangail|টাঙ্গাইল|kishoreganj|কিশোরগঞ্জ|faridpur|ফরিদপুর|madaripur|মাদারীপুর|gopalganj|গোপালগঞ্জ|shariatpur|শরীয়তপুর|rajbari|রাজবাড়ী|manikganj|মানিকগঞ্জ|munshiganj|মুন্সিগঞ্জ|narsingdi|নরসিংদী/, thana: "Mymensingh Sadar", district: "Mymensingh", division: "Mymensingh Division", lat: 24.7471, lng: 90.4203 }, + ]; + + for (const g of gazetteer) { + if (g.rx.test(raw)) { + return { + success: true, + formatted_address: `${req.address}, ${req.thana || g.thana}, ${req.district || g.district}, Bangladesh`, + thana: req.thana || g.thana, + district: req.district || g.district, + division: g.division, + coordinates: { lat: g.lat, lng: g.lng }, + location_type: "GEOMETRIC_CENTER", + is_deliverable: true, + confidence_score: 84, + place_id: `bd_gazetteer_${g.district.toLowerCase().replace(/\s+/g, "_")}`, + }; + } + } + + const isDhaka = /dhaka|ঢাকা/i.test(raw); return { success: true, formatted_address: req.address, - thana: req.thana || "", + thana: req.thana || (isDhaka ? "Dhaka Metro" : "Sadar"), district: req.district || (isDhaka ? "Dhaka" : "Outside Dhaka"), - division: isDhaka ? "Dhaka" : "", - coordinates: isDhaka ? { lat: 23.8103, lng: 90.4125 } : { lat: 22.3569, lng: 91.7832 }, + division: isDhaka ? "Dhaka Division" : "Bangladesh", + coordinates: isDhaka ? { lat: 23.8103, lng: 90.4125 } : { lat: 24.0000, lng: 90.0000 }, location_type: "APPROXIMATE", is_deliverable: true, - confidence_score: 65, + confidence_score: 72, }; } diff --git a/src/http.ts b/src/http.ts index cf186e7..f49c4b7 100644 --- a/src/http.ts +++ b/src/http.ts @@ -55,6 +55,18 @@ function keysFromRequest(req: Request): EnvSource { const app = express(); app.use(express.json({ limit: "1mb" })); +// Robust Malformed JSON Syntax Error Guard +app.use((err: any, req: Request, res: Response, next: any) => { + if (err && (err.type === "entity.parse.failed" || err.status === 400)) { + return res.status(400).json({ + error: "Malformed JSON payload in request body", + details: err.message, + timestamp: new Date().toISOString() + }); + } + next(err); +}); + // Allow browser-based and cross-origin MCP clients. app.use((req, res, next) => { res.header("Access-Control-Allow-Origin", "*"); diff --git a/src/registry.ts b/src/registry.ts index 05e600a..cf9ba36 100644 --- a/src/registry.ts +++ b/src/registry.ts @@ -86,30 +86,54 @@ export class CourierRegistry { return this.googleMaps; } - async createParcel(req: ParcelCreateRequest): Promise { - let courierName: SupportedCourier = "steadfast"; + resolveSmartAutoCourierOrder(address: string, priority: string = "balanced"): SupportedCourier[] { + const addr = String(address || "").toLowerCase(); + const isSubDhaka = /savar|ashulia|keraniganj|gajipur|gazipur|tongi|narayanganj|siddhirganj|fatullah|dohar|nawabganj|সাভার|আশুলিয়া|কেরানীগঞ্জ|গাজীপুর|টঙ্গী|নারায়ণগঞ্জ/.test(addr); + const isDhakaMetro = !isSubDhaka && (/dhaka|ঢাকা|gulshan|banani|dhanmondi|uttara|mirpur|mohammadpur|motijheel|badda|rampura|bashundhara|tejgaon|khilgaon|malibagh|mogbazar|farmgate|shahbag|hatirpool|lalmatia|wari|jatrabari|গুলশান|বনানী|ধানমন্ডি|উত্তরা|মিরপুর|মোহাম্মদপুর|মতিঝিল|বাড্ডা|রামপুরা|বসুন্ধরা|তেজগাঁও|খিলগাঁও|মালিবাগ|মগবাজার|ফার্মগেট|শাহবাগ|হাতিরপুল|ওয়ারী|যাত্রাবাড়ী/.test(addr)); - if (req.courier && req.courier !== "auto") { - courierName = req.courier; + let idealOrder: SupportedCourier[]; + if (isDhakaMetro) { + idealOrder = priority === "cheapest" + ? ["pathao", "redx", "paperfly", "steadfast"] + : ["pathao", "redx", "steadfast", "paperfly"]; + } else if (isSubDhaka) { + idealOrder = priority === "fastest" + ? ["pathao", "steadfast", "redx", "paperfly"] + : ["paperfly", "steadfast", "pathao", "redx"]; } else { - // Smart routing: inside Dhaka metro with Pathao configured prefers Pathao, else Steadfast - const addr = req.recipient_address.toLowerCase(); - if ( - addr.includes("dhaka") && - (addr.includes("gulshan") || - addr.includes("banani") || - addr.includes("dhanmondi") || - addr.includes("uttara") || - addr.includes("mirpur")) - ) { - courierName = this.adapters.get("pathao")?.isConfigured() ? "pathao" : "steadfast"; - } else { - courierName = "steadfast"; + idealOrder = priority === "cheapest" + ? ["paperfly", "pathao", "steadfast", "redx"] + : ["steadfast", "paperfly", "pathao", "redx"]; + } + + const configured = idealOrder.filter((c) => this.adapters.get(c)?.isConfigured()); + return configured.length > 0 ? configured : idealOrder; + } + + async createParcel(req: ParcelCreateRequest): Promise { + const rawChoice = String(req.courier || "auto").toLowerCase(); + const isAuto = rawChoice === "auto" || rawChoice.startsWith("auto_"); + const priority = rawChoice === "auto_fastest" ? "fastest" : rawChoice === "auto_cheapest" ? "cheapest" : "balanced"; + + const candidates: SupportedCourier[] = isAuto + ? this.resolveSmartAutoCourierOrder(req.recipient_address, priority) + : [rawChoice as SupportedCourier]; + + let lastErr: any = null; + for (const courierName of candidates) { + try { + const adapter = this.getAdapter(courierName); + const res = await adapter.createParcel({ ...req, courier: courierName }); + if (res && res.success !== false && (res.tracking_code || res.consignment_id)) { + return res; + } + lastErr = new Error((res as any)?.message || (res as any)?.error || `${courierName} rejected parcel`); + } catch (err: any) { + lastErr = err; } } - const adapter = this.getAdapter(courierName); - return await adapter.createParcel(req); + throw lastErr || new Error("All configured courier gateways failed to book parcel."); } async trackParcel(trackingCode: string, courierName?: SupportedCourier): Promise { @@ -150,6 +174,7 @@ export class CourierRegistry { const cid = req.consignment_id; const name = req.customer_name || "গ্রাহক"; const courier = req.courier; + const phoneTag = req.customer_phone ? ` [Customer Phone: ${req.customer_phone}]` : ""; let status: "DISPUTE_FILED" | "RESCHEDULE_QUEUED" | "ADDRESS_UPDATED" | "CUSTOMER_REENGAGED" = "CUSTOMER_REENGAGED"; let actionTaken = ""; @@ -200,7 +225,7 @@ export class CourierRegistry { resolution_status: status, action_taken: actionTaken, customer_whatsapp_message: customerMsg, - carrier_instruction: carrierInstr, + carrier_instruction: carrierInstr + phoneTag, timestamp: new Date().toISOString(), }; }