Skip to content

fix(zr-express): production hardening from live API audit + label CORS fix - #111

Merged
bighadj22 merged 2 commits into
mainfrom
fix/zr-express-hardening
Sep 11, 2026
Merged

fix(zr-express): production hardening from live API audit + label CORS fix#111
bighadj22 merged 2 commits into
mainfrom
fix/zr-express-hardening

Conversation

@bighadj22

Copy link
Copy Markdown
Owner

Problem

A live audit of the ZR Express integration (every adapter call exercised against the production API, see .agents/skills/zr-express/CONFORMANCE.md) found that only the happy-path single home-delivery flow worked. Several flows were silently broken:

  • Tracking always emptyGET /parcels/{trackingNumber}/state-history 404s; the endpoint only accepts the parcel UUID, and the error was swallowed, so every ZR order showed no tracking events.
  • Cancel never worked — deletion used POST on a DELETE-only endpoint (405), and a failed cancel silently reset the order to ready while the parcel kept moving at the carrier.
  • Address updates always failed — a street-only body answers 400 (CityTerritoryId is required), and the failure was swallowed into a false success.
  • Stop-desk dispatch impossible — a pickup-point territory UUID was sent as hubId; ZR requires a real hub id (404 HubNotFound).
  • Geo resolution fragile — ZR stores accent-free territory names, so accented wilaya/commune searches missed; a fallback could silently pick an unrelated commune; 4 wilayas are not served by ZR at all and failed with no clear error.
  • Webhook status mapping mismatched — defaults assumed English state names; the real default workflow uses French slugs (commande_recue, en_livraison, livre, …), so terminal events recorded as unmapped. Registration also subscribed to only one of the three event types the receiver understands.
  • Print label blocked by CORS — the label proxy streams the PDF via a raw Response, which bypassed Hono's prepared-header application, so the 200 response reached the browser without Access-Control-Allow-Origin and was blocked.

Solution

ZR adapter (cod-server, cod-shared untouched):

  • Tracking: resolve the parcel UUID via GET /parcels/{trackingNumber} first, then read state-history.
  • Cancel: use the documented DELETE /parcels/bulk/by-tracking-number; enable canDeleteBeforeValidation + dashboard cancel; carrier refusals now surface as errors instead of resetting the order.
  • Address updates: send the full DeliveryAddressInputDto (cityTerritoryId + districtTerritoryId, plus hubId for pickup-point parcels); failures throw instead of being swallowed.
  • Stop desk: getStopDesks lists hubs (isPickupPoint) with wilayas resolved from address.cityTerritoryId; dispatch and address updates resolve the real hub for the station.
  • Geo: accent-stripped searches, strict commune matching (fail loudly, never deliver to the wrong commune), clear error naming the 4 unserved wilayas (33, 37, 50, 56).
  • Webhooks: subscribe to all three event types; status mapper defaults use the live-verified slug vocabulary with case/accent-insensitive matching and a description fallback; all order statuses accepted in the custom mapping UI.
  • Bulk create: keep all failure messages per parcel index.

CORS middleware: re-apply the CORS headers to the final response after the handler runs, so raw streamed responses (label PDF proxy, R2 image streaming) carry them.

Dashboard (cod-client-astro): enable the cancel button for ZR orders (non-terminal statuses).

Docs: add the zr-express skill (endpoint references, schemas, conformance audit) under .agents/skills/.

Testing

  • cod-server: typecheck clean, 1919/1919 tests pass (incl. new zr-status-mapper.test.ts with a drift guard over the live-verified slug vocabulary, and CORS regression tests for raw + context-built responses).
  • cod-client-astro: typecheck clean, 147/147 tests pass.
  • Live API verification: geo sweep over all 58 wilayas (54 resolve; 33/37/50/56 confirmed unserved), delete → 200 + parcel gone, hubs/pickup points (77, all with address territories), webhook register/secret/delete with all three event types, label generation + PDF fetch for all existing ZR orders and a fresh test parcel (created and cleaned up via the API).
  • Deployed to production Workers and verified in the live dashboard: print label downloads, cancel works, tracking events render.

No schema changes, no new dependencies, no other carriers touched.

All findings verified against the live ZR Express API (see
.agents/skills/zr-express/CONFORMANCE.md for the full audit):

- Tracking: resolve the parcel UUID via GET /parcels/{trackingNumber}
  before reading state-history — /state-history 404s on tracking
  numbers, so ZR orders silently showed no events.
- Cancel: switch deletion to the documented DELETE
  /parcels/bulk/by-tracking-number (POST answered 405); enable
  canDeleteBeforeValidation + dashboard cancel button; surface carrier
  refusals instead of silently resetting the order.
- Address updates: send the full DeliveryAddressInputDto
  (cityTerritoryId + districtTerritoryId) — a street-only body 400s;
  pickup-point updates additionally require hubId.
- Stop desk: pickup points are HUBS (POST /hubs/search, isPickupPoint),
  not territory UUIDs — passing a territory as hubId 404s HubNotFound.
  getStopDesks now lists hubs with their wilaya resolved from
  address.cityTerritoryId.
- Geo: accent-stripped territory searches (ZR stores accent-free
  names), strict commune matching (no silent wrong-commune fallback),
  and a clear error for the 4 wilayas ZR does not serve (33, 37, 50, 56).
- Webhooks: subscribe to all three event types the receiver
  understands, map the real default-workflow state slugs (case- and
  accent-insensitive, description fallback), and accept all order
  statuses in the custom-mapping UI.
- Bulk create: keep all failure messages per parcel index.
- Add zr-status-mapper.test.ts and the zr-express skill docs.
Handlers that return a raw new Response(...) (label PDF proxy, R2
image streaming) bypass Hono's prepared-header application, so the
Access-Control-Allow-* headers set before next() never reached them.
The browser blocked the 200 response with a CORS error — most visibly
on GET /api/orders/{id}/label (print label).

The middleware now re-applies the headers on the final response after
next(). Covered by regression tests for both raw and context-built
responses.
@bighadj22
bighadj22 merged commit dd4cccb into main Sep 11, 2026
3 checks passed
@bighadj22
bighadj22 deleted the fix/zr-express-hardening branch September 11, 2026 15:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant