Documentation J+SERVICES Guides Référence API

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 :


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 :

  1. 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.
  2. Déclasser efface le reste. {"isLicensed": false} remet service_code et sales_segment à null — sinon un ancien service survivrait à la bascule.
  3. Une mise à jour qui ne mentionne pas isLicensed ne touche pas la classification. Modifier un prix ne reclasse rien.

salesSegment est normalisé en majuscules (retailRETAIL). 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