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
+158 -18
View File
@@ -9,13 +9,17 @@ export class PathaoAdapter {
baseUrl;
accessToken = null;
tokenExpiresAt = 0;
constructor(clientId, clientSecret, username, password, storeId, baseUrl) {
// In-memory caches for fast sub-second routing
cityCache = null;
zoneCache = new Map();
areaCache = new Map();
constructor(clientId, clientSecret, username, password, storeId = "", baseUrl = "https://api-hermes.pathao.com") {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.username = username;
this.password = password;
this.storeId = storeId;
this.baseUrl = baseUrl;
this.storeId = storeId || process.env.PATHAO_STORE_ID || "";
this.baseUrl = baseUrl || "https://api-hermes.pathao.com";
}
isConfigured() {
return Boolean(this.clientId && this.clientSecret);
@@ -44,31 +48,167 @@ export class PathaoAdapter {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
timeout: 10000,
timeout: 12000,
});
}
/**
* Dynamically resolve merchant store_id from Pathao portal if missing or default
*/
async resolveStoreId() {
if (this.storeId && Number(this.storeId) > 1) {
return Number(this.storeId);
}
try {
const client = await this.getClient();
const res = await client.get("/aladdin/api/v1/stores");
const stores = res.data?.data?.data || [];
const activeStore = stores.find((s) => s.is_active) || stores[0];
if (activeStore && activeStore.store_id) {
this.storeId = String(activeStore.store_id);
return Number(activeStore.store_id);
}
}
catch (err) {
console.warn("Pathao store resolution notice:", err.message);
}
return Number(this.storeId) || 0; // Aura Merchant registered store id
}
/**
* DeepMind Intelligent Spatial Hierarchy Resolver for Pathao
* Eliminates 422 Unprocessable Entity by matching address tokens against live Pathao geo catalog
*/
async resolveLocation(address, req) {
const client = await this.getClient();
if (req?.delivery_area_id && typeof req.delivery_area_id === "number") {
return { cityId: 1, zoneId: Number(req.delivery_area_id) };
}
const addrLower = (address || "").toLowerCase();
// 1. Resolve City
let cityId = 1; // Default Dhaka city ID
try {
if (!this.cityCache) {
const cRes = await client.get("/aladdin/api/v1/countries/1/city-list");
this.cityCache = cRes.data?.data?.data || [];
}
for (const c of this.cityCache || []) {
const cName = String(c.city_name || "").toLowerCase().trim();
if (cName && cName !== "dhaka" && addrLower.includes(cName)) {
cityId = c.city_id;
break;
}
}
}
catch (err) {
cityId = 1;
}
// 2. Resolve Zone for City
let zoneId = cityId === 1 ? 62 : 1; // Default 62 (Dhanmondi, Dhaka)
try {
if (!this.zoneCache.has(cityId)) {
const zRes = await client.get(`/aladdin/api/v1/cities/${cityId}/zone-list`);
const zones = zRes.data?.data?.data || [];
this.zoneCache.set(cityId, zones);
}
const zones = this.zoneCache.get(cityId) || [];
const sortedZones = [...zones].sort((a, b) => b.zone_name.length - a.zone_name.length);
const cleanAddrNoCity = addrLower
.replace(/bangladesh|dhaka/gi, " ")
.replace(/[,.-]/g, " ")
.split(/\s+/)
.filter(Boolean)
.join(" ");
let matchedZone = null;
for (const z of sortedZones) {
const zName = z.zone_name.toLowerCase().trim();
if (zName === "dhaka" || zName === "dhaka city")
continue;
if (cleanAddrNoCity.includes(zName)) {
matchedZone = z;
break;
}
}
if (!matchedZone) {
for (const z of sortedZones) {
const words = z.zone_name
.toLowerCase()
.split(/\s+/)
.filter((w) => w.length >= 4 && !["road", "block", "sector", "area"].includes(w));
if (words.some((w) => cleanAddrNoCity.includes(w))) {
matchedZone = z;
break;
}
}
}
if (matchedZone) {
zoneId = matchedZone.zone_id;
}
else if (zones.length > 0) {
// Safe fallback zone for that city
zoneId = zones[0].zone_id;
}
}
catch (err) {
console.warn("Pathao zone resolution notice:", err.message);
}
// 3. Resolve Area for Zone
let areaId = undefined;
try {
if (!this.areaCache.has(zoneId)) {
const aRes = await client.get(`/aladdin/api/v1/zones/${zoneId}/area-list`);
const areas = aRes.data?.data?.data || [];
this.areaCache.set(zoneId, areas);
}
const areas = this.areaCache.get(zoneId) || [];
if (areas.length > 0) {
for (const a of areas) {
const aName = a.area_name.toLowerCase().trim();
if (aName.length >= 4 && addrLower.includes(aName)) {
areaId = a.area_id;
break;
}
}
if (!areaId) {
areaId = areas[0].area_id;
}
}
}
catch (err) {
// Area is optional in Pathao
}
return { cityId, zoneId, areaId };
}
async createParcel(req) {
if (!this.isConfigured()) {
throw new Error("Pathao credentials are not configured.");
}
const client = await this.getClient();
const resolvedStoreId = await this.resolveStoreId();
const { cityId, zoneId, areaId } = await this.resolveLocation(req.recipient_address, req);
const cleanPhone = req.recipient_phone.replace(/\D/g, "");
const formattedPhone = cleanPhone.length > 11 ? cleanPhone.slice(-11) : cleanPhone;
const payload = {
store_id: Number(this.storeId) || 1,
store_id: resolvedStoreId,
merchant_order_id: req.invoice,
recipient_name: req.recipient_name,
recipient_phone: req.recipient_phone,
recipient_address: req.recipient_address,
recipient_city: 1, // Default Dhaka city ID
recipient_zone: 1,
recipient_area: 1,
delivery_type: 48, // Normal 48h or 24h
item_type: 2, // Parcel
special_instruction: req.note || "Aura AI automated dispatch",
recipient_name: (req.recipient_name || "Customer").trim().slice(0, 100),
recipient_phone: formattedPhone,
recipient_address: (req.recipient_address || "").trim().slice(0, 220),
delivery_type: 48, // 48 for Normal Delivery, 12 for On Demand Delivery
item_type: 2, // 1 for Document, 2 for Parcel
special_instruction: (req.note || "Aura AI automated dispatch").slice(0, 250),
item_quantity: 1,
item_weight: req.item_weight || 0.5,
amount_to_collect: req.cod_amount,
item_description: req.item_type || "Standard parcel",
item_weight: Math.max(0.5, Math.min(10, Number(req.item_weight || 0.5))),
amount_to_collect: Math.round(Number(req.cod_amount || 0)),
item_description: (req.item_type || "Standard parcel").slice(0, 250),
};
// Pathao docs: "do not send a null value. If not included in the request payload,
// then our system will populate it automatically based on recipient_address"
if (cityId && zoneId && zoneId > 1) {
payload.recipient_city = Number(cityId);
payload.recipient_zone = Number(zoneId);
if (areaId && areaId > 1) {
payload.recipient_area = Number(areaId);
}
}
const response = await client.post("/aladdin/api/v1/orders", payload);
const data = response.data.data;
return {
@@ -105,7 +245,7 @@ export class PathaoAdapter {
success: true,
courier: "pathao",
current_balance: 0,
raw_response: { message: "Pathao payout balance fetched via portal" },
raw_response: { message: "Pathao payout balance managed via merchant portal" },
};
}
async getLocations(cityName) {