* } */ public function prepare( string $tenantCode, int $ticketCount, int $scannerCount, int $ownerCount, ?int $catalogItemId = null, ?int $variantId = null, ?string $runId = null, ): array { $this->validateInput($tenantCode, $ticketCount, $scannerCount, $ownerCount); $tenant = Tenant::query()->where('codigo', $tenantCode)->firstOrFail(); $catalogItem = $this->resolveCatalogItem($tenant, $catalogItemId); $variant = $this->resolveVariant($catalogItem, $variantId); $runId ??= now()->format('Ymd-His').'-'.Str::lower(Str::random(6)); if (preg_match('/\A[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}\z/', $runId) !== 1) { throw new InvalidArgumentException('run_id contiene caracteres inválidos o es demasiado largo.'); } if ($variant !== null && ! $this->validityResolver->resolveVariant($variant)->isValid()) { throw new InvalidArgumentException( 'La variante seleccionada no tiene una vigencia activa y resoluble.' ); } $scanners = $this->scanners($tenant, $catalogItem, $scannerCount); $owners = $this->owners($tenant, $ownerCount); $tokens = $scanners->map(function (User $scanner): string { $scanner->tokens()->where('name', 'load-test-scanner')->delete(); return $scanner->createToken( 'load-test-scanner', ['scanner'], now()->addMinutes((int) config('sanctum.expiration', 720)), )->plainTextToken; })->values(); $rows = []; $remaining = $ticketCount; $ownerIndex = 0; $scannerIndex = 0; $batchSize = min(500, max(1, (int) ceil($ticketCount / $ownerCount))); while ($remaining > 0) { $quantity = min($batchSize, $remaining); $owner = $owners[$ownerIndex % $owners->count()]; $tickets = $this->ticketGenerator->generate( $catalogItem, $owner, $quantity, $variant?->getKey(), ); foreach ($tickets as $ticket) { if (! $ticket->is_valid) { throw new InvalidArgumentException( 'La configuración seleccionada genera tickets que no están vigentes.' ); } $rows[] = [ 'scanner_token' => $tokens[$scannerIndex % $tokens->count()], 'ticket_uuid' => $ticket->ticket, 'expected_status' => 200, ]; $scannerIndex++; } $remaining -= $quantity; $ownerIndex++; } return [ 'run_id' => $runId, 'tenant_code' => $tenant->codigo, 'catalog_item_id' => $catalogItem->getKey(), 'variant_id' => $variant?->getKey(), 'tickets' => count($rows), 'scanners' => $scanners->count(), 'owners' => $owners->count(), 'rows' => $rows, ]; } private function validateInput( string $tenantCode, int $ticketCount, int $scannerCount, int $ownerCount, ): void { if (! str_starts_with($tenantCode, 'loadtest-')) { throw new InvalidArgumentException('El tenant debe comenzar con `loadtest-`.'); } foreach ([ 'tickets' => [$ticketCount, 100_000], 'scanners' => [$scannerCount, 10_000], 'owners' => [$ownerCount, 100_000], ] as $name => [$value, $maximum]) { if ($value < 1 || $value > $maximum) { throw new InvalidArgumentException("{$name} debe estar entre 1 y {$maximum}."); } } if ($scannerCount > $ticketCount || $ownerCount > $ticketCount) { throw new InvalidArgumentException( 'La cantidad de scanners y propietarios no puede superar la cantidad de tickets.' ); } } private function resolveCatalogItem(Tenant $tenant, ?int $catalogItemId): CatalogItem { $query = $tenant->catalogItems() ->where('has_tickets', true) ->where('type', CatalogItemType::Standard->value); if ($catalogItemId !== null) { $query->whereKey($catalogItemId); } $catalogItem = $query->first(); if ($catalogItem === null) { throw new InvalidArgumentException( 'No se encontró un producto estándar con tickets habilitados para el tenant.' ); } if ($tenant->requiresScannerCategoryValidation() && $catalogItem->category_id === null) { throw new InvalidArgumentException( 'El producto debe tener una categoría para autorizar a los scanners.' ); } return $catalogItem; } private function resolveVariant(CatalogItem $catalogItem, ?int $variantId): ?Variant { if ($variantId === null) { return null; } $variant = $catalogItem->variants()->whereKey($variantId)->first(); if ($variant === null) { throw new InvalidArgumentException('La variante no pertenece al producto seleccionado.'); } return $variant; } /** @return Collection */ private function scanners(Tenant $tenant, CatalogItem $catalogItem, int $count): Collection { return Collection::times($count, function (int $number) use ($tenant, $catalogItem): User { $scanner = User::withTrashed()->updateOrCreate( ['email' => $this->email($tenant, 'scanner', $number)], [ 'nombre_apellido' => "Load test scanner {$number}", 'password' => Str::password(32), 'rol_codigo' => RoleCode::Scanner->value, 'tenant_codigo' => $tenant->codigo, ], ); $scanner->restore(); if ($tenant->requiresScannerCategoryValidation()) { $scanner->scanCategories()->syncWithoutDetaching([$catalogItem->category_id]); } return $scanner; }); } /** @return Collection */ private function owners(Tenant $tenant, int $count): Collection { return Collection::times($count, function (int $number) use ($tenant): User { $owner = User::withTrashed()->updateOrCreate( ['email' => $this->email($tenant, 'owner', $number)], [ 'nombre_apellido' => "Load test owner {$number}", 'password' => Str::password(32), 'rol_codigo' => RoleCode::User->value, 'tenant_codigo' => $tenant->codigo, ], ); $owner->restore(); return $owner; }); } private function email(Tenant $tenant, string $kind, int $number): string { $tenantSlug = Str::lower(preg_replace('/[^a-z0-9]+/i', '-', $tenant->codigo)); return "loadtest+{$tenantSlug}.{$kind}.{$number}@shopit.test"; } }