Contrat API — Catalogue produits et classification commerciale
Destinataire : développeur du dashboard admin (écrans catalogue et marketing)
Base : https://live.jmoai.net · préfixe /admin/api
Enveloppe : { success: boolean, data: …, error?: { code, message } }
Authentification : x-admin-token, cookie de session, ou Authorization: Bearer <jwt>
1. Le problème que ce contrat résout
L’écran /admin/marketing filtrait les produits soldables sur
product_type IN ('WEB_APP', 'MOBILE_APP'). Ce critère décrit la forme d’un produit
(appli web, appli mobile, service), jamais sa nature commerciale. Sur le catalogue
réel, il se trompait des deux côtés :
| Produit | product_type |
Attrapé par l’ancien filtre | Devrait l’être |
|---|---|---|---|
| J+RADIUS | SERVICE |
❌ non | ✅ oui — c’est notre service |
| OpenAI ChatGPT | WEB_APP |
✅ oui | ❌ non — abonnement tiers revendu |
| Google Gemini | WEB_APP |
✅ oui | ❌ non — abonnement tiers revendu |
Ce filtre est à supprimer. Utilisez is_licensed (§3).
2. La classification vit sur le PLAN, pas sur le produit
C’est le point à intégrer avant d’écrire une ligne d’interface.
Nos quatre services sous licence ne sont pas quatre produits : ce sont des plans répartis sur trois produits, avec des doublons assumés.
produit « mikhmoai » produit « jradius » produit « tiketmomo »
├── mikhmoai-pro 17 400 ├── jradius-starter ├── standard 0
├── vpn 8 000 ├── jradius-business └── MOMO_TIER2_ANNUAL
├── vpn-6m 4 000 ├── jradius-enterprise
├── tiketmomo 9 000 └── RADIUS_TIER2_ANNUAL
├── jradius-l1/l2/l3
└── …_TIER2_ANNUAL
Conséquences :
- Aucun filtre au niveau produit ne peut fonctionner. Filtrer sur le produit
mikhmoaisélectionnerait d’un coup VPN, TiketMOMO, J+RADIUS et les offres partenaires. - Un même service apparaît sous deux produits. J+RADIUS existe en
mikhmoai/jradius-l1(10 000) etjradius/jradius-starter(15 000). Groupez parservice_codesi vous affichez « nos services », sinon le même service apparaîtra deux fois à deux prix.
3. Les trois champs, sur chaque plan
Présents dans toute réponse contenant des plans (§4).
| Champ | Valeurs | Sens |
|---|---|---|
is_licensed |
true / false |
true = licence d’un service qui nous appartient. false = abonnement tiers revendu, dont nous ne maîtrisons ni le produit ni la marge. |
service_code |
mikhmoai-pro · vpn · tiketmomo · jradius · full-stack |
Lequel de nos services. null si is_licensed: false. |
sales_segment |
RETAIL · TIER2_PARTNER |
RETAIL = vendu au gérant final sur store.mikhmoai.com. TIER2_PARTNER = offre business, partenaire hébergeant sa propre stack sur son VPS. null si is_licensed: false. |
full-stack est une offre groupée (FULL_TIER2_ANNUAL, 250 000) qui couvre les trois
services à la fois. Ne la comptez pas comme un quatrième service dans un graphe de
répartition — elle les recouvre.
Les trois champs sont liés
Une contrainte en base l’impose : soit is_licensed: false et les deux autres à
null, soit is_licensed: true et les deux autres renseignés. Il n’existe pas de
plan licencié sans service, ni de service sans segment.
Côté écriture (§5), envoyez donc les trois ensemble ou aucun.
État du catalogue au 2026-08-16
16 plans licenciés → mikhmoai-pro (2) · vpn (3) · tiketmomo (3) · jradius (7) · full-stack (1)
dont 4 en TIER2_PARTNER
10 plans non licenciés → ChatGPT (3) · Gemini (3) · Claude IA (2) · Claude Cowork (1) · Telegram (1)
4. Lecture
GET /admin/api/catalog/products/:productId
Renvoie le produit, ses plans et ses applications. Chaque plan porte les trois champs.
{
"success": true,
"data": {
"code": "mikhmoai",
"product_type": "MOBILE_APP", // la FORME — ne vous en servez plus pour filtrer
"plans": [
{
"code": "vpn",
"name": "VPN 1 ANS",
"price_amount": 8000,
"status": "ACTIVE",
"is_licensed": true,
"service_code": "vpn",
"sales_segment": "RETAIL",
},
{
"code": "VPN_TIER2_ANNUAL",
"name": "Tier-2 VPN (VPS)",
"price_amount": 100000,
"is_licensed": true,
"service_code": "vpn",
"sales_segment": "TIER2_PARTNER", // offre business : à ne pas mêler au grand public
},
],
},
}
GET /admin/api/catalog/products/:productId/plans
Les plans seuls, même forme.
GET /admin/api/catalog/products
Liste des produits. Query : search, status, productType, limit (défaut 50).
Ne contient pas les plans — donc pas la classification. Pour l’écran marketing,
passez par le détail produit ou la liste de plans.
5. Écriture
POST /admin/api/catalog/products/:productCode/plans et
PATCH /admin/api/catalog/plans/:planId acceptent :
{ "isLicensed": true, "serviceCode": "vpn", "salesSegment": "RETAIL" }
Trois règles de comportement :
- Un plan créé sans classification n’est pas licencié. Défaut volontairement sûr : un formulaire à moitié rempli ne doit pas glisser un produit dans « nos services », ni dans les soldes qu’on y applique.
- Déclasser efface le reste.
{"isLicensed": false}remetservice_codeetsales_segmentànull— sinon un ancien service survivrait à la bascule. - Une mise à jour qui ne mentionne pas
isLicensedne touche pas la classification. Modifier un prix ne reclasse rien.
salesSegment est normalisé en majuscules (retail → RETAIL). serviceCode est
normalisé en minuscules.
Codes d’erreur
| Code | HTTP | Sens |
|---|---|---|
PLAN_SERVICE_CODE_INVALID |
400 | Service hors des cinq valeurs admises |
PLAN_SALES_SEGMENT_INVALID |
400 | Segment absent ou hors RETAIL / TIER2_PARTNER |
6. Ce que ce contrat ne dit pas encore
Il ne dit pas si un code promo s’appliquera au paiement. C’est une question distincte, qui dépend du canal d’encaissement et non de la classification : un plan hébergé chez Chario porte un pourcentage de remise affichable, mais nos codes promo ne peuvent pas être injectés dans un checkout qui ne nous appartient pas. Les deux notions se ressemblent et ne se recouvrent pas :
| Notion | Où elle vit | Ce qu’elle autorise |
|---|---|---|
| Remise affichée | metadata.promo_discount_pct du plan |
montrer un prix barré |
| Code promo au paiement | autorité de prix du canal (Chario / interne) | réduire réellement la somme encaissée |
Un plan is_licensed: true peut donc être non couponnable. Si l’écran marketing doit
proposer des codes promo, demandez l’exposition de l’applicabilité par plan — elle est
calculée côté serveur mais n’est pas encore publiée.
7. Voir aussi
- Cockpit admin (analytics, santé, kill-switch) :
/docs/CONTRAT-API-ADMIN-COCKPIT - Référence complète générée depuis le code :
https://live.jmoai.net/api-docs/