import { BaseCommand, flags } from '@adonisjs/core/ace' import type { CommandOptions } from '@adonisjs/core/types/ace' import { DateTime } from 'luxon' import User from '#models/user' import Invoice from '#models/invoice' import RelanceTask from '#models/relance_task' /** * Programme une RelanceTask à une date arbitraire pour la démo. * * node ace demo:schedule-relance --email arthurbarre.js@gmail.com --date 2026-05-09 * node ace demo:schedule-relance --email ... --date 2026-05-09 --hour 14 * node ace demo:schedule-relance --email ... --date 2026-05-09 --invoice F-2026-0042 * * Logique : * - Trouve une facture active (pending / in_relance / awaiting) avec un plan * - Pioche le premier step du plan (amical en général) — sinon `--step-order N` * - Crée une RelanceTask `scheduled` à la date demandée * * Utilise l'horloge virtuelle si la démo est active (compare-toi à la date * que tu vois sur l'horloge top-right). */ export default class DemoScheduleRelance extends BaseCommand { static commandName = 'demo:schedule-relance' static description = 'Programme une RelanceTask à une date arbitraire (démo)' static options: CommandOptions = { startApp: true, } @flags.string({ description: 'Email du user', required: true }) declare email: string @flags.string({ description: 'Date YYYY-MM-DD', required: true }) declare date: string @flags.number({ description: 'Heure d\'envoi (0-23)', default: 9 }) declare hour: number @flags.string({ description: 'Numéro de facture spécifique (sinon : 1re active trouvée)', }) declare invoice?: string @flags.number({ description: 'Order du step plan à utiliser (0 = premier)', default: 0, }) declare stepOrder: number async run() { const user = await User.findBy('email', this.email.toLowerCase()) if (!user || !user.organizationId) { this.logger.error(`User introuvable ou sans org : ${this.email}`) this.exitCode = 1 return } let invoiceQuery = Invoice.query() .where('organization_id', user.organizationId) .preload('plan', (q) => q.preload('steps')) .preload('client') .whereIn('status', ['pending', 'in_relance', 'awaiting_user_confirmation']) .orderBy('due_date', 'asc') if (this.invoice) { invoiceQuery = invoiceQuery.where('numero', this.invoice) } const invoice = await invoiceQuery.first() if (!invoice) { this.logger.error( this.invoice ? `Facture ${this.invoice} non trouvée ou inactive.` : 'Aucune facture active (pending/in_relance/awaiting) dans cette org.' ) this.exitCode = 1 return } if (!invoice.plan?.steps?.length) { this.logger.error( `Facture ${invoice.numero} sans plan ou plan sans étapes — assignez un plan d'abord.` ) this.exitCode = 1 return } const sortedSteps = invoice.plan.steps.slice().sort((a, b) => a.order - b.order) const step = sortedSteps[this.stepOrder] if (!step) { this.logger.error( `Step order ${this.stepOrder} introuvable. Plan a ${sortedSteps.length} étape(s).` ) this.exitCode = 1 return } const sendAt = DateTime.fromISO( `${this.date}T${String(this.hour).padStart(2, '0')}:00:00.000`, { zone: 'utc' } ) if (!sendAt.isValid) { this.logger.error( `Date invalide : "${this.date}" (attendu YYYY-MM-DD), heure ${this.hour}.` ) this.exitCode = 1 return } const task = await RelanceTask.create({ organizationId: user.organizationId, invoiceId: invoice.id, planStepId: step.id, sendAt, status: 'scheduled', sentAt: null, queueJobId: null, }) this.logger.success('Relance programmée pour la démo :') this.logger.info(` · facture : ${invoice.numero} → ${invoice.client.name}`) this.logger.info( ` · step : J${step.offsetDays >= 0 ? '+' : ''}${step.offsetDays} (${step.tone}) — "${step.subject}"` ) this.logger.info(` · sendAt : ${sendAt.toFormat('cccc dd LLLL yyyy HH:mm')} UTC`) this.logger.info(` · task id : ${task.id}`) this.logger.info('') this.logger.info( "→ En mode démo, l'horloge déclenchera l'envoi quand virtualNow ≥ sendAt." ) } }