Coupons
POST /admin/coupons
Section titled “POST /admin/coupons”Crée un coupon et ses récompenses sans SQL manuel (remplace le seeding à la main côté BO). Le coupon (coupon) et ses lignes coupon_reward sont écrits dans une seule transaction ⇒ jamais de coupon à moitié configuré.
Le code (id) est sensible à la casse (la rédemption l’est aussi) et doit matcher ^[A-Za-z0-9._-]{1,32}$. Un coupon sans récompense est autorisé (simple compteur d’inscriptions) mais n’accorde rien à la rédemption.
Body (JSON)
| Champ | Type | Requis | Notes |
|---|---|---|---|
id | string | oui | code du coupon, 1..32 chars [A-Za-z0-9._-], casse préservée |
userLimit | int ≥ 0 | non (def 0) | 0 = illimité (la colonne est NOT NULL, pas de NULL possible) |
start | ISO-8601 | null | non | début de validité |
end | ISO-8601 | null | non | fin de validité, doit être ≥ start |
rewards | array | non | liste de { rewardId, value, badgeId? } |
rewards[].rewardId | int | oui (si présent) | doit référencer une ligne du catalogue reward (ex. 1=experience, 2=point) |
rewards[].value | int > 0 | oui (si présent) | montant accordé |
rewards[].badgeId | string(1..6) | null | non | badge optionnel attaché à la récompense |
{ "id": "WELCOME2026", "userLimit": 100, "start": "2026-01-01T00:00:00+00:00", "end": "2026-12-31T23:59:59+00:00", "rewards": [ { "rewardId": 1, "value": 500, "badgeId": null } ]}Réponse (201) — voir le shape commun dans GET /admin/coupons (un seul objet coupon, redeemedCount à 0).
Erreurs
| Status | Body | Sens |
|---|---|---|
400 | { "error": "Field 'id' must match ^[A-Za-z0-9._-]{1,32}$." } | code absent/mal formé |
400 | { "error": "Field 'userLimit' must be an integer >= 0." } | limite invalide |
400 | { "error": "Field 'end' must be greater than or equal to 'start'." } | fenêtre incohérente |
400 | { "error": "rewards[0].rewardId must reference an existing reward." } | reward inconnu |
409 | { "error": "Coupon 'WELCOME2026' already exists." } | code déjà pris |
403 | { "error": "..." } | auth KO |
Exemple curl
curl -s -X POST -H "Authorization: Bearer $ADMIN_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"id":"WELCOME2026","userLimit":100,"rewards":[{"rewardId":1,"value":500}]}' \ http://hydrogen.dev.com/admin/couponsGET /admin/coupons
Section titled “GET /admin/coupons”Liste paginée (keyset) des coupons avec leurs stats d’usage, en remplacement de la requête SQL que l’opérateur reconstruisait à la main. Pour chaque coupon : configuration, compteur de slots dénormalisé (userCount), décompte réel des rédemptions tiré du ledger coupon_user (redeemedCount) et liste des récompenses. Les récompenses de toute la page sont chargées en un appel batch.
userCount(compteur de slots, source de la limite) etredeemedCount(lignes réelles decoupon_user) peuvent légitimement diverger : les deux sont exposés.
Query params
| Param | Valeurs | Défaut | Notes |
|---|---|---|---|
limit | 1..100 | 50 | borné en dur |
cursorAt | ISO-8601 | aucun | created_at du dernier item de la page précédente. À fournir avec cursorId (les deux ou aucun). |
cursorId | code coupon | aucun | id du dernier item — discriminant pour les created_at identiques. |
Tri implicite : created_at DESC, id DESC.
Réponse (200)
{ "items": [ { "id": "WELCOME2026", "userLimit": 100, "isUnlimited": false, // true quand userLimit = 0 "userCount": 12, // slots consommés (compteur dénormalisé) "redeemedCount": 12, // lignes réelles du ledger coupon_user "remaining": 88, // null si illimité, sinon max(0, userLimit - userCount) "start": "2026-01-01T00:00:00+00:00", "end": "2026-12-31T23:59:59+00:00", "createdAt": "2026-01-01T09:00:00+00:00", "rewards": [ { "type": "experience", "value": 500, "badgeId": null } ] } ], "nextCursor": { "at": "2026-01-01T09:00:00+00:00", "id": "WELCOME2026" } // `null` quand la page courante contient < `limit` items (= dernière page)}Erreurs
| Status | Body | Sens |
|---|---|---|
400 | { "error": "Both cursorAt and cursorId must be supplied together." } | une moitié seulement du curseur |
400 | { "error": "cursorAt is not a valid datetime." } | parsing Carbon KO |
403 | { "error": "..." } | auth KO |
GET /admin/coupons/{id}/redemptions
Section titled “GET /admin/coupons/{id}/redemptions”Ledger des rédemptions d’un coupon (coupon_user) : qui l’a consommé et quand, en complément des compteurs agrégés de GET /admin/coupons. Sert l’enquête (fraude, double usage).
Path params
id: code du coupon ([A-Za-z0-9._-]{1,32}, casse préservée).
Query params
| Param | Valeurs | Défaut | Notes |
|---|---|---|---|
limit | 1..100 | 50 | borné en dur |
cursorAt | ISO-8601 | aucun | consumed_at du dernier item précédent. À fournir avec cursorUserId (les deux ou aucun). |
cursorUserId | hex (32 chars) | aucun | id du dernier item — discriminant pour les consumed_at identiques. |
Tri implicite : consumed_at DESC, user_id DESC.
Réponse (200)
{ "items": [ { "userId": "d26d1600cde54bd095e09f8b68ace05f", "consumedAt": "2026-01-02T10:00:00+00:00" } ], "nextCursor": { "at": "2026-01-02T10:00:00+00:00", "userId": "d26d…" }}Erreurs
| Status | Body | Sens |
|---|---|---|
404 | { "error": "Coupon not found." } | code inconnu |
400 | { "error": "Both cursorAt and cursorUserId must be supplied together." } | demi-curseur |
400 | { "error": "cursorUserId is not a valid hex UUID." } | hex malformé |
403 | { "error": "..." } | auth KO |
Exemple curl
curl -s -H "Authorization: Bearer $ADMIN_API_TOKEN" \ http://hydrogen.dev.com/admin/coupons/WELCOME2026/redemptions