Contrat API — État de la boutique TiketMOMO (app mobile)
Date : 2026-08-13
Pour : l’app mobile
Endpoint : GET /api/v1/clients/store/tiketmomo/readiness
1. À quoi ça sert
Répondre à une seule question, avant que le gérant ne se déplace : sa boutique peut-elle vendre et livrer, maintenant ?
Sans cet appel, l’app pose le portail, essaie, échoue, recommence — et personne ne sait quelle pièce manque. Cet état donne la liste complète des pièces manquantes en un appel.
2. Quand l’appeler
- à l’ouverture de l’écran boutique ;
- avant de proposer « poser le portail captif » ;
- après chaque action qui change l’état (génération de tickets, publication d’offres).
Ne pas le mettre en cache long : le stock bouge à chaque vente.
3. Réponse
Toujours 200, y compris quand la boutique n’est pas prête — ce n’est pas une erreur d’appel,
c’est un constat.
{
"success": true,
"data": {
"can_sell": false,
"state": "NO_STOCK",
"blocking_reason": "Stock vide : créez des tickets avant d’encaisser, sinon vos clients paieront sans rien recevoir.",
"delivery": {
"possible": false,
"source": "NONE", // STOCK | RADIUS | DIRECT | NONE
"reason": "tunnel disconnected : la frappe directe est impossible",
},
"checks": [
{ "code": "NO_LICENSE", "ok": true, "detail": null, "action": null },
{ "code": "NO_STORE_PROFILE", "ok": true, "detail": null, "action": null },
{ "code": "NO_SLUG", "ok": true, "detail": null, "action": null },
{ "code": "NO_OFFERS", "ok": true, "detail": null, "action": null },
{ "code": "ROUTER_NOT_LINKED", "ok": true, "detail": null, "action": null },
{
"code": "NO_STOCK",
"ok": false,
"detail": "stock vide",
"action": "Stock vide : créez des tickets…",
},
{
"code": "DELIVERY_IMPOSSIBLE",
"ok": false,
"detail": "tunnel disconnected…",
"action": "Stock vide et routeur injoignable…",
},
],
"store_slug": "chez-moussa",
"plan_code": "TIKETMOMO_PRO",
"expires_at": "2027-01-31T23:59:59Z",
"offers_count": 5,
"vouchers_available": 0,
"access_mode": "VPN",
"radius_refill_available": false,
"router": {
"allocation_id": "uuid",
"serial": "HH40AF7YVNP",
"tunnel_status": "DISCONNECTED",
"tunnel_last_seen_at": "2026-08-13T14:02:11Z",
},
},
}
4. La règle d’affichage qui compte
Afficher
checksen entier, pas seulementblocking_reason.
Montrer un problème à la fois est exactement ce qui fait recommencer trois fois : le gérant corrige, réessaie, découvre le suivant, se redéplace. Une liste à cocher, tout de suite, lui évite les allers-retours.
blocking_reason sert au bandeau d’en-tête ; checks[].action remplit la liste. Chaque action
est déjà rédigée en français, prête à afficher — ne pas la reformuler à partir du code.
5. delivery — la seule question qui décide si encaisser est sûr
Une vente encaissée mais non livrable laisse un client payé sans rien recevoir. delivery.source
dit par où le ticket arrivera :
source |
Sens |
|---|---|
STOCK |
des tickets sont déjà en réserve |
RADIUS |
le serveur RADIUS regarnira le stock, sans passer par le routeur |
DIRECT |
le ticket sera frappé sur le MikroTik au moment de la vente |
NONE |
aucune voie — ne pas encaisser |
Un vouchers_available: 0 n’est pas un blocage en soi : avec un tunnel connecté ou une
licence RADIUS, le ticket sera créé à la volée. C’est delivery.possible qui fait foi, pas le
compteur de stock.
6. Les états
state |
Ce que le gérant doit faire |
|---|---|
READY |
rien, la boutique vend |
NO_LICENSE |
aucune licence n’autorise la vente de tickets |
NO_STORE_PROFILE |
ouvrir l’éditeur de portail et enregistrer la boutique une première fois |
NO_SLUG |
choisir le nom public de la boutique |
NO_OFFERS |
ajouter des forfaits, ou générer des tickets (ils servent alors de catalogue) |
ROUTER_NOT_LINKED |
rattacher un routeur |
NO_STOCK |
générer des tickets — aucune autre voie de livraison n’est disponible |
DELIVERY_IMPOSSIBLE |
remettre le routeur en ligne, ou créer du stock |
Les états sont ordonnés par dépendance : on nomme la cause la plus profonde. Inutile de reprocher un stock vide à quelqu’un qui n’a pas encore de boutique.
7. À ne pas faire
- Ne pas vérifier la clé de paiement du marchand ici. Elle n’est pas déposée côté backend : elle est capturée dans le lien de paiement, l’app accompagne le client pour la renseigner, et le compte plateforme prend le relais à défaut. Cet état ne la contrôle pas, volontairement.
- Ne pas déduire « vente impossible » de
vouchers_available: 0(cf. §5). - Ne pas traiter une réponse
can_sell: falsecomme une erreur réseau : c’est un état valide.
8. Le portefeuille — deux populations de vendeurs
GET /api/v1/store/tiketmomo/metrics renvoie, sous withdrawal_status, de quoi construire
l’écran du portefeuille :
"withdrawal_status": {
"available_balance": 0,
"pending_balance": 0,
"minimum_withdrawal": 1000,
"withdrawals_enabled": true, // gel administrateur — indépendant du mode ci-dessous
"payout_mode": "MERCHANT", // "PLATFORM" | "MERCHANT" | "NONE"
"can_request_withdrawal": false,
"payout_provider": "fedapay", // slug de l'agrégateur, ou null si inconnu
"payout_badge": "FedaPay", // libellé prêt à afficher
"payout_dashboard_url": "https://live.fedapay.com", // peut être null
"payout_notice": "Vos ventes sont encaissées directement sur votre compte FedaPay. Le retrait se fait depuis votre tableau de bord FedaPay."
}
Ce que chaque mode veut dire
payout_mode |
L’argent est… | Écran attendu |
|---|---|---|
PLATFORM |
sur notre compte : nous le détenons, nous le lui devons | bouton de retrait normal |
MERCHANT |
déjà chez lui, sur son compte d’agrégateur | badge + renvoi, pas de bouton |
NONE |
aucune vente réussie : rien à trancher encore | bouton normal |
Ventes, solde et comptabilité (jour / semaine / mois / total) s’affichent pour tout le monde, quel que soit le mode. Seule l’action de retrait change.
En mode MERCHANT : remplacer le bouton, ne pas le griser
Un bouton grisé signifie « bloqué par eux » et déclenche un appel au support. Il n’y a rien à
débloquer : l’argent n’est jamais passé chez nous. Affichez payout_badge sur le
portefeuille, et payout_notice à la place du bouton — avec un lien vers
payout_dashboard_url quand il est présent.
Si la demande est tentée quand même, elle est refusée avec un code dédié :
| Code | Sens |
|---|---|
WITHDRAWAL_MERCHANT_ACCOUNT |
encaissement direct — le retrait se fait chez son agrégateur |
INSUFFICIENT_FUNDS |
mode PLATFORM, mais le solde n’atteint pas le minimum |
⚠️ Ne jamais confondre les deux. Un vendeur en encaissement direct a un solde retirable nul chez nous tout en ayant des centaines de ventes réussies : lui répondre « solde insuffisant » lui fait croire que nous retenons son argent.
Le badge n’est pas toujours « FedaPay »
Plusieurs agrégateurs sont pris en charge (FedaPay, Kkiapay, CinetPay, Chario, Flutterwave,
MyCoolPay, Wave, GeniusPay). payout_badge est dérivé de l’agrégateur réellement lu sur les
ventes du marchand : affichez-le tel quel, ne le déduisez pas côté client.
Deux replis à gérer :
payout_provider: null— agrégateur inconnu ou non enregistré.payout_badgevaut alors"Encaissement direct". N’affichez aucun nom d’agrégateur dans ce cas.payout_dashboard_url: null— le portail de cet agrégateur n’est pas encore confirmé chez nous. Affichez le badge sans lien : envoyer un marchand vers un tableau de bord qui n’est pas le sien est pire que ne pas proposer de lien.
Un vendeur qui a basculé d’un mode à l’autre reste PLATFORM tant qu’il existe des ventes
encaissées chez nous : cette part-là lui est due, quoi qu’il encaisse par ailleurs.
9. Ce que cet état ne couvre pas encore
Il ne dit pas si le portail captif est correctement déployé sur le routeur, ni si le walled-garden autorise les hôtes de paiement. Ces deux contrôles exigent d’interroger le routeur, ce qui est incompatible avec un état demandé à chaque ouverture d’écran.
Autrement dit : un READY garantit que la vente et la livraison sont possibles côté plateforme.
Il ne garantit pas encore que le client capté par le hotspot atteindra la page de paiement.