shipflow 0.1.1 → 0.3.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # ShipFlow
2
2
 
3
- Unified Shipping SDK for MENA region carriers. A single API to create shipments, track packages, manage labels, handle webhooks, and more — across Aymakan, SMSA Express, and future carriers.
3
+ Unified Shipping SDK for MENA region carriers. A single API to create shipments, track packages, manage labels, handle webhooks, and more — across Aymakan, SMSA Express, Aramex, and future carriers.
4
4
 
5
5
  Think EasyPost / Shippo, but purpose-built for Saudi Arabia and the GCC.
6
6
 
@@ -8,9 +8,10 @@ Think EasyPost / Shippo, but purpose-built for Saudi Arabia and the GCC.
8
8
 
9
9
  - **Unified types** — one `CreateShipmentInput`, one `TrackingResult`, one `WebhookEvent`, regardless of carrier
10
10
  - **Tree-shakeable** — only the carriers you import are bundled
11
- - **Auto-validation** — Zod schemas validate every `createShipment()` call before it hits the network
11
+ - **Auto-validation** — Valibot schemas validate every `createShipment()` call before it hits the network
12
12
  - **Webhook parsing** — normalize incoming carrier webhooks into a single event format
13
- - **Zero runtime dependencies** — Bun-native `fetch`, no axios/node-fetch
13
+ - **Smart retries** — dependency-free retry with jittered backoff that honors carrier `Retry-After` on 429/503, surfacing a `RateLimitError` when the wait is too long to absorb inline
14
+ - **Minimal dependencies** — only [Valibot](https://github.com/fabian-hiller/valibot) for validation; uses the runtime's global `fetch` (Node 20+, Deno, Bun, edge/workers), no axios/node-fetch
14
15
  - **TypeScript-first** — strict types, no `any`
15
16
 
16
17
  ## Installation
@@ -25,6 +26,7 @@ bun add shipflow
25
26
  import { ShipFlow } from "shipflow";
26
27
  import { AymakanAdapter, AymakanService } from "shipflow/carriers/aymakan";
27
28
  import { SMSAExpressAdapter, SMSAService } from "shipflow/carriers/smsaexpress";
29
+ import { AramexAdapter } from "shipflow/carriers/aramex";
28
30
 
29
31
  const client = new ShipFlow({
30
32
  adapters: [
@@ -36,6 +38,18 @@ const client = new ShipFlow({
36
38
  mode: "sandbox",
37
39
  credentials: { apiKey: process.env.SMSA_API_KEY! },
38
40
  }),
41
+ // Aramex auth is a ClientInfo object sent in every request body (no API key)
42
+ new AramexAdapter({
43
+ mode: "sandbox",
44
+ credentials: {
45
+ userName: process.env.ARAMEX_USERNAME!,
46
+ password: process.env.ARAMEX_PASSWORD!,
47
+ accountNumber: process.env.ARAMEX_ACCOUNT_NUMBER!,
48
+ accountPin: process.env.ARAMEX_ACCOUNT_PIN!,
49
+ accountEntity: process.env.ARAMEX_ACCOUNT_ENTITY!, // e.g. "RUH"
50
+ accountCountryCode: process.env.ARAMEX_ACCOUNT_COUNTRY_CODE!, // e.g. "SA"
51
+ },
52
+ }),
39
53
  ],
40
54
  });
41
55
 
@@ -91,25 +105,26 @@ Every carrier adapter implements these **required** methods:
91
105
 
92
106
  Plus these **optional** methods (availability varies by carrier):
93
107
 
94
- | Method | Aymakan | SMSA |
95
- | ------------------------------------ | ------- | ---- |
96
- | `createBulkShipments(inputs)` | ✅ | — |
97
- | `cancelByReference(ref)` | ✅ | — |
98
- | `updateDeliveryAddress(tn, address)` | ✅ | — |
99
- | `trackByReference(ref)` | ✅ | ✅ |
100
- | `getBulkLabels(trackingNumbers)` | ✅ | — |
101
- | `getPickupCities()` | ✅ | — |
102
- | `getTimeSlots(city, date)` | ✅ | — |
103
- | `createPickup(input)` | ✅ | — |
104
- | `cancelPickup(id)` | ✅ | — |
105
- | `getPickupRequests()` | ✅ | — |
106
- | `getCities()` | ✅ | ✅ |
107
- | `getDropoffLocations()` | ✅ | ✅ |
108
- | `createCustomerAddress(addr)` | ✅ | — |
109
- | `getCustomerAddresses()` | ✅ | — |
110
- | `updateCustomerAddress(id, addr)` | ✅ | — |
111
- | `deleteCustomerAddress(id)` | ✅ | — |
112
- | `parseWebhook(payload, options)` | | ✅ |
108
+ | Method | Aymakan | SMSA | Aramex |
109
+ | ------------------------------------ | ------- | ---- | ------ |
110
+ | `createBulkShipments(inputs)` | ✅ | — | ✅ |
111
+ | `cancelByReference(ref)` | ✅ | — | — |
112
+ | `updateDeliveryAddress(tn, address)` | ✅ | — | — |
113
+ | `trackByReference(ref)` | ✅ | ✅ | ✅ |
114
+ | `getBulkLabels(trackingNumbers)` | ✅ | — | — |
115
+ | `getPickupCities()` | ✅ | — | — |
116
+ | `getTimeSlots(city, date)` | ✅ | — | — |
117
+ | `createPickup(input)` | ✅ | — | ✅ |
118
+ | `cancelPickup(id)` | ✅ | — | ✅ |
119
+ | `getPickupRequests()` | ✅ | — | — |
120
+ | `getCities()` | ✅ | ✅ | ✅ |
121
+ | `getDropoffLocations()` | ✅ | ✅ | ✅ |
122
+ | `createCustomerAddress(addr)` | ✅ | — | — |
123
+ | `getCustomerAddresses()` | ✅ | — | — |
124
+ | `updateCustomerAddress(id, addr)` | ✅ | — | — |
125
+ | `deleteCustomerAddress(id)` | ✅ | — | — |
126
+ | `getRates(input)` | | — | |
127
+ | `parseWebhook(payload, options)` | ✅ | ✅ | — |
113
128
 
114
129
  SMSA-specific methods:
115
130
 
@@ -123,19 +138,112 @@ SMSA-specific methods:
123
138
 
124
139
  ## Carrier Support
125
140
 
126
- | Feature | Aymakan | SMSA Express |
127
- | ----------------- | ------------------------------- | ---------------------------------- |
128
- | Countries | SA, AE, BH, KW, OM, QA | SA, AE, BH, EG, KW, OM, QA, JO |
129
- | Service types | 10 (ONP, SDD, RVP, EXH, ...) | 3 (EDDL, EDEL, EDCR) |
130
- | Shipment creation | Single + Bulk | B2C + C2B + 2-Way |
131
- | COD | ✅ | ✅ (B2C only) |
132
- | Cancellation | By tracking # or reference | C2B only |
133
- | Tracking | Single, bulk, by reference | Single, bulk, by reference |
134
- | Labels | PDF/PNG, single + bulk | PDF/ZPL |
135
- | Pickups | Full lifecycle | — |
136
- | Webhooks | ✅ (with auth verification) | ✅ (batch, with auth verification) |
137
- | City resolution | Arabic ↔ English smart matching | Code-based lookup |
138
- | Rates | ❌ | ❌ |
141
+ | Feature | Aymakan | SMSA Express | Aramex |
142
+ | ----------------- | ------------------------------- | ---------------------------------- | -------------------------------------- |
143
+ | Countries | SA, AE, BH, KW, OM, QA | SA, AE, BH, EG, KW, OM, QA, JO | SA, AE, BH, KW, OM, QA, JO, EG, LB, IQ |
144
+ | Service types | 10 (ONP, SDD, RVP, EXH, ...) | 3 (EDDL, EDEL, EDCR) | 10 product types (OND, PPX, EPX, ...) |
145
+ | Shipment creation | Single + Bulk | B2C + C2B + 2-Way | Single + Bulk (native batch) |
146
+ | COD | ✅ | ✅ (B2C only) | ✅ |
147
+ | Cancellation | By tracking # or reference | C2B only | Pickups only (no shipment cancel API) |
148
+ | Tracking | Single, bulk, by reference | Single, bulk, by reference | Single, bulk, by reference |
149
+ | Labels | PDF/PNG, single + bulk | PDF/ZPL | URL (HTML/PDF) |
150
+ | Pickups | Full lifecycle | — | Create + cancel |
151
+ | Webhooks | ✅ (with auth verification) | ✅ (batch, with auth verification) | — (poll via tracking) |
152
+ | City resolution | Arabic ↔ English smart matching | Code-based lookup | Name list (FetchCities / FetchOffices) |
153
+ | Rates | ❌ | ❌ | ✅ (CalculateRate) |
154
+
155
+ ## Aramex
156
+
157
+ Aramex is integrated via the **JSON flavor of the classic `ShippingAPI.V2` services**. A few
158
+ things make it different from the other carriers:
159
+
160
+ - **Auth is a `ClientInfo` object in every request body** (no API key / header, no token
161
+ exchange). Pass `userName`, `password`, `accountNumber`, `accountPin`, `accountEntity` (the
162
+ 3-letter origin office, e.g. `RUH`/`DXB`/`AMM`) and `accountCountryCode`.
163
+ - **Four independent services on separate hosts** — Shipping, Tracking, RateCalculator, and
164
+ Location. The adapter holds one HTTP client per service and routes automatically. If your
165
+ account provisions the Location service on a different host (some WSDLs use `anfe02.aramex.com`),
166
+ set `locationBaseUrl` on the config.
167
+ - **"Fake 200 OK" errors** — Aramex returns HTTP 200 even on logical failures, with
168
+ `HasErrors: true` + `Notifications[]`. ShipFlow surfaces these as `APIError`, including
169
+ per-shipment errors inside an otherwise-clean `CreateShipments` batch. Throttling
170
+ notifications in that envelope are surfaced as `RateLimitError` so retries back off.
171
+ - **Rates are supported** (`getRates` → `CalculateRate`), unlike Aymakan/SMSA.
172
+ - **`cancelShipment` is unsupported** (the classic API has no shipment-cancel operation) and
173
+ throws `UnsupportedOperationError`. Pickups can be cancelled via `cancelPickup`.
174
+ - **Labels resolve to a URL** — the `format` argument of `getLabel` can't be honored.
175
+
176
+ ```typescript
177
+ import { AramexAdapter, AramexProductType } from "shipflow/carriers/aramex";
178
+
179
+ const aramex = client.carrier("aramex");
180
+
181
+ // Create a domestic COD shipment (freight prepaid, cash collected on delivery)
182
+ const shipment = await aramex.createShipment({
183
+ shipper: {
184
+ name: "My Store",
185
+ company: "ShipFlow",
186
+ phone: "966500000000",
187
+ line1: "King Fahd Road",
188
+ city: "Riyadh",
189
+ countryCode: "SA",
190
+ },
191
+ consignee: {
192
+ name: "Customer",
193
+ phone: "966500000001",
194
+ line1: "Prince Sultan Road",
195
+ city: "Jeddah",
196
+ countryCode: "SA",
197
+ },
198
+ parcels: [{ weight: { value: 1, unit: "kg" }, pieces: 1 }],
199
+ cod: { enabled: true, amount: 150, currency: "SAR" },
200
+ });
201
+
202
+ // Quote a rate, then track
203
+ const rates = await aramex.getRates!(input);
204
+ const result = await aramex.track(shipment.trackingNumber);
205
+ ```
206
+
207
+ **Product group / type & payment** — ShipFlow infers `DOM` (domestic) when shipper and consignee
208
+ share a country, else `EXP`, and picks a sensible default product type (`OND` for domestic, `EPX`
209
+ for express). Override with `serviceType` (a valid Aramex code) or
210
+ `options.metadata.productGroup` / `productType`.
211
+
212
+ The freight **`PaymentType`** — who pays the *shipping cost* — defaults to `P` and is independent
213
+ of COD: enabling COD adds the `CODS` service and the cash amount to collect from the consignee, but
214
+ does **not** charge them freight. Override the freight payer with `options.metadata.paymentType`:
215
+
216
+ | Value | Freight billed to | When to use |
217
+ | ----- | ----------------- | ----------- |
218
+ | `"P"` _(default)_ | Shipper's Aramex account (prepaid) | Standard KSA/GCC e-commerce — merchant pays shipping, even with COD |
219
+ | `"C"` | Consignee, collected at delivery | Customer pays shipping on top of any COD |
220
+ | `"3"` | A third-party account | Freight billed to someone other than shipper/consignee |
221
+
222
+ ```typescript
223
+ // Default: COD shipment with prepaid freight (PaymentType "P")
224
+ await aramex.createShipment({
225
+ ...input,
226
+ cod: { enabled: true, amount: 150, currency: "SAR" }, // freight stays "P"
227
+ });
228
+
229
+ // Override: charge the customer freight at the door (PaymentType "C")
230
+ await aramex.createShipment({
231
+ ...input,
232
+ cod: { enabled: true, amount: 150, currency: "SAR" },
233
+ options: { metadata: { paymentType: "C" } },
234
+ });
235
+
236
+ // Override: bill freight to a third party (PaymentType "3")
237
+ await aramex.createShipment({
238
+ ...input,
239
+ options: { metadata: { paymentType: "3" } },
240
+ });
241
+ ```
242
+
243
+ > Aramex does not push webhooks — poll `track()` / `trackMultiple()` for status updates.
244
+ > Tracking `UpdateCode`s vary by region and aren't fully published, so ShipFlow maps known codes
245
+ > and falls back to a description-keyword heuristic (then `"unknown"`) — an unmapped code never
246
+ > breaks tracking.
139
247
 
140
248
  ## Webhook Handling
141
249
 
@@ -202,7 +310,7 @@ interface WebhookEvent {
202
310
 
203
311
  ## Input Validation
204
312
 
205
- All `createShipment()` calls are **automatically validated** using Zod schemas before hitting the carrier API. Invalid input throws a `ValidationError` with field-level details:
313
+ All `createShipment()` calls are **automatically validated** using Valibot schemas before hitting the carrier API. Invalid input throws a `ValidationError` with field-level details:
206
314
 
207
315
  ```typescript
208
316
  try {
@@ -234,7 +342,7 @@ validateCreateShipmentInput(input); // throws ValidationError or returns validat
234
342
  validatePickupRequest(pickupInput); // same pattern
235
343
  ```
236
344
 
237
- Exported Zod schemas for advanced use (custom refinements, partial validation, etc.):
345
+ Exported Valibot schemas for advanced use (custom refinements, partial validation, etc.):
238
346
 
239
347
  ```typescript
240
348
  import {
@@ -253,6 +361,7 @@ All errors extend `ShipFlowError` for easy catch-all handling:
253
361
  import {
254
362
  ShipFlowError,
255
363
  NetworkError,
364
+ RateLimitError,
256
365
  APIError,
257
366
  ValidationError,
258
367
  AuthenticationError,
@@ -267,6 +376,8 @@ try {
267
376
  // Bad input — check error.issues
268
377
  } else if (error instanceof AuthenticationError) {
269
378
  // Invalid API key
379
+ } else if (error instanceof RateLimitError) {
380
+ // Rate limited — check error.retryAfterMs (ms to wait), reschedule if set
270
381
  } else if (error instanceof APIError) {
271
382
  // Carrier returned an error — check error.statusCode, error.errors
272
383
  } else if (error instanceof NetworkError) {
@@ -275,6 +386,37 @@ try {
275
386
  }
276
387
  ```
277
388
 
389
+ > `RateLimitError extends APIError`, so check it **before** `APIError` in your
390
+ > `if`/`else` chain (a plain `catch (e) { if (e instanceof APIError) }` still
391
+ > catches it).
392
+
393
+ ### Retries & rate limiting
394
+
395
+ Safe, idempotent requests (GETs, plus tracking endpoints opted in by the
396
+ adapters) are retried automatically with **jittered exponential backoff**.
397
+ Mutating requests (create/cancel) are **not** retried by default, so a timed-out
398
+ `createShipment` never risks a duplicate on the carrier.
399
+
400
+ When a carrier replies `429`/`503` with a `Retry-After` header, ShipFlow:
401
+
402
+ - **honors it inline** if the wait is within the inline cap (15s by default),
403
+ sleeping out the window (plus a little jitter) before retrying; otherwise
404
+ - **stops and throws `RateLimitError`** carrying `retryAfterMs`, so a durable
405
+ queue/worker can reschedule instead of blocking the request for minutes.
406
+
407
+ ```typescript
408
+ try {
409
+ await client.carrier("aymakan").track(trackingNumber);
410
+ } catch (error) {
411
+ if (error instanceof RateLimitError && error.retryAfterMs != null) {
412
+ await scheduleRetryIn(error.retryAfterMs); // your queue/worker
413
+ }
414
+ }
415
+ ```
416
+
417
+ Aramex reports throttling inside its "fake 200" envelope rather than via HTTP
418
+ 429; ShipFlow detects that and raises the same `RateLimitError`.
419
+
278
420
  ## Custom Adapters
279
421
 
280
422
  Implement the `CarrierAdapter` interface or extend `BaseCarrierAdapter`:
@@ -51,6 +51,13 @@ export declare class AramexAdapter extends BaseCarrierAdapter {
51
51
  * Extracts the Aramex "Fake 200 OK" error (envelope-level `HasErrors` +
52
52
  * `Notifications`). Passed to every request so the HttpClient raises APIError.
53
53
  */
54
+ /**
55
+ * Aramex reports throttling inside its "fake 200" envelope (and sometimes on a
56
+ * non-429 status) rather than via HTTP 429, so status-code detection misses it.
57
+ * Match the rate-limit wording in the notifications and flag it so the
58
+ * HttpClient raises a retryable `RateLimitError` instead of a plain APIError.
59
+ */
60
+ private static readonly RATE_LIMIT_PATTERN;
54
61
  private static aramexErrorExtractor;
55
62
  private static notificationsToErrors;
56
63
  protected executeCreateShipment(input: CreateShipmentInput): Promise<Shipment>;
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../../src/carriers/aramex/adapter.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;GAWG;AAYH,OAAO,KAAK,EACV,aAAa,EACb,IAAI,EACJ,mBAAmB,EACnB,QAAQ,EACR,MAAM,EACN,aAAa,EACb,IAAI,EACJ,QAAQ,EACR,cAAc,EACf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAqD7C,MAAM,WAAW,YAAa,SAAQ,aAAa;IACjD,WAAW,EAAE;QACX,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,aAAa,EAAE,MAAM,CAAC;QACtB,UAAU,EAAE,MAAM,CAAC;QACnB,aAAa,EAAE,MAAM,CAAC;QACtB,kBAAkB,EAAE,MAAM,CAAC;KAC5B,CAAC;IACF,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,qBAAa,aAAc,SAAQ,kBAAkB;IACnD,QAAQ,CAAC,IAAI,YAAY;IACzB,QAAQ,CAAC,kBAAkB,WAWzB;IAEF,OAAO,CAAC,YAAY,CAAa;IACjC,OAAO,CAAC,YAAY,CAAa;IACjC,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,YAAY,CAAa;gBAErB,MAAM,EAAE,YAAY;IAuBhC,SAAS,CAAC,UAAU,IAAI,MAAM;IAM9B,8EAA8E;IAC9E,OAAO,CAAC,WAAW;IAInB,OAAO,KAAK,YAAY,GAEvB;IAED,OAAO,CAAC,eAAe;IAQvB;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,oBAAoB;IAyBnC,OAAO,CAAC,MAAM,CAAC,qBAAqB;cAcpB,qBAAqB,CACnC,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC,QAAQ,CAAC;IA6CpB;;;;;;;;OAQG;IACG,mBAAmB,CACvB,MAAM,EAAE,mBAAmB,EAAE,GAC5B,OAAO,CAAC,QAAQ,EAAE,CAAC;IAwDtB;;;OAGG;IACH,cAAc,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAQnD,QAAQ,CACZ,cAAc,EAAE,MAAM,EACtB,OAAO,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,GAC9B,OAAO,CAAC,MAAM,CAAC;IA4BZ,KAAK,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAYtD,aAAa,CAAC,eAAe,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAkBnE,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAU5D,QAAQ,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;IA4BrD,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC;IAgCnD,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAoBzD,SAAS,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;IAehD,mBAAmB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;CAcrE"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../../src/carriers/aramex/adapter.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;GAWG;AAYH,OAAO,KAAK,EACV,aAAa,EACb,IAAI,EACJ,mBAAmB,EACnB,QAAQ,EACR,MAAM,EACN,aAAa,EACb,IAAI,EACJ,QAAQ,EACR,cAAc,EACf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAsD7C,MAAM,WAAW,YAAa,SAAQ,aAAa;IACjD,WAAW,EAAE;QACX,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,aAAa,EAAE,MAAM,CAAC;QACtB,UAAU,EAAE,MAAM,CAAC;QACnB,aAAa,EAAE,MAAM,CAAC;QACtB,kBAAkB,EAAE,MAAM,CAAC;KAC5B,CAAC;IACF,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,qBAAa,aAAc,SAAQ,kBAAkB;IACnD,QAAQ,CAAC,IAAI,YAAY;IACzB,QAAQ,CAAC,kBAAkB,WAWzB;IAEF,OAAO,CAAC,YAAY,CAAa;IACjC,OAAO,CAAC,YAAY,CAAa;IACjC,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,YAAY,CAAa;gBAErB,MAAM,EAAE,YAAY;IAuBhC,SAAS,CAAC,UAAU,IAAI,MAAM;IAM9B,8EAA8E;IAC9E,OAAO,CAAC,WAAW;IAInB,OAAO,KAAK,YAAY,GAEvB;IAED,OAAO,CAAC,eAAe;IAQvB;;;OAGG;IACH;;;;;OAKG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CACe;IAEzD,OAAO,CAAC,MAAM,CAAC,oBAAoB;IA4BnC,OAAO,CAAC,MAAM,CAAC,qBAAqB;cAcpB,qBAAqB,CACnC,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC,QAAQ,CAAC;IA6CpB;;;;;;;;OAQG;IACG,mBAAmB,CACvB,MAAM,EAAE,mBAAmB,EAAE,GAC5B,OAAO,CAAC,QAAQ,EAAE,CAAC;IAwDtB;;;OAGG;IACH,cAAc,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAQnD,QAAQ,CACZ,cAAc,EAAE,MAAM,EACtB,OAAO,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,GAC9B,OAAO,CAAC,MAAM,CAAC;IA8BZ,KAAK,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAetD,aAAa,CAAC,eAAe,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IA4BnE,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAU5D,QAAQ,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;IA4BrD,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC;IAyDnD,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAoBzD,SAAS,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;IAehD,mBAAmB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;CAcrE"}
@@ -6,7 +6,7 @@ import {
6
6
  ValidationError,
7
7
  validateCreateShipmentInput,
8
8
  validatePickupRequest
9
- } from "../../index-qjtxhwzv.js";
9
+ } from "../../index-qnxj8bct.js";
10
10
 
11
11
  // src/carriers/aramex/services.ts
12
12
  var AramexProductGroup = {
@@ -119,7 +119,7 @@ function resolvePaymentType(input) {
119
119
  const meta = getMeta(input, "paymentType");
120
120
  if (meta === "P" || meta === "C" || meta === "3")
121
121
  return meta;
122
- return input.cod?.enabled ? "C" : "P";
122
+ return "P";
123
123
  }
124
124
  function aggregateWeight(input) {
125
125
  const allLb = input.parcels.every((p) => p.weight.unit === "lb");
@@ -141,19 +141,31 @@ function mapDimensions(dims) {
141
141
  Unit: "CM"
142
142
  };
143
143
  }
144
- function mapAddress(addr) {
144
+ function buildPartyAddress(fields) {
145
145
  return {
146
- Line1: addr.line1,
147
- Line2: addr.line2 ?? "",
148
- Line3: addr.neighbourhood ?? "",
149
- City: addr.city,
150
- StateOrProvinceCode: addr.state,
151
- PostCode: addr.postalCode ?? "",
152
- CountryCode: addr.countryCode,
153
- Longitude: addr.coordinates?.longitude,
154
- Latitude: addr.coordinates?.latitude
146
+ Line1: fields.line1,
147
+ Line2: fields.line2 ?? "",
148
+ Line3: fields.line3 ?? "",
149
+ City: fields.city,
150
+ StateOrProvinceCode: fields.state,
151
+ PostCode: fields.postCode ?? "",
152
+ CountryCode: fields.countryCode,
153
+ Longitude: fields.coordinates?.longitude,
154
+ Latitude: fields.coordinates?.latitude
155
155
  };
156
156
  }
157
+ function mapAddress(addr) {
158
+ return buildPartyAddress({
159
+ line1: addr.line1,
160
+ line2: addr.line2,
161
+ line3: addr.neighbourhood,
162
+ city: addr.city,
163
+ state: addr.state,
164
+ postCode: addr.postalCode,
165
+ countryCode: addr.countryCode,
166
+ coordinates: addr.coordinates
167
+ });
168
+ }
157
169
  function buildContact(opts) {
158
170
  return {
159
171
  Department: "",
@@ -257,11 +269,11 @@ function mapPickupRequest(input, ctx) {
257
269
  const closing = new Date(`${input.date}T17:00:00`);
258
270
  return {
259
271
  Reference1: input.trackingNumbers?.[0],
260
- PickupAddress: {
261
- Line1: input.address,
262
- City: input.city,
263
- CountryCode: ctx.countryCode
264
- },
272
+ PickupAddress: buildPartyAddress({
273
+ line1: input.address,
274
+ city: input.city,
275
+ countryCode: ctx.countryCode
276
+ }),
265
277
  PickupContact: buildContact({
266
278
  personName: input.contactName,
267
279
  companyName: ctx.companyName ?? input.contactName,
@@ -347,6 +359,16 @@ function normalizeTrackingResults(raw) {
347
359
  }
348
360
  return raw;
349
361
  }
362
+ function mapNonExistingWaybill(waybill) {
363
+ return {
364
+ trackingNumber: waybill,
365
+ carrier: "aramex",
366
+ status: "unknown",
367
+ statusLabel: "Waybill not found",
368
+ events: [],
369
+ raw: { nonExisting: true, waybill }
370
+ };
371
+ }
350
372
  function mapRate(response, input) {
351
373
  const { productType } = resolveProductGroupAndType(input);
352
374
  return {
@@ -459,6 +481,7 @@ class AramexAdapter extends BaseCarrierAdapter {
459
481
  version: cfg.version
460
482
  });
461
483
  }
484
+ static RATE_LIMIT_PATTERN = /rate.?limit|too many request|throttl|quota exceeded/i;
462
485
  static aramexErrorExtractor(json) {
463
486
  const obj = json;
464
487
  const notifications = obj?.Notifications ?? [];
@@ -467,7 +490,8 @@ class AramexAdapter extends BaseCarrierAdapter {
467
490
  return {
468
491
  hasError,
469
492
  message,
470
- errors: notifications.length ? AramexAdapter.notificationsToErrors(notifications) : undefined
493
+ errors: notifications.length ? AramexAdapter.notificationsToErrors(notifications) : undefined,
494
+ rateLimited: hasError && AramexAdapter.RATE_LIMIT_PATTERN.test(message ?? "")
471
495
  };
472
496
  }
473
497
  static notificationsToErrors(notifications) {
@@ -548,7 +572,7 @@ class AramexAdapter extends BaseCarrierAdapter {
548
572
  Transaction: EMPTY_TRANSACTION,
549
573
  ShipmentNumber: trackingNumber,
550
574
  LabelInfo: DEFAULT_LABEL_INFO
551
- }, { errorExtractor: AramexAdapter.aramexErrorExtractor });
575
+ }, { retry: true, errorExtractor: AramexAdapter.aramexErrorExtractor });
552
576
  const url = response.ShipmentLabel?.LabelURL;
553
577
  if (!url) {
554
578
  throw new APIError("Failed to get label", {
@@ -561,10 +585,10 @@ class AramexAdapter extends BaseCarrierAdapter {
561
585
  async track(trackingNumber) {
562
586
  const results = await this.trackMultiple([trackingNumber]);
563
587
  const result = results[0];
564
- if (!result) {
588
+ if (!result || result.status === "unknown" && result.events.length === 0) {
565
589
  throw new APIError("Shipment not found", {
566
590
  carrier: "aramex",
567
- raw: { trackingNumber }
591
+ raw: result?.raw ?? { trackingNumber }
568
592
  });
569
593
  }
570
594
  return result;
@@ -577,7 +601,9 @@ class AramexAdapter extends BaseCarrierAdapter {
577
601
  GetLastTrackingUpdateOnly: false
578
602
  }, { retry: true, errorExtractor: AramexAdapter.aramexErrorExtractor });
579
603
  const map = normalizeTrackingResults(response.TrackingResults);
580
- return Object.entries(map).map(([waybill, results]) => mapTrackingResult(waybill, results));
604
+ const found = Object.entries(map).map(([waybill, results]) => mapTrackingResult(waybill, results));
605
+ const nonExisting = (response.NonExistingWaybills ?? []).map(mapNonExistingWaybill);
606
+ return [...found, ...nonExisting];
581
607
  }
582
608
  async trackByReference(reference) {
583
609
  const result = await this.track(reference);
@@ -618,6 +644,15 @@ class AramexAdapter extends BaseCarrierAdapter {
618
644
  raw: response
619
645
  });
620
646
  }
647
+ const failedShipments = (processed.ProcessedShipments ?? []).filter((s) => s.HasErrors);
648
+ if (failedShipments.length > 0) {
649
+ const notifications = failedShipments.flatMap((s) => s.Notifications ?? []);
650
+ throw new APIError(notifications.map((n) => n.Message).filter(Boolean).join("; ") || `Pickup created but ${failedShipments.length} shipment(s) failed`, {
651
+ carrier: "aramex",
652
+ errors: AramexAdapter.notificationsToErrors(notifications),
653
+ raw: response
654
+ });
655
+ }
621
656
  return mapPickupResponse(processed, input);
622
657
  }
623
658
  async cancelPickup(pickupId) {
@@ -658,4 +693,4 @@ export {
658
693
  AramexAdapter
659
694
  };
660
695
 
661
- //# debugId=F2518B67CA4397C364756E2164756E21
696
+ //# debugId=A6EBA2B597BA645F64756E2164756E21