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
+113 -3
View File
@@ -718,6 +718,11 @@ async def submit_public_form(
set_artists(session, volunteer, artist_ids)
session.commit()
session.refresh(volunteer)
logger.info(
"Formulaire public : bénévole %s créé (édition %s, sans paiement en ligne)",
volunteer.id,
edition.id,
)
if volunteer.email:
subject, body = render_registration_confirmation(first_name=volunteer.first_name)
background.add_task(
@@ -758,6 +763,14 @@ async def submit_public_form(
session.add(pending)
session.commit()
session.refresh(pending)
logger.info(
"Formulaire public : inscription en attente de paiement %s (édition %s, "
"montant %.2f, mécénat=%s)",
pending.id,
edition.id,
pending.payment_amount,
pending.requires_mecenat_info,
)
if pending.requires_mecenat_info:
return FormSubmissionResult(pending_token=pending.id, requires_mecenat_info=True)
@@ -783,19 +796,53 @@ def _create_pending_checkout(
amount=pending.payment_amount,
currency="EUR",
checkout_reference=f"{pending.id}-{uuid.uuid4().hex[:8]}",
description=f"{pending.payment_label} — édition {edition.year}",
description=_checkout_description(edition, pending),
return_url=f"{base}/api/editions/{edition.id}/form/public/sumup-webhook",
redirect_url=f"{base}/inscription/{template.access_token}?paiement={pending.id}",
)
if checkout is None:
logger.warning(
"Checkout SumUp non créé pour l'inscription en attente %s (édition %s) — "
"le bénévole devra réessayer",
pending.id,
edition.id,
)
return None
pending.checkout_id = checkout.id
pending.checkout_status = FeePaymentStatus.PENDING
session.add(pending)
session.commit()
logger.info(
"Checkout SumUp %s créé pour l'inscription en attente %s (édition %s)",
checkout.id,
pending.id,
edition.id,
)
if not checkout.hosted_checkout_url:
logger.warning(
"Checkout SumUp %s sans hosted_checkout_url — le paiement hébergé "
"est-il activé sur le compte marchand ?",
checkout.id,
)
return checkout.hosted_checkout_url
# Longueur prudente : la doc SumUp ne fixe pas de maximum pour `description`.
_CHECKOUT_DESCRIPTION_MAX = 255
def _checkout_description(edition: Edition, pending: PendingFormSubmission) -> str:
"""Libellé de la transaction côté SumUp (tableau de bord, exports) : le
nom du bénévole en tête, pour qu'un paiement s'y rapproche d'un coup
d'œil de la bonne personne — l'API de création de checkout n'a pas de
champ de métadonnées libre (voir `app/sumup/client.py:create_checkout`).
Donnée transmise au prestataire de paiement, pas un log : la règle RGPD
de `app/logging_config.py` ne s'applique pas ici."""
name = " ".join(p for p in (pending.first_name, pending.last_name) if p)
parts = [name, pending.payment_label, f"édition {edition.year}"]
return " — ".join(p for p in parts if p)[:_CHECKOUT_DESCRIPTION_MAX]
def _promote_pending(
session: Session,
edition: Edition,
@@ -824,6 +871,8 @@ def _promote_pending(
fee_checkout_id=checkout.id,
fee_payment_status=FeePaymentStatus.PAID,
fee_paid_at=local_now(),
fee_transaction_code=checkout.transaction_code,
fee_transaction_id=checkout.transaction_id,
mecenat_address=pending.mecenat_address,
mecenat_city=pending.mecenat_city,
mecenat_postal_code=pending.mecenat_postal_code,
@@ -866,6 +915,22 @@ def _promote_pending(
pending.mecenat_country = None
session.add(pending)
session.commit()
logger.info(
"Paiement confirmé : inscription en attente %s → bénévole %s (édition %s, "
"checkout %s, transaction %s, reçu mécénat %s)",
pending.id,
volunteer.id,
edition.id,
checkout.id,
checkout.transaction_code or "inconnue",
volunteer.mecenat_receipt_number or "-",
)
if not checkout.transaction_code:
logger.warning(
"Checkout SumUp %s payé sans code de transaction renvoyé — rapprochement "
"à faire depuis le tableau de bord SumUp",
checkout.id,
)
if volunteer.email:
# Paiement « mécénat » (adresse postale saisie) : le cerfa rempli part
@@ -906,7 +971,10 @@ def _send_paid_confirmation(
pdf = generate_mecenat_receipts_pdf([receipt])
attachments.append((pdf, "application/pdf", RECEIPT_FILENAME))
except Exception:
logger.exception("Reçu fiscal mécénat non généré — email envoyé sans pièce jointe")
logger.exception(
"Reçu fiscal mécénat n° %s non généré — email envoyé sans pièce jointe",
receipt.number or "?",
)
send(to, subject, body, logo=logo, attachments=attachments)
@@ -925,10 +993,21 @@ def _apply_pending_checkout_result(
try:
new_status = FeePaymentStatus(checkout.status)
except ValueError:
logger.warning(
"Checkout SumUp %s : statut inconnu %r ignoré", checkout.id, checkout.status
)
return
if new_status == FeePaymentStatus.PAID:
_promote_pending(session, edition, pending, checkout, background)
else:
if new_status != pending.checkout_status:
logger.info(
"Checkout SumUp %s (inscription en attente %s) : %s → %s",
checkout.id,
pending.id,
pending.checkout_status,
new_status,
)
pending.checkout_status = new_status
session.add(pending)
session.commit()
@@ -1043,6 +1122,12 @@ def retry_pending_payment(
)
template = _get_or_create_template(session, edition.id)
logger.info(
"Relance du paiement de l'inscription en attente %s (édition %s, statut %s)",
pending.id,
edition.id,
pending.checkout_status,
)
checkout_url = _create_pending_checkout(session, edition, template, pending)
return FormSubmissionResult(pending_token=pending.id, sumup_checkout_url=checkout_url)
@@ -1059,13 +1144,23 @@ def sumup_webhook(
identifiant : jamais fait confiance sans relire l'état réel via l'API
(voir `app/sumup/client.py:get_checkout`). Répond toujours `200`, y
compris sur un événement inconnu — SumUp réessaie sinon inutilement."""
logger.info(
"Webhook SumUp reçu (édition %s, événement %s, checkout %s)",
edition.id,
payload.event_type,
payload.id,
)
template = _get_or_create_template(session, edition.id)
if not template.sumup_api_key:
logger.warning(
"Webhook SumUp ignoré : aucune clé API SumUp configurée (édition %s)",
edition.id,
)
return Response(status_code=status.HTTP_200_OK)
checkout = get_checkout(api_key=template.sumup_api_key, checkout_id=payload.id)
if checkout is None:
logger.warning("Webhook SumUp : relecture du checkout impossible.")
logger.warning("Webhook SumUp : relecture du checkout %s impossible.", payload.id)
return Response(status_code=status.HTTP_200_OK)
pending = session.exec(
@@ -1094,6 +1189,21 @@ def sumup_webhook(
volunteer.fee_payment_status = new_status
if new_status == FeePaymentStatus.PAID and volunteer.fee_paid_at is None:
volunteer.fee_paid_at = local_now()
if checkout.transaction_code and not volunteer.fee_transaction_code:
volunteer.fee_transaction_code = checkout.transaction_code
volunteer.fee_transaction_id = checkout.transaction_id
session.add(volunteer)
session.commit()
logger.info(
"Webhook SumUp : bénévole %s (checkout %s) → %s",
volunteer.id,
payload.id,
new_status,
)
return Response(status_code=status.HTTP_200_OK)
logger.warning(
"Webhook SumUp : checkout %s inconnu dans l'édition %s (inscription purgée ?)",
payload.id,
edition.id,
)
return Response(status_code=status.HTTP_200_OK)