Introduction
Types de remboursements
Remboursement total
"`typescript"`
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-12-18.acacia',
});
async function refundFull(paymentIntentId: string) {
const refund = await stripe.refunds.create({
payment_intent: paymentIntentId,
// Sans montant = remboursement total
});
return refund;
}
Remboursement partiel
"`typescript"`
async function refundPartial(paymentIntentId: string, amountInCents: number) {
const refund = await stripe.refunds.create({
payment_intent: paymentIntentId,
amount: amountInCents, // Ex: 5000 = 50€
});
return refund;
}
Remboursements multiples
"`typescript"`
// Premier remboursement de 30€
await stripe.refunds.create({
payment_intent: 'pi_xxx',
amount: 3000,
});
// Deuxième remboursement de 20€
await stripe.refunds.create({
payment_intent: 'pi_xxx',
amount: 2000,
});
// Total remboursé = 50€
Implémentation complète
Use Case : Annulation de réservation
"`typescript"`
interface CancellationResult {
amountToRefund: number;
amountToKeep: number;
refundPercentage: number;
appliedRule: string;
}
async function processRefund(
reservationId: string,
calculation: CancellationResult
): Promise<Stripe.Refund | null> {
// 1. Récupérer le paiement depuis votre DB
const payment = await db.payment.findFirst({
where: {
reservationId,
status: 'SUCCEEDED',
source: 'STRIPE',
},
});
if (!payment || !payment.stripePaymentIntentId) {
throw new Error('Paiement Stripe non trouvé');
}
// 2. Vérifier qu'il y a quelque chose à rembourser
if (calculation.amountToRefund <= 0) {
console.log('Aucun montant à rembourser');
return null;
}
// 3. Effectuer le remboursement Stripe
const refund = await stripe.refunds.create({
payment_intent: payment.stripePaymentIntentId,
amount: calculation.amountToRefund,
reason: 'requested_by_customer',
metadata: {
reservationId,
appliedRule: calculation.appliedRule,
},
});
// 4. Mettre à jour votre base de données
await db.payment.update({
where: { id: payment.id },
data: {
status: 'REFUNDED',
refundedAmount: calculation.amountToRefund,
},
});
return refund;
}
Règles de remboursement (exemple métier)
"`typescript"`
interface RefundRule {
name: string;
daysBeforeEvent: number;
refundPercentage: number;
}
const REFUND_RULES: RefundRule[] = [
{ name: 'DAYS_30_OR_MORE', daysBeforeEvent: 30, refundPercentage: 100 },
{ name: 'DAYS_21_TO_29', daysBeforeEvent: 21, refundPercentage: 70 },
{ name: 'DAYS_8_TO_20', daysBeforeEvent: 8, refundPercentage: 40 },
{ name: 'DAYS_7_OR_LESS', daysBeforeEvent: 0, refundPercentage: 0 },
];
function calculateRefund(totalPrice: number, eventDate: Date): CancellationResult {
const now = new Date();
const daysUntil = Math.floor(
(eventDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)
);
// Trouver la règle applicable
const rule = REFUND_RULES.find(r => daysUntil >= r.daysBeforeEvent)
?? REFUND_RULES[REFUND_RULES.length - 1];
const amountToRefund = Math.floor(totalPrice * (rule.refundPercentage / 100));
const amountToKeep = totalPrice - amountToRefund;
return {
amountToRefund,
amountToKeep,
refundPercentage: rule.refundPercentage,
appliedRule: rule.name,
};
}
Gérer les webhooks de remboursement
"`typescript"`
async function handleRefundWebhook(event: Stripe.Event) {
if (event.type !== 'charge.refunded') return;
const charge = event.data.object as Stripe.Charge;
// Récupérer les infos du remboursement
const refundAmount = charge.amount_refunded;
const paymentIntentId = charge.payment_intent as string;
// Mettre à jour votre DB
const payment = await db.payment.findFirst({
where: { stripePaymentIntentId: paymentIntentId },
});
if (payment) {
await db.payment.update({
where: { id: payment.id },
data: {
status: 'REFUNDED',
refundedAmount: refundAmount,
},
});
// Notifier le client
await sendRefundConfirmationEmail(payment.userId, refundAmount);
}
}
Gestion des litiges (Disputes)
Qu’est-ce qu’un litige ?
Recevoir les notifications de litige
"`typescript"`
async function handleDisputeWebhook(event: Stripe.Event) {
switch (event.type) {
case 'charge.dispute.created':
await handleDisputeCreated(event.data.object as Stripe.Dispute);
break;
case 'charge.dispute.updated':
await handleDisputeUpdated(event.data.object as Stripe.Dispute);
break;
case 'charge.dispute.closed':
await handleDisputeClosed(event.data.object as Stripe.Dispute);
break;
}
}
async function handleDisputeCreated(dispute: Stripe.Dispute) {
console.log(`Litige créé: ${dispute.id}`);
console.log(`Montant: ${dispute.amount / 100}€`);
console.log(`Raison: ${dispute.reason}`);
// Alerter l'équipe
await sendAlertToTeam({
type: 'DISPUTE_CREATED',
disputeId: dispute.id,
amount: dispute.amount,
reason: dispute.reason,
});
// Mettre à jour le paiement
const chargeId = dispute.charge as string;
await db.payment.updateMany({
where: { stripeChargeId: chargeId },
data: { status: 'DISPUTING' },
});
}
Répondre à un litige
"`typescript""
async function respondToDispute(disputeId: string, evidence: object) {
const dispute = await stripe.disputes.update(disputeId, {
evidence: {
customer_name: 'Jean Dupont',
customer_email_address: '[email protected]',
product_description: 'Réservation voyage Istanbul',
// Documents de preuve
customer_signature: 'file_xxx', // Upload préalable
receipt: 'file_yyy',
// ...
},
submit: true, // Soumettre la réponse
});
return dispute;
}
Types de raisons de litige
| Raison | Description | Action recommandée | |--------|-------------|-------------------| | `fraudulent` | Utilisation frauduleuse | Fournir preuves d'authenticité | | `duplicate` | Double facturation | Prouver l'unicité | | `product_not_received` | Produit non reçu | Preuve de livraison | | `product_unacceptable` | Produit non conforme | Conditions de vente | | `subscription_canceled` | Abo annulé mais facturé | Historique d'annulation | | `unrecognized` | Transaction non reconnue | Détails de la transaction |
Idempotence des remboursements
"`typescript"`
async function safeRefund(
paymentId: string,
amountToRefund: number
): Promise<Stripe.Refund | null> {
// Verrouiller le paiement pour éviter les doublons
const payment = await db.payment.findUnique({
where: { id: paymentId },
});
if (!payment) {
throw new Error('Paiement non trouvé');
}
// Vérifier si déjà remboursé
if (payment.status === 'REFUNDED') {
console.log(`Paiement ${paymentId} déjà remboursé, ignoré`);
return null;
}
// Vérifier le montant restant à rembourser
const remainingAmount = payment.paidAmount - (payment.refundedAmount ?? 0);
if (amountToRefund > remainingAmount) {
throw new Error(`Montant demandé (${amountToRefund}) > restant (${remainingAmount})`);
}
// Effectuer le remboursement avec clé d'idempotence
const refund = await stripe.refunds.create(
{
payment_intent: payment.stripePaymentIntentId!,
amount: amountToRefund,
},
{
idempotencyKey: `refund-${paymentId}-${amountToRefund}`,
}
);
// Mettre à jour la DB
await db.payment.update({
where: { id: paymentId },
data: {
refundedAmount: (payment.refundedAmount ?? 0) + amountToRefund,
status: remainingAmount - amountToRefund === 0 ? 'REFUNDED' : 'PARTIALLY_REFUNDED',
},
});
return refund;
}
Erreurs courantes
Montant trop élevé
"`typescript"`
try {
await stripe.refunds.create({
payment_intent: 'pi_xxx',
amount: 100000, // Plus que le montant original
});
} catch (error) {
if (error.code === 'amount_too_large') {
console.error('Le montant dépasse le montant original');
}
}
Paiement déjà remboursé
"`typescript"`
try {
await stripe.refunds.create({
payment_intent: 'pi_xxx', // Déjà remboursé
});
} catch (error) {
if (error.code === 'charge_already_refunded') {
console.error('Ce paiement a déjà été remboursé');
}
}
Checklist production
Conclusion
📚 Série complète : Maîtriser Stripe avec Node.js et React
-
Chapitre 1 : Configuration initiale Intégrer Stripe dans React/Next.js Premier formulaire de paiement et installation.
-
Chapitre 2 : Automatisation Gérer les webhooks Stripe dans Node.js Recevoir et traiter les événements de paiement en temps réel.
-
Chapitre 3 : Stratégie de Paiement (Précédent) Stripe Checkout vs Elements : Quel choix pour votre projet ? Comparatif complet pour choisir la meilleure UX.
-
Chapitre 4 : Gestion après-vente (Vous êtes ici) Implémenter les remboursements Stripe Gérer les annulations, les remboursements et les litiges.
-
Chapitre 5 : Concepts Avancés (Suivant) PaymentIntent et capture différée Autoriser les paiements et les capturer plus tard.
