info('Subscriptions are disabled; skipping the AI cohort report.'); return self::SUCCESS; } $weeks = $this->option('weeks') !== null ? (int) $this->option('weeks') : null; $report = $this->collector->collect($weeks); $summary = $this->summarizer->summarize( 'ai-cohort-report', self::SUMMARY_CONTEXT, $this->summaryPayload($report), remember: ! $this->option('no-discord'), ); $embed = $this->buildEmbed($report, $summary); if ($this->option('no-discord')) { $this->printEmbeds([$embed]); $this->info('Skipped Discord (--no-discord).'); return self::SUCCESS; } $webhookUrl = config('services.discord.ai_cohort_webhook_url') ?: config('services.discord.webhook_url'); (new DiscordWebhook($webhookUrl))->send('', [$embed]); $this->info('AI cohort report sent to Discord.'); return self::SUCCESS; } /** * The figures the AI summary may talk about — the same ones the table shows. * * @param array{releaseAt: ?CarbonImmutable, releaseWeek: ?string, weeks: list>} $report * @return array */ private function summaryPayload(array $report): array { return [ 'release_week' => $report['releaseWeek'], 'weeks' => array_map(fn (array $row): array => [ 'week' => $row['week'], 'eligible' => $row['eligible'], 'retained_rate' => $row['retainedRate'], 'trial_rate' => $row['trialRate'], 'paid_rate' => $row['paidRate'], 'ai_accepted_rate' => $row['aiAcceptedRate'], 'phase' => $row['phase'], 'retention_mature' => $row['retentionMature'], 'paid_mature' => $row['paidMature'], 'signup_surge' => $row['surge'], ], $report['weeks']), ]; } /** * @param array{releaseAt: ?CarbonImmutable, releaseWeek: ?string, weeks: list>} $report * @return array */ private function buildEmbed(array $report, ?string $summary): array { $lines = [sprintf('%-9s %5s %6s %6s %6s %5s', 'Semana', 'Eleg', 'Ret', 'Prueba', 'Pago', 'IA')]; foreach ($report['weeks'] as $row) { $flags = ''; if ($report['releaseWeek'] !== null && $row['week'] === $report['releaseWeek']) { $flags .= ' 🚀'; } if ($row['surge']) { $flags .= ' ⚡'; } $lines[] = sprintf( '%-9s %5d %6s %6s %6s %5s%s', $row['week'], $row['eligible'], $this->rateCell($row['retainedRate'], $row['retentionMature'], $row['eligible']), $this->rateCell($row['trialRate'], $row['retentionMature'], $row['eligible']), $this->rateCell($row['paidRate'], $row['paidMature'], $row['eligible']), $this->aiCell($row['aiAcceptedRate'], $row['eligible']), $flags, ); } $release = $report['releaseAt'] !== null ? 'Primer consentimiento de IA (ancla de lanzamiento): '.$report['releaseAt']->copy()->locale('es')->translatedFormat('D, d M Y').' · semana '.$report['releaseWeek'].' 🚀' : 'Todavía no hay ningún consentimiento de IA — la función no está activa en producción.'; $table = "```\n".implode("\n", $lines)."\n```"; return [ 'title' => '🤖 Sugerencias con IA — informe de cohortes semanales', 'description' => $summary !== null ? $summary."\n\n".$table : $table, 'color' => 0x5865F2, 'fields' => [ [ 'name' => 'Ancla de lanzamiento', 'value' => $release, 'inline' => false, ], [ 'name' => 'Leyenda', 'value' => 'Eleg = usuarios con ≥50 transacciones en sus primeros 7 días · Ret = activos ≥14d después de registrarse · Prueba = se suscribieron ≤14d · Pago = suscripción activa ≤30d · IA = aceptaron el consentimiento de IA · `pdte` = cohorte demasiado joven para puntuarla · ⚡ = pico de registros', 'inline' => false, ], [ 'name' => '⚠️ Solo orientativo', 'value' => 'Es una comparación antes/después, no un test aleatorizado. Las cohortes se comparan a la misma edad. Las semanas con pico (⚡, p. ej. lanzamiento o YouTube) llegan por otro canal de adquisición y no están controladas — compara semanas orgánicas entre sí. La confianza se construye a lo largo de un trimestre, no en un solo mes.', 'inline' => false, ], ], ]; } private function rateCell(?float $rate, bool $mature, int $eligible): string { if ($eligible === 0) { return '—'; } if (! $mature || $rate === null) { return 'pdte'; } return ((int) round($rate * 100)).'%'; } private function aiCell(?float $rate, int $eligible): string { if ($eligible === 0 || $rate === null) { return '—'; } return ((int) round($rate * 100)).'%'; } }