Code d'activation (ActivationCode)
En une phrase : la carte-cadeau de road-trip — le titre que le client achète (ou reçoit), présente pour déclencher son voyage, et autour duquel se joue toute la relation commerciale entre Travelise et ses revendeurs.
Rôle métier
Le code d'activation est le produit vendu en amont du voyage : un revendeur (ou Travelise en direct) remet un code à un client ; le client l'utilisera pour activer un road-trip surprise. C'est la première entité « de caisse » du système : elle porte un montant, une devise, des dates, et fonde le suivi des activations, la facturation partenaire et le traitement des litiges (motivation d'origine du journal d'audit).
Deux provenances coexistent, mutuellement exclusives :
- Généré — émis par lot depuis la plateforme :
préfixe-suffixe, le préfixe identifiant le lot (ex.RESELLER-A), le suffixe étant aléatoire. Un lot partage montant, devise, durée, description — et porte la date d'expiration que ses codes suivent. Toute génération crée un lot, même pour un seul code. - Hérité — importé de l'ancien système à l'identique (les clients détiennent encore ces codes sur des cartes physiques) : chaîne arbitraire, sans structure, jamais rattachée à un lot.
Le full_code est l'identité orientée client — la chaîne exacte imprimée sur la carte — et est unique sur toute la plateforme.
Cycle de vie — un statut calculé, jamais stocké
Le statut est dérivé en temps réel des timestamps (et de l'horloge), pour qu'aucune tâche planifiée ne soit nécessaire pour « faire expirer » des codes :
| Statut | Sens métier |
|---|---|
in stock | Émis, encore chez le revendeur — pas encore vendu |
purchased | Acheté par un client (purchased_at est le marqueur canonique ; l'assignation à un client est un acte distinct) |
offered | Offert en cadeau par son propriétaire — en attente que le destinataire le réclame en saisissant le code complet |
pre-active | Activation planifiée : used_at (début de la fenêtre d'activation) est dans le futur ; annulable tant que la fenêtre n'a pas commencé |
active | Fenêtre d'activation en cours (used_at passé, duration heures) — le voyage est en cours |
used | Consommé, fenêtre écoulée — le voyage a eu lieu : sa timeline et ses souvenirs restent lisibles pour toujours (voir Routage d'étape) |
expired | Date limite dépassée sans consommation |
pending-extension | Code expiré dont la demande d'extension est ouverte (postdate l'octroi et le refus) — une demande ouverte sur un code encore valide ne change pas son statut ni son utilisabilité |
pending-reimbursement | Demande de remboursement en attente d'une décision admin — un refus (avec motif) rend le code à son état naturel et à l'usage |
cancelled | Annulé (litige, erreur d'émission) |
reimbursed | Remboursé — l'état terminal qui prime sur tous les autres |
Priorité de lecture : remboursé > annulé > attente remboursement > pré-actif > actif > utilisé > attente extension > expiré > offert > acheté > en stock. Deux conséquences de cet ordre valent d'être connues : un statut posé après coup (annulation, remboursement) masque used sans effacer le voyage vécu, et un code sans duration — la colonne est nullable, les imports hérités n'en portent pas — saute la branche active et se lit used dès la première seconde de sa fenêtre. Ce qui dépend du voyage réellement vécu se lit donc sur used_at, pas sur le statut. Les transitions admin se font par groupe avec succès partiel : les codes non éligibles (déjà annulés, etc.) sont ignorés avec une raison, pas bloquants. Les intentions voyageur (activer/planifier, offrir, réclamer, demander remboursement ou extension) sont des endpoints mono-code en verbes explicites — gardés atomiquement dans l'UPDATE lui-même. Offrir et réclamer refusent en 422 avec, en plus du message lisible, un code reason stable et machine-interprétable (énum ActivationCodeRejectionReason, ex. already_claimed, expired) — le frontend branche sur ce code, jamais sur le texte ; l'énum par endpoint est documentée dans le contrat.
Activer, c'est choisir son voyage : le voyageur désigne un tour active, sa date de départ et la taille de son groupe (traveller_count, obligatoire) d'un même geste — used_at, tour_id et traveller_count s'écrivent atomiquement dans le même UPDATE gardé, et l'annulation d'une planification les efface ensemble (une réactivation peut choisir un autre tour ou un autre groupe ; un code annulé puis offert ne transmet pas un choix périmé). Le groupe alimente les agrégats d'occupation de Capacité & saturation ; un traveller_count NULL (activation héritée) y compte pour un voyageur.
Cadeau et claim : offrir un code (gifted_at) le gèle jusqu'au claim ; le destinataire n'a pas d'identifiant, le code complet fait office de référence. Le même geste de claim importe aussi un code encore en stock (le claim vaut alors achat). Le claim efface gifted_at, comme l'annulation d'une planification efface used_at et tour_id — les seuls retours à NULL du cycle de vie, chacun défaisant une intention avant qu'elle ne se réalise — si bien qu'un cadeau réclamé peut être offert à nouveau.
Demandes et résolutions : l'extension se demande sur un code acheté ou expiré (une seule demande ouverte à la fois) ; une demande ouverte sur un code valide n'entrave jamais son usage — activer, offrir, réclamer restent possibles, et l'activation ne dépense pas la demande. Les approbations n'ont pas d'endpoints dédiés : prolonger le lot ou renouveler le code approuve l'extension en tamponnant l'octroi ; rembourser ou annuler résout la demande de remboursement (priorité de cascade). Les refus, eux, ont leurs endpoints super-admin, avec un motif obligatoire visible sur le code et tracé dans l'audit ; rien ne s'efface — une nouvelle demande rouvre, un nouveau refus remplace le motif. Les listings se filtrent sur ces demandes via les booléens pending_extension (toute demande d'extension ouverte, y compris sur un code encore valide) et pending_reimbursement (équivaut au statut pending-reimbursement).
Expiration — le lot expire, les grâces s'accumulent : un code généré naît sans date propre et suit la date de son lot — dans les deux sens, un admin qui déplace la date du lot déplace tout le stock. La date effective d'un code est GREATEST(date du code, date du lot) : un renouvellement individuel (purchased + nouvelle expiration) pose un plancher personnel que rien ne raccourcit — tuer un code précis est le rôle de l'annulation, qui prime sur l'expiration. Un renouvellement pur dont la date ne dépasse pas la date effective courante est ignoré avec une raison (no-op sous GREATEST) ; le renouvellement s'applique aussi aux codes encore in stock — prolonger la durée de vie d'un stock invendu — sans les marquer achetés : purchased_at reste vide et le code reste (ou redevient, s'il était expiré) en stock.
Relations métier
| Relation | Sens métier |
|---|---|
→ Lot (batch_id) | Le lot de génération dont le code suit l'expiration ; NULL pour les codes hérités. Le code meurt avec son lot |
| → Organisation émettrice | Le partenaire dont c'est le chiffre d'affaires et le stock ; repli sur Travelise si non désignée. Le code meurt avec son émettrice |
→ Client (customer_id) | La personne à qui le code est assigné ; le code survit à la suppression du compte (l'historique commercial prime) |
→ Tour (tour_id) | Le voyage choisi à l'activation ; vide tant que le code n'est pas activé, effacé si la planification est annulée |
| ← Routages d'étape | Le parcours réellement vécu, étape par étape — la mémoire du voyage, qui meurt avec le code |
| ← Journal d'audit | Chaque génération de lot et chaque transition de statut est tracée — c'est la pièce à conviction des litiges B2B |
Règles métier
- Le voyageur agit sur ses propres codes, par intentions : activer maintenant ou planifier (et annuler une planification tant qu'elle n'a pas commencé), offrir, réclamer un code par sa chaîne complète, demander un remboursement ou une extension. L'émission reste réservée au super-admin, les transitions groupées à l'organisation émettrice ou au super-admin — l'autorisation d'un groupe est tout-ou-rien. La demande d'extension est aussi ouverte à l'organisation émettrice (un revendeur demande pour son client) ; un admin peut rembourser sans demande préalable (divergence assumée avec le diagramme produit, souplesse opérationnelle) et refuser une demande (extension ou remboursement) avec un motif obligatoire.
- Chaque lot expire : toute génération pose une date limite sur le lot (fournie, ou un an par défaut) ; les codes la suivent tant qu'ils n'ont pas de plancher individuel plus tardif. Seuls des codes hérités (sans lot) peuvent n'avoir aucune date.
- Notes internes aux deux niveaux, enrichissement, pas d'écrasement : le lot porte le contexte de campagne, le code des consignes ponctuelles. Les deux sont réservées à l'équipe (super-admins) — jamais aux voyageurs, comme pour les prestataires.
- La visibilité suit la casquette : un revendeur ne voit que ses codes, un client que les siens, le super-admin tout.
- L'import de l'ancien système est tout-ou-rien par requête et peut antidater
created_atpour préserver l'historique.
Détail technique (invariants SQL, vue de statut, génération par lot) : Base de données — activation_codes.

