feat(tickets): enhance AdminAppTicketIndexRequest and AdminAppTicketService with additional filters and update tests for ticket filtering functionality
This commit is contained in:
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
namespace App\Domains\Ticket\Requests;
|
namespace App\Domains\Ticket\Requests;
|
||||||
|
|
||||||
|
use App\Domains\Ticket\Models\Ticket;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
class AdminAppTicketIndexRequest extends FormRequest
|
class AdminAppTicketIndexRequest extends FormRequest
|
||||||
{
|
{
|
||||||
@@ -16,6 +18,19 @@ class AdminAppTicketIndexRequest extends FormRequest
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'q' => ['sometimes', 'nullable', 'string', 'max:255'],
|
'q' => ['sometimes', 'nullable', 'string', 'max:255'],
|
||||||
|
'category' => ['sometimes', 'nullable', 'string', 'max:255'],
|
||||||
|
'product' => ['sometimes', 'nullable', 'string', 'max:255'],
|
||||||
|
'type' => ['sometimes', 'nullable', 'string', 'max:255'],
|
||||||
|
'date' => ['sometimes', 'nullable', 'date_format:Y-m-d'],
|
||||||
|
'status' => [
|
||||||
|
'sometimes',
|
||||||
|
'nullable',
|
||||||
|
Rule::in([
|
||||||
|
Ticket::STATUS_ACTIVE,
|
||||||
|
Ticket::STATUS_USED,
|
||||||
|
Ticket::STATUS_EXPIRED,
|
||||||
|
]),
|
||||||
|
],
|
||||||
'page' => ['sometimes', 'integer', 'min:1'],
|
'page' => ['sometimes', 'integer', 'min:1'],
|
||||||
'per_page' => ['sometimes', 'integer', 'min:1', 'max:100'],
|
'per_page' => ['sometimes', 'integer', 'min:1', 'max:100'],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use Illuminate\Database\Eloquent\Builder;
|
|||||||
class AdminAppTicketService
|
class AdminAppTicketService
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @param array{q?: string|null, page?: int, per_page?: int} $filters
|
* @param array{q?: string|null, category?: string|null, product?: string|null, type?: string|null, date?: string|null, status?: string|null, page?: int, per_page?: int} $filters
|
||||||
*/
|
*/
|
||||||
public function search(Tenant $tenant, array $filters = []): AdminAppTicketResult
|
public function search(Tenant $tenant, array $filters = []): AdminAppTicketResult
|
||||||
{
|
{
|
||||||
@@ -36,14 +36,14 @@ class AdminAppTicketService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array{q?: string|null, page?: int, per_page?: int} $filters
|
* @param array{q?: string|null, category?: string|null, product?: string|null, type?: string|null, date?: string|null, status?: string|null, page?: int, per_page?: int} $filters
|
||||||
* @return Builder<Ticket>
|
* @return Builder<Ticket>
|
||||||
*/
|
*/
|
||||||
private function baseQuery(Tenant $tenant, array $filters): Builder
|
private function baseQuery(Tenant $tenant, array $filters): Builder
|
||||||
{
|
{
|
||||||
$search = trim((string) ($filters['q'] ?? ''));
|
$search = trim((string) ($filters['q'] ?? ''));
|
||||||
|
|
||||||
return Ticket::query()
|
$query = Ticket::query()
|
||||||
->where('tenant_code', $tenant->codigo)
|
->where('tenant_code', $tenant->codigo)
|
||||||
->when($search !== '', function (Builder $query) use ($search): void {
|
->when($search !== '', function (Builder $query) use ($search): void {
|
||||||
$query->when(
|
$query->when(
|
||||||
@@ -53,6 +53,102 @@ class AdminAppTicketService
|
|||||||
fn (Builder $searchQuery): Builder => $searchQuery
|
fn (Builder $searchQuery): Builder => $searchQuery
|
||||||
->where('ticket', 'like', "%{$search}%"),
|
->where('ticket', 'like', "%{$search}%"),
|
||||||
);
|
);
|
||||||
|
})
|
||||||
|
->when($filters['category'] ?? null, function (Builder $query, string $category): void {
|
||||||
|
$query->whereHas('sourceCatalogItem.category', fn (Builder $categoryQuery): Builder => $categoryQuery
|
||||||
|
->whereRaw('LOWER(nombre) = ?', [mb_strtolower(trim($category))]));
|
||||||
|
})
|
||||||
|
->when($filters['product'] ?? null, function (Builder $query, string $product) use ($filters): void {
|
||||||
|
$this->applyProductFilter($query, (string) ($filters['category'] ?? ''), $product);
|
||||||
|
})
|
||||||
|
->when($filters['type'] ?? null, function (Builder $query, string $type) use ($filters): void {
|
||||||
|
$this->applyTypeFilter($query, (string) ($filters['category'] ?? ''), $type);
|
||||||
|
})
|
||||||
|
->when($filters['date'] ?? null, fn (Builder $query, string $date): Builder => $query
|
||||||
|
->whereHas('sourcePurchase', fn (Builder $purchaseQuery): Builder => $purchaseQuery
|
||||||
|
->whereDate('created_at', $date)));
|
||||||
|
|
||||||
|
$this->applyStatusFilter($query, $filters['status'] ?? null);
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param Builder<Ticket> $query */
|
||||||
|
private function applyProductFilter(Builder $query, string $category, string $product): void
|
||||||
|
{
|
||||||
|
$category = $this->normalizedCategory($category);
|
||||||
|
|
||||||
|
if (in_array($category, ['alojamientos', 'camping'], true)) {
|
||||||
|
$this->whereVariantDefinition($query, 'tipo_alojamiento', $product);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($category, ['comidas', 'comida'], true)) {
|
||||||
|
$query->whereHas('sourceVariant', function (Builder $variantQuery) use ($product): void {
|
||||||
|
$variantQuery->where(function (Builder $dateQuery) use ($product): void {
|
||||||
|
$dateQuery
|
||||||
|
->where('event_date_id', $product)
|
||||||
|
->orWhereHas('eventDates', fn (Builder $eventDateQuery): Builder => $eventDateQuery
|
||||||
|
->whereKey($product));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query->whereHas('sourceCatalogItem', fn (Builder $itemQuery): Builder => $itemQuery
|
||||||
|
->where('slug', $product));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param Builder<Ticket> $query */
|
||||||
|
private function applyTypeFilter(Builder $query, string $category, string $type): void
|
||||||
|
{
|
||||||
|
$attribute = match ($this->normalizedCategory($category)) {
|
||||||
|
'comidas', 'comida' => 'horario',
|
||||||
|
'merchandising' => 'color',
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($attribute !== null) {
|
||||||
|
$this->whereVariantDefinition($query, $attribute, $type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param Builder<Ticket> $query */
|
||||||
|
private function whereVariantDefinition(Builder $query, string $attribute, string $value): void
|
||||||
|
{
|
||||||
|
$query->whereHas('sourceVariant.definitions', fn (Builder $definitionQuery): Builder => $definitionQuery
|
||||||
|
->where('value', $value)
|
||||||
|
->whereHas('itemAttribute.attribute', fn (Builder $attributeQuery): Builder => $attributeQuery
|
||||||
|
->where('codigo', $attribute)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param Builder<Ticket> $query */
|
||||||
|
private function applyStatusFilter(Builder $query, ?string $status): void
|
||||||
|
{
|
||||||
|
if ($status === null || $status === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($status === Ticket::STATUS_USED) {
|
||||||
|
$query->whereNotNull('used_at');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$matchingIds = (clone $query)
|
||||||
|
->whereNull('used_at')
|
||||||
|
->with(TicketValidityResolver::RELATIONS)
|
||||||
|
->get()
|
||||||
|
->filter(fn (Ticket $ticket): bool => $ticket->status === $status)
|
||||||
|
->pluck('id');
|
||||||
|
|
||||||
|
$query->whereIn('tickets.id', $matchingIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizedCategory(string $category): string
|
||||||
|
{
|
||||||
|
return mb_strtolower(trim($category));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ class AdminAppTicketFilterFormControllerTest extends TestCase
|
|||||||
});
|
});
|
||||||
$this->assertNotNull($historicalVariant);
|
$this->assertNotNull($historicalVariant);
|
||||||
|
|
||||||
Ticket::query()->create([
|
$ticket = Ticket::query()->create([
|
||||||
'tenant_code' => $tenant->codigo,
|
'tenant_code' => $tenant->codigo,
|
||||||
'ticket' => (string) Str::uuid(),
|
'ticket' => (string) Str::uuid(),
|
||||||
'user_id' => $admin->id,
|
'user_id' => $admin->id,
|
||||||
@@ -206,6 +206,15 @@ class AdminAppTicketFilterFormControllerTest extends TestCase
|
|||||||
['Cena'],
|
['Cena'],
|
||||||
collect($products->first()['children']['options'])->pluck('label')->all(),
|
collect($products->first()['children']['options'])->pluck('label')->all(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$this->getJson('/api/v1/adminapp/tenant/tickets?'.http_build_query([
|
||||||
|
'category' => 'comidas',
|
||||||
|
'product' => $products->first()['value'],
|
||||||
|
'type' => 'Cena',
|
||||||
|
]))
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonCount(1, 'data')
|
||||||
|
->assertJsonPath('data.0.id', $ticket->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createFiestaFutbolInfantilTenant(): Tenant
|
private function createFiestaFutbolInfantilTenant(): Tenant
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ use App\Domains\Shared\Enums\FieldType;
|
|||||||
use App\Domains\Tenant\Models\Tenant;
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
use App\Domains\Tenant\Models\WebsiteType;
|
use App\Domains\Tenant\Models\WebsiteType;
|
||||||
use App\Domains\Ticket\Models\Ticket;
|
use App\Domains\Ticket\Models\Ticket;
|
||||||
|
use Database\Seeders\AttributeSeeder;
|
||||||
use Database\Seeders\AuthorizationSeeder;
|
use Database\Seeders\AuthorizationSeeder;
|
||||||
|
use Database\Seeders\FiestaFutbolInfantilProductSeeder;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Laravel\Sanctum\Sanctum;
|
use Laravel\Sanctum\Sanctum;
|
||||||
@@ -188,6 +190,100 @@ class AdminAppTicketControllerTest extends TestCase
|
|||||||
->assertJsonPath('data.0.variant_properties.0.values.0.label', 'XL');
|
->assertJsonPath('data.0.variant_properties.0.values.0.label', 'XL');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_it_applies_the_ticket_filter_form_values(): void
|
||||||
|
{
|
||||||
|
$tenant = $this->createTenant('fiesta_futbol_infantil');
|
||||||
|
$admin = $this->createAdminAppUser($tenant);
|
||||||
|
$this->grantTicketsMenu($tenant);
|
||||||
|
$this->seed([AttributeSeeder::class, FiestaFutbolInfantilProductSeeder::class]);
|
||||||
|
Sanctum::actingAs($admin);
|
||||||
|
|
||||||
|
$date = $tenant->eventDates()->orderByDesc('date')->firstOrFail();
|
||||||
|
$food = CatalogItem::query()
|
||||||
|
->where('tenant_code', $tenant->codigo)
|
||||||
|
->where('slug', 'comida')
|
||||||
|
->with([
|
||||||
|
'variants.definitions.itemAttribute.attribute.options',
|
||||||
|
'variants.eventDates',
|
||||||
|
'variants.eventDate',
|
||||||
|
])
|
||||||
|
->firstOrFail();
|
||||||
|
$dinner = $food->variants->first(fn (Variant $variant): bool => $variant
|
||||||
|
->selectedEventDates()->contains('id', $date->id)
|
||||||
|
&& $variant->selectionValues()->get('horario') === 'Cena');
|
||||||
|
$lunch = $food->variants->first(fn (Variant $variant): bool => $variant
|
||||||
|
->selectedEventDates()->contains('id', $date->id)
|
||||||
|
&& $variant->selectionValues()->get('horario') === 'Almuerzo');
|
||||||
|
$this->assertNotNull($dinner);
|
||||||
|
$this->assertNotNull($lunch);
|
||||||
|
|
||||||
|
$matchingPurchase = $this->createPurchase($tenant, $admin, '2026-08-20 10:00:00');
|
||||||
|
$otherPurchase = $this->createPurchase($tenant, $admin, '2026-08-21 10:00:00');
|
||||||
|
$matching = $this->createTicket($tenant, $admin, [
|
||||||
|
'source_purchase_id' => $matchingPurchase->id,
|
||||||
|
'source_catalog_item_id' => $food->id,
|
||||||
|
'source_variant_id' => $dinner->id,
|
||||||
|
'used_at' => now(),
|
||||||
|
]);
|
||||||
|
$this->createTicket($tenant, $admin, [
|
||||||
|
'source_purchase_id' => $matchingPurchase->id,
|
||||||
|
'source_catalog_item_id' => $food->id,
|
||||||
|
'source_variant_id' => $lunch->id,
|
||||||
|
'used_at' => now(),
|
||||||
|
]);
|
||||||
|
$this->createTicket($tenant, $admin, [
|
||||||
|
'source_purchase_id' => $otherPurchase->id,
|
||||||
|
'source_catalog_item_id' => $food->id,
|
||||||
|
'source_variant_id' => $dinner->id,
|
||||||
|
'used_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->getJson('/api/v1/adminapp/tenant/tickets?'.http_build_query([
|
||||||
|
'category' => 'comidas',
|
||||||
|
'product' => (string) $date->id,
|
||||||
|
'type' => 'Cena',
|
||||||
|
'date' => '2026-08-20',
|
||||||
|
'status' => Ticket::STATUS_USED,
|
||||||
|
]))
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonCount(1, 'data')
|
||||||
|
->assertJsonPath('data.0.id', $matching->id)
|
||||||
|
->assertJsonPath('scanned_tickets', 1)
|
||||||
|
->assertJsonPath('total_tickets', 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_filters_computed_active_and_expired_statuses(): void
|
||||||
|
{
|
||||||
|
$tenant = $this->createTenant('fiesta_futbol_infantil');
|
||||||
|
$admin = $this->createAdminAppUser($tenant);
|
||||||
|
$this->grantTicketsMenu($tenant);
|
||||||
|
$this->seed([AttributeSeeder::class, FiestaFutbolInfantilProductSeeder::class]);
|
||||||
|
Sanctum::actingAs($admin);
|
||||||
|
|
||||||
|
$food = CatalogItem::query()
|
||||||
|
->where('tenant_code', $tenant->codigo)
|
||||||
|
->where('slug', 'comida')
|
||||||
|
->with('variants')
|
||||||
|
->firstOrFail();
|
||||||
|
$expired = $this->createTicket($tenant, $admin, [
|
||||||
|
'source_catalog_item_id' => $food->id,
|
||||||
|
'source_variant_id' => $food->variants->firstOrFail()->id,
|
||||||
|
]);
|
||||||
|
$active = $this->createTicket($tenant, $admin);
|
||||||
|
|
||||||
|
$this->travelTo('2026-10-20 12:00:00');
|
||||||
|
|
||||||
|
$this->getJson('/api/v1/adminapp/tenant/tickets?status=expired')
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonCount(1, 'data')
|
||||||
|
->assertJsonPath('data.0.id', $expired->id);
|
||||||
|
|
||||||
|
$this->getJson('/api/v1/adminapp/tenant/tickets?status=active')
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonCount(1, 'data')
|
||||||
|
->assertJsonPath('data.0.id', $active->id);
|
||||||
|
}
|
||||||
|
|
||||||
private function createTenant(string $code): Tenant
|
private function createTenant(string $code): Tenant
|
||||||
{
|
{
|
||||||
$headerLogo = $this->createAttachment("{$code}-header.png");
|
$headerLogo = $this->createAttachment("{$code}-header.png");
|
||||||
@@ -227,6 +323,20 @@ class AdminAppTicketControllerTest extends TestCase
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function createPurchase(Tenant $tenant, User $user, string $createdAt): Purchase
|
||||||
|
{
|
||||||
|
$purchase = Purchase::query()->create([
|
||||||
|
'tenant_codigo' => $tenant->codigo,
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'status' => Purchase::STATUS_PAID,
|
||||||
|
'nombre_apellido' => $user->nombre_apellido,
|
||||||
|
'total' => '0.00',
|
||||||
|
]);
|
||||||
|
$purchase->forceFill(['created_at' => $createdAt, 'updated_at' => $createdAt])->saveQuietly();
|
||||||
|
|
||||||
|
return $purchase;
|
||||||
|
}
|
||||||
|
|
||||||
private function grantTicketsMenu(Tenant $tenant): void
|
private function grantTicketsMenu(Tenant $tenant): void
|
||||||
{
|
{
|
||||||
$menu = Menu::query()->create([
|
$menu = Menu::query()->create([
|
||||||
|
|||||||
Reference in New Issue
Block a user