Contrat API — interroger par équipement (NAS / routeur)
TiketMOMO et J+RADIUS — 2026-09-06
Ce document décrit la convention unique pour demander « ce que fait CET équipement » plutôt que « ce que fait tout mon parc ». Elle est identique sur toutes les routes concernées : une seule chose à apprendre, un seul comportement à tester.
Pourquoi une convention unique
Chaque route avait inventé sa manière de recevoir « quel routeur » : l’une acceptait
nasid, l’autre nas_id, une troisième un numéro de série. Aucune ne disait ce qu’elle
faisait en cas d’ambiguïté. Résultat mesuré en production le 2026-09-06 : nas_id — un
nom de serveur hotspot — servait de clé alors que sept gestionnaires partagent
« hotspot1 », et remote_access_allocations.nas_id porte cette valeur sur 94
allocations.
Les trois façons de désigner un équipement
| Paramètre | Ce que c’est | Ambiguïté |
|---|---|---|
equipementId |
l’identifiant d’allocation (uuid) | aucune — à préférer toujours |
routerSerial |
le numéro de série du matériel | aucune ; survit à un changement d’allocation |
nasId |
le nom du serveur hotspot | ambigu par nature |
Les graphies equipement_id / allocationId / allocation_id, router_serial /
serial, nas_id / nasid sont acceptées : l’app et le portail n’ont jamais eu la
même, et imposer l’une casserait l’autre sans rien apporter.
Trois règles, sans exception
- Un seul critère à la fois. Deux critères qui se contredisent n’ont pas de réponse
juste ; en privilégier un en silence produirait un chiffre faux qui se lit comme un
fait. →
400 EQUIPEMENT_CRITERES_MULTIPLES. nasIdn’est accepté que s’il ne désigne qu’un équipement chez CE client. Sinon la réponse liste les candidats et refuse de choisir. →409 EQUIPEMENT_AMBIGU.- Aucun paramètre = tout le parc. Le comportement par défaut ne change pas : une route qui se mettrait soudain à ne rendre qu’un équipement casserait tous les appelants existants.
Isolation
Le client vient toujours du jeton, jamais de la requête. Un equipementId valide mais
appartenant à un autre client rend 404 EQUIPEMENT_INTROUVABLE — pas une erreur
d’autorisation : on ne confirme pas l’existence de ce qui ne vous appartient pas.
Erreurs
{
"success": false,
"error": {
"code": "EQUIPEMENT_AMBIGU",
"message": "Ce nom désigne plusieurs équipements de votre parc : utilisez equipementId ou routerSerial.",
"critere": "nasId",
"valeur": "hotspot1",
"equipements": [
{
"equipement_id": "e4034eb4-…",
"numero_serie": "HH40AF7YVNP",
"libelle": "hotspot1",
"statut": "ACTIVE"
},
{
"equipement_id": "7a0ea8f5-…",
"numero_serie": null,
"libelle": "hotspot1",
"statut": "ACTIVE"
}
]
}
}
AMBIGU rend 409 et non 400 : la demande est bien formée, c’est l’état du parc qui
ne permet pas d’y répondre — et la liste est là pour trancher. Les trois codes sont
stables ; l’app peut s’en servir pour réagir (proposer la liste, ou signaler un critère
faux).
Routes qui acceptent le filtre
TiketMOMO
| Route | Ce que le filtre change |
|---|---|
GET /vouchers |
le stock de ce point de vente |
GET /sales/stats |
chiffre d’affaires, stock et transactions de ce point de vente |
GET /clients/dashboard/ventes-par-equipement |
déjà ventilé par équipement (voir plus bas) |
GET /vouchers gagne aussi deux corrections : used et offset étaient acceptés et
silencieusement ignorés. Demander « seulement les disponibles » rendait tout le
stock, et la pagination rendait toujours la première page.
GET /api/v1/vouchers?equipementId=e4034eb4-…&used=false&limit=100&offset=0
{
"success": true,
"data": [ … ],
"equipement": { "equipement_id": "e4034eb4-…", "numero_serie": "HH40AF7YVNP", "libelle": "MoailtePro", "statut": "ACTIVE" },
"pagination": { "limit": 100, "offset": 0, "retournes": 87 }
}
equipement rend ce sur quoi le filtre a réellement porté : un appelant qui a
filtré par nom doit pouvoir le vérifier sans redemander.
J+RADIUS
| Route | Ce que le filtre change |
|---|---|
GET /radius/analytics/sales/history |
vouchers émis par cet équipement |
GET /radius/analytics/sessions/history |
sessions consommées sur cet équipement |
⚠️ Émis ≠ consommé. Un voucher J+RADIUS vaut sur tout le parc de son propriétaire
(radius_scope = GLOBAL_RADIUS_SCOPE). sales/history dit où il a été émis ;
sessions/history dit où il a été utilisé. Ne pas présenter l’un pour l’autre.
sessions/history rend filtre_applique :
{ "stats": [ … ], "equipement": { … }, "filtre_applique": true }
false signifie que l’équipement n’a pas de NAS déclaré : le filtre n’a pas pu
s’appliquer et la réponse porte tout le parc. Le dire vaut mieux que rendre un résultat
qui a l’air filtré et ne l’est pas. Sur 30 NAS déclarés, 26 portent leur allocation.
Le filtre NAS s’ajoute au filtre de tenant, il ne le remplace jamais : seul le préfixe garantit qu’on ne lit que les sessions du bon client.
Vue d’ensemble par équipement
GET /api/v1/clients/dashboard/ventes-par-equipement?fenetre=7j|30j|90j|tout
Rend, pour chaque équipement : ventes, montant, clients finaux, stock restant par forfait (trié du plus rare au plus abondant), et la part non attribuée — toujours affichée, jamais répartie sur les autres.
Chaque attribution dit d’où elle vient (attribution_source) : VOUCHER et CHECKOUT
sont des mesures, EQUIPEMENT_UNIQUE une déduction. Détail dans
BRIEF-MOBILE-AGENT-ATTRIBUTION-EQUIPEMENT-2026-09-06.md.
Ce qui reste à faire côté app
Envoyer allocationId (ou routerSerial) à l’émission des vouchers. Sans cela
l’attribution reste une déduction — inopérante précisément pour les clients
multi-sites, ceux qui ont besoin du détail. Sur la branche J+RADIUS, 0 voucher sur
16 896 portait son équipement émetteur avant ce chantier.
ssh root@161.97.64.197 'cat /root/projects/jservices-api/docs/CONTRAT-API-EQUIPEMENT-2026-09-06.md'