Skip to content

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)

ChampTypeRequisNotes
idstringouicode du coupon, 1..32 chars [A-Za-z0-9._-], casse préservée
userLimitint ≥ 0non (def 0)0 = illimité (la colonne est NOT NULL, pas de NULL possible)
startISO-8601 | nullnondébut de validité
endISO-8601 | nullnonfin de validité, doit être ≥ start
rewardsarraynonliste de { rewardId, value, badgeId? }
rewards[].rewardIdintoui (si présent)doit référencer une ligne du catalogue reward (ex. 1=experience, 2=point)
rewards[].valueint > 0oui (si présent)montant accordé
rewards[].badgeIdstring(1..6) | nullnonbadge 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

StatusBodySens
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

Terminal window
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/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) et redeemedCount (lignes réelles de coupon_user) peuvent légitimement diverger : les deux sont exposés.

Query params

ParamValeursDéfautNotes
limit1..10050borné en dur
cursorAtISO-8601aucuncreated_at du dernier item de la page précédente. À fournir avec cursorId (les deux ou aucun).
cursorIdcode couponaucunid 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

StatusBodySens
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

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

ParamValeursDéfautNotes
limit1..10050borné en dur
cursorAtISO-8601aucunconsumed_at du dernier item précédent. À fournir avec cursorUserId (les deux ou aucun).
cursorUserIdhex (32 chars)aucunid 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

StatusBodySens
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

Terminal window
curl -s -H "Authorization: Bearer $ADMIN_API_TOKEN" \
http://hydrogen.dev.com/admin/coupons/WELCOME2026/redemptions