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 :

ParcelStatus
pending | created | in_transit | delivered | returned | cancelled | unknown
StatutSignificationExemples YalidineExemples Maystro (code)
pendingCommande 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
createdConfirmé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_transitEnlevé 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
deliveredLivré au destinataire (COD encaissé).« Livré »41 DELIVERED
returnedRetourné 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
cancelledAnnulé / supprimé avant livraison.« Annulé », « Supprimé »50 ABORTED
unknownStatut 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.

TypeScript
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
PHP
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 :

codeQuand
AUTH_FAILED401 / 403, identifiants manquants ou invalides.
VALIDATION_ERROR400 / 422, ou échec de la validation côté SDK (avant tout appel HTTP).
NOT_FOUND404, colis ou localité introuvable, ou échec de la résolution d’un nom (wilayaName, communeName).
RATE_LIMITED429.
NOT_SUPPORTEDLe transporteur ne propose pas cette opération (par exemple l’annulation).
PROVIDER_ERROR5xx, ou réponse inattendue / illisible du transporteur.
NETWORK_ERRORTimeout, DNS, connexion refusée.
UNKNOWNTout 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

SujetRègle
NommagecamelCase 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).
IdentifiantsTous 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.
MontantsEn DZD, sous forme de nombres (dinars, pas centimes). currency vaut toujours "DZD".
Poids / dimensionsPoids en kg, dimensions en cm.
DatesChaînes ISO 8601 avec fuseau horaire (par exemple 2025-12-23T15:23:54+00:00), en TS comme en PHP.
TéléphonesChaînes. Chaque provider normalise au format du transporteur (par exemple 0550123456 ↔ +213550123456).
Optionnel? = optionnel. Une valeur optionnelle absente vaut undefined (TS) / null (PHP).
AsynchroneLes méthodes TS renvoient Promise<T>. Les méthodes PHP sont synchrones et renvoient T.
ErreursTous 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).

  • raw sert 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.
  • raw est rempli par défaut ; l’option client includeRaw: false le 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)

ChampTypeRequisDescription
recipientNamestringouiNom complet du destinataire.
phonestringouiNuméro principal.
phone2stringnonNuméro secondaire.
addressstringouiAdresse / texte libre.
wilayaIdstring | intl’un des deux¹Location.id du transporteur ou code officiel de wilaya 1–58.
wilayaNamestringl’un des deux¹Nom de la wilaya (résolu par le provider, sans tenir compte de la casse ni des accents).
communeIdstring | intl’un des deux²Location.id de la commune chez le transporteur.
communeNamestringl’un des deux²Nom de la commune (résolu dans la wilaya).
productNamestringl’un des deux³Description libre d’un seul produit.
productListProductItem[]l’un des deux³Liste détaillée des produits.
pricenumberouiMontant à encaisser auprès du destinataire (COD), en DZD. 0 si prépayé.
weightnumbernonPoids total en kg.
length, width, heightnumbernonDimensions du colis en cm.
isStopDeskbooleannontrue = livraison en stop desk / point relais. Par défaut false (domicile).
stopDeskIdstringsi isStopDeskId du stop desk / hub chez le transporteur.
remarkstringnonNote pour le livreur.
orderIdstringnonVotre référence de commande (stockée comme id externe quand c’est possible).
stockType"none" | "local" | "warehouse"nonMode 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

ChampTypeRequisDescription
namestringouiNom du produit.
quantityintouiQuantité (≥ 1).
unitPricenumbernonPrix unitaire en DZD.
skustringnonSKU marchand.
productIdstringnonId produit du catalogue transporteur (ZRExpress avec stockType local / warehouse).
stockType"none" | "local" | "warehouse"nonMode de stock de l’article ; remplace ParcelInput.stockType.

Parcel

ChampTypeRequisDescription
idstringouiId interne du colis chez le transporteur (peut être égal à trackingNumber).
trackingNumberstringouiNuméro de suivi montré au client.
statusParcelStatusouiStatut unifié (voir Statuts).
providerstringouiClé du provider, par exemple "zrexpress", "yalidine".
labelUrlstringnonURL 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.
rawunknown / mixednonRéponse native.

TrackingEvent et TrackResult

ChampTypeRequisDescription
statusParcelStatusouiStatut unifié après cet événement.
labelstringouiLibellé natif lisible (par exemple "Prêt à expédier").
atstringouiHorodatage ISO 8601 de l’événement.
rawunknown / mixednonÉ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)

ChampTypeRequisDescription
toWilayaIdstring | intouiWilaya de destination (Location.id ou code 1–58).
toCommuneIdstring | intnonCommune de destination, pour les transporteurs qui tarifent par commune.
fromWilayaIdstring | intnonWilaya d’origine. Par défaut celle du compte quand le transporteur la connaît.
weightnumbernonPoids en kg (pour les suppléments de surpoids).
length, width, heightnumbernonDimensions en cm, pour les transporteurs qui facturent le poids volumétrique.
isStopDeskbooleannonTarif stop desk au lieu du tarif domicile.

FeeQuote

ChampTypeRequisDescription
deliveryFeenumberouiTarif du mode demandé (domicile, ou stop desk si isStopDesk: true).
returnFeenumbernonFrais facturés en cas de retour.
stopDeskFeenumbernonTarif stop desk, quand le transporteur renvoie les deux prix.
currency"DZD"ouiToujours "DZD".
rawunknown / mixednonRéponse native.

Location (wilayas et communes)

ChampTypeRequisDescription
idstringouiId chez le transporteur, à repasser en wilayaId / communeId.
namestringouiNom affiché.
parentIdstringnonPour une commune : l’id de sa wilaya. Absent pour les wilayas.
codestringnonCode 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.
rawunknown / mixednonObjet natif.

ShippingError

Levée en exception (TS : class ShippingError extends Error ; PHP : class ShippingError extends \RuntimeException).

ChampTypeRequisDescription
codeShippingErrorCodeouiCode normalisé (voir Codes d’erreur).
messagestringouiMessage lisible (message natif quand il existe).
httpStatusintnonStatut HTTP de l’appel en échec.
detailsobject / arraynonDétails normalisés, par exemple erreurs de validation [{ field?, message }].
rawunknown / mixednonCorps d’erreur natif.
providerstringnonClé du provider à l’origine de l’erreur.
retryAfterintnonRATE_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 StopDeskTypeRequisDescription
idstringouiÀ repasser en ParcelInput.stopDeskId.
namestringouiNom affiché.
addressstringnonAdresse, quand le transporteur la renvoie.
communeIdstringnonId de commune chez le transporteur.
wilayaIdstringnonId de wilaya chez le transporteur.
phonestringnonTéléphone principal, quand il est renvoyé.
openingHoursstringnonHoraires d’ouverture (texte), si disponibles.
lat, lngnumbernonLatitude, longitude.
typestringnonType de hub / bureau quand il est exposé (par exemple ZRExpress "stopdesk").
rawunknown / mixednonObjet natif.

ParcelListQuery et ParcelListPage

Champ ParcelListQueryTypeDescription
pageintPage, à partir de 1. Par défaut 1.
pageSizeintTaille de page. Par défaut 50. Peut être plafonnée par le transporteur (ZRExpress ≤ 1000).
keywordstringRecherche libre (numéro de suivi, nom du client, téléphone… selon le transporteur).
orderIdstringRéférence de commande marchand (ParcelInput.orderId / id externe).
statusParcelStatusFiltre sur le statut unifié, appliqué après le mapping quand le transporteur n’a pas de filtre équivalent (cas habituel).
updatedSincestringISO 8601 : seulement les colis dont le statut a changé à partir de cet instant (si le transporteur expose une date de mise à jour).
allPagesbooleantrue : suit hasNext et fusionne les pages (avec un plafond de sécurité). Par défaut false.
Champ ParcelListPageTypeDescription
parcelsParcel[]Colis de cette réponse (ou de toutes les pages avec allPages).
pageintPage récupérée (1 quand allPages a tout fusionné).
pageSizeintTaille de page demandée / effective.
totalCountintTotal 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.
hasNextbooleanIl reste des pages (false quand allPages a terminé ou que le transporteur n’en a plus).
rawunknown / mixedPage native (la dernière avec allPages).

Interface ShippingProvider

Tous les clients implémentent la même interface : ShippingProvider en TypeScript, ShippingProviderInterface en PHP.

MéthodeEntréeSortie
createParcel(input)ParcelInputParcel
track(trackingNumber)stringTrackResult = { parcel, events }
getFees(query)FeeQueryFeeQuote
getWilayas()—Location[]
getCommunes(wilayaId)string | int (id du provider ou code 1–58)Location[]
cancelParcel(trackingNumberOrId)stringvoid
getStopDesks(query)StopDeskQueryStopDesk[]
listParcels(query?)ParcelListQueryParcelListPage
healthCheck()—HealthCheckResult = { ok: true, provider, latencyMs }
TypeScript
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>;
}
PHP
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_ERROR avant tout appel HTTP) et résout wilayaName / 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 remplir trackingNumber et status.
  • track : renvoie le Parcel actuel 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_FOUND si 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_SUPPORTED si le transporteur ne permet pas l’annulation, VALIDATION_ERROR / PROVIDER_ERROR si le colis n’est plus annulable. Renvoie void car certains transporteurs répondent 204 No Content : appelez track() ensuite pour connaître le nouveau statut.
  • getStopDesks : liste les stop desks / points relais d’une wilaya (éventuellement d’une commune). VALIDATION_ERROR si le transporteur exige communeId et qu’il manque, NOT_SUPPORTED sans liste disponible. Peut lever NOT_FOUND avec une indication dans details quand 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 leur status unifié, sans l’historique (utilisez track()). NOT_SUPPORTED si 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 une ShippingError (AUTH_FAILED, NETWORK_ERROR, RATE_LIMITED…). Ne lève jamais NOT_SUPPORTED.

Créer un client

TypeScript
const client = createClient('zrexpress', { apiKey, tenant, /* baseUrl?, timeoutMs?, includeRaw? */ });
PHP
$client = ClientFactory::create('zrexpress', ['apiKey' => ..., 'tenant' => ..., /* 'baseUrl', 'timeoutMs', 'includeRaw' */]);
Option communeTypeDéfautDescription
baseUrlstringURL du providerRemplace l’URL de base de l’API (sandbox, proxy, tests).
timeoutMsint15000Timeout HTTP.
includeRawbooleantrueRemplit les champs raw.

Identifiants propres à chaque transporteur :

ProviderClés de configurationVariables d’environnement (.env.example)
zrexpressapiKey, tenantZREXPRESS_API_KEY, ZREXPRESS_TENANT
yalidineapiId, apiToken, fromWilayaId ou fromWilayaName (wilaya d’origine, requise par createParcel et getFees)YALIDINE_API_ID, YALIDINE_API_TOKEN, YALIDINE_FROM_WILAYA_ID
maystroapiTokenMAYSTRO_API_TOKEN
ecotracktoken, courier ou baseUrl (https)ECOTRACK_TOKEN, ECOTRACK_COURIER / ECOTRACK_BASE_URL
noestapiToken, userGuid, autoValidate?NOEST_API_TOKEN, NOEST_USER_GUID
ecomapiKey, apiToken, autoValidate?ECOM_API_KEY, ECOM_API_TOKEN
mdmapiKey, storeId (requis par createParcel)MDM_API_KEY, MDM_STORE_ID
elogistiaapiKey, 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.

Aller plus loin