230 lines
7.8 KiB
PHP
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";
|
|
}
|
|
}
|