comment(Inspiring::quote()); })->purpose('Display an inspiring quote'); Artisan::command('logs:purge-old-security', function () { $fechaLimite = now()->subMonths(2); $eliminados = LogSeguridad::query() ->where('fechahora', '<', $fechaLimite) ->delete(); $this->info('Logs eliminados: ' . $eliminados); })->purpose('Elimina logs de seguridad con más de 2 meses de antigüedad'); Artisan::command('seed:bulk {--modo=masivo} {--clientes=} {--profesionales=} {--servicios=} {--profesiones=} {--fresh}', function () { $presets = [ 'rapido' => ['clientes' => 100, 'profesionales' => 20, 'servicios' => 10, 'profesiones' => 2], 'medio' => ['clientes' => 500, 'profesionales' => 50, 'servicios' => 20, 'profesiones' => 3], 'masivo' => ['clientes' => 1000, 'profesionales' => 100, 'servicios' => 30, 'profesiones' => 4], ]; $modo = strtolower((string) $this->option('modo')); if (!array_key_exists($modo, $presets)) { $this->error('Modo invalido. Usa: rapido, medio o masivo.'); return 1; } $clientes = max(0, (int) ($this->option('clientes') ?? $presets[$modo]['clientes'])); $profesionales = max(0, (int) ($this->option('profesionales') ?? $presets[$modo]['profesionales'])); $servicios = max(0, (int) ($this->option('servicios') ?? $presets[$modo]['servicios'])); $profesiones = max(0, (int) ($this->option('profesiones') ?? $presets[$modo]['profesiones'])); config([ 'bulk_seed.enabled' => false, 'bulk_seed.clientes' => $clientes, 'bulk_seed.profesionales' => $profesionales, 'bulk_seed.servicios' => $servicios, 'bulk_seed.profesiones' => $profesiones, ]); if ((bool) $this->option('fresh')) { $this->info('Ejecutando migrate:fresh --seed...'); $exitMigrate = Artisan::call('migrate:fresh', ['--seed' => true]); $this->line(Artisan::output()); if ($exitMigrate !== 0) { $this->error('Fallo migrate:fresh --seed'); return $exitMigrate; } } $this->info("Cargando datos masivos (modo={$modo})..."); $exitBulk = Artisan::call('db:seed', ['--class' => BulkDataSeeder::class]); $this->line(Artisan::output()); if ($exitBulk !== 0) { $this->error('Fallo la ejecucion de BulkDataSeeder.'); return $exitBulk; } $this->info("Datos creados: {$profesiones} profesiones, {$servicios} servicios, {$clientes} clientes y {$profesionales} profesionales."); return 0; })->purpose('Carga datos de prueba masivos por modo o por cantidades personalizadas'); Artisan::command('formularios:sync-rejected-status', function () { $formulariosPendientes = Formulario::query() ->where('estado', 'Pendiente') ->get(['id', 'profesion_id']); $actualizados = 0; foreach ($formulariosPendientes as $formulario) { $formularioId = (int) $formulario->id; $profesionFormularioId = (int) $formulario->profesion_id; $asignadosActivos = DB::table('profesionales_formularios as pf') ->join('profesionales as p', 'p.id', '=', 'pf.profesional_id') ->where('pf.formulario_id', $formularioId) ->where('p.baja_id', 1) ->where(function ($query) use ($profesionFormularioId): void { $query->where('p.profesion_id', $profesionFormularioId) ->orWhereExists(function ($subquery) use ($profesionFormularioId): void { $subquery->select(DB::raw(1)) ->from('profesionales_profesiones as pp') ->whereColumn('pp.profesional_id', 'p.id') ->where('pp.profesion_id', $profesionFormularioId); }); }) ->count(); if ($asignadosActivos === 0) { continue; } $hayActivosSinRechazar = DB::table('profesionales_formularios as pf') ->join('profesionales as p', 'p.id', '=', 'pf.profesional_id') ->where('pf.formulario_id', $formularioId) ->where('p.baja_id', 1) ->where(function ($query) use ($profesionFormularioId): void { $query->where('p.profesion_id', $profesionFormularioId) ->orWhereExists(function ($subquery) use ($profesionFormularioId): void { $subquery->select(DB::raw(1)) ->from('profesionales_profesiones as pp') ->whereColumn('pp.profesional_id', 'p.id') ->where('pp.profesion_id', $profesionFormularioId); }); }) ->whereRaw("LOWER(TRIM(pf.estado)) <> 'rechazado'") ->exists(); if (!$hayActivosSinRechazar) { Formulario::query() ->where('id', $formularioId) ->update(['estado' => 'Rechazado por todos']); $actualizados++; } } $this->info('Formularios actualizados: ' . $actualizados); })->purpose('Sincroniza formularios pendientes a Rechazado por todos cuando todos los profesionales activos rechazaron'); Artisan::command('backup:daily-email {--keep=4}', function () { $includeEnv = (bool) filter_var((string) env('BACKUP_INCLUDE_ENV', false), FILTER_VALIDATE_BOOLEAN); $archivePassword = trim((string) env('BACKUP_ARCHIVE_PASSWORD', '')); if ($includeEnv && $archivePassword === '') { $this->error('BACKUP_INCLUDE_ENV=true requiere BACKUP_ARCHIVE_PASSWORD configurado para cifrar el ZIP.'); return 1; } $this->info('Iniciando backup semanal de base de datos y archivos...'); $backupExitCode = Artisan::call('backup:run'); $this->line(Artisan::output()); if ($backupExitCode !== 0) { $this->error('No se pudo generar el backup.'); return 1; } $disk = Storage::disk('local'); $backupFolder = trim((string) config('backup.backup.name', 'Laravel'), '/'); $files = collect($disk->files($backupFolder)) ->filter(fn (string $file): bool => str_ends_with(strtolower($file), '.zip')) ->sortByDesc(fn (string $file): int => $disk->lastModified($file)) ->values(); if ($files->isEmpty()) { $this->error('No se encontró el archivo ZIP del backup para enviar por email.'); return 1; } $latestBackup = (string) $files->first(); $adminRecipients = Administrador::query() ->select('correo') ->whereNotNull('correo') ->pluck('correo') ->map(fn ($correo): string => trim((string) $correo)) ->filter(fn (string $correo): bool => $correo !== '' && filter_var($correo, FILTER_VALIDATE_EMAIL) !== false) ->unique() ->values() ->all(); $fallbackRecipient = (string) env('BACKUP_MAIL_TO', (string) config('backup.notifications.mail.to')); $recipients = !empty($adminRecipients) ? $adminRecipients : array_values(array_filter([(string) $fallbackRecipient])); if (empty($recipients)) { $this->error('No hay correos de administradores ni BACKUP_MAIL_TO configurado para enviar el backup.'); return 1; } try { Mail::raw('Se adjunta el backup semanal de base de datos y archivos.', function ($message) use ($disk, $latestBackup, $recipients): void { $message->to($recipients) ->subject('Backup semanal - ' . now()->format('d/m/Y')) ->attachData( $disk->get($latestBackup), basename($latestBackup), ['mime' => 'application/zip'] ); }); } catch (\Throwable $e) { $this->error('El backup se generó, pero falló el envío de email: ' . $e->getMessage()); return 1; } $keep = max(1, (int) $this->option('keep')); $files->slice($keep)->each(function (string $file) use ($disk): void { $disk->delete($file); }); $deletedCount = max(0, $files->count() - $keep); $this->info('Backup enviado a: ' . implode(', ', $recipients)); $this->info('Backups eliminados por retención: ' . $deletedCount); $this->info('Backups conservados: ' . $keep); return 0; })->purpose('Genera backup semanal de BD y archivos, lo envía por mail y conserva solo los últimos N backups'); Schedule::command('logs:purge-old-security')->daily(); Schedule::command('backup:daily-email --keep=4')->dailyAt('16:00'); //ejecuta backup:run Schedule::command('backup:clean')->dailyAt('16:20'); Schedule::command('backup:monitor')->dailyAt('16:30');