Documentation J+SERVICES Guides Référence API

Contrat API — Compter juste dans le cockpit (parc, revenus, clients)

Le principe en une phrase : le cockpit doit compter des choses réelles — des équipements, de l’argent encaissé, des exploitants — et jamais des lignes de base.

Ce guide s’adresse au développeur du tableau de bord d’administration. Il fixe les règles de comptage et les champs à afficher. Les écarts cités ont été relevés en production le 2026-08-16 ; les valeurs sont volontairement exprimées en ordres de grandeur.


1. La règle qui prime sur toutes les autres

Trois registres d’argent coexistent, et ils ne s’additionnent JAMAIS.

Registre À qui appartient l’argent Ce qui nous revient
Licences SaaS à nous la totalité
Vouchers TiketMOMO aux tenanciers les frais uniquement
Hub techniciens aux professionnels la commission

Un total fondu gonflait le résultat de 45 %. L’API ne renvoie donc aucun champ « total », et cette absence est intentionnelle : un champ présent finirait affiché.


2. status = 'SUCCESS' ne prouve aucun encaissement

C’est le piège le plus coûteux de la base.

Le statut est écrit à la création de la session de paiement, avant que l’acheteur n’ait saisi quoi que ce soit. Sur les achats de licence, la quasi-totalité des lignes SUCCESS portent un identifiant cs_live_… et metadata.claimed = false : ce sont des clics sur « acheter », jamais payés. Les sommer multipliait le chiffre d’affaires annoncé par ~5,6.

La règle : le revenu licences se calcule sur les licences réellement délivrées (licenses.source_tx_id), en remontant vers la transaction. On part du fait accompli, jamais de l’intention d’achat.

"licences": {
  "cumule": 0,             // somme des transactions liées à une licence existante
  "periode": 0,
  "payantes_cumule": 0     // nombre de licences réellement payées
}

⚠️ Ne jamais reconstruire ce montant côté client à partir d’une liste de transactions.


3. Vouchers : nos frais d’un côté, le volume des tenanciers de l’autre

"vouchers": {
  "nos_frais_cumule": 0,
  "nos_frais_periode": 0,
  "volume_tenanciers_periode": 0,   // ⚠️ PAS un revenu
  "ventes_periode": 0
}

volume_tenanciers_periode porte son propriétaire dans son nom, exprès. C’est l’argent des tenanciers qui transite par notre compte ; l’afficher comme du revenu est la faute décrite au §1. Seuls les frais des ventes encaissées sur notre caisse (fee_origin = 'DEFAULT_STORE') nous reviennent.


4. Le parc se compte en matériels, jamais en allocations

L’ancien calcul annonçait ~88 % de routeurs muets, au-dessus du seuil critique tous les jours de l’année. Une alerte toujours allumée n’alerte plus : elle apprend à ignorer l’écran. Le taux réel est d’environ 10 %.

Trois confusions se cumulaient :

  1. Une allocation n’est pas un routeur. Les deux tiers des lignes n’ont jamais vu de matériel : un port réservé, un pair déclaré, personne au bout.
  2. Un matériel peut porter plusieurs allocations. Des routeurs étaient comptés deux fois.
  3. « Pas connecté » n’est pas « en panne ». Un hotspot éteint le soir par son propriétaire n’est pas une avarie.
"parc": {
  "materiels": 0,           // numéros de série UNIQUES, allocations ACTIVE
  "sain": 0,
  "muet": 0,                // ⚠️ la SEULE catégorie qui appelle une intervention
  "instable": 0,
  "hors_ligne": 0,
  "pct_muets": 0,
  "complet": true,
  "allocations_brutes": 0   // nombre de LIGNES — ne pas présenter comme un parc
}

Ce que « muet » veut dire

Un tunnel WireGuard se maintient sans adresse IP ni règle de pare-feu. Un routeur peut donc afficher CONNECTED tout en étant totalement sourd — c’est la panne la plus trompeuse du parc. Elle se reconnaît à la conjonction tunnel_status = CONNECTED et health_status = UNHEALTHY, jamais à l’un des deux seul.

Les autres catégories décrivent, elles n’alertent pas.


5. Les clients : un entonnoir, pas un total

Le nombre d’inscrits ne dit rien de l’activité — moins d’un inscrit sur huit a branché un équipement, et une fraction de ceux-là a déjà encaissé une vente.

Afficher les trois étages, dans cet ordre :

inscrits  →  ont un matériel verrouillé  →  ont vendu au moins une fois

C’est la chute entre les étages qui est l’information ; les valeurs absolues sont secondaires. Même règle pour les licences : montrer « actives » sans distinguer les payantes donne une image fausse de l’entreprise, le parc étant très majoritairement en promotion ou en essai.

Route dédiée : GET /api/v2/analytics/customers. Elle est séparée exprès — fondue dans un agrégat, la distinction se rediluerait et le total repasserait pour un nombre de clients.


6. complet: false doit s’afficher

PostgREST plafonne les lectures à 1 000 lignes, en silence. Tout agrégat au-delà serait faux sans le moindre signe.

Les instantanés portent donc un booléen complet. Quand il vaut false, le décompte a été interrompu : affichez-le. Un total tronqué présenté comme exhaustif est la même faute que celles corrigées ici, sous une autre forme.


7. Les anomalies méritent une tuile

Elles faussent tous les autres chiffres tant qu’elles existent :

"anomalies": {
  "transactions_sans_type": 0,      // n'entrent dans AUCUN registre
  "en_attente_plus_30j": 0,         // paiements qui n'aboutiront jamais
  "licences_liens_orphelins": 0     // licence référençant une transaction inexistante
}

Ce ne sont pas des détails techniques : une transaction sans type est de l’argent hors comptabilité, et une licence au lien orphelin est un montant payé définitivement introuvable.


8. À ne pas afficher

À bannir Pourquoi
nombre d’allocations ce sont des lignes, pas des équipements
somme des SUCCESS le statut précède l’encaissement
CA « toutes activités » additionne l’argent des tenanciers au nôtre
« clients actifs » compte des inscriptions, pas des exploitants
tout indicateur en Go aucune donnée de trafic n’existe : elle serait inventée
taux de résiliation sous 30 titulaires payants, une résiliation déplace le taux de plusieurs points

9. Quota — à lire avant d’implémenter le rafraîchissement

100 requêtes / 15 min par IP, partagées par TOUTES les routes /api.

Le cockpit santé est mis en cache 30 secondes côté serveur ; inutile de le solliciter plus vite. Interrogez à l’ouverture de l’écran, rafraîchissez sur action explicite, une seule sonde à la fois.


10. Voir aussi