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
+68 -4
View File
@@ -9,6 +9,7 @@ from app.models import (
AssignmentSource,
BadgeAlign,
BadgeFieldType,
FeePaymentStatus,
FormFieldType,
PositionCategory,
Role,
@@ -92,6 +93,15 @@ class VolunteerRead(ApiModel):
companion_wish_note: str | None
self_declared_available: bool
willing_to_pay_fee: bool
# Paiement de la cotisation via SumUp (voir CLAUDE.md § 2, `Volunteer`) —
# `fee_payment_label`/`fee_amount` : copie figée du type de paiement
# choisi (pas la config actuelle, qui peut avoir changé depuis).
# `fee_payment_status` reste `None` tant qu'aucun checkout n'a jamais été
# créé pour ce bénévole.
fee_payment_label: str | None
fee_amount: float | None
fee_payment_status: FeePaymentStatus | None
fee_paid_at: dt.datetime | None
created_at: dt.datetime
updated_at: dt.datetime
@@ -493,16 +503,43 @@ class FormTemplateRead(ApiModel):
fields: list[FormFieldType]
has_logo: bool
description: str | None
yapla_url: str | None
sumup_merchant_code: str | None
# `sumup_api_key` (secret) n'est jamais renvoyé — seulement s'il est
# défini, comme `has_logo` pour le logo.
sumup_api_key_set: bool
class FormTemplateUpdate(ApiModel):
"""`None`/absent = inchangé pour chaque champ (`exclude_unset`) — permet
de changer `is_open` sans toucher `description`/`yapla_url`, etc."""
de changer `is_open` sans toucher `description`/`sumup_*`, etc. Chaîne
vide sur `sumup_api_key` = efface le secret enregistré."""
is_open: bool | None = None
description: str | None = None
yapla_url: str | None = None
sumup_merchant_code: str | None = None
sumup_api_key: str | None = None
class FormPaymentOptionRead(ApiModel):
id: uuid.UUID
edition_id: uuid.UUID
label: str
amount: float
order: int
created_at: dt.datetime
updated_at: dt.datetime
class FormPaymentOptionCreate(ApiModel):
label: str
amount: float
order: int = 0
class FormPaymentOptionUpdate(ApiModel):
label: str | None = None
amount: float | None = None
order: int | None = None
class PublicPositionOption(ApiModel):
@@ -516,6 +553,15 @@ class PublicArtistOption(ApiModel):
performance_date: dt.date
class PublicPaymentOption(ApiModel):
"""Type de paiement proposé au bénévole sur le formulaire public — un
montant n'est pas un secret, contrairement aux identifiants SumUp."""
id: uuid.UUID
label: str
amount: float
class PublicFormConfig(ApiModel):
"""Tout ce qu'il faut pour afficher le formulaire public — jamais plus :
ni les bénévoles déjà inscrits, ni les créneaux/affectations de
@@ -527,8 +573,26 @@ class PublicFormConfig(ApiModel):
artists: list[PublicArtistOption]
has_logo: bool
description: str | None
yapla_url: str | None
# `True` si l'édition a configuré SumUp (identifiants marchand + au
# moins un type de paiement) — utilisé par le front-end pour afficher le
# choix de paiement près de `WILLING_TO_PAY_FEE`, sans jamais exposer les
# identifiants eux-mêmes.
sumup_enabled: bool
payment_options: list[PublicPaymentOption]
class FormSubmissionResult(ApiModel):
volunteer_id: uuid.UUID
# Lien de paiement hébergé SumUp, si un checkout a pu être créé (voir
# `app/api/forms.py:submit_public_form`) — `None` si le bénévole n'a
# choisi aucun type de paiement, si SumUp n'est pas configuré, ou en cas
# d'échec de l'appel API (best effort, l'inscription reste valide).
sumup_checkout_url: str | None = None
class SumUpWebhookPayload(ApiModel):
"""Corps envoyé par SumUp à `return_url` — voir la doc SumUp sur les
webhooks. Le seul événement existant à ce jour."""
event_type: str
id: str