Jalons d'intégrité documentaire
Le problème que ça résout
Depuis la mission signature électronique, l'API calcule et stocke un hash
du contenu du document à chaque signature (signatures.document_hash).
Un hash prouve qu'un contenu présenté correspond à ce qui a été signé
— mais il ne permet jamais de reconstituer ce que le document
contenait réellement à un instant passé. Concrètement : si un litige
survient entre la collecte et la livraison ("la marchandise déclarée à la
livraison n'est pas celle collectée"), rien ne permettait de comparer les
deux états, seulement de vérifier qu'un état donné correspondait à un hash
donné.
C'est l'équivalent numérique d'un problème bien connu sur le CMR papier : la lettre de voiture existe en plusieurs exemplaires carbone (expéditeur, transporteur, destinataire), et détecter une falsification consiste à superposer les calques pour repérer une divergence — jamais à empêcher quelqu'un d'écrire autre chose sur son exemplaire après séparation. Cette mission construit l'équivalent numérique exact : un instantané complet du contenu légal à chaque jalon du cycle de vie, et un endpoint qui superpose deux jalons pour vous montrer précisément ce qui a changé.
Ce que ce mécanisme ne fait pas : il ne verrouille aucun champ en
écriture. goods_lines, les dates planifiées, etc. restent modifiables
après ISSUED, exactement comme avant cette mission — c'est un mécanisme
de détection, pas un contrôle d'accès. Si vous avez besoin qu'un champ
précis devienne immuable après un certain point du cycle de vie, c'est une
question distincte, à traiter séparément.
Les jalons capturés
| Jalon | Déclenché par |
|---|---|
DRAFT_CREATED |
POST /v1/freightdocuments |
ISSUED |
POST /v1/freightdocuments/{id}/issue |
COLLECTION_SIGNED |
Signature de type COLLECTION complétée |
COLLECTION_ACCEPTANCE_SIGNED |
Signature de type COLLECTION_ACCEPTANCE complétée (le chauffeur/transporteur, au moment de la collecte — voir Signature électronique) |
HANDOVER_SIGNED |
Signature de type HANDOVER complétée |
ACCEPTANCE_SIGNED |
Signature de type ACCEPTANCE complétée |
DELIVERY_SIGNED |
Signature de type DELIVERY complétée — la transition qui en résulte est directement CLOSED (voir Cycle de vie) |
CLOSED |
Valeur historique du jalon CLOSED pour les documents clôturés avant la refonte du modèle de statuts (transition DELIVERED → CLOSED, alors déclenchée par une pièce jointe POD) ; aucun nouveau document ne produit ce jalon séparément — DELIVERY_SIGNED couvre déjà l'instant de la signature qui clôture directement le document (voir Cycle de vie) |
DRAFT_CREATED est informatif — un DRAFT reste librement modifiable,
donc ce jalon ne fait pas encore partie de la chaîne de détection de
falsification à proprement parler. Celle-ci commence réellement à
ISSUED, une fois le document juridiquement engageant.
Lister les jalons disponibles
GET /v1/freightdocuments/{id}/snapshots
Requiert la permission VIEW — cette donnée n'est jamais plus sensible
que ce qu'un GET normal sur le document montre déjà.
{
"snapshots": [
{ "id": "...", "milestone": "DRAFT_CREATED", "content_hash": "a1b2...", "created_at": "..." },
{ "id": "...", "milestone": "ISSUED", "content_hash": "c3d4...", "created_at": "..." }
]
}
Réponse volontairement légère (pas de contenu complet) — c'est l'enumération de ce qui est disponible à comparer, pas un export de contenu.
Comparer deux jalons — la fonctionnalité centrale
GET /v1/freightdocuments/{id}/snapshots/compare?from=ISSUED&to=DELIVERY_SIGNED
Retourne un diff structuré : une liste plate de changements, chacun avec un chemin explicite (notation point/crochet, valable pour n'importe quelle partie du document, y compris un champ imbriqué dans un rôle ou une ligne de marchandise) et l'ancienne/nouvelle valeur.
{
"from_milestone": "ISSUED",
"to_milestone": "DELIVERY_SIGNED",
"from_snapshot_id": "...",
"to_snapshot_id": "...",
"changes": [
{
"field": "goods_lines[0].nature_of_goods",
"from": "Grain de café",
"to": "Autre chose"
},
{
"field": "status",
"from": "ISSUED",
"to": "DELIVERED"
}
],
"comparison_method": "Items inside a list (goods_lines, roles) are matched by content, not raw position: an item that is byte-identical on both sides is never reported as changed, even if it moved — only a genuinely different item at its resolved position is. [...]"
}
comparison_method est toujours présent, à lire avant de tirer une
conclusion de changes sur quelle ligne précise a changé : les éléments
d'une liste (goods_lines, roles) sont appariés par contenu, pas
par position brute — une ligne insérée au milieu d'une liste n'est jamais
faussement rapportée comme "la ligne suivante a été modifiée", elle
apparaît comme une insertion propre à sa position réelle.
Dans cet exemple, la marchandise déclarée à l'émission n'est plus celle
déclarée à la livraison — exactement le type de divergence que ce
mécanisme existe pour révéler. Le changement de status lui-même
apparaît aussi dans le diff : c'est une évolution légitime attendue entre
ces deux jalons, pas une anomalie — c'est à vous d'interpréter le diff
selon le contexte, l'endpoint ne juge pas ce qui est normal.
Une ligne ajoutée ou supprimée dans une liste (goods_lines, roles)
apparaît comme une seule entrée avec la valeur complète de la ligne, pas
un champ par champ :
{ "field": "goods_lines[1]", "from": null, "to": { "nature_of_goods": "...", "package_count": 5, "...": "..." } }
Second usage : reconstituer le PDF à un jalon passé
Les jalons ne servent plus seulement à comparer deux états entre eux :
GET .../pdf?milestone=<jalon> (voir Pièces jointes & PDF)
s'appuie sur le même mécanisme pour régénérer le PDF exactement comme il
existait à ce jalon, plutôt que dans son état courant — le contenu du
snapshot fournit le statut/rôles/marchandises/références/dates à cet
instant, les signatures et pièces jointes étant filtrées par leur propre
horodatage pour ne montrer que celles qui existaient déjà. Demander un
jalon non encore atteint renvoie la même erreur
freight_document.snapshot_not_found que l'endpoint de comparaison
ci-dessus — pas un code distinct.
Codes d'erreur spécifiques
| Code | HTTP | Sens |
|---|---|---|
freight_document.snapshot_not_found |
404 | from/to nomment un jalon réel, mais ce document n'a pas encore de instantané pour l'un des deux (ex : comparer DELIVERY_SIGNED avant toute signature de livraison). |
freight_document.invalid_snapshot_comparison |
400 | from/to ne correspondent à aucun jalon valide, ou from est égal à to. |
Voir Codes d'erreur pour le format complet.