Coupons
Système de codes promotionnels saisis manuellement par l’administration dans la table hxa.coupon. Aucune API d’administration ne les expose — Hydrogen se contente de les consommer quand un utilisateur en fournit un.
Schéma des tables
Section titled “Schéma des tables”hxa.coupon(existante, non gérée par Hydrogen) :id VARCHAR(32)— code lisible saisi par l’admin (PK).user_limit INT UNSIGNED— plafond d’utilisations.0= illimité.start DATETIME NULL,end DATETIME NULL— fenêtre de validité optionnelle.user_count INT UNSIGNED— compteur courant, bumpé atomiquement par Hydrogen à chaque rédemption.
hxa.reward(existante) : catalogue des types de récompense. À ce jour :experience,point. Seulexperienceest actuellement appliqué ;pointest silencieusement ignoré jusqu’à ce que le domaine “points” existe.hxa.coupon_reward(existante) : paramétrage(coupon_id, reward_id, value, badge_id?). Plusieurs rewards par coupon possibles.badge_idest lu mais ignoré côté Hydrogen.hxa.coupon_user(créée par Hydrogen — migration2026_06_10_160000_create_coupon_user_table.sql) : registre append-only des rédemptions, PK composite(coupon_id, user_id)qui garantit “1 fois par user, à jamais”.
Règles de rédemption
Section titled “Règles de rédemption”Dans une transaction unique (CouponRedemptionService) :
- Lookup du coupon par
id. Inexistant →coupon.notFound. - Vérification de
start(si renseigné) etend(si renseigné) contrenow. Hors fenêtre →coupon.notStartedoucoupon.expired. - INSERT dans
coupon_user. Collision PK →coupon.alreadyRedeemed(cet utilisateur a déjà consommé ce coupon). UPDATE coupon SET user_count = user_count + 1 WHERE id = ? AND (user_limit = 0 OR user_count < user_limit). SirowCount = 0→coupon.userLimitReached. Le check est atomique et race-safe.- Lecture de
coupon_reward(joint avecreward) et application :reward.type = 'experience'avecvalue > 0→ ExperienceService::award (rejoint la transaction en cours, pas de double-XP possible).reward.type = 'point'→ no-op silencieux.- Type inconnu → no-op silencieux (forward-compat : un admin peut ajouter un type avant le déploiement du code correspondant).
Toute exception en cours de route déclenche un rollBack complet : pas de demi-rédemption.
Codes de résultat
Section titled “Codes de résultat”| Code | Sens |
|---|---|
coupon.notFound | Le couponId ne correspond à aucune ligne de coupon. |
coupon.notStarted | start est dans le futur. |
coupon.expired | end est dans le passé. |
coupon.userLimitReached | user_count a atteint user_limit (cap strict). |
coupon.alreadyRedeemed | Cet utilisateur a déjà utilisé ce coupon. |
coupon.applyFailed | Erreur technique inattendue (rollback) — à journaliser côté ops. |
Utilisation à l’inscription
Section titled “Utilisation à l’inscription”L’unique point d’entrée actuel est le champ optionnel couponId de POST /api/auth/register. Toute défaillance est non bloquante : la réponse 201 contient meta.coupon = { applied, couponId, reason?, message?, rewards? }. Le client est libre d’afficher un message d’info ou de proposer une nouvelle saisie post-inscription (endpoint dédié non implémenté à ce jour).