Formulaire public : paiement de la cotisation via l'API SumUp (remplace Yapla)
ci-cd / build-backend (push) Failing after 4m12s
ci-cd / build-frontend (push) Skipped
ci-cd / deploy (push) Skipped

Remplace le simple lien Yapla statique par une vraie intégration API SumUp :
checkout hébergé créé à la soumission du formulaire, statut de paiement
(PENDING/PAID/FAILED/EXPIRED) tenu à jour par un webhook SumUp et visible sur
la fiche du bénévole. La cotisation n'est plus un montant unique : la
coordination configure autant de types de paiement qu'elle veut (libellé +
montant, ex. « Cotisation classique », « Mécénat »), et le bénévole en
choisit un à l'inscription — le libellé et le montant choisis sont figés sur
le bénévole, indépendamment d'une modification ultérieure de la config.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
AdamThomas
2026-09-29 19:56:04 +02:00
co-authored by Claude Sonnet 5
parent 0be5daa89c
commit 0bd38a6465
25 changed files with 1457 additions and 125 deletions
+60 -10
View File
@@ -80,9 +80,11 @@ les tables sauf les tables d'association.
forcément encore en base au moment de l'inscription, donc pas de lien
direct vers un `VolunteerCompanionWish` ; la coordination la résout à la
main depuis la fiche une fois les deux bénévoles inscrits),
`self_declared_available`/`willing_to_pay_fee` (cases à cocher **purement
informatives**, sans lien avec `Availability` ni aucun workflow de
paiement).
`self_declared_available` (case à cocher **purement informative**, sans
lien avec `Availability`) et `willing_to_pay_fee` (« je participe à la
cotisation » — déclenche, lui, un vrai paiement : voir `fee_payment_label`/
`fee_amount`/`fee_checkout_id`/`fee_payment_status`/`fee_paid_at` et le
paiement SumUp plus bas).
- **`Artist`** : programmation de l'édition (`name`, `performance_date`,
`start_time`, `end_time` — `end_time <= start_time` ⇒ le passage finit le
lendemain). Unique (`edition_id`, `name`, `performance_date`). La
@@ -194,7 +196,22 @@ les tables sauf les tables d'association.
affectation n'apparaît pas (rien à imprimer).
- **`FormTemplate`** : configuration du formulaire public d'inscription
d'une édition (une ligne, créée à la demande). `is_open` coupe les
nouvelles soumissions sans effacer la configuration.
nouvelles soumissions sans effacer la configuration. `sumup_merchant_code`/
`sumup_api_key` sont les identifiants du compte marchand SumUp pour le
paiement de la cotisation (`app/sumup/`) — `sumup_api_key` est un secret,
jamais renvoyé par l'API (`FormTemplateRead.sumup_api_key_set: bool` à la
place ; chaîne vide en écriture ⇒ l'efface).
- **`FormPaymentOption`** : un type de paiement de la cotisation (ex.
« Cotisation classique », « Mécénat ») — `label` + `amount` (montant
fixe), `order`. Liste libre par édition, autant que la coordination veut
(comme les créneaux types `DayPart`), gérée depuis l'écran « Inscriptions »
(bloc « Types de paiement »). CRUD `GET/POST/PATCH/DELETE
/editions/{id}/form/payment-options` (COORDINATION). Unique
(`edition_id`, `label`) — 409 `form.duplicate_payment_option_label` sinon ;
montant `> 0` requis — 422 `form.invalid_sumup_amount` sinon. Le bénévole
en choisit au plus un à l'inscription (voir `Paiement de la cotisation`
plus bas) ; supprimer une option n'affecte jamais les bénévoles qui
l'avaient déjà choisie (copie figée sur `Volunteer`).
- **`FormField`** : un champ activé sur le formulaire (`field_type` ∈
`EMAIL` / `PHONE` / `POSITION_WISHES` / `ARTIST_WISHES` / `COMPANION_WISH`
/ `AVAILABLE` / `WILLING_TO_PAY_FEE` / `PHOTO` / `COMMENT`, au plus un par
@@ -205,14 +222,45 @@ les tables sauf les tables d'association.
limite) : au plus **deux** vœux de poste, et au plus **un** artiste
souhaité par jour de festival (`app/api/forms.py:_check_one_artist_wish_per_day`).
`GET/PUT/PATCH /editions/{id}/form[...]` (COORDINATION) configurent ;
`GET /editions/{id}/form/public` et `POST .../form/public/submit` sont les
**deux seuls endpoints de toute l'API sans authentification** — protégés
par hCaptcha (`app/forms/captcha.py`, `HCAPTCHA_SECRET_KEY` ; vérification
`GET /editions/{id}/form/public`, `POST .../form/public/submit` et
`POST .../form/public/sumup-webhook` sont les **trois seuls endpoints de
toute l'API sans authentification** — les deux premiers protégés par
hCaptcha (`app/forms/captcha.py`, `HCAPTCHA_SECRET_KEY` ; vérification
désactivée si absente, comme `EMAIL_ENABLED=false`) et un filet de
sécurité global par édition (`app/auth/rate_limit.py:form_submission_limiter`,
hCaptcha reste la défense principale). Le bénévole créé par une soumission
apparaît immédiatement dans la liste de la coordination, comme n'importe
quel autre bénévole.
hCaptcha reste la défense principale) ; le webhook est appelé par SumUp
lui-même, donc sans hCaptcha possible — voir plus bas. Le bénévole créé
par une soumission apparaît immédiatement dans la liste de la
coordination, comme n'importe quel autre bénévole.
- **Paiement de la cotisation (SumUp)** : si le bénévole choisit un type de
paiement (`FormPaymentOption`, ci-dessus — le formulaire public
n'affiche ce choix, à la place de la case `WILLING_TO_PAY_FEE` toute
seule, que si `sumup_enabled` sur `PublicFormConfig` est vrai, c.-à-d.
identifiants SumUp renseignés **et** au moins un type de paiement
configuré ; sinon la case reste une simple déclaration, comme avant SumUp)
et que l'édition a configuré ses identifiants, la soumission crée un
checkout hébergé (`app/sumup/client.py:create_checkout`, API SumUp
`POST /v0.1/checkouts`, `hosted_checkout.enabled=true`, montant et
libellé de l'option choisie) et renvoie son URL de paiement dans
`FormSubmissionResult.sumup_checkout_url` (le bénévole y est redirigé
juste après son inscription — pas avant, contrairement à l'ancien lien
Yapla statique). Le libellé et le montant de l'option choisie sont
recopiés sur `Volunteer.fee_payment_label`/`fee_amount` **au moment du
choix** — une copie figée, jamais recalculée si l'option est éditée ou
supprimée ensuite (`FormPaymentOption` reste une pure configuration,
jamais une référence vivante depuis l'historique des bénévoles).
`Volunteer.fee_checkout_id` (identifiant SumUp, jamais exposé) et
`fee_payment_status` (`PENDING`/`PAID`/`FAILED`/`EXPIRED`, `NULL` tant
qu'aucun checkout n'a été créé) suivent l'état ; `fee_paid_at` est posé à
la première confirmation `PAID`. Best effort, comme l'envoi d'emails : un
échec de l'appel API SumUp (réseau, identifiants invalides…) ne bloque
jamais l'inscription. Le statut est tenu à jour par
`POST .../form/public/sumup-webhook` (`event_type:
CHECKOUT_STATUS_CHANGED`, `id`) : le payload n'étant qu'un identifiant,
l'app relit toujours l'état réel via l'API (`get_checkout`) avant de
l'appliquer, jamais fait confiance au seul contenu du webhook. Visible par
la coordination sur la fiche du bénévole (`ConfigVolunteers.tsx`, libellé
+ montant + statut), en lecture seule (pas de bascule manuelle).
Conventions techniques :
@@ -537,6 +585,7 @@ l'étape 21.)*
│ │ ├── badges/ # génération PDF des planches (pdf.py, ReportLab)
│ │ ├── sheets/ # génération PDF des feuilles de poste (pdf.py)
│ │ ├── forms/ # hCaptcha (captcha.py) — voir api/forms.py
│ │ ├── sumup/ # client API SumUp (client.py) — paiement de la cotisation
│ │ ├── bootstrap.py # instance vierge : édition + 1er compte
│ │ ├── seed.py # jeu de démonstration (python -m app.seed)
│ │ ├── i18n/
@@ -651,6 +700,7 @@ Aucune dépendance locale requise hors Docker.
| 22 | Feuilles de poste (PDF, complément des badges) : une page par poste et par jour avec le référent, les créneaux, les bénévoles affectés et leur téléphone — bouton depuis la grille des besoins (COORDINATION) ; photo du bénévole affichée sur son écran et sur l'écran référent (en face du nom de chaque membre de l'équipe) | à valider |
| 23 | Créneaux : plus de génération automatique à la création d'un poste — création manuelle à horaire libre (`ConfigGrid.tsx`), génération depuis les créneaux types conservée en option (« Générer les créneaux ») ; grille des besoins redessinée en vrai axe des temps (une colonne = 30 min, bornée à la plage utilisée par jour, cases fusionnées pour chaque créneau — `buildTimeGrid`/`placeInTimeGrid`) | à valider |
| 24 | Corrections de stabilité de la grille des besoins (suite à l'étape 23) : positionnement explicite (`gridColumn`/`gridRow`) de chaque cellule (un poste aux créneaux non alignés sur 30 min ne décale plus les postes suivants) ; hiérarchie de `z-index` sans égalité entre les en-têtes figés et isolation de l'infobulle de survol dans son propre contexte d'empilement (ne recouvrait plus la ligne « Affectés / idéal » pendant un défilement) | à valider |
| 25 | Paiement de la cotisation via l'API SumUp (remplace le lien statique Yapla) : plusieurs types de paiement configurables par édition (`FormPaymentOption` — libellé + montant, ex. « Cotisation classique », « Mécénat »), le bénévole en choisit un à l'inscription, checkout hébergé créé à la soumission si l'édition a configuré ses identifiants SumUp, lien de paiement affiché juste après l'inscription, statut (`PENDING`/`PAID`/`FAILED`/`EXPIRED`) tenu à jour par un webhook SumUp et visible (libellé + montant + statut) sur la fiche du bénévole | à valider |
**Reste hors périmètre** (modèle prêt à les accueillir) : SMS, constructeur
de formulaire, pointage sur site, IA. Frontend : squelette + 6 écrans +