comment(Inspiring::quote()); })->purpose('Display an inspiring quote'); Artisan::command('reservations:expire', function (): void { $expired = app(ExpireStockReservationsService::class)->expireOverdue(); $this->info("Expired purchases: {$expired['purchases']}"); $this->info("Expired cart reservations: {$expired['cart_reservations']}"); $this->info("Expired orphan reservations: {$expired['orphan_reservations']}"); $this->info("Failed reservations: {$expired['failed']}"); })->purpose('Release expired stock reservations from purchases and abandoned carts'); Schedule::command('reservations:expire') ->everyMinute() ->withoutOverlapping(); Artisan::command( 'load-test:tickets:prepare {tenant : Código del tenant que recibirá los datos de carga} {--tickets=1000 : Cantidad de tickets válidos} {--scanners=10 : Cantidad de identidades scanner} {--owners=100 : Cantidad de propietarios de tickets} {--catalog-item= : Producto estándar con tickets habilitados} {--variant= : Variante opcional que define la vigencia} {--run= : Identificador opcional de la ejecución} {--output= : Ruta relativa dentro del disco local}', function (LoadTestTicketDatasetService $datasetService): int { if (! app()->environment(['local', 'testing', 'staging', 'homo', 'homologation'])) { $this->error('Este comando sólo puede ejecutarse en local u homologación.'); return self::FAILURE; } $requestedOutput = filled($this->option('output')) ? ltrim((string) $this->option('output'), '/\\') : null; if ($requestedOutput !== null && ($requestedOutput === '' || str_contains(str_replace('\\', '/', $requestedOutput), '../'))) { $this->error('La ruta de salida debe permanecer dentro del disco local.'); return self::FAILURE; } try { $dataset = $datasetService->prepare( (string) $this->argument('tenant'), (int) $this->option('tickets'), (int) $this->option('scanners'), (int) $this->option('owners'), filled($this->option('catalog-item')) ? (int) $this->option('catalog-item') : null, filled($this->option('variant')) ? (int) $this->option('variant') : null, filled($this->option('run')) ? (string) $this->option('run') : null, ); } catch (Throwable $exception) { $this->error($exception->getMessage()); return self::FAILURE; } $relativePath = $requestedOutput !== null ? $requestedOutput : "load-tests/{$dataset['run_id']}.postman.json"; $payload = json_encode($dataset['rows'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); if (! is_string($payload) || ! Storage::disk('local')->put($relativePath, $payload.PHP_EOL)) { $this->error('No se pudo escribir el dataset.'); return self::FAILURE; } $this->info('Dataset de carga generado.'); $this->table(['Dato', 'Valor'], [ ['run_id', $dataset['run_id']], ['tenant', $dataset['tenant_code']], ['catalog_item_id', $dataset['catalog_item_id']], ['variant_id', $dataset['variant_id'] ?? 'sin variante (vigencia irrestricta)'], ['tickets', $dataset['tickets']], ['scanners', $dataset['scanners']], ['owners', $dataset['owners']], ['archivo', Storage::disk('local')->path($relativePath)], ]); $this->warn('El archivo contiene tokens secretos. No lo subas al repositorio.'); $this->comment("Limpieza: php artisan tenants:reset-transactions {$dataset['tenant_code']}"); return self::SUCCESS; }, )->purpose('Prepare disposable scanner tickets and a Postman performance dataset'); Artisan::command( 'tenants:reset-transactions {tenant : Código del tenant que se limpiará} {--dry-run : Mostrar el alcance sin modificar datos} {--force : Omitir la confirmación interactiva}', function ( TenantTransactionResetService $resetService, AdminCredentialVerifier $adminCredentialVerifier, ): int { $tenantCode = (string) $this->argument('tenant'); try { $preview = $resetService->preview($tenantCode); } catch (InvalidArgumentException $exception) { $this->error($exception->getMessage()); return self::FAILURE; } $this->warn("Se eliminarán los datos transaccionales de: {$tenantCode}"); $this->table( ['Dato', 'Cantidad'], collect($preview)->map(fn (int $count, string $label): array => [$label, $count])->values(), ); $this->info('Los usuarios, el catálogo, los eventos y la configuración se conservarán.'); if ($this->option('dry-run')) { $this->comment('Vista previa finalizada; no se modificaron datos.'); return self::SUCCESS; } if (! $this->option('force') && ! $this->confirm('¿Confirmás esta limpieza irreversible?')) { $this->comment('Operación cancelada.'); return self::SUCCESS; } $this->newLine(); $this->warn('Autorización administrativa requerida.'); $adminEmail = (string) $this->ask('Email del administrador'); $adminPassword = (string) $this->secret('Contraseña del administrador'); if (! $adminCredentialVerifier->verify($adminEmail, $adminPassword)) { $this->error('Las credenciales no son válidas o el usuario no posee el rol admin.'); return self::FAILURE; } $summary = $resetService->reset($tenantCode); $this->info('Limpieza completada correctamente.'); $this->table( ['Resultado', 'Cantidad'], collect($summary)->map(fn (int $count, string $label): array => [$label, $count])->values(), ); return self::SUCCESS; }, )->purpose('Delete tenant sales, carts, tickets and reservations while preserving users and catalog');