charge.refunded : un seul type d’événement Stripe couvre à la fois le remboursement total et le remboursement partiel d’un don, avec une seule différence détectable, le montant remboursé comparé au montant initial de la charge. C’est cette nuance, mal gérée initialement, qui a fait qu’un don remboursé à 30 % apparaissait comme intégralement annulé dans le tableau de bord d’une association cliente.
Le tunnel de don, construit sur Stripe Checkout, générait un reçu fiscal Cerfa dès la confirmation du paiement. Le jour où un donateur a demandé un remboursement partiel suite à une erreur de saisie du montant, le webhook a bien été reçu, mais le code traitait tout charge.refunded comme une annulation complète, supprimant le don de la liste des reçus valides au lieu d’en ajuster simplement le montant.
Distinguer partiel et total dans la charge du webhook
L’objet charge transmis par Stripe contient deux champs déterminants : amount, le montant initial, et amount_refunded, le montant cumulé remboursé. Un remboursement est total seulement quand les deux sont strictement égaux.
function determiner_type_remboursement(array $charge): string
{
if ($charge['amount_refunded'] === 0) {
return 'aucun';
}
return $charge['amount_refunded'] === $charge['amount']
? 'total'
: 'partiel';
}
Le test qui rejoue l’événement Stripe
Plutôt que de tester la fonction isolément avec un tableau construit à la main, le test le plus fiable rejoue une charge utile réaliste, proche de ce que Stripe envoie réellement en webhook.

class RemboursementDonTest extends WP_UnitTestCase
{
public function test_un_remboursement_partiel_ajuste_le_montant_sans_annuler_le_don(): void
{
$don_id = self::factory()->post->create(['post_type' => 'don']);
update_post_meta($don_id, '_stripe_charge_id', 'ch_test_123');
update_post_meta($don_id, '_montant_centimes', 5000);
$evenement = [
'type' => 'charge.refunded',
'data' => ['object' => [
'id' => 'ch_test_123',
'amount' => 5000,
'amount_refunded' => 1500,
]],
];
(new GestionnaireWebhookStripe())->traiter($evenement);
$this->assertSame('rembourse_partiellement', get_post_meta($don_id, '_statut', true));
$this->assertSame(3500, (int) get_post_meta($don_id, '_montant_net_centimes', true));
}
}
Vérifier l’impact sur le reçu fiscal
Le point le plus sensible pour un client associatif : le reçu Cerfa déjà émis ne doit jamais être modifié rétroactivement, pour des raisons de conformité documentaire, mais un nouveau don ne doit plus pouvoir être créé sur la base du montant initial non ajusté.
- Le reçu déjà généré reste inchangé, daté et archivé tel quel.
- Le tableau de bord de l’association affiche le montant net réellement conservé, distinct du montant du reçu émis.
- Un export comptable mensuel signale les dons partiellement remboursés dans une colonne dédiée plutôt que de les fondre dans le total.
Couvrir le remboursement total séparément
Le remboursement total mérite son propre test, car le comportement attendu diffère réellement : le don doit alors passer à un statut annule, et une alerte doit prévenir l’équipe association pour vérifier si un reçu fiscal a déjà été transmis au donateur, cas qui nécessite une régularisation manuelle.
public function test_un_remboursement_total_annule_le_don(): void
{
$evenement = [
'type' => 'charge.refunded',
'data' => ['object' => [
'id' => 'ch_test_456',
'amount' => 2000,
'amount_refunded' => 2000,
]],
];
(new GestionnaireWebhookStripe())->traiter($evenement);
$this->assertSame('annule', get_post_meta($this->don_id, '_statut', true));
}
Un webhook Stripe qui porte deux significations sous un seul type d’événement est un piège classique : le test doit explicitement couvrir la frontière entre les deux, pas seulement l’un des deux cas.
En résumé
Un remboursement partiel Stripe n’est pas une simple variante mineure du remboursement total : il exige un traitement métier distinct, un statut différent, et une préservation stricte du reçu fiscal déjà émis. Rejouer l’événement webhook complet dans les tests, plutôt que de tester une fonction isolée, garantit que le code réagit correctement à ce que Stripe envoie réellement, pas à une simplification de confort.