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
@@ -0,0 +1,92 @@
|
||||
"""Client de l'API SumUp — paiement de la cotisation (voir CLAUDE.md § 2,
|
||||
`FormTemplate`, et `app/api/forms.py`).
|
||||
|
||||
`create_checkout`/`get_checkout` sont les points d'injection uniques pour les
|
||||
tests (remplacés comme `app.forms.captcha.verify_hcaptcha`). Best effort,
|
||||
comme l'envoi d'emails : toute erreur réseau/HTTP est journalisée (code/
|
||||
classe d'exception seulement, jamais de PII) et renvoyée comme `None` —
|
||||
l'inscription au formulaire public n'échoue jamais pour autant.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
|
||||
import httpx
|
||||
|
||||
logger = logging.getLogger("app.sumup")
|
||||
|
||||
_BASE_URL = "https://api.sumup.com"
|
||||
_TIMEOUT_SECONDS = 10.0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SumUpCheckout:
|
||||
id: str
|
||||
status: str # PENDING / PAID / FAILED / EXPIRED (voir FeePaymentStatus)
|
||||
hosted_checkout_url: str | None
|
||||
|
||||
|
||||
def _call(method: str, path: str, api_key: str, json: dict | None = None) -> dict | None:
|
||||
try:
|
||||
resp = httpx.request(
|
||||
method,
|
||||
f"{_BASE_URL}{path}",
|
||||
json=json,
|
||||
headers={"Authorization": f"Bearer {api_key}"},
|
||||
timeout=_TIMEOUT_SECONDS,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
except httpx.HTTPError as exc:
|
||||
logger.warning(
|
||||
"Échec de l'appel API SumUp (%s %s) : %s", method, path, exc.__class__.__name__
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def _to_checkout(data: dict) -> SumUpCheckout:
|
||||
return SumUpCheckout(
|
||||
id=data["id"],
|
||||
status=data["status"],
|
||||
hosted_checkout_url=data.get("hosted_checkout_url"),
|
||||
)
|
||||
|
||||
|
||||
def create_checkout(
|
||||
*,
|
||||
api_key: str,
|
||||
merchant_code: str,
|
||||
amount: float,
|
||||
currency: str,
|
||||
checkout_reference: str,
|
||||
description: str,
|
||||
return_url: str,
|
||||
redirect_url: str,
|
||||
) -> SumUpCheckout | None:
|
||||
"""Crée un checkout hébergé (`hosted_checkout.enabled=true`) : la page de
|
||||
paiement SumUp est à `hosted_checkout_url`. `None` en cas d'échec (réseau,
|
||||
identifiants invalides…)."""
|
||||
data = _call(
|
||||
"POST",
|
||||
"/v0.1/checkouts",
|
||||
api_key,
|
||||
json={
|
||||
"checkout_reference": checkout_reference,
|
||||
"amount": amount,
|
||||
"currency": currency,
|
||||
"merchant_code": merchant_code,
|
||||
"description": description,
|
||||
"return_url": return_url,
|
||||
"redirect_url": redirect_url,
|
||||
"hosted_checkout": {"enabled": True},
|
||||
},
|
||||
)
|
||||
return _to_checkout(data) if data is not None else None
|
||||
|
||||
|
||||
def get_checkout(*, api_key: str, checkout_id: str) -> SumUpCheckout | None:
|
||||
"""Relit l'état réel d'un checkout — un webhook SumUp ne donne qu'un
|
||||
identifiant, jamais un statut de confiance en lui-même (voir la doc
|
||||
SumUp sur les webhooks et `app/api/forms.py:sumup_webhook`)."""
|
||||
data = _call("GET", f"/v0.1/checkouts/{checkout_id}", api_key)
|
||||
return _to_checkout(data) if data is not None else None
|
||||
Reference in New Issue
Block a user