import { BaseCommand, flags } from '@adonisjs/core/ace' import type { CommandOptions } from '@adonisjs/core/types/ace' import db from '@adonisjs/lucid/services/db' import User from '#models/user' import Organization from '#models/organization' import Plan from '#models/plan' import Client from '#models/client' import Invoice from '#models/invoice' import ActivityEvent from '#models/activity_event' import RelanceTask from '#models/relance_task' import CheckinTask from '#models/checkin_task' import { provisionDefaultPlans } from '#services/default_plans' import { seedDemoOrg, wipeOrgInvoicePdfs } from '#database/factories' /** * Peuple l'org d'un user existant avec des données de démo réalistes — * pour visualiser dashboard, factures, plans en conditions réelles. * * node ace seed:demo --email arthurbarre.js@gmail.com * node ace seed:demo --email ... --reset # wipe avant */ export default class SeedDemo extends BaseCommand { static commandName = 'seed:demo' static description = "Peuple l'organisation d'un user existant avec des données de démo (clients, factures, activité)" static options: CommandOptions = { startApp: true, } @flags.string({ description: "Email du user dont on peuple l'org", required: true, }) declare email: string @flags.boolean({ description: "Supprime les clients/factures/activité existants de l'org avant le seed", default: false, }) declare reset: boolean @flags.string({ description: "Nom à donner à l'organisation (ex. 'Arthur Barré'). Si vide, on garde le nom existant ou fallback.", }) declare orgName?: string async run() { const email = this.email if (!email) { this.logger.error('Argument requis : --email ') this.exitCode = 1 return } const user = await User.findBy('email', String(email).toLowerCase()) if (!user) { this.logger.error(`User introuvable : ${email}`) this.exitCode = 1 return } this.logger.info(`User trouvé : ${user.fullName ?? user.email} (${user.id})`) if (!user.organizationId) { this.logger.error('Le user n\'a pas d\'organization rattachée — flow signup pas terminé ?') this.exitCode = 1 return } await db.transaction(async (trx) => { const org = await Organization.findOrFail(user.organizationId!, { client: trx }) if (this.reset) { this.logger.warning('--reset : suppression des clients/factures/activity existants…') // Ordre : enfants d'abord pour respecter les FK await CheckinTask.query({ client: trx }) .whereIn( 'invoice_id', db.from('invoices').where('organization_id', org.id).select('id') ) .delete() await RelanceTask.query({ client: trx }) .whereIn( 'invoice_id', db.from('invoices').where('organization_id', org.id).select('id') ) .delete() await ActivityEvent.query({ client: trx }).where('organization_id', org.id).delete() await Invoice.query({ client: trx }).where('organization_id', org.id).delete() await Client.query({ client: trx }).where('organization_id', org.id).delete() // Drop tous les PDFs MinIO de l'org pour repartir d'un drive propre. // Hors transaction (drive n'a pas de notion de tx) — si la suite // échoue, les PDFs sont déjà partis : pas grave, ils étaient destinés // au reset de toute façon. const wiped = await wipeOrgInvoicePdfs(org.id) if (wiped > 0) { this.logger.info(`MinIO : ${wiped} PDFs de facture supprimés.`) } } // Configure l'org : nom (si fourni) + bucket volume mensuel const targetName = this.orgName ?? (org.name && org.name.length > 0 ? org.name : `Maison ${user.fullName?.split(' ')[1] ?? 'Démo'}`) org.useTransaction(trx) org.name = targetName if (!org.monthlyVolumeBucket) { org.monthlyVolumeBucket = '20-50' } // Reset rubisCount, on le rechargera en fonction des factures seedées. org.rubisCount = 0 await org.save() this.logger.info(`Org configurée : "${org.name}"`) // Plans : provision si manquants (idempotent) await provisionDefaultPlans(org.id, trx) const plans = await Plan.query({ client: trx }).where('organization_id', org.id) this.logger.info(`Plans disponibles : ${plans.length} (${plans.map((p) => p.name).join(', ')})`) // Seed la data const result = await seedDemoOrg({ organizationId: org.id, plans, trx, }) // Met à jour le compteur rubis de l'org en fonction du seed await trx .from('organizations') .where('id', org.id) .update({ rubis_count: result.rubisEarned }) this.logger.success( `Seed terminé : ${result.clients.length} clients · ${result.invoices.length} factures · ${result.rubisEarned} rubis` ) // Petit récap par statut const byStatus: Record = {} for (const inv of result.invoices) { byStatus[inv.status] = (byStatus[inv.status] ?? 0) + 1 } for (const [status, count] of Object.entries(byStatus)) { this.logger.info(` · ${status} : ${count}`) } // User : signature par défaut si vide (utile pour les previews/tests email) if (!user.signature) { user.useTransaction(trx) user.signature = `Cordialement,\n${user.fullName ?? 'L\'équipe'}\n${org.name}` await user.save() this.logger.info('Signature email par défaut posée sur le user.') } }) this.logger.success('Done.') } }