- Lien d'inscription protégé par un jeton (FormTemplate.access_token, /inscription/<token>), avec régénération depuis l'écran de configuration. - Reçus fiscaux mécénat (cerfa 11580*05) : un PDF par bénévole ayant payé en mécénat, donateur/montant (chiffres et toutes lettres en français, app/mecenat/number_words.py)/date complétés par-dessus le modèle officiel. - Jeu de démonstration : bénévoles avec cotisation classique ou mécénat payée. - Infobulles (HelpTip et détail de cellule de la grille des besoins) repositionnées en position: fixed via JS, bornées à l'écran. - Revue de sécurité : logo des emails en pièce jointe inline au lieu d'une URL (fuyait le jeton d'inscription), sujet retiré du log de remise des emails (pouvait porter un nom de bénévole), photo du formulaire public revalidée par décodage réel avant stockage, export Excel protégé contre l'injection de formule, sondage du statut de paiement limité en fréquence, pyjwt mis à jour. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
645 lines
20 KiB
Python
645 lines
20 KiB
Python
"""Schémas d'entrée/sortie de l'API CRUD et de la matrice."""
|
||
|
||
import datetime as dt
|
||
import uuid
|
||
|
||
from pydantic import BaseModel, ConfigDict, Field
|
||
|
||
from app.models import (
|
||
AssignmentSource,
|
||
BadgeAlign,
|
||
BadgeFieldType,
|
||
FeePaymentStatus,
|
||
FormFieldType,
|
||
PositionCategory,
|
||
Role,
|
||
)
|
||
from app.validation import Violation
|
||
|
||
|
||
class ApiModel(BaseModel):
|
||
"""Base commune : autorise la construction depuis un objet ORM."""
|
||
|
||
model_config = ConfigDict(from_attributes=True)
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Edition #
|
||
# --------------------------------------------------------------------------- #
|
||
class EditionRead(ApiModel):
|
||
id: uuid.UUID
|
||
year: int
|
||
name: str | None
|
||
start_date: dt.date
|
||
end_date: dt.date
|
||
retention_days: int | None
|
||
created_at: dt.datetime
|
||
updated_at: dt.datetime
|
||
|
||
|
||
class EditionUpdate(ApiModel):
|
||
name: str | None = None
|
||
year: int | None = None
|
||
start_date: dt.date | None = None
|
||
end_date: dt.date | None = None
|
||
retention_days: int | None = None
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Volunteer #
|
||
# --------------------------------------------------------------------------- #
|
||
class VolunteerCreate(ApiModel):
|
||
first_name: str
|
||
last_name: str
|
||
email: str | None = None
|
||
phone: str | None = None
|
||
comment: str | None = None
|
||
# Vœux du bénévole : artistes à voir, postes souhaités, bénévoles avec
|
||
# qui il voudrait être affecté (à sens unique, 0 à N).
|
||
artist_ids: list[uuid.UUID] = Field(default_factory=list)
|
||
wished_position_ids: list[uuid.UUID] = Field(default_factory=list)
|
||
wished_companion_ids: list[uuid.UUID] = Field(default_factory=list)
|
||
|
||
|
||
class VolunteerUpdate(ApiModel):
|
||
first_name: str | None = None
|
||
last_name: str | None = None
|
||
email: str | None = None
|
||
phone: str | None = None
|
||
comment: str | None = None
|
||
# None = inchangé ; [] = vider. Idem pour artist_ids / wished_position_ids
|
||
# / wished_companion_ids.
|
||
artist_ids: list[uuid.UUID] | None = None
|
||
wished_position_ids: list[uuid.UUID] | None = None
|
||
wished_companion_ids: list[uuid.UUID] | None = None
|
||
# Issus du formulaire public (`app/api/forms.py`), éditables ensuite
|
||
# depuis la fiche — voir CLAUDE.md § 2 (Volunteer).
|
||
companion_wish_note: str | None = None
|
||
self_declared_available: bool | None = None
|
||
willing_to_pay_fee: bool | None = None
|
||
|
||
|
||
class VolunteerRead(ApiModel):
|
||
id: uuid.UUID
|
||
edition_id: uuid.UUID
|
||
first_name: str
|
||
last_name: str
|
||
email: str | None
|
||
phone: str | None
|
||
comment: str | None
|
||
artist_ids: list[uuid.UUID]
|
||
wished_position_ids: list[uuid.UUID]
|
||
wished_companion_ids: list[uuid.UUID]
|
||
has_photo: bool
|
||
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
|
||
# Coordonnées pour un reçu fiscal (cerfa) — seulement si payé via un
|
||
# type de paiement « mécénat » (voir `FormPaymentOption.requires_mecenat_info`).
|
||
mecenat_address: str | None
|
||
mecenat_city: str | None
|
||
mecenat_postal_code: str | None
|
||
mecenat_country: str | None
|
||
created_at: dt.datetime
|
||
updated_at: dt.datetime
|
||
|
||
|
||
class VolunteerPlanningEntry(ApiModel):
|
||
"""Un créneau affecté, enrichi (poste, lieu, référent à contacter) — pour
|
||
l'écran bénévole (`GET .../volunteers/{id}/planning`), sans jamais
|
||
exposer la liste complète des postes/créneaux de l'édition à un compte
|
||
VOLUNTEER (lecture restreinte à sa propre fiche, cf. § 2 matrice)."""
|
||
|
||
assignment_id: uuid.UUID
|
||
shift_id: uuid.UUID
|
||
starts_at: dt.datetime
|
||
ends_at: dt.datetime
|
||
position_name: str
|
||
position_location: str | None
|
||
# None si le poste n'a pas (encore) de référent, ou si son compte n'est
|
||
# pas rattaché à une fiche bénévole (pas de téléphone à donner alors).
|
||
referent_name: str | None
|
||
referent_phone: str | None
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Artist (programmation) #
|
||
# --------------------------------------------------------------------------- #
|
||
class ArtistCreate(ApiModel):
|
||
name: str
|
||
performance_date: dt.date
|
||
# end_time <= start_time ⇒ le passage finit le lendemain.
|
||
start_time: dt.time
|
||
end_time: dt.time
|
||
|
||
|
||
class ArtistUpdate(ApiModel):
|
||
name: str | None = None
|
||
performance_date: dt.date | None = None
|
||
start_time: dt.time | None = None
|
||
end_time: dt.time | None = None
|
||
|
||
|
||
class ArtistRead(ApiModel):
|
||
id: uuid.UUID
|
||
edition_id: uuid.UUID
|
||
name: str
|
||
performance_date: dt.date
|
||
start_time: dt.time
|
||
end_time: dt.time
|
||
created_at: dt.datetime
|
||
updated_at: dt.datetime
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Position #
|
||
# --------------------------------------------------------------------------- #
|
||
class PositionCreate(ApiModel):
|
||
name: str
|
||
location: str | None = None
|
||
category: PositionCategory
|
||
lead_account_id: uuid.UUID | None = None
|
||
|
||
|
||
class PositionUpdate(ApiModel):
|
||
name: str | None = None
|
||
location: str | None = None
|
||
category: PositionCategory | None = None
|
||
lead_account_id: uuid.UUID | None = None
|
||
|
||
|
||
class PositionRead(ApiModel):
|
||
id: uuid.UUID
|
||
edition_id: uuid.UUID
|
||
name: str
|
||
location: str | None
|
||
category: PositionCategory
|
||
lead_account_id: uuid.UUID | None
|
||
created_at: dt.datetime
|
||
updated_at: dt.datetime
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Shift #
|
||
# --------------------------------------------------------------------------- #
|
||
class ShiftCreate(ApiModel):
|
||
position_id: uuid.UUID
|
||
starts_at: dt.datetime
|
||
ends_at: dt.datetime
|
||
min_staff: int = Field(default=0, ge=0)
|
||
ideal_staff: int = Field(default=0, ge=0)
|
||
|
||
|
||
class ShiftUpdate(ApiModel):
|
||
position_id: uuid.UUID | None = None
|
||
starts_at: dt.datetime | None = None
|
||
ends_at: dt.datetime | None = None
|
||
min_staff: int | None = Field(default=None, ge=0)
|
||
ideal_staff: int | None = Field(default=None, ge=0)
|
||
|
||
|
||
class ShiftRead(ApiModel):
|
||
id: uuid.UUID
|
||
edition_id: uuid.UUID
|
||
position_id: uuid.UUID
|
||
day_part_id: uuid.UUID | None
|
||
starts_at: dt.datetime
|
||
ends_at: dt.datetime
|
||
min_staff: int
|
||
ideal_staff: int
|
||
created_at: dt.datetime
|
||
updated_at: dt.datetime
|
||
|
||
|
||
class GenerateShiftsResult(ApiModel):
|
||
created: int
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# DayPart (créneau type) #
|
||
# --------------------------------------------------------------------------- #
|
||
class DayPartRead(ApiModel):
|
||
id: uuid.UUID
|
||
edition_id: uuid.UUID
|
||
code: str
|
||
label: str
|
||
start_time: dt.time
|
||
end_time: dt.time
|
||
order: int
|
||
created_at: dt.datetime
|
||
updated_at: dt.datetime
|
||
|
||
|
||
class DayPartCreate(ApiModel):
|
||
label: str
|
||
start_time: dt.time
|
||
end_time: dt.time
|
||
# Déduit du label si absent. Immuable ensuite (référencé par l'import).
|
||
code: str | None = None
|
||
order: int = 0
|
||
|
||
|
||
class DayPartUpdate(ApiModel):
|
||
label: str | None = None
|
||
start_time: dt.time | None = None
|
||
end_time: dt.time | None = None
|
||
order: int | None = None
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Availability (disponibilités déclarées par le bénévole) #
|
||
# --------------------------------------------------------------------------- #
|
||
class AvailabilityWindowIn(ApiModel):
|
||
date: dt.date
|
||
start_time: dt.time
|
||
# end_time <= start_time ⇒ la fenêtre se termine le lendemain.
|
||
end_time: dt.time
|
||
|
||
|
||
class AvailabilitySet(ApiModel):
|
||
"""Remplace l'intégralité des disponibilités d'un bénévole."""
|
||
|
||
windows: list[AvailabilityWindowIn] = Field(default_factory=list)
|
||
|
||
|
||
class AvailabilityRead(ApiModel):
|
||
id: uuid.UUID
|
||
edition_id: uuid.UUID
|
||
volunteer_id: uuid.UUID
|
||
date: dt.date
|
||
start_time: dt.time
|
||
end_time: dt.time
|
||
created_at: dt.datetime
|
||
updated_at: dt.datetime
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Assignment #
|
||
# --------------------------------------------------------------------------- #
|
||
class AssignmentCreate(ApiModel):
|
||
volunteer_id: uuid.UUID
|
||
shift_id: uuid.UUID
|
||
source: AssignmentSource = AssignmentSource.MANUAL
|
||
locked: bool = False
|
||
|
||
|
||
class AssignmentUpdate(ApiModel):
|
||
source: AssignmentSource | None = None
|
||
locked: bool | None = None
|
||
|
||
|
||
class AssignmentRead(ApiModel):
|
||
id: uuid.UUID
|
||
edition_id: uuid.UUID
|
||
volunteer_id: uuid.UUID
|
||
shift_id: uuid.UUID
|
||
source: AssignmentSource
|
||
locked: bool
|
||
created_at: dt.datetime
|
||
updated_at: dt.datetime
|
||
|
||
|
||
class AssignmentWriteResult(ApiModel):
|
||
assignment: AssignmentRead
|
||
warnings: list[Violation] = Field(default_factory=list)
|
||
|
||
|
||
class AssignmentValidateRequest(ApiModel):
|
||
volunteer_id: uuid.UUID
|
||
shift_id: uuid.UUID
|
||
|
||
|
||
class AssignmentValidateResult(ApiModel):
|
||
blocking: bool
|
||
violations: list[Violation] = Field(default_factory=list)
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Solveur (suggestions, CP-SAT) — ne place jamais personne #
|
||
# --------------------------------------------------------------------------- #
|
||
class SolverSuggestion(ApiModel):
|
||
shift_id: uuid.UUID
|
||
# Bénévoles suggérés pour ce créneau, meilleur en tête.
|
||
volunteer_ids: list[uuid.UUID]
|
||
|
||
|
||
class SolverRunResult(ApiModel):
|
||
suggestions: list[SolverSuggestion]
|
||
shifts_considered: int
|
||
volunteers_considered: int
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Matrice poste × créneau #
|
||
# --------------------------------------------------------------------------- #
|
||
class MatrixShift(ApiModel):
|
||
shift_id: uuid.UUID
|
||
starts_at: dt.datetime
|
||
ends_at: dt.datetime
|
||
min_staff: int
|
||
ideal_staff: int
|
||
assigned: int
|
||
|
||
|
||
class MatrixPosition(ApiModel):
|
||
id: uuid.UUID
|
||
name: str
|
||
category: PositionCategory
|
||
location: str | None
|
||
shifts: list[MatrixShift]
|
||
|
||
|
||
class MatrixResponse(ApiModel):
|
||
edition_id: uuid.UUID
|
||
positions: list[MatrixPosition]
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Badges (planches PDF) #
|
||
# --------------------------------------------------------------------------- #
|
||
class BadgeFieldIn(ApiModel):
|
||
field_type: BadgeFieldType
|
||
x_mm: float
|
||
y_mm: float
|
||
width_mm: float
|
||
height_mm: float
|
||
font_size: float = 10.0
|
||
align: BadgeAlign = BadgeAlign.LEFT
|
||
bold: bool = False
|
||
color: str = "#000000"
|
||
|
||
|
||
class BadgeFieldsSet(ApiModel):
|
||
"""Remplace l'intégralité des champs du recto — au plus un par
|
||
`field_type` (décocher un champ dans l'éditeur retire sa ligne)."""
|
||
|
||
fields: list[BadgeFieldIn] = Field(default_factory=list)
|
||
|
||
|
||
class BadgeFieldRead(BadgeFieldIn):
|
||
id: uuid.UUID
|
||
edition_id: uuid.UUID
|
||
|
||
|
||
class BadgeTemplateRead(ApiModel):
|
||
edition_id: uuid.UUID
|
||
has_front_image: bool
|
||
has_back_image: bool
|
||
# Dérivée de l'image du recto (voir `app/badges/pdf.py:image_size_mm`) ;
|
||
# format CR80 par défaut tant qu'aucune image n'est importée.
|
||
badge_width_mm: float
|
||
badge_height_mm: float
|
||
fields: list[BadgeFieldRead]
|
||
|
||
|
||
class BadgeSizeUpdate(ApiModel):
|
||
"""Ajuste la taille du badge indépendamment d'un nouvel import d'image —
|
||
ex. agrandir proportionnellement une image importée trop petite (voir
|
||
`app/api/badges.py:set_size`). Écrase la taille déduite de l'image."""
|
||
|
||
badge_width_mm: float
|
||
badge_height_mm: float
|
||
|
||
|
||
class BadgeGenerateRequest(ApiModel):
|
||
# None/absent = tous les bénévoles de l'édition.
|
||
volunteer_ids: list[uuid.UUID] | None = None
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Feuilles de poste (PDF) #
|
||
# --------------------------------------------------------------------------- #
|
||
class PositionSheetsRequest(ApiModel):
|
||
# None/absent = tous les postes de l'édition.
|
||
position_ids: list[uuid.UUID] | None = None
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Authentification et comptes #
|
||
# --------------------------------------------------------------------------- #
|
||
class MagicLinkRequest(ApiModel):
|
||
email: str
|
||
|
||
|
||
class VerifyRequest(ApiModel):
|
||
token: str
|
||
|
||
|
||
class LoginRequest(ApiModel):
|
||
email: str
|
||
password: str
|
||
|
||
|
||
class ChangePasswordRequest(ApiModel):
|
||
current_password: str
|
||
new_password: str
|
||
|
||
|
||
class AccountRead(ApiModel):
|
||
id: uuid.UUID
|
||
edition_id: uuid.UUID
|
||
email: str
|
||
role: Role
|
||
volunteer_id: uuid.UUID | None
|
||
# Mode de connexion : "password" (admins) ou "magic_link".
|
||
auth_method: str
|
||
created_at: dt.datetime
|
||
updated_at: dt.datetime
|
||
|
||
|
||
class TokenResponse(ApiModel):
|
||
access_token: str
|
||
token_type: str
|
||
account: AccountRead
|
||
|
||
|
||
class AccountCreate(ApiModel):
|
||
email: str
|
||
role: Role
|
||
volunteer_id: uuid.UUID | None = None
|
||
# Requis pour un compte COORDINATION, interdit sinon.
|
||
password: str | None = None
|
||
|
||
|
||
class AccountUpdate(ApiModel):
|
||
# Seul le rôle est modifiable, et seulement entre LEAD et VOLUNTEER (pas
|
||
# de mot de passe en jeu) — voir `api/accounts.py:update_account`.
|
||
role: Role | None = None
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Notifications par email #
|
||
# --------------------------------------------------------------------------- #
|
||
class PlanningNotificationRequest(ApiModel):
|
||
# Modèle saisi par la coordination. Variables : {prenom}, {nom},
|
||
# {nb_creneaux}, {nb_heures}, {creneaux}, {lien_connexion}.
|
||
subject: str | None = None
|
||
body: str | None = None
|
||
# None = tous les bénévoles de l'édition qui ont un email.
|
||
volunteer_ids: list[uuid.UUID] | None = None
|
||
|
||
|
||
class PlanningNotificationResult(ApiModel):
|
||
queued: int
|
||
skipped_no_email: int
|
||
recipients: list[uuid.UUID] = Field(default_factory=list)
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Formulaire public d'inscription #
|
||
# --------------------------------------------------------------------------- #
|
||
class FormFieldsSet(ApiModel):
|
||
"""Remplace l'intégralité des champs activés du formulaire — nom/prénom
|
||
toujours collectés, pas dans cette liste (voir `FormFieldType`)."""
|
||
|
||
fields: list[FormFieldType] = Field(default_factory=list)
|
||
|
||
|
||
class FormTemplateRead(ApiModel):
|
||
edition_id: uuid.UUID
|
||
is_open: bool
|
||
fields: list[FormFieldType]
|
||
has_logo: bool
|
||
description: 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
|
||
# Secret nécessaire pour atteindre le formulaire public
|
||
# (`/inscription/<access_token>`, voir CLAUDE.md § 2, `FormTemplate`) —
|
||
# exposé seulement ici (COORDINATION), jamais par `PublicFormConfig`.
|
||
access_token: str
|
||
|
||
|
||
class FormTemplateUpdate(ApiModel):
|
||
"""`None`/absent = inchangé pour chaque champ (`exclude_unset`) — permet
|
||
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
|
||
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
|
||
requires_mecenat_info: bool
|
||
created_at: dt.datetime
|
||
updated_at: dt.datetime
|
||
|
||
|
||
class FormPaymentOptionCreate(ApiModel):
|
||
label: str
|
||
amount: float
|
||
order: int = 0
|
||
requires_mecenat_info: bool = False
|
||
|
||
|
||
class FormPaymentOptionUpdate(ApiModel):
|
||
label: str | None = None
|
||
amount: float | None = None
|
||
order: int | None = None
|
||
requires_mecenat_info: bool | None = None
|
||
|
||
|
||
class PublicPositionOption(ApiModel):
|
||
id: uuid.UUID
|
||
name: str
|
||
|
||
|
||
class PublicArtistOption(ApiModel):
|
||
id: uuid.UUID
|
||
name: str
|
||
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
|
||
requires_mecenat_info: bool
|
||
|
||
|
||
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
|
||
l'édition (voir `app/api/forms.py:get_public_form`)."""
|
||
|
||
is_open: bool
|
||
fields: list[FormFieldType]
|
||
positions: list[PublicPositionOption]
|
||
artists: list[PublicArtistOption]
|
||
has_logo: bool
|
||
description: 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):
|
||
"""Trois formes possibles, selon le choix de paiement (voir
|
||
`app/api/forms.py:submit_public_form`) :
|
||
- aucun paiement en ligne choisi (case désactivée, ou « je paierai sur
|
||
place ») : `volunteer_id` seul, le bénévole est déjà en base ;
|
||
- paiement en ligne, type « mécénat » : `pending_token` +
|
||
`requires_mecenat_info=True`, rien en base encore — direction l'étape
|
||
d'adresse (`submit_mecenat_info`) ;
|
||
- paiement en ligne, type simple : `pending_token` + `sumup_checkout_url`,
|
||
direction la page de paiement SumUp. Toujours rien en base tant que le
|
||
paiement n'est pas confirmé (voir `PendingFormSubmission`)."""
|
||
|
||
volunteer_id: uuid.UUID | None = None
|
||
pending_token: uuid.UUID | None = None
|
||
requires_mecenat_info: bool = False
|
||
sumup_checkout_url: str | None = None
|
||
|
||
|
||
class MecenatInfoSubmit(ApiModel):
|
||
"""Étape d'adresse postale pour un type de paiement « mécénat »
|
||
(`POST .../form/public/pending/{token}/mecenat-info`) — tous obligatoires,
|
||
nécessaires à l'édition d'un reçu fiscal (cerfa)."""
|
||
|
||
address: str
|
||
city: str
|
||
postal_code: str
|
||
country: str
|
||
|
||
|
||
class PendingSubmissionStatus(ApiModel):
|
||
"""Réponse de `GET .../form/public/pending/{token}` — interrogée en
|
||
direct par la page de confirmation jusqu'à ce que le statut ne soit plus
|
||
`PENDING`. Relit toujours l'état réel auprès de SumUp (jamais seulement
|
||
la dernière valeur connue), comme le webhook."""
|
||
|
||
status: FeePaymentStatus
|
||
label: str
|
||
amount: float
|
||
volunteer_id: uuid.UUID | 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
|