Documentation J+SERVICES Guides Référence API

Contrat API — redirection checkout boutique

POST /api/store/checkout

Cette route valide un plan et retourne l’URL du Hub de paiement officiel. Elle ne crée aucune transaction et ne délivre aucune licence.

Requête

{
  "product_id": "uuid-du-plan",
  "payment_method": "fedapay",
  "coupon_code": "OPTIONNEL",
  "referral_code": "OPTIONNEL",
  "ref_code": "CODE_REVENDEUR_OPTIONNEL",
  "markup_pct": 20
}

Seul product_id est requis ; payment_method est facultatif et ignoré (le moyen de paiement se choisit sur le Hub). Un code de réduction invalide renvoie une erreur 404.

ref_code attribue la vente au revendeur et applique le markup configuré pour le plan dans sa boutique. markup_pct est facultatif et ne peut pas dépasser le taux du plan ni le plafond markup_percentage enregistré par le revendeur. Il ne remplace pas coupon_code : une remise coupon éventuelle s’applique d’abord, puis le markup est calculé sur le prix après remise. Le serveur recalcule toujours le montant.

Le catalogue public est GET /api/store/catalog?ref=CODE&markup=20. Pour une boutique revendeur, il ne renvoie que les plans que le vendeur a sélectionnés. Sa réponse conserve items et ajoute list_price, base_price (prix résolu par le serveur avant markup), retail_price, markup_amount et markup_percentage à chaque article, ainsi qu’un objet reseller lorsque le code est reconnu. La consultation peut être anonyme ou authentifiée ; dans ce dernier cas, la résolution tarifaire utilise le compte du jeton.

Sans markup, chaque plan sélectionné applique son propre taux, plafonné par markup_percentage enregistré sur le profil. Avec markup, le taux demandé doit être compris entre 0 et le plafond du profil; il est plafonné au markup configuré pour chaque plan. Le champ price est un alias de retail_price pour les consommateurs qui utilisent déjà ce nom.

Catalogue choisi par le revendeur

Les routes suivantes sont réservées au revendeur approuvé (cookie partenaire ou Bearer) :

GET /api/v1/resellers/store/products
PUT /api/v1/resellers/store/products/{planId}

La lecture renvoie les plans actifs éligibles et leur état selected (false par défaut), avec le markup enregistré par plan. La mise à jour exige { "enabled": true, "markup_percentage": 15 }; enabled: false retire le plan de la boutique publique. Le taux est compris entre 0 et 100, avec deux décimales. Le plan doit être actif et son identifiant doit être un UUID. Il est impossible d’ajouter un produit arbitraire ou un plan inactif. L’enregistrement d’un plan actif relève automatiquement le plafond historique du profil si nécessaire. Le plafond ne peut pas être abaissé sous le markup d’un plan actif (MARKUP_BELOW_PRODUCT, 409).

Lors de la migration initiale, les revendeurs déjà ACTIVE reçoivent une sélection activée à 0 % pour chaque plan licencié RETAIL déjà actif. Cela conserve leur ancien catalogue public. Les revendeurs créés après cette migration choisissent leurs plans; un plan ultérieur n’est pas activé automatiquement. Cette initialisation ne vaut pas approbation partenaire et ne convertit pas resellers.commission_rate en marge de vente.

La boutique publique ne présente que les plans actifs sélectionnés. Le checkout avec ref_code est refusé avec STORE_PRODUCT_NOT_LISTED si le plan n’est pas sélectionné. Le serveur détermine le markup depuis la configuration du revendeur; un markup_pct explicite peut le réduire, jamais l’augmenter. Sans ref_code, le catalogue public non revendeur conserve son comportement existant.

Erreurs de gestion : PRODUCT_PLAN_NOT_FOUND (404), STORE_PRODUCT_NOT_LISTED (404), MARKUP_ABOVE_PRODUCT (400), MARKUP_BELOW_PRODUCT (409), REFERRAL_CODE_CONFLICT (400) si referral_code désigne un revendeur différent de ref_code. Une boutique où aucun plan n’est sélectionné renvoie un catalogue vide; ce n’est pas une erreur.

Réponse catalogue 200

{
  "items": [
    {
      "id": "0d5633f8-f322-4fce-9e8a-bf22e101c701",
      "type": "VPN",
      "name": "Licence VPN 30 jours",
      "price": 1200,
      "base_price": 1000,
      "list_price": 1000,
      "retail_price": 1200,
      "markup_amount": 200,
      "markup_percentage": 20,
      "currency": "XOF"
    }
  ],
  "reseller": {
    "code": "EXPERT-2026",
    "pricing_mode": "PER_PRODUCT",
    "markup_ceiling_pct": 20,
    "applied_markup_pct": 20
  }
}

La réponse conserve reseller.applied_markup_pct pour compatibilité; ce champ indique le taux demandé/profil avant plafonnement par article. Le taux effectivement appliqué est markup_percentage sur chaque article. Les consommateurs doivent privilégier les taux par article.

Les erreurs métier sont RESELLER_CODE_REQUIRED (markup sans référence), INVALID_MARKUP_PERCENTAGE (taux hors de 0–100), RESELLER_NOT_FOUND (code absent ou suspendu) et MARKUP_ABOVE_PROFILE (taux demandé supérieur au taux enregistré). Le storefront doit traiter ces réponses comme un échec de cotation et ne pas afficher les prix publics à la place des prix revendeur.

Affichage storefront et transfert au Hub

La page publique est /store. Les liens revendeur utilisent https://store.jmoai.net/store?ref=<promo_code> ; le profil fournit le plafond, et chaque plan sélectionné fournit son taux configuré. Un lien peut ajouter &markup=<taux> pour appliquer un taux inférieur ou égal au plafond du profil. Le storefront charge le catalogue public, demande la cotation revendeur à GET /api/store/catalog, puis affiche retail_price, le prix de base et le taux retournés. Si la cotation revendeur échoue, il masque les articles concernés au lieu de présenter un prix non vérifié.

À l’achat, le storefront passe ref_code=<promo_code> à /api/store/checkout. Cette API transmet partnerId=<reseller_id> au Hub et, si le lien avait un paramètre markup, markupPct=<taux> au Hub de paiement des licences. En l’absence de paramètre, le Hub résout le taux depuis le profil. Le Hub et le webhook recalculent et vérifient les montants côté serveur ; le prix affiché par le navigateur ne fait pas autorité.

Réponse 200

{
  "checkout_url": "https://live.mikhmoai.com/api/v1/payments/hub?productCode=...&planCode=...",
  "final_price": 20880,
  "base_price": 17400,
  "markup_amount": 3480,
  "applied_markup_pct": 20
}

Le client redirige l’acheteur vers checkout_url. Le Hub crée ensuite une transaction PENDING, démarre le prestataire de paiement et attend son webhook authentifié. Seule cette confirmation peut entraîner la délivrance d’une licence.

Garanties et limites

Fournisseur MoneyFusion (SaaS)

MoneyFusion peut être proposé dans le Hub de paiement des licences lorsque MONEYFUSION_HUB_ENABLED=true, MONEYFUSION_API_URL et MONEYFUSION_WEBHOOK_TOKEN sont configurés. L’URL API vient du tableau de bord MoneyFusion. Le serveur transmet le montant autoritaire du plan, la référence locale comme personal_Info.orderId, le return_url et le webhook_url dédié.

Le retour navigateur ne confirme jamais le paiement. Le webhook utilise un jeton secret dans son chemin, puis le serveur interroge l’API MoneyFusion et vérifie le token, la référence locale, le statut distant strictement égal à paid et le montant avant de lancer la livraison de licence. Le montant confirmé Montant doit égaler le prix attendu, ou Montant + frais doit l’égaler lorsque MoneyFusion présente les frais séparément. Les notifications pending, cancelled et les statuts inconnus ne délivrent rien. Les notifications répétées suivent les gardes d’idempotence existants.

MoneyFusion exige aussi que l’IP publique sortante du serveur soit autorisée dans l’application du compte marchand. La documentation consultée ne décrit pas de signature cryptographique du webhook. L’intégration reste désactivée par défaut ; une transaction réelle de bout en bout doit être qualifiée avant son activation en production.

Déviation historique

Avant le 29 août 2026, la route inscrivait à tort une transaction SUCCESS avant toute confirmation du prestataire. Ce comportement est supprimé : le Hub officiel est l’unique parcours de paiement.