shopit-back/app/Domains/Ticket/Services/LoadTestTicketDatasetServic...

230 lines
7.8 KiB
PHP

<?php
namespace App\Domains\Ticket\Services;
use App\Domains\Auth\Models\User;
use App\Domains\Authorization\Enums\RoleCode;
use App\Domains\Catalog\Enums\CatalogItemType;
use App\Domains\Catalog\Models\CatalogItem;
use App\Domains\Catalog\Models\Variant;
use App\Domains\Tenant\Models\Tenant;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use InvalidArgumentException;
class LoadTestTicketDatasetService
{
public function __construct(
private readonly TicketGeneratorService $ticketGenerator,
private readonly TicketValidityResolver $validityResolver,
) {}
/**
* @return array{
* run_id: string,
* tenant_code: string,
* catalog_item_id: int,
* variant_id: int|null,
* tickets: int,
* scanners: int,
* owners: int,
* rows: array<int, array{scanner_token: string, ticket_uuid: string, expected_status: int}>
* }
*/
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 {
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<int, User> */
private function scanners(Tenant $tenant, CatalogItem $catalogItem, int $count): Collection
{
return Collection::times($count, function (int $number) use ($tenant, $catalogItem): User {
$scanner = User::query()->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,
],
);
if ($tenant->requiresScannerCategoryValidation()) {
$scanner->scanCategories()->syncWithoutDetaching([$catalogItem->category_id]);
}
return $scanner;
});
}
/** @return Collection<int, User> */
private function owners(Tenant $tenant, int $count): Collection
{
return Collection::times($count, function (int $number) use ($tenant): User {
$owner = User::query()->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,
],
);
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";
}
}