Logs de diagnostic et rapprochement SumUp ↔ bénévole
ci-cd / build-backend (push) Successful in 38s
ci-cd / build-frontend (push) Successful in 20s
ci-cd / deploy (push) Successful in 13s

- Une ligne de log par requête (route modèle, statut, durée, code d'erreur,
  compte), identifiant X-Request-ID repris dans chaque log et affiché à
  l'utilisateur sous une erreur, exceptions non gérées tracées (500 JSON)
- Journal d'accès d'uvicorn coupé : il exposait le jeton du formulaire public
- Logs métier : paiement SumUp, connexion, import, plannings, solveur
- Nom du bénévole dans la description du checkout SumUp
- Code et identifiant de transaction SumUp enregistrés au paiement
  (migration 0023), affichés sur la fiche et dans l'export Excel

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AdamThomas
2026-10-12 18:54:35 +02:00
co-authored by Claude Opus 5.5
parent d722a81a87
commit d0fa1e109c
27 changed files with 798 additions and 16 deletions
+70 -3
View File
@@ -9,6 +9,7 @@ l'inscription au formulaire public n'échoue jamais pour autant.
"""
import logging
import time
from dataclasses import dataclass
import httpx
@@ -24,9 +25,17 @@ class SumUpCheckout:
id: str
status: str # PENDING / PAID / FAILED / EXPIRED (voir FeePaymentStatus)
hosted_checkout_url: str | None
# Transaction qui a réglé le checkout (renseignés par SumUp une fois
# payé, `None` avant) : `transaction_code` est le code court affiché dans
# le tableau de bord SumUp et sur ses reçus (ex. `TEENSK4W2K`) — c'est
# lui que la coordination recherche pour rapprocher un paiement d'un
# bénévole ; `transaction_id` est l'identifiant technique (API).
transaction_code: str | None = None
transaction_id: str | None = None
def _call(method: str, path: str, api_key: str, json: dict | None = None) -> dict | None:
started = time.monotonic()
try:
resp = httpx.request(
method,
@@ -36,19 +45,71 @@ def _call(method: str, path: str, api_key: str, json: dict | None = None) -> dic
timeout=_TIMEOUT_SECONDS,
)
resp.raise_for_status()
return resp.json()
except httpx.HTTPError as exc:
data = resp.json()
except httpx.HTTPStatusError as exc:
# Le corps d'erreur SumUp (`error_code`/`message`, voir leur doc) dit
# pourquoi l'appel est refusé (identifiants invalides, montant,
# référence déjà utilisée…) — il ne contient que des codes, pas de
# données du payeur.
logger.warning(
"Échec de l'appel API SumUp (%s %s) : HTTP %d %s",
method,
path,
exc.response.status_code,
_error_summary(exc.response),
)
return None
except (httpx.HTTPError, ValueError) as exc:
logger.warning(
"Échec de l'appel API SumUp (%s %s) : %s", method, path, exc.__class__.__name__
)
return None
logger.debug(
"API SumUp %s %s : OK en %d ms",
method,
path,
(time.monotonic() - started) * 1000,
)
return data
def _error_summary(resp: httpx.Response) -> str:
try:
body = resp.json()
except ValueError:
return ""
if isinstance(body, list): # SumUp renvoie parfois une liste d'erreurs
body = body[0] if body else {}
if not isinstance(body, dict):
return ""
return " ".join(
str(body[key])[:200] for key in ("error_code", "message", "param") if body.get(key)
)
def _to_checkout(data: dict) -> SumUpCheckout:
transaction_code = data.get("transaction_code")
transaction_id = data.get("transaction_id")
if not transaction_code:
# Selon la version de l'API, seuls les essais de paiement
# (`transactions`) portent le code : on retient celui qui a abouti.
successful = next(
(
tx
for tx in data.get("transactions") or []
if isinstance(tx, dict) and tx.get("status") == "SUCCESSFUL"
),
None,
)
if successful is not None:
transaction_code = successful.get("transaction_code")
transaction_id = transaction_id or successful.get("id")
return SumUpCheckout(
id=data["id"],
status=data["status"],
hosted_checkout_url=data.get("hosted_checkout_url"),
transaction_code=transaction_code,
transaction_id=transaction_id,
)
@@ -65,7 +126,13 @@ def create_checkout(
) -> 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…)."""
identifiants invalides…).
L'API de création n'accepte pas de métadonnées libres : `description`
(« affichée dans les outils SumUp ») et `checkout_reference` sont les
seuls champs du marchand repris sur la transaction — c'est donc par eux
que passe le nom du bénévole (voir `app/api/forms.py:
_checkout_description`)."""
data = _call(
"POST",
"/v0.1/checkouts",