import { BaseCommand } from '@adonisjs/core/ace' import type { CommandOptions } from '@adonisjs/core/types/ace' import { getStripe, STRIPE_LOOKUP_KEYS } from '#services/stripe' /** * Crée / met à jour les Products + Prices Stripe pour Rubis. * * Idempotent : on cherche par `lookup_key` avant de créer. Si déjà * existant, on log "OK" et on passe. Pas d'écrasement (les prices Stripe * sont immuables — on ne peut pas modifier le montant d'un Price existant, * il faut en créer un nouveau). * * Lance ce command UNE FOIS au setup initial (test ou prod), puis quand * tu veux ajouter de nouveaux Prices. * * node ace stripe:setup * * Pour rebattre les cartes : aller manuellement archive les Prices/Products * dans le dashboard Stripe puis relancer. */ export default class StripeSetup extends BaseCommand { static commandName = 'stripe:setup' static description = 'Crée les Products et Prices Stripe (Pro / Business, monthly + yearly)' static options: CommandOptions = { startApp: true, } async run() { // Lazy validation : provoque l'exception immédiate si la clé manque. getStripe() this.logger.info('Stripe setup — création des Products + Prices') // ---------------- PRO ---------------- const proProduct = await this.ensureProduct({ name: 'Rubis Pro', description: "Factures illimitées, OCR illimité, automatisation complète des relances. Pour les TPE actives qui ne veulent plus jamais y penser.", metadata: { plan_key: 'pro' }, }) await this.ensurePrice({ productId: proProduct.id, lookupKey: STRIPE_LOOKUP_KEYS.pro_monthly, unitAmount: 1900, // 19,00 € interval: 'month', nickname: 'Pro mensuel', }) await this.ensurePrice({ productId: proProduct.id, lookupKey: STRIPE_LOOKUP_KEYS.pro_yearly, // 19 € × 12 = 228 €. -17% (= ~190 €) pour récompenser l'engagement annuel. unitAmount: 19_000, interval: 'year', nickname: 'Pro annuel (-17%)', }) // ---------------- BUSINESS ---------------- const bizProduct = await this.ensureProduct({ name: 'Rubis Business', description: "Factures illimitées + 5 sièges utilisateurs + réponses depuis l'email de l'utilisateur. Pour les PME qui ont une vraie équipe.", metadata: { plan_key: 'business' }, }) await this.ensurePrice({ productId: bizProduct.id, lookupKey: STRIPE_LOOKUP_KEYS.business_monthly, unitAmount: 4900, // 49,00 € interval: 'month', nickname: 'Business mensuel', }) await this.ensurePrice({ productId: bizProduct.id, lookupKey: STRIPE_LOOKUP_KEYS.business_yearly, // 49 € × 12 = 588 €. -17% (= ~490 €) annuel. unitAmount: 49_000, interval: 'year', nickname: 'Business annuel (-17%)', }) this.logger.success('Stripe setup terminé.') } private async ensureProduct(input: { name: string description: string metadata: Record }) { const stripe = getStripe() // Lookup via metadata (Stripe ne permet pas de lookup_key sur Product, // seulement sur Price). On utilise donc list + filter. const existing = await stripe.products.list({ active: true, limit: 100 }) const found = existing.data.find( (p) => p.metadata?.['plan_key'] === input.metadata['plan_key'] ) if (found) { this.logger.info(` Product ${input.name} : déjà existant (${found.id})`) return found } const created = await stripe.products.create({ name: input.name, description: input.description, metadata: input.metadata, }) this.logger.success(` Product ${input.name} créé : ${created.id}`) return created } private async ensurePrice(input: { productId: string lookupKey: string unitAmount: number interval: 'month' | 'year' nickname: string }) { const stripe = getStripe() const existing = await stripe.prices.list({ lookup_keys: [input.lookupKey], limit: 1, }) if (existing.data[0]) { this.logger.info( ` Price ${input.nickname} : déjà existant (${existing.data[0].id})` ) return existing.data[0] } const created = await stripe.prices.create({ product: input.productId, unit_amount: input.unitAmount, currency: 'eur', recurring: { interval: input.interval }, lookup_key: input.lookupKey, nickname: input.nickname, }) this.logger.success( ` Price ${input.nickname} créé : ${created.id} (lookup=${input.lookupKey})` ) return created } }