/** * Script d'initialisation Stripe. * * Crée (ou récupère s'ils existent déjà) les produits et prix de Freedge, * puis affiche les IDs à copier dans `.env`. Idempotent — peut être relancé * sans danger grâce aux `lookup_key` sur les prix. * * Usage : * pnpm stripe:setup # juste afficher les IDs * pnpm stripe:setup --write-env # écrire directement dans backend/.env * * Nécessite `STRIPE_SECRET_KEY` dans l'environnement (chargé depuis .env). */ import 'dotenv/config'; import Stripe from 'stripe'; import * as fs from 'node:fs'; import * as path from 'node:path'; // --------------------------------------------------------------------------- // Configuration des plans à créer // --------------------------------------------------------------------------- interface PlanDefinition { envKey: 'STRIPE_PRICE_ID_ESSENTIAL' | 'STRIPE_PRICE_ID_PREMIUM'; lookupKey: string; product: { name: string; description: string; }; price: { unitAmount: number; // centimes currency: string; interval: 'month' | 'year'; }; } const PLANS: PlanDefinition[] = [ { envKey: 'STRIPE_PRICE_ID_ESSENTIAL', lookupKey: 'freedge_essential_monthly', product: { name: 'Freedge Essentiel', description: '15 recettes par mois, reconnaissance vocale, préférences culinaires, sauvegarde illimitée.', }, price: { unitAmount: 300, // 3,00 € currency: 'eur', interval: 'month', }, }, { envKey: 'STRIPE_PRICE_ID_PREMIUM', lookupKey: 'freedge_premium_monthly', product: { name: 'Freedge Premium', description: 'Recettes illimitées, images haute qualité, reconnaissance vocale, support prioritaire.', }, price: { unitAmount: 500, // 5,00 € currency: 'eur', interval: 'month', }, }, ]; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- const YELLOW = '\x1b[33m'; const GREEN = '\x1b[32m'; const RED = '\x1b[31m'; const CYAN = '\x1b[36m'; 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); } function fail(msg: string): never { log(`${RED}✗ ${msg}${RESET}`); process.exit(1); } /** * Trouve un prix existant par `lookup_key`, ou crée le produit + prix * sinon. Retourne toujours un objet Stripe.Price. */ async function ensurePlan( stripe: Stripe, plan: PlanDefinition ): Promise { // Cherche par lookup_key (idempotent) const existing = await stripe.prices.list({ lookup_keys: [plan.lookupKey], expand: ['data.product'], limit: 1, }); if (existing.data.length > 0) { const price = existing.data[0]; const product = price.product as Stripe.Product; log( ` ${DIM}→${RESET} ${plan.product.name} ${DIM}(déjà existant)${RESET}` ); log(` ${DIM}product: ${product.id}${RESET}`); log(` ${DIM}price: ${price.id}${RESET}`); return price; } // Sinon, crée le produit log(` ${CYAN}+${RESET} Création de ${plan.product.name}…`); const product = await stripe.products.create({ name: plan.product.name, description: plan.product.description, metadata: { app: 'freedge', plan_key: plan.lookupKey, }, }); // Puis le prix récurrent avec lookup_key pour la prochaine fois const price = await stripe.prices.create({ product: product.id, unit_amount: plan.price.unitAmount, currency: plan.price.currency, recurring: { interval: plan.price.interval }, lookup_key: plan.lookupKey, nickname: `${plan.product.name} — mensuel`, metadata: { app: 'freedge', plan_key: plan.lookupKey, }, }); log(` ${GREEN}✓${RESET} product: ${product.id}`); log(` ${GREEN}✓${RESET} price: ${price.id}`); return price; } /** * Met à jour (ou ajoute) une variable dans le fichier .env. * Préserve l'ordre et les commentaires des autres lignes. */ function upsertEnvVar(envPath: string, key: string, value: string): void { let content = ''; if (fs.existsSync(envPath)) { content = fs.readFileSync(envPath, 'utf-8'); } const lineRegex = new RegExp(`^${key}=.*$`, 'm'); if (lineRegex.test(content)) { content = content.replace(lineRegex, `${key}=${value}`); } else { content = (content.trimEnd() + `\n${key}=${value}\n`).trimStart(); } fs.writeFileSync(envPath, content); } // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- async function main() { const writeEnv = process.argv.includes('--write-env'); const secretKey = process.env.STRIPE_SECRET_KEY; if (!secretKey) { fail( 'STRIPE_SECRET_KEY manquante. Définis-la dans backend/.env puis relance.' ); } if (!secretKey.startsWith('sk_')) { fail(`STRIPE_SECRET_KEY ne ressemble pas à une clé Stripe : ${secretKey.slice(0, 8)}…`); } const isLive = secretKey.startsWith('sk_live_'); log(''); log(`${BOLD}🔧 Freedge — Initialisation Stripe${RESET}`); log( `${DIM}Mode : ${isLive ? `${RED}LIVE ⚠️${RESET}${DIM}` : `${GREEN}TEST${RESET}${DIM}`}${RESET}` ); log(''); if (isLive) { log( `${YELLOW}⚠️ Tu utilises une clé LIVE. Les produits seront créés en production.${RESET}` ); log(`${YELLOW} Appuie sur Ctrl+C dans les 5 secondes pour annuler.${RESET}`); log(''); await new Promise((r) => setTimeout(r, 5000)); } const stripe = new Stripe(secretKey, { apiVersion: (process.env.STRIPE_API_VERSION as Stripe.StripeConfig['apiVersion']) ?? '2023-10-16', typescript: true, }); log(`${BOLD}Plans à provisionner :${RESET}`); const results: Array<{ envKey: string; priceId: string }> = []; for (const plan of PLANS) { try { const price = await ensurePlan(stripe, plan); results.push({ envKey: plan.envKey, priceId: price.id }); } catch (err) { log(` ${RED}✗${RESET} ${plan.product.name} : ${(err as Error).message}`); process.exit(1); } } log(''); log(`${BOLD}Résultat :${RESET}`); log(''); for (const { envKey, priceId } of results) { log(` ${CYAN}${envKey}${RESET}=${priceId}`); } log(''); if (writeEnv) { const envPath = path.resolve(process.cwd(), '.env'); for (const { envKey, priceId } of results) { upsertEnvVar(envPath, envKey, priceId); } log(`${GREEN}✓${RESET} ${envPath} mis à jour.`); } else { log( `${DIM}Ajoute ces lignes à backend/.env (ou relance avec --write-env pour le faire automatiquement).${RESET}` ); } log(''); // Rappels utiles log(`${BOLD}Prochaines étapes :${RESET}`); log(` 1. ${DIM}Activer le Customer Portal${RESET}`); log( ` ${DIM}https://dashboard.stripe.com/${isLive ? '' : 'test/'}settings/billing/portal${RESET}` ); log(` 2. ${DIM}Lancer le listener webhook en local${RESET}`); log(` ${DIM}stripe listen --forward-to localhost:3000/stripe/webhook${RESET}`); log(` 3. ${DIM}Copier le whsec_ affiché dans STRIPE_WEBHOOK_SECRET${RESET}`); log(''); } main().catch((err) => { log(''); fail(err instanceof Error ? err.message : String(err)); });