shipflow 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/dist/carriers/aramex/adapter.d.ts.map +1 -1
- package/dist/carriers/aramex/index.js +12 -11
- package/dist/carriers/aramex/index.js.map +4 -4
- package/dist/carriers/aramex/mappers.d.ts +13 -5
- package/dist/carriers/aramex/mappers.d.ts.map +1 -1
- package/dist/carriers/aymakan/adapter.d.ts +17 -21
- package/dist/carriers/aymakan/adapter.d.ts.map +1 -1
- package/dist/carriers/aymakan/index.js +67 -53
- package/dist/carriers/aymakan/index.js.map +4 -4
- package/dist/carriers/aymakan/mappers.d.ts +2 -0
- package/dist/carriers/aymakan/mappers.d.ts.map +1 -1
- package/dist/carriers/aymakan/types.d.ts +1 -1
- package/dist/carriers/aymakan/types.d.ts.map +1 -1
- package/dist/carriers/smsaexpress/adapter.d.ts +43 -4
- package/dist/carriers/smsaexpress/adapter.d.ts.map +1 -1
- package/dist/carriers/smsaexpress/index.js +141 -25
- package/dist/carriers/smsaexpress/index.js.map +5 -5
- package/dist/carriers/smsaexpress/mappers.d.ts +12 -3
- package/dist/carriers/smsaexpress/mappers.d.ts.map +1 -1
- package/dist/carriers/smsaexpress/services.d.ts +3 -1
- package/dist/carriers/smsaexpress/services.d.ts.map +1 -1
- package/dist/core/city-resolver.d.ts +44 -0
- package/dist/core/city-resolver.d.ts.map +1 -0
- package/dist/core/http.d.ts +8 -0
- package/dist/core/http.d.ts.map +1 -1
- package/dist/core/retry.d.ts +8 -3
- package/dist/core/retry.d.ts.map +1 -1
- package/dist/{index-qnxj8bct.js → index-8cpdcbsf.js} +32 -6
- package/dist/{index-qnxj8bct.js.map → index-8cpdcbsf.js.map} +5 -5
- package/dist/index-dfx6rbm2.js +36 -0
- package/dist/index-dfx6rbm2.js.map +10 -0
- package/dist/index.js +3 -3
- package/dist/index.js.map +3 -3
- package/package.json +1 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// src/core/city-resolver.ts
|
|
2
|
+
var CITIES_CACHE_TTL = 60 * 60 * 1000;
|
|
3
|
+
var CITIES_FAILURE_COOLDOWN = 60 * 1000;
|
|
4
|
+
function normalizeArabicCity(text) {
|
|
5
|
+
return text.trim().replace(/[\u064B-\u065F\u0670]/g, "").replace(/[أإآ]/g, "ا").replace(/ة/g, "ه");
|
|
6
|
+
}
|
|
7
|
+
function findCityMatch(inputCity, cities) {
|
|
8
|
+
const trimmed = inputCity.trim();
|
|
9
|
+
if (!trimmed || cities.length === 0)
|
|
10
|
+
return null;
|
|
11
|
+
const lower = trimmed.toLowerCase();
|
|
12
|
+
const exactEn = cities.find((c) => c.nameEn.toLowerCase() === lower);
|
|
13
|
+
if (exactEn)
|
|
14
|
+
return exactEn;
|
|
15
|
+
const exactAr = cities.find((c) => c.nameAr === trimmed);
|
|
16
|
+
if (exactAr)
|
|
17
|
+
return exactAr;
|
|
18
|
+
const normalizedInput = normalizeArabicCity(trimmed);
|
|
19
|
+
const normalizedAr = cities.find((c) => c.nameAr && normalizeArabicCity(c.nameAr) === normalizedInput);
|
|
20
|
+
if (normalizedAr)
|
|
21
|
+
return normalizedAr;
|
|
22
|
+
const withoutAl = normalizedInput.startsWith("ال") ? normalizedInput.slice(2) : `ال${normalizedInput}`;
|
|
23
|
+
const alMatch = cities.find((c) => c.nameAr && normalizeArabicCity(c.nameAr) === withoutAl);
|
|
24
|
+
if (alMatch)
|
|
25
|
+
return alMatch;
|
|
26
|
+
if (lower.length >= 3) {
|
|
27
|
+
const containsEn = cities.find((c) => c.nameEn.toLowerCase().includes(lower));
|
|
28
|
+
if (containsEn)
|
|
29
|
+
return containsEn;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export { CITIES_CACHE_TTL, CITIES_FAILURE_COOLDOWN, findCityMatch };
|
|
35
|
+
|
|
36
|
+
//# debugId=3A9ABF919E4CEDC364756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/core/city-resolver.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"// file: src/core/city-resolver.ts\n/**\n * Shared city matching for carrier adapters that validate the shipper /\n * consignee city against the carrier's own city list before booking.\n *\n * Extracted verbatim from the Aymakan adapter's proven matching strategy so\n * SMSA (and future carriers) enforce identical semantics. The strategy never\n * silently reroutes a distinct longer city to a shorter candidate (see the\n * step-5 direction guard).\n */\n\nimport type { City } from \"./types\";\n\n/** How long a fetched carrier city list stays fresh (1 hour). */\nexport const CITIES_CACHE_TTL = 60 * 60 * 1000;\n\n/**\n * After a failed city-list fetch, don't retry for this long (1 minute) —\n * adapters fall back to pass-through resolution so a carrier /cities outage\n * never hard-blocks shipment creation.\n */\nexport const CITIES_FAILURE_COOLDOWN = 60 * 1000;\n\n/**\n * Normalize Arabic text for comparison:\n * - Strip diacritics (tashkeel)\n * - Normalize alef variants (أ إ آ → ا)\n * - Normalize taa marbuta (ة → ه)\n * - Trim whitespace\n */\nexport function normalizeArabicCity(text: string): string {\n return text\n .trim()\n .replace(/[\\u064B-\\u065F\\u0670]/g, \"\") // strip tashkeel\n .replace(/[أإآ]/g, \"ا\") // normalize alef\n .replace(/ة/g, \"ه\"); // normalize taa marbuta\n}\n\n/**\n * Match a user-input city name against a carrier city list.\n *\n * Matching strategy (first match wins):\n * 1. Exact match on English name (case-insensitive)\n * 2. Exact match on Arabic name\n * 3. Normalized Arabic match (handles tashkeel, alef variants, taa marbuta)\n * 4. Match after toggling the \"ال\" prefix on the Arabic input\n * 5. Candidate-contains-input match on English name (candidate name contains\n * the input, e.g. \"Riyadh\" -> \"Riyadh City\"; requires input length >= 3).\n * The reverse direction is intentionally excluded so a distinct longer\n * city like \"Riyadh Al Khabra\" is never rerouted to \"Riyadh\".\n *\n * Returns null when nothing matches — the caller decides whether that means\n * pass-through (lenient) or a validation error (strict).\n */\nexport function findCityMatch(inputCity: string, cities: City[]): City | null {\n const trimmed = inputCity.trim();\n if (!trimmed || cities.length === 0) return null;\n const lower = trimmed.toLowerCase();\n\n // 1. Exact English match (case-insensitive)\n const exactEn = cities.find((c) => c.nameEn.toLowerCase() === lower);\n if (exactEn) return exactEn;\n\n // 2. Exact Arabic match\n const exactAr = cities.find((c) => c.nameAr === trimmed);\n if (exactAr) return exactAr;\n\n // 3. Normalized Arabic match\n const normalizedInput = normalizeArabicCity(trimmed);\n const normalizedAr = cities.find(\n (c) => c.nameAr && normalizeArabicCity(c.nameAr) === normalizedInput,\n );\n if (normalizedAr) return normalizedAr;\n\n // 4. Arabic with the \"ال\" prefix toggled\n const withoutAl = normalizedInput.startsWith(\"ال\")\n ? normalizedInput.slice(2)\n : `ال${normalizedInput}`;\n const alMatch = cities.find(\n (c) => c.nameAr && normalizeArabicCity(c.nameAr) === withoutAl,\n );\n if (alMatch) return alMatch;\n\n // 5. Candidate-contains-input match on English name (e.g. input \"Riyadh\"\n // resolves to the canonical \"Riyadh City\"). We deliberately match ONLY\n // this direction. The reverse direction (input contains a candidate's\n // name) is excluded because a longer, distinct city such as\n // \"Riyadh Al Khabra\" would otherwise be silently rerouted to \"Riyadh\".\n // Guarded by a minimum input length so very short inputs (1-2 chars)\n // don't spuriously substring-match many candidates.\n if (lower.length >= 3) {\n const containsEn = cities.find((c) =>\n c.nameEn.toLowerCase().includes(lower),\n );\n if (containsEn) return containsEn;\n }\n\n return null;\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";AAcO,IAAM,mBAAmB,KAAK,KAAK;AAOnC,IAAM,0BAA0B,KAAK;AASrC,SAAS,mBAAmB,CAAC,MAAsB;AAAA,EACxD,OAAO,KACJ,KAAK,EACL,QAAQ,0BAA0B,EAAE,EACpC,QAAQ,UAAU,GAAG,EACrB,QAAQ,MAAM,GAAG;AAAA;AAmBf,SAAS,aAAa,CAAC,WAAmB,QAA6B;AAAA,EAC5E,MAAM,UAAU,UAAU,KAAK;AAAA,EAC/B,IAAI,CAAC,WAAW,OAAO,WAAW;AAAA,IAAG,OAAO;AAAA,EAC5C,MAAM,QAAQ,QAAQ,YAAY;AAAA,EAGlC,MAAM,UAAU,OAAO,KAAK,CAAC,MAAM,EAAE,OAAO,YAAY,MAAM,KAAK;AAAA,EACnE,IAAI;AAAA,IAAS,OAAO;AAAA,EAGpB,MAAM,UAAU,OAAO,KAAK,CAAC,MAAM,EAAE,WAAW,OAAO;AAAA,EACvD,IAAI;AAAA,IAAS,OAAO;AAAA,EAGpB,MAAM,kBAAkB,oBAAoB,OAAO;AAAA,EACnD,MAAM,eAAe,OAAO,KAC1B,CAAC,MAAM,EAAE,UAAU,oBAAoB,EAAE,MAAM,MAAM,eACvD;AAAA,EACA,IAAI;AAAA,IAAc,OAAO;AAAA,EAGzB,MAAM,YAAY,gBAAgB,WAAW,IAAI,IAC7C,gBAAgB,MAAM,CAAC,IACvB,KAAK;AAAA,EACT,MAAM,UAAU,OAAO,KACrB,CAAC,MAAM,EAAE,UAAU,oBAAoB,EAAE,MAAM,MAAM,SACvD;AAAA,EACA,IAAI;AAAA,IAAS,OAAO;AAAA,EASpB,IAAI,MAAM,UAAU,GAAG;AAAA,IACrB,MAAM,aAAa,OAAO,KAAK,CAAC,MAC9B,EAAE,OAAO,YAAY,EAAE,SAAS,KAAK,CACvC;AAAA,IACA,IAAI;AAAA,MAAY,OAAO;AAAA,EACzB;AAAA,EAEA,OAAO;AAAA;",
|
|
8
|
+
"debugId": "3A9ABF919E4CEDC364756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
WeightSchema,
|
|
21
21
|
validateCreateShipmentInput,
|
|
22
22
|
validatePickupRequest
|
|
23
|
-
} from "./index-
|
|
23
|
+
} from "./index-8cpdcbsf.js";
|
|
24
24
|
|
|
25
25
|
// src/client.ts
|
|
26
26
|
class ShipFlow {
|
|
@@ -28,7 +28,7 @@ class ShipFlow {
|
|
|
28
28
|
constructor(options) {
|
|
29
29
|
this.adapters = new Map;
|
|
30
30
|
for (const adapter of options.adapters) {
|
|
31
|
-
this.adapters.set(adapter.name, adapter);
|
|
31
|
+
this.adapters.set(adapter.name.toLowerCase(), adapter);
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
carrier(name) {
|
|
@@ -108,4 +108,4 @@ export {
|
|
|
108
108
|
APIError
|
|
109
109
|
};
|
|
110
110
|
|
|
111
|
-
//# debugId=
|
|
111
|
+
//# debugId=BCEF0C2C69F900CD64756E2164756E21
|
package/dist/index.js.map
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/client.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"// file: src/client.ts\n/**\n * ShipFlow Client\n * Tree-shakeable factory that accepts carrier adapters via dependency injection.\n * Only the carriers you inject will be bundled in your final application.\n */\n\nimport type { CarrierAdapter } from \"./carriers/base\";\nimport { ShipFlowError } from \"./core/errors\";\nimport type { CreateShipmentInput, Rate, TrackingResult } from \"./core/types\";\n\nexport interface ShipFlowOptions {\n adapters: CarrierAdapter[];\n}\n\nexport class ShipFlow {\n private adapters: Map<string, CarrierAdapter>;\n\n constructor(options: ShipFlowOptions) {\n this.adapters = new Map();\n\n for (const adapter of options.adapters) {\n this.adapters.set(adapter.name, adapter);\n }\n }\n\n /**\n * Get a specific carrier adapter by name.\n * @throws ShipFlowError if carrier is not configured\n */\n carrier(name: string): CarrierAdapter {\n const adapter = this.adapters.get(name.toLowerCase());\n if (!adapter) {\n throw new ShipFlowError(`Carrier \"${name}\" is not configured`, {\n carrier: name,\n });\n }\n return adapter;\n }\n\n /**\n * Get all configured carrier names.\n */\n get carriers(): string[] {\n return [...this.adapters.keys()];\n }\n\n /**\n * Check if a carrier is configured.\n */\n hasCarrier(name: string): boolean {\n return this.adapters.has(name.toLowerCase());\n }\n\n /**\n * Get rates from all configured carriers that support rate calculation.\n * Returns a map of carrier name to rates (or error).\n */\n async getRatesFromAll(\n input: CreateShipmentInput,\n ): Promise<Map<string, Rate[] | Error>> {\n const results = new Map<string, Rate[] | Error>();\n\n const promises = [...this.adapters.entries()].map(\n async ([name, adapter]) => {\n if (!adapter.getRates) {\n return; // Skip carriers that don't support rates\n }\n\n try {\n const rates = await adapter.getRates(input);\n results.set(name, rates);\n } catch (error) {\n results.set(\n name,\n error instanceof Error ? error : new Error(String(error)),\n );\n }\n },\n );\n\n await Promise.allSettled(promises);\n return results;\n }\n\n /**\n * Try to track a shipment across all configured carriers in parallel.\n * Useful when you don't know which carrier handles a tracking number.\n * Returns the first successful result with a known status.\n */\n async trackAcrossCarriers(\n trackingNumber: string,\n ): Promise<TrackingResult | null> {\n const promises = [...this.adapters.values()].map(async (adapter) => {\n const result = await adapter.track(trackingNumber);\n if (result && result.status !== \"unknown\") {\n return result;\n }\n return null;\n });\n\n const results = await Promise.allSettled(promises);\n for (const result of results) {\n if (result.status === \"fulfilled\" && result.value) {\n return result.value;\n }\n }\n return null;\n }\n}\n\n/**\n * Factory function for creating ShipFlow client.\n * This is the recommended way to instantiate the client.\n */\nexport function createShipFlow(options: ShipFlowOptions): ShipFlow {\n return new ShipFlow(options);\n}\n"
|
|
5
|
+
"// file: src/client.ts\n/**\n * ShipFlow Client\n * Tree-shakeable factory that accepts carrier adapters via dependency injection.\n * Only the carriers you inject will be bundled in your final application.\n */\n\nimport type { CarrierAdapter } from \"./carriers/base\";\nimport { ShipFlowError } from \"./core/errors\";\nimport type { CreateShipmentInput, Rate, TrackingResult } from \"./core/types\";\n\nexport interface ShipFlowOptions {\n adapters: CarrierAdapter[];\n}\n\nexport class ShipFlow {\n private adapters: Map<string, CarrierAdapter>;\n\n constructor(options: ShipFlowOptions) {\n this.adapters = new Map();\n\n for (const adapter of options.adapters) {\n this.adapters.set(adapter.name.toLowerCase(), adapter);\n }\n }\n\n /**\n * Get a specific carrier adapter by name.\n * @throws ShipFlowError if carrier is not configured\n */\n carrier(name: string): CarrierAdapter {\n const adapter = this.adapters.get(name.toLowerCase());\n if (!adapter) {\n throw new ShipFlowError(`Carrier \"${name}\" is not configured`, {\n carrier: name,\n });\n }\n return adapter;\n }\n\n /**\n * Get all configured carrier names.\n */\n get carriers(): string[] {\n return [...this.adapters.keys()];\n }\n\n /**\n * Check if a carrier is configured.\n */\n hasCarrier(name: string): boolean {\n return this.adapters.has(name.toLowerCase());\n }\n\n /**\n * Get rates from all configured carriers that support rate calculation.\n * Returns a map of carrier name to rates (or error).\n */\n async getRatesFromAll(\n input: CreateShipmentInput,\n ): Promise<Map<string, Rate[] | Error>> {\n const results = new Map<string, Rate[] | Error>();\n\n const promises = [...this.adapters.entries()].map(\n async ([name, adapter]) => {\n if (!adapter.getRates) {\n return; // Skip carriers that don't support rates\n }\n\n try {\n const rates = await adapter.getRates(input);\n results.set(name, rates);\n } catch (error) {\n results.set(\n name,\n error instanceof Error ? error : new Error(String(error)),\n );\n }\n },\n );\n\n await Promise.allSettled(promises);\n return results;\n }\n\n /**\n * Try to track a shipment across all configured carriers in parallel.\n * Useful when you don't know which carrier handles a tracking number.\n * Returns the first successful result with a known status.\n */\n async trackAcrossCarriers(\n trackingNumber: string,\n ): Promise<TrackingResult | null> {\n const promises = [...this.adapters.values()].map(async (adapter) => {\n const result = await adapter.track(trackingNumber);\n if (result && result.status !== \"unknown\") {\n return result;\n }\n return null;\n });\n\n const results = await Promise.allSettled(promises);\n for (const result of results) {\n if (result.status === \"fulfilled\" && result.value) {\n return result.value;\n }\n }\n return null;\n }\n}\n\n/**\n * Factory function for creating ShipFlow client.\n * This is the recommended way to instantiate the client.\n */\nexport function createShipFlow(options: ShipFlowOptions): ShipFlow {\n return new ShipFlow(options);\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;AAeO,MAAM,SAAS;AAAA,EACZ;AAAA,EAER,WAAW,CAAC,SAA0B;AAAA,IACpC,KAAK,WAAW,IAAI;AAAA,IAEpB,WAAW,WAAW,QAAQ,UAAU;AAAA,MACtC,KAAK,SAAS,IAAI,QAAQ,
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;AAeO,MAAM,SAAS;AAAA,EACZ;AAAA,EAER,WAAW,CAAC,SAA0B;AAAA,IACpC,KAAK,WAAW,IAAI;AAAA,IAEpB,WAAW,WAAW,QAAQ,UAAU;AAAA,MACtC,KAAK,SAAS,IAAI,QAAQ,KAAK,YAAY,GAAG,OAAO;AAAA,IACvD;AAAA;AAAA,EAOF,OAAO,CAAC,MAA8B;AAAA,IACpC,MAAM,UAAU,KAAK,SAAS,IAAI,KAAK,YAAY,CAAC;AAAA,IACpD,IAAI,CAAC,SAAS;AAAA,MACZ,MAAM,IAAI,cAAc,YAAY,2BAA2B;AAAA,QAC7D,SAAS;AAAA,MACX,CAAC;AAAA,IACH;AAAA,IACA,OAAO;AAAA;AAAA,MAML,QAAQ,GAAa;AAAA,IACvB,OAAO,CAAC,GAAG,KAAK,SAAS,KAAK,CAAC;AAAA;AAAA,EAMjC,UAAU,CAAC,MAAuB;AAAA,IAChC,OAAO,KAAK,SAAS,IAAI,KAAK,YAAY,CAAC;AAAA;AAAA,OAOvC,gBAAe,CACnB,OACsC;AAAA,IACtC,MAAM,UAAU,IAAI;AAAA,IAEpB,MAAM,WAAW,CAAC,GAAG,KAAK,SAAS,QAAQ,CAAC,EAAE,IAC5C,QAAQ,MAAM,aAAa;AAAA,MACzB,IAAI,CAAC,QAAQ,UAAU;AAAA,QACrB;AAAA,MACF;AAAA,MAEA,IAAI;AAAA,QACF,MAAM,QAAQ,MAAM,QAAQ,SAAS,KAAK;AAAA,QAC1C,QAAQ,IAAI,MAAM,KAAK;AAAA,QACvB,OAAO,OAAO;AAAA,QACd,QAAQ,IACN,MACA,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC,CAC1D;AAAA;AAAA,KAGN;AAAA,IAEA,MAAM,QAAQ,WAAW,QAAQ;AAAA,IACjC,OAAO;AAAA;AAAA,OAQH,oBAAmB,CACvB,gBACgC;AAAA,IAChC,MAAM,WAAW,CAAC,GAAG,KAAK,SAAS,OAAO,CAAC,EAAE,IAAI,OAAO,YAAY;AAAA,MAClE,MAAM,SAAS,MAAM,QAAQ,MAAM,cAAc;AAAA,MACjD,IAAI,UAAU,OAAO,WAAW,WAAW;AAAA,QACzC,OAAO;AAAA,MACT;AAAA,MACA,OAAO;AAAA,KACR;AAAA,IAED,MAAM,UAAU,MAAM,QAAQ,WAAW,QAAQ;AAAA,IACjD,WAAW,UAAU,SAAS;AAAA,MAC5B,IAAI,OAAO,WAAW,eAAe,OAAO,OAAO;AAAA,QACjD,OAAO,OAAO;AAAA,MAChB;AAAA,IACF;AAAA,IACA,OAAO;AAAA;AAEX;AAMO,SAAS,cAAc,CAAC,SAAoC;AAAA,EACjE,OAAO,IAAI,SAAS,OAAO;AAAA;",
|
|
8
|
+
"debugId": "BCEF0C2C69F900CD64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|