Documentation J+SERVICES Guides Référence API

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

  1. 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.
  2. nasId n’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.
  3. 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'