#!/usr/bin/env node /** * stripe-sync.mjs * ──────────────── * Synchronise les produits de la BDD vers Stripe : * 1. Pour chaque produit avec un prix (price != null) : * - Crée le Product Stripe (ou le retrouve via metadata.slug) * - Crée le Price Stripe * - Met à jour le stripePriceId dans la BDD * 2. Les produits sans prix sont ignorés * * Usage : * node scripts/stripe-sync.mjs # mode dry-run * node scripts/stripe-sync.mjs --confirm # exécution réelle */ import 'dotenv/config' import Stripe from 'stripe' import { PrismaClient } from '@prisma/client' const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY if (!STRIPE_SECRET_KEY) { console.error('❌ STRIPE_SECRET_KEY manquant dans .env') process.exit(1) } const stripe = new Stripe(STRIPE_SECRET_KEY) const prisma = new PrismaClient() const dryRun = !process.argv.includes('--confirm') if (dryRun) { console.log('🔍 MODE DRY-RUN — rien ne sera créé ni modifié') console.log(' Ajouter --confirm pour exécuter réellement\n') } /** * Cherche un produit Stripe existant par metadata.slug */ async function findExistingStripeProduct(slug) { let hasMore = true let startingAfter = undefined while (hasMore) { const params = { limit: 100, active: true } if (startingAfter) params.starting_after = startingAfter const products = await stripe.products.list(params) for (const product of products.data) { if (product.metadata?.slug === slug) { return product } } hasMore = products.has_more if (products.data.length > 0) { startingAfter = products.data[products.data.length - 1].id } } return null } /** * Cherche un prix actif existant pour un produit Stripe * avec le bon montant et la bonne devise */ async function findExistingPrice(stripeProductId, amount, currency) { const prices = await stripe.prices.list({ product: stripeProductId, active: true, limit: 100, }) return prices.data.find( p => p.unit_amount === amount && p.currency === currency.toLowerCase() ) } async function main() { // Récupérer tous les produits publiés avec un prix const products = await prisma.product.findMany({ where: { isPublished: true, price: { not: null }, }, orderBy: { sortOrder: 'asc' }, }) if (products.length === 0) { console.log('ℹ️ Aucun produit avec un prix trouvé dans la BDD') return } console.log(`📦 ${products.length} produit(s) à synchroniser vers Stripe\n`) let created = 0 let updated = 0 let skipped = 0 for (const product of products) { const priceCents = product.price const currency = product.currency.toLowerCase() console.log(`── ${product.productDisplayName} (${product.slug})`) console.log(` Prix: ${priceCents / 100} ${currency.toUpperCase()}`) // 1. Chercher ou créer le Product Stripe let stripeProduct = await findExistingStripeProduct(product.slug) if (stripeProduct) { console.log(` ✓ Produit Stripe existant: ${stripeProduct.id}`) if (!dryRun) { // Mettre à jour les infos du produit stripeProduct = await stripe.products.update(stripeProduct.id, { name: product.productDisplayName, description: product.description, images: product.ogImage ? [product.ogImage] : undefined, metadata: { slug: product.slug, stripeKey: product.stripeKey || product.slug, dbId: product.id, }, }) console.log(` ✓ Produit Stripe mis à jour`) } } else { if (dryRun) { console.log(` 📌 Produit Stripe SERAIT créé`) } else { stripeProduct = await stripe.products.create({ name: product.productDisplayName, description: product.description, images: product.ogImage ? [product.ogImage] : [], metadata: { slug: product.slug, stripeKey: product.stripeKey || product.slug, dbId: product.id, }, }) console.log(` ✓ Produit Stripe créé: ${stripeProduct.id}`) } } // 2. Chercher ou créer le Price Stripe let stripePrice = null if (stripeProduct) { stripePrice = await findExistingPrice(stripeProduct.id, priceCents, currency) } if (stripePrice) { console.log(` ✓ Prix Stripe existant: ${stripePrice.id} (${priceCents / 100} ${currency.toUpperCase()})`) // Vérifier si le stripePriceId dans la BDD est à jour if (product.stripePriceId === stripePrice.id) { console.log(` ✓ BDD déjà à jour`) skipped++ } else { if (dryRun) { console.log(` 📌 stripePriceId SERAIT mis à jour: ${product.stripePriceId || '(vide)'} → ${stripePrice.id}`) updated++ } else { await prisma.product.update({ where: { id: product.id }, data: { stripePriceId: stripePrice.id }, }) console.log(` ✓ BDD mise à jour: stripePriceId = ${stripePrice.id}`) updated++ } } } else { // Créer un nouveau prix if (dryRun) { console.log(` 📌 Prix Stripe SERAIT créé: ${priceCents / 100} ${currency.toUpperCase()}`) console.log(` 📌 stripePriceId SERAIT mis à jour dans la BDD`) created++ } else { stripePrice = await stripe.prices.create({ product: stripeProduct.id, unit_amount: priceCents, currency: currency, metadata: { slug: product.slug, dbId: product.id, }, }) console.log(` ✓ Prix Stripe créé: ${stripePrice.id}`) // 3. Mettre à jour le stripePriceId dans la BDD await prisma.product.update({ where: { id: product.id }, data: { stripePriceId: stripePrice.id }, }) console.log(` ✓ BDD mise à jour: stripePriceId = ${stripePrice.id}`) created++ } } console.log() } console.log('─'.repeat(50)) if (dryRun) { console.log(`📊 Résumé (dry-run) :`) console.log(` ${created} produit(s)/prix SERAIENT créés`) console.log(` ${updated} stripePriceId SERAIENT mis à jour`) console.log(` ${skipped} déjà synchronisé(s)`) console.log(`\n👉 Relancer avec --confirm pour exécuter`) } else { console.log(`✅ Synchronisation terminée :`) console.log(` ${created} créé(s), ${updated} mis à jour, ${skipped} inchangé(s)`) } } main() .catch(err => { console.error('❌ Erreur:', err.message) process.exit(1) }) .finally(() => prisma.$disconnect())