feat(desfile): list paginated entry reservations

This commit is contained in:
2026-09-24 10:02:26 -03:00
parent 5bb61fc74c
commit d8d8354070
6 changed files with 157 additions and 1 deletions

View File

@@ -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(

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Domains\Ticketing\Desfile\Requests;
use App\Domains\Ticketing\Desfile\Enums\EntryReservationPaymentType;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class IndexEntryReservationsRequest extends FormRequest
{
public function authorize(): bool
{
return $this->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'],
];
}
}

View File

@@ -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;
}
}

View File

@@ -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<EntryReservation>
*/
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<array{variant_id: int, tipo_pago: string}> $rows */
public function reserve(User $user, string $key, array $rows): Collection
{

View File

@@ -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');

View File

@@ -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);