Formulaire public : paiement de la cotisation via l'API SumUp (remplace Yapla)
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:
co-authored by
Claude Sonnet 5
parent
0be5daa89c
commit
0bd38a6465
@@ -15,6 +15,7 @@ from app.models.domain import (
|
||||
DayPart,
|
||||
Edition,
|
||||
FormField,
|
||||
FormPaymentOption,
|
||||
FormTemplate,
|
||||
MagicLinkToken,
|
||||
Position,
|
||||
@@ -28,6 +29,7 @@ from app.models.enums import (
|
||||
AssignmentSource,
|
||||
BadgeAlign,
|
||||
BadgeFieldType,
|
||||
FeePaymentStatus,
|
||||
FormFieldType,
|
||||
PositionCategory,
|
||||
Role,
|
||||
@@ -45,8 +47,10 @@ __all__ = [
|
||||
"BadgeTemplate",
|
||||
"DayPart",
|
||||
"Edition",
|
||||
"FeePaymentStatus",
|
||||
"FormField",
|
||||
"FormFieldType",
|
||||
"FormPaymentOption",
|
||||
"FormTemplate",
|
||||
"MagicLinkToken",
|
||||
"Position",
|
||||
|
||||
@@ -18,6 +18,7 @@ from app.models.enums import (
|
||||
AssignmentSource,
|
||||
BadgeAlign,
|
||||
BadgeFieldType,
|
||||
FeePaymentStatus,
|
||||
FormFieldType,
|
||||
PositionCategory,
|
||||
Role,
|
||||
@@ -192,6 +193,24 @@ class Volunteer(TimestampMixin, table=True):
|
||||
default=False,
|
||||
sa_column=Column(Boolean, nullable=False, server_default="false"),
|
||||
)
|
||||
# Paiement de la cotisation via l'API SumUp (`app/sumup/`, voir
|
||||
# `app/api/forms.py:submit_public_form`/`sumup_webhook`) — créé si le
|
||||
# bénévole choisit un type de paiement (`FormPaymentOption`) et que
|
||||
# l'édition a configuré SumUp (`FormTemplate.sumup_*`). `fee_payment_label`/
|
||||
# `fee_amount` sont une copie figée du libellé/montant de l'option
|
||||
# choisie au moment du choix (jamais recalculés si l'option est éditée
|
||||
# ou supprimée ensuite). `fee_checkout_id` (identifiant SumUp du
|
||||
# checkout) sert seulement à faire le lien depuis le webhook ; jamais
|
||||
# exposé (voir `app/api/volunteers.py:_read`). `fee_payment_status` reste
|
||||
# `NULL` tant qu'aucun checkout n'a été créé.
|
||||
fee_payment_label: str | None = None
|
||||
fee_amount: float | None = None
|
||||
fee_checkout_id: str | None = Field(default=None, index=True)
|
||||
fee_payment_status: FeePaymentStatus | None = Field(
|
||||
default=None,
|
||||
sa_column=Column(SAEnum(FeePaymentStatus, native_enum=False), nullable=True),
|
||||
)
|
||||
fee_paid_at: dt.datetime | None = None
|
||||
|
||||
wished_artists: list[Artist] = Relationship(
|
||||
back_populates="fans",
|
||||
@@ -467,10 +486,12 @@ class FormTemplate(TimestampMixin, table=True):
|
||||
"""Configuration du formulaire public d'une édition. Une ligne par
|
||||
édition (créée à la demande). `is_open` coupe les nouvelles soumissions
|
||||
sans effacer la configuration (les champs activés restent enregistrés).
|
||||
`logo`/`logo_content_type`, `description` et `yapla_url` habillent la
|
||||
page publique (`/inscription`) — aucun rapport avec les champs collectés
|
||||
par `FormField` ; stockage en base comme les autres images de l'appli
|
||||
(badges, photo de bénévole), pas de volume de fichiers à gérer."""
|
||||
`logo`/`logo_content_type` et `description` habillent la page publique
|
||||
(`/inscription`) — aucun rapport avec les champs collectés par
|
||||
`FormField` ; stockage en base comme les autres images de l'appli
|
||||
(badges, photo de bénévole), pas de volume de fichiers à gérer.
|
||||
`sumup_*` configurent le paiement de la cotisation via l'API SumUp (voir
|
||||
`app/sumup/`)."""
|
||||
|
||||
__tablename__ = "form_template"
|
||||
|
||||
@@ -485,12 +506,45 @@ class FormTemplate(TimestampMixin, table=True):
|
||||
logo: bytes | None = Field(default=None, sa_column=Column(LargeBinary))
|
||||
logo_content_type: str | None = None
|
||||
description: str | None = None
|
||||
# Lien vers la page de cotisation Yapla (yapla.com, plateforme externe de
|
||||
# paiement pour associations) — encart affiché sur `/inscription` près de
|
||||
# la case « je participe à la cotisation » (`FormFieldType.
|
||||
# WILLING_TO_PAY_FEE`). Rien n'est vérifié côté appli : la case reste
|
||||
# une déclaration, le paiement se passe entièrement chez Yapla.
|
||||
yapla_url: str | None = None
|
||||
# Paiement de la cotisation via l'API SumUp (`app/sumup/`) : un checkout
|
||||
# est créé à la soumission si le bénévole choisit un des types de
|
||||
# paiement configurés (`FormPaymentOption`, voir plus bas) et que ces
|
||||
# deux identifiants sont renseignés (voir `app/api/forms.py`,
|
||||
# `sumup_enabled` sur `PublicFormConfig`). `sumup_api_key` n'est jamais
|
||||
# renvoyé par l'API (voir `FormTemplateRead.sumup_api_key_set`) — c'est
|
||||
# un secret, au même titre qu'un mot de passe SMTP.
|
||||
sumup_merchant_code: str | None = None
|
||||
sumup_api_key: str | None = None
|
||||
|
||||
|
||||
class FormPaymentOption(TimestampMixin, table=True):
|
||||
"""Un type de paiement proposé sur le formulaire public (ex. « Cotisation
|
||||
classique », « Mécénat ») : libellé + montant fixe, configurés librement
|
||||
par la coordination (autant qu'elle veut, comme les créneaux types
|
||||
`DayPart`) — voir `app/api/forms.py`. Le bénévole en choisit au plus un
|
||||
à l'inscription ; le libellé et le montant réellement proposés sont
|
||||
recopiés sur `Volunteer` au moment de la création du checkout
|
||||
(`fee_payment_label`/`fee_amount`), pour ne jamais changer
|
||||
rétroactivement si cette configuration est éditée ou supprimée plus
|
||||
tard."""
|
||||
|
||||
__tablename__ = "form_payment_option"
|
||||
__table_args__ = (
|
||||
UniqueConstraint(
|
||||
"edition_id", "label", name="uq_form_payment_option_edition_label"
|
||||
),
|
||||
)
|
||||
|
||||
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
||||
edition_id: uuid.UUID = Field(
|
||||
foreign_key="edition.id", index=True, ondelete="CASCADE"
|
||||
)
|
||||
label: str
|
||||
amount: float
|
||||
order: int = Field(
|
||||
default=0,
|
||||
sa_column=Column(Integer, nullable=False, server_default="0"),
|
||||
)
|
||||
|
||||
|
||||
class FormField(SQLModel, table=True):
|
||||
|
||||
@@ -45,6 +45,18 @@ class BadgeAlign(enum.StrEnum):
|
||||
RIGHT = "RIGHT"
|
||||
|
||||
|
||||
class FeePaymentStatus(enum.StrEnum):
|
||||
"""Statut d'un paiement de cotisation via l'API SumUp (voir `app/sumup/`
|
||||
et `app/api/forms.py`). Reprend tel quel le statut d'un checkout SumUp —
|
||||
`NULL` sur `Volunteer.fee_payment_status` tant qu'aucun checkout n'a
|
||||
jamais été créé pour ce bénévole."""
|
||||
|
||||
PENDING = "PENDING"
|
||||
PAID = "PAID"
|
||||
FAILED = "FAILED"
|
||||
EXPIRED = "EXPIRED"
|
||||
|
||||
|
||||
class FormFieldType(enum.StrEnum):
|
||||
"""Champ activable sur le formulaire public d'inscription (voir
|
||||
`app/api/forms.py`). Nom/prénom sont toujours collectés, pas au choix de
|
||||
|
||||
Reference in New Issue
Block a user