Inscription en deux temps, emails de confirmation et export Excel des bénévoles
Formulaire public — paiement en ligne : - Le bénévole n'est plus compté en base tant qu'un paiement en ligne choisi n'est pas confirmé (PendingFormSubmission, promu en Volunteer à la confirmation, webhook ou vérification en direct). - Type de paiement « mécénat » : étape d'adresse postale avant le paiement, conservée sur le bénévole pour un reçu fiscal (cerfa) ultérieur. - Page de confirmation au retour de SumUp, avec relance possible après échec/expiration sans ressaisir le formulaire. - « Je paierai sur place » remplace « je ne participe pas » ; tous les champs activés par la coordination sont désormais obligatoires. - La fiche bénévole n'affiche plus l'état des cases à cocher, seulement le résultat réel du paiement (libellé, montant, statut). Emails : - Email de confirmation d'inscription à la création du bénévole (tient lieu de reçu de paiement quand une cotisation est payée en ligne). - Mise en forme HTML au thème de l'application (logo de l'édition en en-tête) pour tous les emails envoyés, texte brut conservé en repli. Export Excel des bénévoles (COORDINATION) : coordonnées, vœux résolus en noms lisibles, cotisation et mécénat. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
68d42ee303
commit
a2b748566b
@@ -211,6 +211,15 @@ class Volunteer(TimestampMixin, table=True):
|
||||
sa_column=Column(SAEnum(FeePaymentStatus, native_enum=False), nullable=True),
|
||||
)
|
||||
fee_paid_at: dt.datetime | None = None
|
||||
# Coordonnées collectées uniquement pour un type de paiement « mécénat »
|
||||
# (`FormPaymentOption.requires_mecenat_info`), avant le paiement (voir
|
||||
# `PendingFormSubmission` et `app/api/forms.py:submit_mecenat_info`) —
|
||||
# conservées pour générer un reçu fiscal (cerfa) ultérieurement. `None`
|
||||
# pour un bénévole n'ayant pas payé via une option de mécénat.
|
||||
mecenat_address: str | None = None
|
||||
mecenat_city: str | None = None
|
||||
mecenat_postal_code: str | None = None
|
||||
mecenat_country: str | None = None
|
||||
|
||||
wished_artists: list[Artist] = Relationship(
|
||||
back_populates="fans",
|
||||
@@ -545,6 +554,93 @@ class FormPaymentOption(TimestampMixin, table=True):
|
||||
default=0,
|
||||
sa_column=Column(Integer, nullable=False, server_default="0"),
|
||||
)
|
||||
# Type « mécénat » : avant le paiement, le bénévole passe par une étape
|
||||
# supplémentaire (adresse postale) pour permettre l'édition d'un reçu
|
||||
# fiscal (cerfa) — voir `Volunteer.mecenat_*` et
|
||||
# `app/api/forms.py:submit_mecenat_info`.
|
||||
requires_mecenat_info: bool = Field(
|
||||
default=False,
|
||||
sa_column=Column(Boolean, nullable=False, server_default="false"),
|
||||
)
|
||||
|
||||
|
||||
class PendingFormSubmission(TimestampMixin, table=True):
|
||||
"""Inscription en attente de paiement en ligne (voir CLAUDE.md § 2,
|
||||
`Paiement de la cotisation`). Créée à la place d'un `Volunteer` quand le
|
||||
bénévole choisit un type de paiement en ligne (`FormPaymentOption`) sur
|
||||
le formulaire public — le `Volunteer` n'est créé qu'à la confirmation du
|
||||
paiement (`app/api/forms.py:_promote_pending`), pour ne jamais compter en
|
||||
base quelqu'un qui a rempli le formulaire sans aller au bout du
|
||||
paiement.
|
||||
|
||||
Reprend les champs du formulaire (identiques à `Volunteer`, en plus
|
||||
volatil) ; `position_wish_ids_json`/`artist_wish_ids_json` : listes
|
||||
JSON stringifiées (pas d'association SQL pour une ligne éphémère), leurs
|
||||
UUID sont résolus (silencieusement filtrés si un poste/artiste a
|
||||
entretemps disparu — le paiement ne doit jamais échouer pour ça) au
|
||||
moment de la promotion en `Volunteer`.
|
||||
|
||||
`id` sert aussi de jeton public (`GET .../form/public/pending/{id}`,
|
||||
aucune authentification) : un UUID v4 est déjà pratiquement impossible
|
||||
à deviner, comme un identifiant de session de paiement chez la plupart
|
||||
des prestataires.
|
||||
|
||||
`payment_label`/`payment_amount`/`requires_mecenat_info` sont une copie
|
||||
figée de l'option choisie (même principe que `Volunteer.fee_payment_label`)
|
||||
— indépendante d'une modification de `FormPaymentOption` pendant que le
|
||||
paiement est en cours. `checkout_id`/`checkout_status` suivent le
|
||||
checkout SumUp *courant* : relancés (nouvel identifiant) à chaque
|
||||
nouvelle tentative après un échec/une expiration
|
||||
(`app/api/forms.py:retry_pending_payment`), sans jamais perdre les
|
||||
informations déjà saisies.
|
||||
|
||||
`promoted_volunteer_id` : posé au moment du paiement confirmé (`PAID`) —
|
||||
le `Volunteer` créé. Les champs nominatifs sont alors vidés (RGPD :
|
||||
la même information vit désormais uniquement sur `Volunteer`), seuls
|
||||
les champs de suivi du paiement restent. Une ligne jamais promue est
|
||||
purgée après un délai (voir `_prune_stale_pending`)."""
|
||||
|
||||
__tablename__ = "pending_form_submission"
|
||||
|
||||
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
||||
edition_id: uuid.UUID = Field(
|
||||
foreign_key="edition.id", index=True, ondelete="CASCADE"
|
||||
)
|
||||
|
||||
first_name: str | None = None
|
||||
last_name: str | None = None
|
||||
email: str | None = None
|
||||
phone: str | None = None
|
||||
comment: str | None = None
|
||||
companion_wish_note: str | None = None
|
||||
available: bool = Field(
|
||||
default=False,
|
||||
sa_column=Column(Boolean, nullable=False, server_default="false"),
|
||||
)
|
||||
position_wish_ids_json: str | None = None
|
||||
artist_wish_ids_json: str | None = None
|
||||
photo: bytes | None = Field(default=None, sa_column=Column(LargeBinary))
|
||||
photo_content_type: str | None = None
|
||||
|
||||
payment_label: str
|
||||
payment_amount: float
|
||||
requires_mecenat_info: bool = Field(
|
||||
default=False,
|
||||
sa_column=Column(Boolean, nullable=False, server_default="false"),
|
||||
)
|
||||
mecenat_address: str | None = None
|
||||
mecenat_city: str | None = None
|
||||
mecenat_postal_code: str | None = None
|
||||
mecenat_country: str | None = None
|
||||
|
||||
checkout_id: str | None = Field(default=None, index=True)
|
||||
checkout_status: FeePaymentStatus | None = Field(
|
||||
default=None,
|
||||
sa_column=Column(SAEnum(FeePaymentStatus, native_enum=False), nullable=True),
|
||||
)
|
||||
promoted_volunteer_id: uuid.UUID | None = Field(
|
||||
default=None, foreign_key="volunteer.id", ondelete="SET NULL"
|
||||
)
|
||||
|
||||
|
||||
class FormField(SQLModel, table=True):
|
||||
|
||||
Reference in New Issue
Block a user