// ============================================================================= // Firebase Cloud Functions pour DPAI // Gestion des confirmations de paiement Stripe // ============================================================================= const functions = require('firebase-functions'); const admin = require('firebase-admin'); // Initialiser Firebase Admin SDK si ce n'est pas déjà fait if (!admin.apps.length) { admin.initializeApp(); } const db = admin.firestore(); // ============================================================================= // CONFIGURATION STRIPE // ============================================================================= const stripe = require('stripe')(functions.config().stripe.secret || process.env.STRIPE_SECRET_KEY); // ============================================================================= // FONCTION: Confirmer un abonnement après paiement Stripe // ============================================================================= exports.confirmStripeSubscription = functions.https.onCall(async (data, context) => { // Vérifier l'authentification if (!context.auth) { throw new functions.https.HttpsError('unauthenticated', 'Non autorisé'); } const { userId, sessionId } = data; try { // 1. Récupérer la session Stripe const session = await stripe.checkout.sessions.retrieve(sessionId); // Vérifier que le paiement a réussi if (session.payment_status !== 'paid') { console.log('Paiement non confirmé pour session:', sessionId); return { success: false, error: 'Paiement non confirmé' }; } // 2. Récupérer les données de l'achat en attente const pendingDoc = await db.collection('pending_purchases').doc(userId).get(); if (!pendingDoc.exists) { console.log('Aucun achat en attente trouvé pour:', userId); return { success: false, error: 'Aucun achat en attente trouvé' }; } const purchaseData = pendingDoc.data(); const planId = purchaseData.planId || 'pro'; const isAnnual = purchaseData.isAnnual || false; // 3. Configurer les tokens selon le plan const planConfigs = { pro: { baseTokens: 500, bonusRate: 0.20, monthlyTokenRate: 1 }, enterprise: { baseTokens: 5000, bonusRate: 0.30, monthlyTokenRate: 1 } }; const config = planConfigs[planId] || planConfigs.pro; const baseTokens = config.baseTokens; const bonusTokens = Math.floor(baseTokens * config.bonusRate); const welcomeBonus = 10; const totalTokens = baseTokens + bonusTokens + welcomeBonus; // 4. Mettre à jour l'utilisateur dans Firestore const userRef = db.collection('users').doc(userId); await userRef.update({ plan: planId, subscriptionStartDate: admin.firestore.FieldValue.serverTimestamp(), subscriptionEndDate: isAnnual ? admin.firestore.Timestamp.fromDate( new Date(Date.now() + 365 * 24 * 60 * 60 * 1000) ) : null, // Débloquer les services selon le plan hasAccessToPremiumSuggestions: true, hasAccessToAdvancedAnalytics: planId === 'pro' || planId === 'enterprise', hasAccessToAPI: planId === 'enterprise', // Mettre à jour le tokenState 'tokenState.plan': planId, 'tokenState.baseTokens': baseTokens, 'tokenState.bonusTokens': bonusTokens, 'tokenState.totalTokens': totalTokens, 'tokenState.availableTokens': totalTokens, 'tokenState.usedTokens': 0, 'tokenState.monthlyTokensUsed': 0, 'tokenState.lastTokenUpdate': admin.firestore.FieldValue.serverTimestamp(), 'tokenState.lastMonthlyReset': admin.firestore.FieldValue.serverTimestamp(), 'tokenState.firstAnalysisDone': false }); // 5. Nettoyer l'achat en attente await db.collection('pending_purchases').doc(userId).delete(); console.log('Abonnement confirmé pour:', userId, 'Plan:', planId); return { success: true, planId, tokenAmount: totalTokens }; } catch (error) { console.error('Erreur confirmation abonnement:', error); throw new functions.https.HttpsError('internal', error.message); } }); // ============================================================================= // FONCTION: Confirmer un achat de pack de tokens après paiement Stripe // ============================================================================= exports.confirmTokenPurchase = functions.https.onCall(async (data, context) => { // Vérifier l'authentification if (!context.auth) { throw new functions.https.HttpsError('unauthenticated', 'Non autorisé'); } const { userId, sessionId } = data; try { // 1. Récupérer la session Stripe const session = await stripe.checkout.sessions.retrieve(sessionId); // Vérifier que le paiement a réussi if (session.payment_status !== 'paid') { console.log('Paiement non confirmé pour session:', sessionId); return { success: false, error: 'Paiement non confirmé' }; } // 2. Récupérer les données de l'achat en attente const pendingDoc = await db.collection('pending_purchases').doc(userId).get(); if (!pendingDoc.exists) { console.log('Aucun achat en attente trouvé pour:', userId); return { success: false, error: 'Aucun achat en attente trouvé' }; } const purchaseData = pendingDoc.data(); const packId = purchaseData.packId; const tokenAmount = purchaseData.tokenAmount || 0; // 3. Ajouter les tokens à l'utilisateur const userRef = db.collection('users').doc(userId); await userRef.update({ 'tokenState.availableTokens': admin.firestore.FieldValue.increment(tokenAmount), 'tokenState.totalTokens': admin.firestore.FieldValue.increment(tokenAmount) }); // 4. Nettoyer l'achat en attente await db.collection('pending_purchases').doc(userId).delete(); console.log('Pack de tokens confirmé pour:', userId, 'Pack:', packId, 'Tokens:', tokenAmount); return { success: true, packId, tokenAmount }; } catch (error) { console.error('Erreur confirmation achat tokens:', error); throw new functions.https.HttpsError('internal', error.message); } }); // ============================================================================= // FONCTION: Webhook Stripe (optionnel - pour une confirmation en temps réel) // ============================================================================= // Cette fonction est appelée par Stripe via un webhook // Elle permet de confirmer les paiements sans passer par le frontend exports.stripeWebhook = functions.https.onRequest(async (req, res) => { const sig = req.headers['stripe-signature']; const endpointSecret = functions.config().stripe.webhook || process.env.STRIPE_WEBHOOK_SECRET; let event; try { event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret); } catch (err) { console.error('Webhook signature verification failed:', err.message); return res.status(400).send(`Webhook Error: ${err.message}`); } // Gérer l'événement de paiement réussi if (event.type === 'checkout.session.completed') { const session = event.data.object; // Extraire les données du client_reference_id let clientRef; try { clientRef = JSON.parse(session.client_reference_id || '{}'); } catch (e) { clientRef = {}; } const userId = clientRef.userId; const type = clientRef.type; if (!userId || !type) { console.log('Données manquantes dans client_reference_id'); return res.status(200).json({ received: true }); } try { if (type === 'subscription') { const planId = clientRef.planId || 'pro'; const isAnnual = clientRef.isAnnual || false; // Mettre à jour l'utilisateur (même logique que confirmStripeSubscription) const planConfigs = { pro: { baseTokens: 500, bonusRate: 0.20 }, enterprise: { baseTokens: 5000, bonusRate: 0.30 } }; const config = planConfigs[planId] || planConfigs.pro; const baseTokens = config.baseTokens; const bonusTokens = Math.floor(baseTokens * config.bonusRate); const totalTokens = baseTokens + bonusTokens + 10; await db.collection('users').doc(userId).update({ plan: planId, subscriptionStartDate: admin.firestore.FieldValue.serverTimestamp(), subscriptionEndDate: isAnnual ? admin.firestore.Timestamp.fromDate( new Date(Date.now() + 365 * 24 * 60 * 60 * 1000) ) : null, hasAccessToPremiumSuggestions: true, hasAccessToAdvancedAnalytics: planId === 'pro' || planId === 'enterprise', hasAccessToAPI: planId === 'enterprise', 'tokenState.plan': planId, 'tokenState.baseTokens': baseTokens, 'tokenState.bonusTokens': bonusTokens, 'tokenState.totalTokens': totalTokens, 'tokenState.availableTokens': totalTokens, 'tokenState.usedTokens': 0, 'tokenState.lastTokenUpdate': admin.firestore.FieldValue.serverTimestamp() }); await db.collection('pending_purchases').doc(userId).delete(); } else if (type === 'token_pack') { const packId = clientRef.packId; const tokenAmount = clientRef.tokenAmount; if (packId && tokenAmount) { await db.collection('users').doc(userId).update({ 'tokenState.availableTokens': admin.firestore.FieldValue.increment(tokenAmount), 'tokenState.totalTokens': admin.firestore.FieldValue.increment(tokenAmount) }); await db.collection('pending_purchases').doc(userId).delete(); } } console.log('Webhook traité avec succès pour:', userId); } catch (err) { console.error('Erreur traitement webhook:', err); } } // Gérer d'autres événements si nécessaire // if (event.type === 'invoice.paid') { ... } // Répondre avec un statut 200 pour indiquer que l'événement a été reçu res.json({ received: true }); }); // ============================================================================= // EXPORT POUR TESTS LOCAUX // ============================================================================= module.exports = { confirmStripeSubscription: exports.confirmStripeSubscription, confirmTokenPurchase: exports.confirmTokenPurchase, stripeWebhook: exports.stripeWebhook };