Flux métier
Les workflows critiques du Hub : inscription + paiement, annulation + remboursement, attribution des badges, tâches planifiées et modération.
Inscription, paiement et webhook HelloAsso
Étapes détaillées :
-
Saisie inscription (
app/(inscription)/checkout/) : formulaire capture prénom, nom, email, téléphone (opt), sélection tier tarifaire (solidaire/classique/soutien). -
Validation (
app/api/checkout/route.ts, ligne 31 sqq) :- Rate limit par IP (10 requêtes / 10 min,
lib/rate-limit.ts). - Vérification email unique (pas encore inscrit avec
paymentStatus='paid'). - Vérification capacité restante (
workshop.capacity - count registrations payées). - Vérification tarif valide pour
targetPublicde l'atelier. - Vérification ateliers futurs seulement.
- Rate limit par IP (10 requêtes / 10 min,
-
Création intention de paiement (
createCheckoutIntent()danslib/helloasso.ts) :- Token OAuth2 HelloAsso (credentials flow, cachés en mémoire).
- POST
/v5/checkout-intentsavec détails. - Retour
redirectUrlvalide 15 min.
-
Création Registration :
- Une
RegistrationavecpaymentStatus='pending',helloassoCheckoutIntentId,cancellationTokengénéré. - L'email et le workshop ID sont uniques ensemble (
@@unique([workshopId, email])).
- Une
-
Redirection vers HelloAsso : l'app redéploie vers
redirectUrl. -
Paiement : l'utilisateur paie chez HelloAsso, qui redirige vers
returnUrl?checkoutIntentId=xxx&code=succeeded&orderId=yyy. -
Webhook HelloAsso (
app/api/webhooks/helloasso/route.ts) :- Valide le token en query string (
?token=HELLOASSO_WEBHOOK_TOKEN). - Extrait
checkoutIntentIddedata.checkoutIntentIdoudata.formSlug. - Cherche toutes les
Registrationen attente (paymentStatus='pending') pour ce checkout. - Pour chaque registration :
- Upsert
Participantpar email (CRM). - UPDATE
Registration:paymentStatus='paid', lienparticipantId. - Génère PDF facture.
- Appel Resend pour envoi email confirmation + PDF.
- Upsert
- Idempotence : si
orderIddéjà traité, retour 200 immédiate.
- Valide le token en query string (
Si le webhook est perdu :
- Le participant paie mais reste en
paymentStatus='pending'côté Hub. - Réconciliation quotidienne (job cron, voir LeRunbook) : script
reconcile-registrations.tsqui requête HelloAsso parcheckoutIntentIdstocké, retrouve les ordres et met à jour. - Conséquence : participant n'a pas l'email de confirmation jusqu'au rattrapage.
Annulation et remboursement
Étapes détaillées :
-
Lien annulation : l'email de confirmation contient un lien
https://hub.fresquesystemique.org/api/public/registrations/[cancellationToken]/cancel/(voirlib/email-template-defaults.ts). -
Endpoint annulation (
app/api/public/registrations/[token]/cancel/route.ts) :- Rate limit par IP (10 tentatives / 15 min).
- Trouve
RegistrationparcancellationToken. - Appelle
cancelRegistration(registrationId)(lib/registration-cancellation.ts).
-
Logique décision (
lib/registration-cancellation.ts) :- Lit
cancellationPolicy(lib/cancellation-policy.ts) — délais par événement type. - Calcule si annulation encore possible :
workshop.date - 48h> maintenant ? - Remboursement : délai ok + paiement accepté → appel HelloAsso refund (async).
- Avoir : délai ok + paiement accepté + refund échoue → génère code avantage unique.
- Blocage : délai dépassé → refuse l'annulation (contact admin requis).
- Lit
-
Appel HelloAsso refund :
- Requête HelloAsso
POST /v5/refund(en attente du privilègeRefundManagement). - Actuellement désactivé (
Settings.helloassoRefundEnabled=false) → la refund passe par une notif Telegram admin pour traitement manuel. - Admin doit refunder manuellement dans l'interface HelloAsso, puis marquer en DB.
- Requête HelloAsso
-
Code avantage émis :
DiscountCodecréé avecamountOffTTCégal au montant inscrit,maxUses=1,restrictedToEmaildu participant. -
Mise à jour Registration :
cancelledAt=now(),refundMethod= 'refund' / 'voucher' / 'blocked'.
Gestion de l'atelier annulé :
- Admin peut annuler tout un
Workshop→ tous les inscrits payés sont refundés automatiquement. - Atelier en état
cancelled: inscrits visibles (lecture seule), animateurs non éditables, atelier non modifiable.
Si le refund HelloAsso échoue :
- Notif Telegram admin (via
notifyAdmin()danslib/notifications.ts). - Participant voit un message d'erreur mais inscription reste
paid. - Admin traite manuellement et met à jour le statut en DB.
Attribution des badges
Types de badges (lib/badges.ts) :
| Type | Rôle | Prérequis |
|---|---|---|
ANIMATOR_PUBLIC | Animateur grand public | Animateur habilité grand public |
ANIMATOR_PRO | Animateur professionnel | Animateur avec habilitation professionnelle |
TRAINER_PUBLIC | Formateur grand public | Formateur habilité grand public |
TRAINER_PRO | Formateur professionnel | Formateur avec habilitation professionnelle |
PARTICIPANT | Participant | 1+ atelier suivi |
Émission :
- Par atelier : cron quotidien (après chaque atelier) requête
WorkshopAnimator+ participants ayant assisté. - Par habilitation : admin page
/admin/membersattribuehabilitationAnimation/habilitationFormation→ badges générés en masse. - Fonction centrale :
issueParticipantBadge(memberId, badgeType)qui :- Crée un UUID v7 comme
id. - Génère un JWT EdDSA signé (clé privée de
BADGE_PRIVATE_KEY, voirlib/badge-keys.ts). - Structure VerifiableCredential OB3 : type, recipientEmail, issuedAt, credentialSubject, proof.
- Crée une
BadgeAssertionen DB. - Envoie email avec lien badge public.
- Crée un UUID v7 comme
Format badge JWT (VerifiableCredential OB3) :
{
"iss": "https://hub.fresquesystemique.org",
"sub": "badge-uuid",
"aud": "https://w3c-ccg.github.io/vc-api/",
"exp": ...,
"iat": ...,
"vc": {
"type": ["VerifiableCredential", "OpenBadgeCredential"],
"credentialSubject": {
"type": "AchievementSubject",
"achievement": {
"id": "https://hub.fresquesystemique.org/badges/[type]",
"type": "Achievement",
"name": "Animateur·ice citoyen·ne de la Fresque Systémique",
"criteria": "..."
},
"name": "Prénom Nom"
}
}
}
Vérification (app/verify/page.tsx) : saisie d'un ID badge → affichage avec QR code, lien vérification en ligne.
Les tâches planifiées (crons)
Cron : Rappels J-2 (app/api/cron/reminders/route.ts) :
- Déclenchement : appel quotidien depuis le VPS via
curl -H "Authorization: Bearer $CRON_SECRET"(voir.env.example). - Protection : vérifie
CRON_SECRETen headerAuthorization: Bearer. - Fenêtre : ateliers avec
dateentre maintenant+24h et maintenant+72h. - Traitement :
- Récupère les
Registrationpayées sansreminderSentAt. - Pour chaque : envoie email rappel (template
rappel_atelier) avec date/lieu/horaire en timezone de l'atelier. - UPDATE
reminderSentAt=now()pour dédupliquer (même atelier reste dans la fenêtre plusieurs jours).
- Récupère les
- Gestion du genre : applique le genre du membre pour «Chère/Cher» etc. via
applyGender()(fallback neutre si pas de compte membre). - En cas d'erreur : log et continue (ne bloque pas les autres). Notif Telegram admin si plusieurs erreurs d'envoi.
Cron : Questionnaire satisfaction (LeRunbook, non visible ici) :
- Ateliers terminés (date + duration < maintenant).
- J+1 : génère un
surveyTokenunique pour chaqueRegistration. - Envoie
surveySentAt=now(). - Lien email pointe vers
/satisfaction/[token](page publiqueapp/(fullscreen)/satisfaction/[token]/page.tsx). - Collecte réponses JSON dans
SatisfactionResponse.answers. - Résultats agrégés (NPS, commentaires) visibles sur la fiche atelier pour l'animateur responsable.
Cron : Réconciliation inscriptions (LeRunbook, script reconcile-registrations.ts) :
- Quotidien.
- Récupère toutes les
RegistrationenpaymentStatus='pending'. - Pour chaque, appelle HelloAsso
getCheckoutIntent(helloassoCheckoutIntentId). - Si
checkoutIntent.orderexiste (paiement accepté), récréé le flux webhook (upsert Participant, UPDATE registration, envoi email). - Déduplique sur
helloassoOrderIdpour éviter double-traitement.
Cron : Réconciliation rapports utilisation (LeRunbook, script reconcile-usage-reports.ts) :
- Similaire à ci-dessus pour
UsageReportenpaymentStatus='pending'.
Modération des contenus
Modération :
- Flag global
Settings.workshopModerationOnbascule le mode. - Mode off (défaut) : animateurs publient directement (
status='published'), atelier visible immédiatement. - Mode on : ateliers créés par animateur restent en
draft, admin doit cliquer « Publier » pour changer àpublished. - Endpoint publication :
PUT /api/admin/workshops/[id]/publish(gatingrequireAdmin()). - Ateliers figés : une fois un atelier
cancelled, ses inscrits restent visibles en lecture seule (pour historique).
CRM modération :
- Admin
/admin/participantsmodère les notes et étiquettes des participants (notes internes, tags texte libre). - Anonymisation RGPD : soft delete sur
Participantpuis suppression définitive après délai légal.
Articles modération :
- Admin
/admin/blogcrée articles enpublished=false(draft). - Click « Publier » →
published=true+publishedAt=now(). - Revalidation ISR LeSite sur publication (push de revalidation secret vers LeSite
/api/revalidate).