From d8d8354070da1800b02a221d0827b03073c90a1f Mon Sep 17 00:00:00 2001 From: ncoronel Date: Thu, 24 Sep 2026 10:02:26 -0300 Subject: [PATCH] feat(desfile): list paginated entry reservations --- .../EntryReservationController.php | 8 +++ .../IndexEntryReservationsRequest.php | 24 +++++++ .../Resources/EntryReservationResource.php | 26 ++++++++ .../Services/EntryReservationService.php | 34 ++++++++++ app/Domains/Ticketing/Desfile/routes/api.php | 3 + .../Desfile/EntryReservationServiceTest.php | 63 ++++++++++++++++++- 6 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 app/Domains/Ticketing/Desfile/Requests/IndexEntryReservationsRequest.php diff --git a/app/Domains/Ticketing/Desfile/Controllers/EntryReservationController.php b/app/Domains/Ticketing/Desfile/Controllers/EntryReservationController.php index 7376e2a..383a75b 100644 --- a/app/Domains/Ticketing/Desfile/Controllers/EntryReservationController.php +++ b/app/Domains/Ticketing/Desfile/Controllers/EntryReservationController.php @@ -2,6 +2,7 @@ namespace App\Domains\Ticketing\Desfile\Controllers; +use App\Domains\Ticketing\Desfile\Requests\IndexEntryReservationsRequest; use App\Domains\Ticketing\Desfile\Requests\StoreEntryReservationsRequest; use App\Domains\Ticketing\Desfile\Resources\EntryReservationResource; use App\Domains\Ticketing\Desfile\Services\EntryReservationService; @@ -10,6 +11,13 @@ use Illuminate\Http\Resources\Json\AnonymousResourceCollection; class EntryReservationController extends Controller { + public function index(IndexEntryReservationsRequest $request, EntryReservationService $service): AnonymousResourceCollection + { + return EntryReservationResource::collection( + $service->reservations($request->user(), $request->validated()), + ); + } + public function store(StoreEntryReservationsRequest $request, EntryReservationService $service): AnonymousResourceCollection { return EntryReservationResource::collection($service->reserve( diff --git a/app/Domains/Ticketing/Desfile/Requests/IndexEntryReservationsRequest.php b/app/Domains/Ticketing/Desfile/Requests/IndexEntryReservationsRequest.php new file mode 100644 index 0000000..e546136 --- /dev/null +++ b/app/Domains/Ticketing/Desfile/Requests/IndexEntryReservationsRequest.php @@ -0,0 +1,24 @@ +user()?->tenant_codigo === 'desfile_pura_tendencia'; + } + + public function rules(): array + { + return [ + 'tipo_pago' => ['sometimes', 'nullable', Rule::enum(EntryReservationPaymentType::class)], + 'page' => ['sometimes', 'integer', 'min:1'], + 'per_page' => ['sometimes', 'integer', 'min:1', 'max:100'], + ]; + } +} diff --git a/app/Domains/Ticketing/Desfile/Resources/EntryReservationResource.php b/app/Domains/Ticketing/Desfile/Resources/EntryReservationResource.php index 29d9e73..9ef77d3 100644 --- a/app/Domains/Ticketing/Desfile/Resources/EntryReservationResource.php +++ b/app/Domains/Ticketing/Desfile/Resources/EntryReservationResource.php @@ -9,13 +9,39 @@ class EntryReservationResource extends JsonResource { public function toArray(Request $request): array { + $selection = $this->relationLoaded('variant') + ? $this->variant->selectionOptions() + : collect(); + return [ 'id' => $this->id, 'variant_id' => $this->variant_id, 'ticket_id' => $this->ticket_id, 'fecha_reserva' => $this->fecha_reserva->toIso8601String(), 'tipo_pago' => $this->tipo_pago->value, + 'tipo_pago_label' => $this->tipo_pago->label(), 'importe' => $this->importe, + 'entrada' => [ + 'tipo' => $this->selectionLabel($selection->get('tipo')), + 'sector' => $this->selectionLabel($selection->get('sector')), + 'fila' => $this->selectionLabel($selection->get('fila')), + 'asiento' => $this->selectionLabel($selection->get('asiento')), + ], ]; } + + private function selectionLabel(mixed $selection): ?string + { + if (! is_array($selection)) { + return null; + } + + if (array_is_list($selection)) { + $labels = collect($selection)->pluck('label')->filter()->implode(', '); + + return $labels !== '' ? $labels : null; + } + + return isset($selection['label']) ? (string) $selection['label'] : null; + } } diff --git a/app/Domains/Ticketing/Desfile/Services/EntryReservationService.php b/app/Domains/Ticketing/Desfile/Services/EntryReservationService.php index dc92183..c21a763 100644 --- a/app/Domains/Ticketing/Desfile/Services/EntryReservationService.php +++ b/app/Domains/Ticketing/Desfile/Services/EntryReservationService.php @@ -11,6 +11,8 @@ use App\Domains\Ticketing\Desfile\Enums\EntryReservationPaymentType; use App\Domains\Ticketing\Desfile\Models\EntryReservation; use App\Domains\Ticketing\Ticket\Exceptions\TicketGenerationException; use App\Domains\Ticketing\Ticket\Services\TicketGeneratorService; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; use Illuminate\Validation\ValidationException; @@ -19,6 +21,38 @@ class EntryReservationService { public function __construct(private readonly TicketGeneratorService $tickets) {} + /** + * @param array{tipo_pago?: string|null, page?: int, per_page?: int} $filters + * @return LengthAwarePaginator + */ + public function reservations(User $user, array $filters = []): LengthAwarePaginator + { + abort_unless($user->tenant_codigo === 'desfile_pura_tendencia', 403); + + return EntryReservation::query() + ->whereHas('variant.catalogItem', fn (Builder $query): Builder => $query + ->where('tenant_code', $user->tenant_codigo) + ->where('slug', 'entrada')) + ->when( + $filters['tipo_pago'] ?? null, + fn (Builder $query, string $paymentType): Builder => $query->where('tipo_pago', $paymentType), + ) + ->with([ + 'variant.catalogItem.itemAttributes.attribute.options', + 'variant.definitions.itemAttribute.attribute.options', + 'variant.eventDates', + 'variant.eventDate', + ]) + ->orderByDesc('fecha_reserva') + ->orderByDesc('id') + ->paginate( + perPage: $filters['per_page'] ?? 15, + pageName: 'page', + page: $filters['page'] ?? 1, + ) + ->withQueryString(); + } + /** @param list $rows */ public function reserve(User $user, string $key, array $rows): Collection { diff --git a/app/Domains/Ticketing/Desfile/routes/api.php b/app/Domains/Ticketing/Desfile/routes/api.php index df2e545..40c4305 100644 --- a/app/Domains/Ticketing/Desfile/routes/api.php +++ b/app/Domains/Ticketing/Desfile/routes/api.php @@ -4,6 +4,9 @@ use App\Domains\Ticketing\Desfile\Controllers\EntryController; use App\Domains\Ticketing\Desfile\Controllers\EntryReservationController; use Illuminate\Support\Facades\Route; +Route::get('v1/adminapp/tenant/desfile/entry-reservations', [EntryReservationController::class, 'index']) + ->middleware(['auth:sanctum', 'adminapp.tenant', 'tenant.menu:adminapp.desfile.reservas']) + ->name('adminapp.desfile.entry-reservations.index'); Route::post('v1/adminapp/tenant/desfile/entry-reservations', [EntryReservationController::class, 'store']) ->middleware(['auth:sanctum', 'adminapp.tenant', 'tenant.menu:adminapp.desfile.reservas']) ->name('adminapp.desfile.entry-reservations.store'); diff --git a/tests/Feature/Desfile/EntryReservationServiceTest.php b/tests/Feature/Desfile/EntryReservationServiceTest.php index 58c4131..a849bf8 100644 --- a/tests/Feature/Desfile/EntryReservationServiceTest.php +++ b/tests/Feature/Desfile/EntryReservationServiceTest.php @@ -4,13 +4,16 @@ namespace Tests\Feature\Desfile; use App\Domains\Commerce\Catalog\Models\Inventory; use App\Domains\Core\Auth\Models\User; +use App\Domains\Ticketing\Desfile\Requests\IndexEntryReservationsRequest; use App\Domains\Ticketing\Desfile\Requests\StoreEntryReservationsRequest; +use App\Domains\Ticketing\Desfile\Resources\EntryReservationResource; use App\Domains\Ticketing\Desfile\Services\EntryReservationService; use App\Domains\Ticketing\Ticket\Models\Ticket; use App\Domains\Ticketing\Ticket\Services\ResolvedTicketValidity; use App\Domains\Ticketing\Ticket\Services\TicketGeneratorService; use App\Domains\Ticketing\Ticket\Services\TicketValidityResolver; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Validator; @@ -75,8 +78,17 @@ class EntryReservationServiceTest extends TestCase }); Schema::create('item_attributes', function (Blueprint $table): void { $table->id(); + $table->foreignId('catalog_item_id'); $table->foreignId('attribute_id'); $table->boolean('allow_multi_select')->default(false); + $table->integer('sort_order')->default(0); + }); + Schema::create('attribute_options', function (Blueprint $table): void { + $table->id(); + $table->foreignId('attribute_id'); + $table->string('value'); + $table->string('label'); + $table->integer('sort_order')->default(0); }); Schema::create('variant_values', function (Blueprint $table): void { $table->id(); @@ -113,7 +125,15 @@ class EntryReservationServiceTest extends TestCase } foreach (['tipo' => 'NORMAL', 'sector' => 'A', 'fila' => '3', 'asiento' => '17'] as $code => $value) { $attributeId = DB::table('attribute')->insertGetId(['codigo' => $code]); - $itemAttributeId = DB::table('item_attributes')->insertGetId(['attribute_id' => $attributeId]); + $itemAttributeId = DB::table('item_attributes')->insertGetId([ + 'catalog_item_id' => 1, + 'attribute_id' => $attributeId, + ]); + DB::table('attribute_options')->insert([ + 'attribute_id' => $attributeId, + 'value' => $value, + 'label' => $value, + ]); foreach ([1, 2] as $variantId) { DB::table('variant_values')->insert([ 'variant_id' => $variantId, 'item_attribute_id' => $itemAttributeId, 'value' => $value, @@ -247,6 +267,32 @@ class EntryReservationServiceTest extends TestCase Inventory::findOrFail(1)->reserve(1, true); } + public function test_lists_paginated_reservations_filtered_by_payment_type(): void + { + $service = $this->service(); + $user = User::findOrFail(1); + $service->reserve($user, (string) Str::uuid(), $this->rows()); + + $reservations = $service->reservations($user, [ + 'tipo_pago' => 'otro_metodo', + 'page' => 1, + 'per_page' => 1, + ]); + + $this->assertSame(1, $reservations->total()); + $this->assertSame(1, $reservations->perPage()); + $reservation = $reservations->sole(); + $this->assertSame('otro_metodo', $reservation->tipo_pago->value); + $this->assertNotNull($reservation->ticket_id); + $this->assertSame('NORMAL', $reservation->variant->selectionValues()->get('tipo')); + + $payload = (new EntryReservationResource($reservation))->toArray(Request::create('/')); + $this->assertSame($reservation->id, $payload['id']); + $this->assertSame($reservation->ticket_id, $payload['ticket_id']); + $this->assertSame('NORMAL', $payload['entrada']['tipo']); + $this->assertSame('Otro método', $payload['tipo_pago_label']); + } + public function test_request_rejects_duplicate_variants_and_client_amounts(): void { $rules = (new StoreEntryReservationsRequest)->rules(); @@ -263,6 +309,21 @@ class EntryReservationServiceTest extends TestCase $this->assertArrayHasKey('rows.1.tipo_pago', $validator->errors()->toArray()); } + public function test_index_request_validates_payment_type_and_pagination(): void + { + $validator = Validator::make([ + 'tipo_pago' => 'invalid', + 'page' => 0, + 'per_page' => 101, + ], (new IndexEntryReservationsRequest)->rules()); + + $this->assertTrue($validator->fails()); + $this->assertSame( + ['tipo_pago', 'page', 'per_page'], + array_keys($validator->errors()->toArray()), + ); + } + public function test_real_ticket_generator_links_admin_variant_and_reservation(): void { $validity = Mockery::mock(TicketValidityResolver::class);