PROMPT : Gestion des Webhooks Stripe
Contexte
Implémenter la gestion robuste des webhooks Stripe pour synchroniser en temps réel les événements de paiement avec la logique métier Upiik (tokens, abonnements, notifications).
Prérequis
✅ Configuration Stripe (01-config-initiale.md)
✅ Architecture domaine (02-architecture-domaine.md)
✅ Modèles de données (03-models-donnees.md)
✅ Routes API (04-routes-api.md)
Tâche : Implémenter les webhooks Stripe
1. Service de gestion des webhooks
Fichier : src/domains/payments/services/webhookService.js
Fonctionnalités principales :
class WebhookService {
// Validation et parsing des événements
async validateAndParseEvent(rawBody, signature) {
// Valider signature avec STRIPE_WEBHOOK_SECRET
// Parser l'événement Stripe
// Vérifier l'idempotence (éviter double traitement)
// Retourner événement validé
}
// Dispatcher principal
async processEvent(event) {
// Switch sur event.type
// Router vers le bon handler
// Gestion d'erreurs et retry
// Logging détaillé
}
// Handlers spécifiques pour chaque type d'événement
async handleCustomerSubscriptionCreated(subscription) {
// Activer l'abonnement en DB
// Mettre à jour tokens utilisateur
// Déclencher notifications
}
async handleCustomerSubscriptionUpdated(subscription) {
// Synchroniser changements (plan, statut)
// Ajuster tokens si changement de plan
}
async handleCustomerSubscriptionDeleted(subscription) {
// Désactiver abonnement
// Retour au plan gratuit (1 échange/mois)
// Notification utilisateur
}
async handleInvoicePaymentSucceeded(invoice) {
// Enregistrer paiement en DB
// Envoyer email de confirmation
// Renouveler période d'abonnement
}
async handleInvoicePaymentFailed(invoice) {
// Enregistrer échec en DB
// Notification utilisateur
// Éventuellement suspendre abonnement
}
async handleSetupIntentSucceeded(setupIntent) {
// Confirmer ajout méthode de paiement
// Mettre à jour customer
}
}
2. Gestion de l'idempotence
Système anti-doublons :
// Table/Collection pour tracking événements traités
const WebhookEvent = {
stripeEventId: String, // evt_xyz unique
eventType: String, // type d'événement
processedAt: Date, // Date de traitement
status: String, // 'processed' | 'failed' | 'retrying'
attempts: Number, // Nombre de tentatives
lastError: String, // Dernière erreur si échec
metadata: Object, // Données additionnelles
}
// Dans validateAndParseEvent :
const existingEvent = await WebhookEvent.findOne({ stripeEventId: event.id })
if (existingEvent && existingEvent.status === 'processed') {
// Événement déjà traité, ignorer
return { alreadyProcessed: true }
}
3. Intégration avec le système de tokens Upiik
Logique d'activation des abonnements :
async handleSubscriptionActivation(stripeSubscription) {
// 1. Identifier l'utilisateur
const customer = await StripeCustomer.findOne({
stripeCustomerId: stripeSubscription.customer
});
if (!customer) {
throw new Error('Customer not found in Upiik DB');
}
// 2. Déterminer le type de plan
const planType = this.determinePlanType(stripeSubscription);
// 3. Activer tokens illimités
await User.findByIdAndUpdate(customer.userId, {
'subscription.isActive': true,
'subscription.type': planType,
'subscription.tokensRemaining': -1, // -1 = illimité
'subscription.renewsAt': new Date(stripeSubscription.current_period_end * 1000)
});
// 4. Créer/Mettre à jour l'abonnement en DB
await Subscription.findOneAndUpdate(
{ stripeSubscriptionId: stripeSubscription.id },
{
userId: customer.userId,
stripeCustomerId: customer.stripeCustomerId,
planType,
status: stripeSubscription.status,
currentPeriodStart: new Date(stripeSubscription.current_period_start * 1000),
currentPeriodEnd: new Date(stripeSubscription.current_period_end * 1000),
tokensGranted: { unlimited: true }
},
{ upsert: true }
);
// 5. Déclencher événements internes Upiik
eventEmitter.emit('subscription.activated', {
userId: customer.userId,
planType,
subscriptionId: stripeSubscription.id
});
}
4. Intégration avec le système d'événements Upiik
Fichier : src/domains/payments/events/paymentEvents.js
Événements à déclencher :
// Événements pour le système Upiik
const paymentEvents = {
SUBSCRIPTION_ACTIVATED: 'subscription.activated',
SUBSCRIPTION_CANCELLED: 'subscription.cancelled',
PAYMENT_SUCCEEDED: 'payment.succeeded',
PAYMENT_FAILED: 'payment.failed',
TOKENS_RENEWED: 'tokens.renewed',
}
// Handlers dans src/events/subscribers/
class SubscriptionSubscriber {
async onSubscriptionActivated({ userId, planType }) {
// Envoyer email de bienvenue
// Notifier via push notification
// Logger l'activation
}
async onSubscriptionCancelled({ userId, endDate }) {
// Email de confirmation annulation
// Programmer retour plan gratuit
// Proposer feedback
}
async onPaymentFailed({ userId, reason, amount }) {
// Email d'alerte paiement échoué
// Notification push urgente
// Proposer mise à jour méthode paiement
}
}
5. Gestion des notifications utilisateur
Intégration avec Nodemailer existant :
// Utiliser le système email d'Upiik
const { sendEmail } = require('../../../utils/emailService'); // Adapter chemin
async sendSubscriptionConfirmation(userId, planType) {
const user = await User.findById(userId);
await sendEmail({
to: user.email,
template: 'subscription-activated', // Template à créer
data: {
firstName: user.firstName,
planType,
planPrice: planType === 'monthly' ? '4€' : '30€',
tokensInfo: 'Échanges illimités'
},
language: user.preferredLanguage || 'fr'
});
}
// Intégration push notifications existantes
const { sendPushNotification } = require('../../../services/pushService');
async sendPaymentFailedNotification(userId) {
await sendPushNotification(userId, {
title: 'Problème de paiement',
body: 'Votre paiement a échoué. Vérifiez votre méthode de paiement.',
data: { type: 'payment_failed', action: 'open_payment_settings' }
});
}
6. Système de retry et gestion d'erreurs
Retry automatique pour échecs temporaires :
class WebhookRetryService {
async processWithRetry(event, maxRetries = 3) {
let lastError
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await this.processEvent(event)
// Succès : marquer comme traité
await WebhookEvent.updateOne(
{ stripeEventId: event.id },
{
status: 'processed',
processedAt: new Date(),
attempts: attempt,
}
)
return { success: true }
} catch (error) {
lastError = error
// Logger la tentative échouée
webhookLogger.warn(`Webhook retry ${attempt}/${maxRetries}`, {
eventId: event.id,
eventType: event.type,
error: error.message,
})
// Attendre avant retry (backoff exponentiel)
if (attempt < maxRetries) {
await this.delay(Math.pow(2, attempt) * 1000) // 2s, 4s, 8s
}
}
}
// Échec définitif après tous les retries
await WebhookEvent.updateOne(
{ stripeEventId: event.id },
{
status: 'failed',
attempts: maxRetries,
lastError: lastError.message,
}
)
// Alerter les développeurs
webhookLogger.error('Webhook processing failed permanently', {
eventId: event.id,
eventType: event.type,
error: lastError.message,
})
}
}
7. Monitoring et alerting
Métriques importantes à tracker :
// Dashboard metrics
const webhookMetrics = {
totalEventsReceived: Number,
totalEventsProcessed: Number,
totalEventsFailed: Number,
averageProcessingTime: Number,
lastEventReceivedAt: Date,
failureRate: Number,
}
// Health check endpoint
exports.getWebhookHealth = async (req, res) => {
const last24h = new Date(Date.now() - 24 * 60 * 60 * 1000)
const stats = await WebhookEvent.aggregate([
{ $match: { processedAt: { $gte: last24h } } },
{
$group: {
_id: '$status',
count: { $sum: 1 },
},
},
])
res.json({
status: 'healthy', // ou 'degraded' si taux échec > 5%
last24h: stats,
lastEventAt: await WebhookEvent.findOne().sort({ processedAt: -1 })
?.processedAt,
})
}
Sécurité et bonnes pratiques
Validation stricte
// Validation signature webhook Stripe
const validateWebhookSignature = (rawBody, signature, secret) => {
const expectedSig = crypto
.createHmac('sha256', secret)
.update(rawBody, 'utf8')
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expectedSig, 'hex')
)
}
Protection contre replay attacks
// Vérifier timestamp de l'événement (max 5 minutes)
const eventTimestamp = event.created
const currentTimestamp = Math.floor(Date.now() / 1000)
if (currentTimestamp - eventTimestamp > 300) {
// 5 minutes
throw new Error('Event too old, possible replay attack')
}
Critères de réussite
✅ Service webhook complet avec tous les handlers ✅ Système d'idempotence fonctionnel ✅ Intégration avec tokens Upiik ✅ Événements internes connectés ✅ Notifications email/push configurées ✅ Système de retry implémenté ✅ Monitoring et health checks ✅ Sécurité webhook validée
Instructions d'exécution
- Examiner le système d'événements existant
src/events/ - Implémenter le service webhook complet
- Configurer les handlers pour chaque type d'événement
- Intégrer avec les notifications existantes
- Tester avec des webhooks Stripe de test
- Valider l'idempotence et la gestion d'erreurs
Prochaine étape : 06-integration-tokens.md