Statuts de colis, modèles et erreurs
Chaque transporteur algérien a ses propres statuts : libellés français chez Yalidine, codes numériques chez Maystro, états propres à ZRExpress, Ecotrack ou NoEST. dz-delivery les ramène à sept statuts unifiés, avec les mêmes modèles et les mêmes codes d’erreur en TypeScript et en PHP.
Les 7 statuts de colis unifiés
Parcel.status et TrackingEvent.status valent toujours l’une de ces valeurs, quel que soit le transporteur :
pending | created | in_transit | delivered | returned | cancelled | unknown
| Statut | Signification | Exemples Yalidine | Exemples Maystro (code) |
|---|---|---|---|
pending | Commande enregistrée mais pas encore confirmée ni prête pour l’enlèvement (brouillon, « commande reçue »). | « En préparation », « A vérifier » | 4 CREATED, 11 PENDING, 12 OUT_OF_STOCK |
created | Confirmée et prête à être enlevée ou expédiée par le transporteur. | « Prêt à expédier », « Pas encore ramassé », « Pas encore expédié », « En passation » | 5 PICK_UP_REQUESTED, 6 IN_PROCESS |
in_transit | Enlevé et en mouvement : en hub, en transfert, en cours de livraison, tentative échouée, etc. | « Ramassé », « En transit », « Vers Wilaya », « Sorti en livraison », « Tentative échouée », « En alerte »… | 8 WAITING_TRANSIT, 9 IN_TRANSIT_TO_BE_SHIPPED, 15 READY_TO_SHIP, 22 ASSIGNED, 31 SHIPPED, 32 ALERTED, 42 POSTPONED |
delivered | Livré au destinataire (COD encaissé). | « Livré » | 41 DELIVERED |
returned | Retourné ou en cours de retour vers l’expéditeur. | « Retour … », « Retourné … », « Echange échoué », « Colis abandonné » | 10 IN_TRANSIT_TO_BE_RETURNED, 51 READY_TO_RETURN, 52 TAKEN_BY_STORE, 53 NOT_RECEIVED |
cancelled | Annulé / supprimé avant livraison. | « Annulé », « Supprimé » | 50 ABORTED |
unknown | Statut natif non mappé. Le SDK ne lève jamais d’erreur pour un statut inconnu : il renvoie unknown et garde le libellé natif dans TrackingEvent.label / raw. | tout autre libellé | tout autre code |
Les exemples ci-dessus sont des extraits. La table natif → unifié de chaque transporteur se trouve dans PROVIDERS.md ; elle est identique en TypeScript et en PHP, ce que garantissent des fixtures partagées (docs/fixtures/).
Convertir un statut natif (webhooks, imports)
Les mappers sont exportés : utilisez-les quand vous recevez un statut natif en dehors de track(), par exemple dans un webhook Maystro.
import { mapYalidineStatus, mapMaystroStatus, PARCEL_STATUSES } from '@dz-delivery/core'; mapYalidineStatus('Sorti en livraison'); // 'in_transit' mapMaystroStatus(41); // 'delivered' (code ou nom) PARCEL_STATUSES; // les 7 statuts, dans l'ordre // Aussi : mapZRExpressStatus, mapEcotrackStatus, mapNoestStatus, mapEcomStatus, mapMdmStatus, mapElogistiaStatus
use DzDelivery\Support\StatusMapper; use DzDelivery\Models\ParcelStatus; StatusMapper::map('yalidine', 'Sorti en livraison'); // 'in_transit' StatusMapper::maystro(41); // 'delivered' (code ou nom) ParcelStatus::DELIVERED; // constantes, ParcelStatus::ALL pour la liste
Codes d’erreur
Toute erreur est levée sous forme de ShippingError (champs détaillés dans Modèles). Le champ code vaut l’une de ces valeurs :
code | Quand |
|---|---|
AUTH_FAILED | 401 / 403, identifiants manquants ou invalides. |
VALIDATION_ERROR | 400 / 422, ou échec de la validation côté SDK (avant tout appel HTTP). |
NOT_FOUND | 404, colis ou localité introuvable, ou échec de la résolution d’un nom (wilayaName, communeName). |
RATE_LIMITED | 429. |
NOT_SUPPORTED | Le transporteur ne propose pas cette opération (par exemple l’annulation). |
PROVIDER_ERROR | 5xx, ou réponse inattendue / illisible du transporteur. |
NETWORK_ERROR | Timeout, DNS, connexion refusée. |
UNKNOWN | Tout le reste. |
Correspondance HTTP → code par défaut : 400, 422 → VALIDATION_ERROR, 401, 403 → AUTH_FAILED, 404 → NOT_FOUND, 429 → RATE_LIMITED, 5xx → PROVIDER_ERROR, autres 4xx → UNKNOWN.
Exemples de gestion (relance sur RATE_LIMITED, affichage des erreurs de validation) : guide PHP.
Conventions
| Sujet | Règle |
|---|---|
| Nommage | camelCase pour les champs et les méthodes, en TS comme en PHP (les objets PHP exposent des propriétés publiques readonly aux mêmes noms). |
| Identifiants | Tous les ids renvoyés par le SDK sont des chaînes (les transporteurs utilisent des UUID, des entiers ou des codes). Les entrées acceptent string | int. |
| Montants | En DZD, sous forme de nombres (dinars, pas centimes). currency vaut toujours "DZD". |
| Poids / dimensions | Poids en kg, dimensions en cm. |
| Dates | Chaînes ISO 8601 avec fuseau horaire (par exemple 2025-12-23T15:23:54+00:00), en TS comme en PHP. |
| Téléphones | Chaînes. Chaque provider normalise au format du transporteur (par exemple 0550123456 ↔ +213550123456). |
| Optionnel | ? = optionnel. Une valeur optionnelle absente vaut undefined (TS) / null (PHP). |
| Asynchrone | Les méthodes TS renvoient Promise<T>. Les méthodes PHP sont synchrones et renvoient T. |
| Erreurs | Tous les échecs sont levés en ShippingError (voir Codes d’erreur). Jamais de null / false pour signaler une erreur. |
Le champ raw
Chaque modèle renvoyé (Parcel, TrackingEvent, FeeQuote, Location, ShippingError) peut porter un champ optionnel raw contenant la réponse native non modifiée de l’API (ou le fragment utile).
rawsert au débogage et aux usages avancés. Sa forme ne fait pas partie du contrat et peut changer quand le transporteur modifie son API.- Votre code applicatif ne devrait pas en dépendre pour sa logique principale.
rawest rempli par défaut ; l’option clientincludeRaw: falsele désactive.
Modèles
Les mêmes modèles, avec les mêmes noms de champs, en TypeScript et en PHP.
ParcelInput (entrée de createParcel)
| Champ | Type | Requis | Description |
|---|---|---|---|
recipientName | string | oui | Nom complet du destinataire. |
phone | string | oui | Numéro principal. |
phone2 | string | non | Numéro secondaire. |
address | string | oui | Adresse / texte libre. |
wilayaId | string | int | l’un des deux¹ | Location.id du transporteur ou code officiel de wilaya 1–58. |
wilayaName | string | l’un des deux¹ | Nom de la wilaya (résolu par le provider, sans tenir compte de la casse ni des accents). |
communeId | string | int | l’un des deux² | Location.id de la commune chez le transporteur. |
communeName | string | l’un des deux² | Nom de la commune (résolu dans la wilaya). |
productName | string | l’un des deux³ | Description libre d’un seul produit. |
productList | ProductItem[] | l’un des deux³ | Liste détaillée des produits. |
price | number | oui | Montant à encaisser auprès du destinataire (COD), en DZD. 0 si prépayé. |
weight | number | non | Poids total en kg. |
length, width, height | number | non | Dimensions du colis en cm. |
isStopDesk | boolean | non | true = livraison en stop desk / point relais. Par défaut false (domicile). |
stopDeskId | string | si isStopDesk | Id du stop desk / hub chez le transporteur. |
remark | string | non | Note pour le livreur. |
orderId | string | non | Votre référence de commande (stockée comme id externe quand c’est possible). |
stockType | "none" | "local" | "warehouse" | non | Mode de stock par défaut des produits sans ProductItem.stockType. Utilisé par ZRExpress, ignoré par les autres. Par défaut "none" (pas de catalogue). |
¹ Exactement un de wilayaId / wilayaName. ² Exactement un de communeId / communeName. ³ Exactement un de productName / productList : les providers qui exigent une liste convertissent productName en [{ name, quantity: 1, unitPrice: price }], ceux qui exigent une chaîne joignent les noms de productList.
L’inclusion ou non des frais de livraison dans price dépend du transporteur (documenté dans PROVIDERS.md).
ProductItem
| Champ | Type | Requis | Description |
|---|---|---|---|
name | string | oui | Nom du produit. |
quantity | int | oui | Quantité (≥ 1). |
unitPrice | number | non | Prix unitaire en DZD. |
sku | string | non | SKU marchand. |
productId | string | non | Id produit du catalogue transporteur (ZRExpress avec stockType local / warehouse). |
stockType | "none" | "local" | "warehouse" | non | Mode de stock de l’article ; remplace ParcelInput.stockType. |
Parcel
| Champ | Type | Requis | Description |
|---|---|---|---|
id | string | oui | Id interne du colis chez le transporteur (peut être égal à trackingNumber). |
trackingNumber | string | oui | Numéro de suivi montré au client. |
status | ParcelStatus | oui | Statut unifié (voir Statuts). |
provider | string | oui | Clé du provider, par exemple "zrexpress", "yalidine". |
labelUrl | string | non | URL du bordereau quand le transporteur la renvoie avec le colis (Yalidine). Les transporteurs dont l’étiquette exige un appel séparé (ZRExpress, PDF Maystro) l’exposent via un extra du provider. |
raw | unknown / mixed | non | Réponse native. |
TrackingEvent et TrackResult
| Champ | Type | Requis | Description |
|---|---|---|---|
status | ParcelStatus | oui | Statut unifié après cet événement. |
label | string | oui | Libellé natif lisible (par exemple "Prêt à expédier"). |
at | string | oui | Horodatage ISO 8601 de l’événement. |
raw | unknown / mixed | non | Événement natif. |
track() renvoie un TrackResult = { parcel, events } : parcel est l’état actuel du colis, events l’historique trié par at croissant (le plus ancien en premier), éventuellement vide si le transporteur n’expose pas d’historique.
FeeQuery (entrée de getFees)
| Champ | Type | Requis | Description |
|---|---|---|---|
toWilayaId | string | int | oui | Wilaya de destination (Location.id ou code 1–58). |
toCommuneId | string | int | non | Commune de destination, pour les transporteurs qui tarifent par commune. |
fromWilayaId | string | int | non | Wilaya d’origine. Par défaut celle du compte quand le transporteur la connaît. |
weight | number | non | Poids en kg (pour les suppléments de surpoids). |
length, width, height | number | non | Dimensions en cm, pour les transporteurs qui facturent le poids volumétrique. |
isStopDesk | boolean | non | Tarif stop desk au lieu du tarif domicile. |
FeeQuote
| Champ | Type | Requis | Description |
|---|---|---|---|
deliveryFee | number | oui | Tarif du mode demandé (domicile, ou stop desk si isStopDesk: true). |
returnFee | number | non | Frais facturés en cas de retour. |
stopDeskFee | number | non | Tarif stop desk, quand le transporteur renvoie les deux prix. |
currency | "DZD" | oui | Toujours "DZD". |
raw | unknown / mixed | non | Réponse native. |
Location (wilayas et communes)
| Champ | Type | Requis | Description |
|---|---|---|---|
id | string | oui | Id chez le transporteur, à repasser en wilayaId / communeId. |
name | string | oui | Nom affiché. |
parentId | string | non | Pour une commune : l’id de sa wilaya. Absent pour les wilayas. |
code | string | non | Code officiel quand il est connu (wilaya "1"–"58", ou code postal pour les communes). Permet de faire correspondre les wilayas entre transporteurs, certains (comme ZRExpress) utilisant des UUID comme ids. |
raw | unknown / mixed | non | Objet natif. |
ShippingError
Levée en exception (TS : class ShippingError extends Error ; PHP : class ShippingError extends \RuntimeException).
| Champ | Type | Requis | Description |
|---|---|---|---|
code | ShippingErrorCode | oui | Code normalisé (voir Codes d’erreur). |
message | string | oui | Message lisible (message natif quand il existe). |
httpStatus | int | non | Statut HTTP de l’appel en échec. |
details | object / array | non | Détails normalisés, par exemple erreurs de validation [{ field?, message }]. |
raw | unknown / mixed | non | Corps d’erreur natif. |
provider | string | non | Clé du provider à l’origine de l’erreur. |
retryAfter | int | non | RATE_LIMITED uniquement : secondes d’attente, issues de l’en-tête Retry-After quand le transporteur l’envoie. |
StopDeskQuery et StopDesk
Entrée de getStopDesks : wilayaId (string | int, requis : Location.id, code 1–58 ou nom) et communeId (string | int, optionnel : Location.id ou nom). Certains transporteurs exigent communeId (Maystro) ; ailleurs c’est un filtre optionnel, et en cas de doute préférez la wilaya seule.
Champ StopDesk | Type | Requis | Description |
|---|---|---|---|
id | string | oui | À repasser en ParcelInput.stopDeskId. |
name | string | oui | Nom affiché. |
address | string | non | Adresse, quand le transporteur la renvoie. |
communeId | string | non | Id de commune chez le transporteur. |
wilayaId | string | non | Id de wilaya chez le transporteur. |
phone | string | non | Téléphone principal, quand il est renvoyé. |
openingHours | string | non | Horaires d’ouverture (texte), si disponibles. |
lat, lng | number | non | Latitude, longitude. |
type | string | non | Type de hub / bureau quand il est exposé (par exemple ZRExpress "stopdesk"). |
raw | unknown / mixed | non | Objet natif. |
ParcelListQuery et ParcelListPage
Champ ParcelListQuery | Type | Description |
|---|---|---|
page | int | Page, à partir de 1. Par défaut 1. |
pageSize | int | Taille de page. Par défaut 50. Peut être plafonnée par le transporteur (ZRExpress ≤ 1000). |
keyword | string | Recherche libre (numéro de suivi, nom du client, téléphone… selon le transporteur). |
orderId | string | Référence de commande marchand (ParcelInput.orderId / id externe). |
status | ParcelStatus | Filtre sur le statut unifié, appliqué après le mapping quand le transporteur n’a pas de filtre équivalent (cas habituel). |
updatedSince | string | ISO 8601 : seulement les colis dont le statut a changé à partir de cet instant (si le transporteur expose une date de mise à jour). |
allPages | boolean | true : suit hasNext et fusionne les pages (avec un plafond de sécurité). Par défaut false. |
Champ ParcelListPage | Type | Description |
|---|---|---|
parcels | Parcel[] | Colis de cette réponse (ou de toutes les pages avec allPages). |
page | int | Page récupérée (1 quand allPages a tout fusionné). |
pageSize | int | Taille de page demandée / effective. |
totalCount | int | Total annoncé par le transporteur pour la requête (avant les filtres côté SDK comme status, ou orderId chez E-com). Sans total pour une page (E-com après la page 1) : une borne basse, (page - 1) * pageSize + éléments de la page. |
hasNext | boolean | Il reste des pages (false quand allPages a terminé ou que le transporteur n’en a plus). |
raw | unknown / mixed | Page native (la dernière avec allPages). |
Interface ShippingProvider
Tous les clients implémentent la même interface : ShippingProvider en TypeScript, ShippingProviderInterface en PHP.
| Méthode | Entrée | Sortie |
|---|---|---|
createParcel(input) | ParcelInput | Parcel |
track(trackingNumber) | string | TrackResult = { parcel, events } |
getFees(query) | FeeQuery | FeeQuote |
getWilayas() | — | Location[] |
getCommunes(wilayaId) | string | int (id du provider ou code 1–58) | Location[] |
cancelParcel(trackingNumberOrId) | string | void |
getStopDesks(query) | StopDeskQuery | StopDesk[] |
listParcels(query?) | ParcelListQuery | ParcelListPage |
healthCheck() | — | HealthCheckResult = { ok: true, provider, latencyMs } |
export type ParcelStatus = | 'pending' | 'created' | 'in_transit' | 'delivered' | 'returned' | 'cancelled' | 'unknown'; export interface HealthCheckResult { ok: true; provider: string; /** Durée aller-retour en millisecondes. */ latencyMs: number; } export interface ShippingProvider { readonly name: string; // 'zrexpress' | 'yalidine' | ... createParcel(input: ParcelInput): Promise<Parcel>; track(trackingNumber: string): Promise<TrackResult>; getFees(query: FeeQuery): Promise<FeeQuote>; getWilayas(): Promise<Location[]>; getCommunes(wilayaId: string | number): Promise<Location[]>; getStopDesks(query: StopDeskQuery): Promise<StopDesk[]>; listParcels(query?: ParcelListQuery): Promise<ParcelListPage>; cancelParcel(trackingNumberOrId: string): Promise<void>; healthCheck(): Promise<HealthCheckResult>; }
namespace DzDelivery\Contracts; use DzDelivery\Models\FeeQuery; use DzDelivery\Models\FeeQuote; use DzDelivery\Models\HealthCheckResult; use DzDelivery\Models\Location; use DzDelivery\Models\Parcel; use DzDelivery\Models\ParcelInput; use DzDelivery\Models\ParcelListPage; use DzDelivery\Models\ParcelListQuery; use DzDelivery\Models\StopDesk; use DzDelivery\Models\StopDeskQuery; use DzDelivery\Models\TrackResult; interface ShippingProviderInterface { public function getName(): string; public function createParcel(ParcelInput|array $input): Parcel; public function track(string $trackingNumber): TrackResult; public function getFees(FeeQuery|array $query): FeeQuote; /** @return Location[] */ public function getWilayas(): array; /** @return Location[] */ public function getCommunes(string|int $wilayaId): array; /** @return StopDesk[] */ public function getStopDesks(StopDeskQuery|array $query): array; public function listParcels(ParcelListQuery|array|null $query = null): ParcelListPage; public function cancelParcel(string $trackingNumberOrId): void; public function healthCheck(): HealthCheckResult; }
Comportement des méthodes
createParcel: valide les champs requis côté client (VALIDATION_ERRORavant tout appel HTTP) et résoutwilayaName/communeName/ codes de wilaya en ids du transporteur. Si le transporteur ne renvoie qu’un id à la création, le provider peut relire le colis pour remplirtrackingNumberetstatus.track: renvoie leParcelactuel et son historique. Si le transporteur a besoin d’un id interne pour l’historique, le provider le retrouve à partir du numéro de suivi.getFees: renvoie un seul tarif pour la destination et le mode demandés.NOT_FOUNDsi la destination n’a pas de tarif.getWilayas/getCommunes: ne renvoient que les localités livrables quand le transporteur fournit l’information. Les résultats peuvent être mis en cache en mémoire pendant la vie du client.cancelParcel: accepte un numéro de suivi ou un id interne.NOT_SUPPORTEDsi le transporteur ne permet pas l’annulation,VALIDATION_ERROR/PROVIDER_ERRORsi le colis n’est plus annulable. Renvoievoidcar certains transporteurs répondent204 No Content: appeleztrack()ensuite pour connaître le nouveau statut.getStopDesks: liste les stop desks / points relais d’une wilaya (éventuellement d’une commune).VALIDATION_ERRORsi le transporteur exigecommuneIdet qu’il manque,NOT_SUPPORTEDsans liste disponible. Peut leverNOT_FOUNDavec une indication dansdetailsquand le filtre commune ne donne rien alors que la wilaya a des bureaux (cas typique des hubs ZRExpress).listParcels: liste paginée des colis du compte avec leurstatusunifié, sans l’historique (utiliseztrack()).NOT_SUPPORTEDsi le transporteur n’expose pas de liste. Préférez les webhooks à un polling agressif quand ils existent.healthCheck: vérifie identifiants et connectivité avec une requête authentifiée légère (liste des wilayas). Appelle toujours le réseau (ignore le cache). Renvoie{ ok: true, provider, latencyMs }, sinon lève uneShippingError(AUTH_FAILED,NETWORK_ERROR,RATE_LIMITED…). Ne lève jamaisNOT_SUPPORTED.
Créer un client
const client = createClient('zrexpress', { apiKey, tenant, /* baseUrl?, timeoutMs?, includeRaw? */ });
$client = ClientFactory::create('zrexpress', ['apiKey' => ..., 'tenant' => ..., /* 'baseUrl', 'timeoutMs', 'includeRaw' */]);
| Option commune | Type | Défaut | Description |
|---|---|---|---|
baseUrl | string | URL du provider | Remplace l’URL de base de l’API (sandbox, proxy, tests). |
timeoutMs | int | 15000 | Timeout HTTP. |
includeRaw | boolean | true | Remplit les champs raw. |
Identifiants propres à chaque transporteur :
| Provider | Clés de configuration | Variables d’environnement (.env.example) |
|---|---|---|
zrexpress | apiKey, tenant | ZREXPRESS_API_KEY, ZREXPRESS_TENANT |
yalidine | apiId, apiToken, fromWilayaId ou fromWilayaName (wilaya d’origine, requise par createParcel et getFees) | YALIDINE_API_ID, YALIDINE_API_TOKEN, YALIDINE_FROM_WILAYA_ID |
maystro | apiToken | MAYSTRO_API_TOKEN |
ecotrack | token, courier ou baseUrl (https) | ECOTRACK_TOKEN, ECOTRACK_COURIER / ECOTRACK_BASE_URL |
noest | apiToken, userGuid, autoValidate? | NOEST_API_TOKEN, NOEST_USER_GUID |
ecom | apiKey, apiToken, autoValidate? | ECOM_API_KEY, ECOM_API_TOKEN |
mdm | apiKey, storeId (requis par createParcel) | MDM_API_KEY, MDM_STORE_ID |
elogistia | apiKey, email? | ELOGISTIA_API_KEY |
Un baseUrl personnalisé doit être une URL https:, car les identifiants accompagnent chaque requête (en en-tête, ou dans la query string chez Elogistia). Les clients Ecotrack, NoEST, E-com, MDM et Elogistia refusent toute autre URL avec VALIDATION_ERROR. Configuration détaillée en PHP : guide PHP.
Hors du contrat unifié
Volontairement absents de l’interface commune : endpoints de catalogue (produits, catégories, stock), modification de colis (adresse, montant, produits), échanges, remboursements, sacs d’enlèvement, réclamations, clients, trésorerie et paiements, rapports, gestion des endpoints webhook. Ils pourront arriver plus tard sous forme de modules propres à un transporteur, sans changer ShippingProvider.
Les fonctions plus riches d’un transporteur sont exposées comme extras sur sa classe concrète, par exemple pour ZRExpress la création / suppression en masse, les étiquettes et la vérification des webhooks (page ZRExpress, ZREXPRESS.md).
Ce contrat est versionné (v0.1) : tout changement cassant des modèles ou de l’interface implique une nouvelle version et une publication des deux paquets.