/** * Script de support : synchronise l'abonnement d'un utilisateur depuis * Stripe vers la base de données locale. * * À utiliser quand un webhook a été raté (par exemple `stripe listen` * n'était pas démarré pendant le checkout) et qu'un user payant est * resté bloqué en plan "gratuit" dans la DB. * * Usage : * npm run stripe:sync -- user@example.com * npm run stripe:sync -- --all # synchronise tous les users avec stripeId */ import 'dotenv/config'; import Stripe from 'stripe'; import { PrismaClient } from '@prisma/client'; const GREEN = '\x1b[32m'; const RED = '\x1b[31m'; const YELLOW = '\x1b[33m'; const DIM = '\x1b[2m'; const BOLD = '\x1b[1m'; const RESET = '\x1b[0m'; function log(msg: string) { // eslint-disable-next-line no-console console.log(msg); } interface PlanMapping { priceId: string | undefined; plan: 'essential' | 'premium'; } function getPlans(): PlanMapping[] { return [ { priceId: process.env.STRIPE_PRICE_ID_ESSENTIAL, plan: 'essential' }, { priceId: process.env.STRIPE_PRICE_ID_PREMIUM, plan: 'premium' }, ]; } async function syncUser( stripe: Stripe, prisma: PrismaClient, user: { id: string; email: string; stripeId: string | null } ): Promise<{ plan: string; status: string | null } | null> { if (!user.stripeId) { log(` ${YELLOW}↷${RESET} ${user.email} ${DIM}(pas de stripeId)${RESET}`); return null; } const subscriptions = await stripe.subscriptions.list({ customer: user.stripeId, status: 'all', limit: 5, }); const active = subscriptions.data .filter((s) => ['active', 'trialing', 'past_due'].includes(s.status)) .sort((a, b) => b.created - a.created)[0]; if (!active) { await prisma.user.update({ where: { id: user.id }, data: { subscription: 'free', subscriptionStatus: null, stripeSubscriptionId: null, subscriptionPriceId: null, subscriptionCurrentPeriodEnd: null, }, }); log(` ${DIM}−${RESET} ${user.email} ${DIM}→ free (aucun abo actif sur Stripe)${RESET}`); return { plan: 'free', status: null }; } const priceId = active.items.data[0]?.price.id; const mapping = getPlans().find((p) => p.priceId === priceId); const planId = mapping?.plan ?? 'essential'; await prisma.user.update({ where: { id: user.id }, data: { subscription: planId, subscriptionStatus: active.status, stripeSubscriptionId: active.id, subscriptionPriceId: priceId ?? null, subscriptionCurrentPeriodEnd: new Date(active.current_period_end * 1000), }, }); log( ` ${GREEN}✓${RESET} ${user.email} ${DIM}→${RESET} ${BOLD}${planId}${RESET} ${DIM}(${active.status})${RESET}` ); return { plan: planId, status: active.status }; } async function main() { const args = process.argv.slice(2).filter((a) => a !== '--'); if (args.length === 0) { log('Usage : npm run stripe:sync -- user@example.com'); log(' npm run stripe:sync -- --all'); process.exit(1); } const secretKey = process.env.STRIPE_SECRET_KEY; if (!secretKey) { log(`${RED}✗ STRIPE_SECRET_KEY manquante${RESET}`); process.exit(1); } const stripe = new Stripe(secretKey, { apiVersion: (process.env.STRIPE_API_VERSION as Stripe.StripeConfig['apiVersion']) ?? '2023-10-16', }); const prisma = new PrismaClient(); log(''); log(`${BOLD}🔄 Synchronisation Stripe → DB${RESET}`); log(''); try { if (args.includes('--all')) { const users = await prisma.user.findMany({ where: { stripeId: { not: null } }, select: { id: true, email: true, stripeId: true }, }); log(`${DIM}${users.length} utilisateur(s) avec un stripeId${RESET}`); log(''); for (const user of users) { try { await syncUser(stripe, prisma, user); } catch (err) { log(` ${RED}✗${RESET} ${user.email} : ${(err as Error).message}`); } } } else { const email = args[0]; const user = await prisma.user.findUnique({ where: { email }, select: { id: true, email: true, stripeId: true }, }); if (!user) { log(`${RED}✗ Utilisateur introuvable : ${email}${RESET}`); process.exit(1); } await syncUser(stripe, prisma, user); } log(''); log(`${GREEN}✓ Terminé${RESET}`); } catch (err) { log(`${RED}✗ ${(err as Error).message}${RESET}`); process.exit(1); } finally { await prisma.$disconnect(); } } main();