Compare commits

..

2 Commits

Author SHA1 Message Date
6f191b27f9 feat(seeder): add ticket products to test events 2026-09-22 12:26:34 -03:00
3401c934b2 feat(tickets): group user tickets by event 2026-09-22 12:26:16 -03:00
14 changed files with 449 additions and 34 deletions

View File

@@ -5,6 +5,7 @@ namespace App\Domains\Ticketing\Event\Models;
use App\Domains\Commerce\Catalog\Models\CatalogItem; use App\Domains\Commerce\Catalog\Models\CatalogItem;
use App\Domains\Core\Tenant\Models\SocialMedia; use App\Domains\Core\Tenant\Models\SocialMedia;
use App\Domains\Core\Tenant\Models\Tenant; use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Ticketing\Ticket\Models\Ticket;
use App\Shared\Attachable\Models\Attachment; use App\Shared\Attachable\Models\Attachment;
use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -53,6 +54,12 @@ class Event extends Model
return $this->hasMany(CatalogItem::class)->whereAvailable()->orderBy('group_order')->orderBy('id'); return $this->hasMany(CatalogItem::class)->whereAvailable()->orderBy('group_order')->orderBy('id');
} }
/** @return HasMany<Ticket, $this> */
public function tickets(): HasMany
{
return $this->hasMany(Ticket::class);
}
/** @return BelongsToMany<SocialMedia, $this> */ /** @return BelongsToMany<SocialMedia, $this> */
public function socialMedia(): BelongsToMany public function socialMedia(): BelongsToMany
{ {

View File

@@ -6,31 +6,47 @@ use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Ticketing\Ticket\Exceptions\TicketNotAvailableException; use App\Domains\Ticketing\Ticket\Exceptions\TicketNotAvailableException;
use App\Domains\Ticketing\Ticket\Models\Ticket; use App\Domains\Ticketing\Ticket\Models\Ticket;
use App\Domains\Ticketing\Ticket\Requests\DownloadTicketsPdfRequest; use App\Domains\Ticketing\Ticket\Requests\DownloadTicketsPdfRequest;
use App\Domains\Ticketing\Ticket\Requests\ListTicketsRequest;
use App\Domains\Ticketing\Ticket\Resources\TicketEventResource;
use App\Domains\Ticketing\Ticket\Resources\TicketResource; use App\Domains\Ticketing\Ticket\Resources\TicketResource;
use App\Domains\Ticketing\Ticket\Services\TicketPdfService; use App\Domains\Ticketing\Ticket\Services\TicketPdfService;
use App\Domains\Ticketing\Ticket\Services\TicketPresentationResolver; use App\Domains\Ticketing\Ticket\Services\TicketPresentationResolver;
use App\Domains\Ticketing\Ticket\Services\TicketValidityResolver; use App\Domains\Ticketing\Ticket\Services\TicketValidityResolver;
use App\Domains\Ticketing\Ticket\Services\UserTicketQueryService;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Http\Response; use Illuminate\Http\Response;
class TicketController extends Controller class TicketController extends Controller
{ {
public function __construct(private readonly TicketPdfService $ticketPdfService) {} public function __construct(
private readonly TicketPdfService $ticketPdfService,
private readonly UserTicketQueryService $userTickets,
) {}
public function index(Request $request, Tenant $tenant): JsonResponse public function index(ListTicketsRequest $request, Tenant $tenant): JsonResponse
{ {
$tickets = Ticket::query() $eventId = $request->validated('event_id');
->where('tenant_code', $tenant->codigo) $tickets = $this->userTickets->tickets(
->where('user_id', $request->user()->getKey()) $tenant,
->with([...TicketValidityResolver::RELATIONS, ...TicketPresentationResolver::RELATIONS]) $request->user(),
->orderByDesc('id') $eventId === null ? null : (int) $eventId,
->get(); );
return TicketResource::collection($tickets)->response(); return TicketResource::collection($tickets)->response();
} }
public function events(Request $request, Tenant $tenant): AnonymousResourceCollection
{
abort_unless($tenant->storefront_website_type_code === 'onticket_multi_event', 404);
return TicketEventResource::collection(
$this->userTickets->events($tenant, $request->user())
);
}
public function downloadPdf(DownloadTicketsPdfRequest $request, Tenant $tenant): Response public function downloadPdf(DownloadTicketsPdfRequest $request, Tenant $tenant): Response
{ {
$ticketIds = $request->validated('ticket_ids'); $ticketIds = $request->validated('ticket_ids');

View File

@@ -2,16 +2,17 @@
namespace App\Domains\Ticketing\Ticket\Models; namespace App\Domains\Ticketing\Ticket\Models;
use App\Domains\Core\Auth\Models\User;
use App\Domains\Commerce\Catalog\Models\CatalogItem; use App\Domains\Commerce\Catalog\Models\CatalogItem;
use App\Domains\Commerce\Catalog\Models\Variant; use App\Domains\Commerce\Catalog\Models\Variant;
use App\Shared\Logging\Models\Concerns\LogsValueChanges;
use App\Domains\Commerce\Purchase\Models\PurchaseItem; use App\Domains\Commerce\Purchase\Models\PurchaseItem;
use App\Domains\Core\Auth\Models\User;
use App\Domains\Core\Tenant\Models\Tenant; use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Ticketing\Event\Models\Event;
use App\Domains\Ticketing\Ticket\Services\ResolvedTicketValidity; use App\Domains\Ticketing\Ticket\Services\ResolvedTicketValidity;
use App\Domains\Ticketing\Ticket\Services\ResolvedValidityGroup; use App\Domains\Ticketing\Ticket\Services\ResolvedValidityGroup;
use App\Domains\Ticketing\Ticket\Services\TicketPresentationResolver; use App\Domains\Ticketing\Ticket\Services\TicketPresentationResolver;
use App\Domains\Ticketing\Ticket\Services\TicketValidityResolver; use App\Domains\Ticketing\Ticket\Services\TicketValidityResolver;
use App\Shared\Logging\Models\Concerns\LogsValueChanges;
use Carbon\CarbonInterface; use Carbon\CarbonInterface;
use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -24,6 +25,7 @@ use Illuminate\Validation\ValidationException;
#[Fillable([ #[Fillable([
'tenant_code', 'tenant_code',
'event_id',
'ticket', 'ticket',
'source_purchase_item_id', 'source_purchase_item_id',
'source_catalog_item_id', 'source_catalog_item_id',
@@ -75,6 +77,7 @@ class Ticket extends Model
protected function casts(): array protected function casts(): array
{ {
return [ return [
'event_id' => 'integer',
'source_catalog_item_id' => 'integer', 'source_catalog_item_id' => 'integer',
'source_variant_id' => 'integer', 'source_variant_id' => 'integer',
'source_purchase_item_id' => 'integer', 'source_purchase_item_id' => 'integer',
@@ -136,6 +139,12 @@ class Ticket extends Model
return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo'); return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo');
} }
/** @return BelongsTo<Event, $this> */
public function event(): BelongsTo
{
return $this->belongsTo(Event::class);
}
public function allow_refund(): bool public function allow_refund(): bool
{ {
return $this->tenant?->allow_refund() ?? false; return $this->tenant?->allow_refund() ?? false;

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Domains\Ticketing\Ticket\Requests;
use App\Domains\Core\Tenant\Models\Tenant;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class ListTicketsRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/** @return array<string, mixed> */
public function rules(): array
{
/** @var Tenant $tenant */
$tenant = $this->route('tenant');
return [
'event_id' => [
'sometimes',
'integer',
Rule::exists('events', 'id')->where('tenant_code', $tenant->codigo),
],
];
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Domains\Ticketing\Ticket\Resources;
use App\Domains\Ticketing\Event\Models\Event;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/** @mixin Event */
class TicketEventResource extends JsonResource
{
/** @return array<string, mixed> */
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'image' => $this->attachment?->getTemporaryUrl(1440),
];
}
}

View File

@@ -15,6 +15,7 @@ class TicketResource extends JsonResource
return [ return [
'id' => $this->id, 'id' => $this->id,
'tenant_code' => $this->tenant_code, 'tenant_code' => $this->tenant_code,
'event_id' => $this->event_id,
'ticket' => $this->ticket, 'ticket' => $this->ticket,
'status' => $this->status, 'status' => $this->status,
'status_label' => $this->status_label, 'status_label' => $this->status_label,

View File

@@ -2,9 +2,9 @@
namespace App\Domains\Ticketing\Ticket\Services; namespace App\Domains\Ticketing\Ticket\Services;
use App\Domains\Core\Auth\Models\User;
use App\Domains\Commerce\Catalog\Models\CatalogItem; use App\Domains\Commerce\Catalog\Models\CatalogItem;
use App\Domains\Commerce\Catalog\Models\Variant; use App\Domains\Commerce\Catalog\Models\Variant;
use App\Domains\Core\Auth\Models\User;
use App\Domains\Ticketing\Ticket\Exceptions\TicketGenerationException; use App\Domains\Ticketing\Ticket\Exceptions\TicketGenerationException;
use App\Domains\Ticketing\Ticket\Models\Ticket; use App\Domains\Ticketing\Ticket\Models\Ticket;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@@ -44,6 +44,7 @@ class TicketGeneratorService
$variant = $target['variant']; $variant = $target['variant'];
$ticket = Ticket::query()->create([ $ticket = Ticket::query()->create([
'tenant_code' => $item->tenant_code, 'tenant_code' => $item->tenant_code,
'event_id' => $item->event_id,
'ticket' => (string) Str::uuid(), 'ticket' => (string) Str::uuid(),
'source_purchase_item_id' => $sourcePurchaseItemId, 'source_purchase_item_id' => $sourcePurchaseItemId,
'source_catalog_item_id' => $item->getKey(), 'source_catalog_item_id' => $item->getKey(),

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Domains\Ticketing\Ticket\Services;
use App\Domains\Core\Auth\Models\User;
use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Ticketing\Event\Models\Event;
use App\Domains\Ticketing\Ticket\Models\Ticket;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
class UserTicketQueryService
{
/** @return Collection<int, Ticket> */
public function tickets(Tenant $tenant, User $user, ?int $eventId = null): Collection
{
return $this->ticketQuery($tenant, $user)
->when($eventId !== null, fn (Builder $query): Builder => $query
->where('event_id', $eventId))
->with([...TicketValidityResolver::RELATIONS, ...TicketPresentationResolver::RELATIONS])
->orderByDesc('id')
->get();
}
/** @return Collection<int, Event> */
public function events(Tenant $tenant, User $user): Collection
{
return Event::query()
->where('tenant_code', $tenant->codigo)
->whereHas('tickets', fn (Builder $query): Builder => $query
->where('tenant_code', $tenant->codigo)
->where('user_id', $user->getKey()))
->with('attachment')
->orderByDesc('id')
->get();
}
/** @return Builder<Ticket> */
private function ticketQuery(Tenant $tenant, User $user): Builder
{
return Ticket::query()
->where('tenant_code', $tenant->codigo)
->where('user_id', $user->getKey());
}
}

View File

@@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Route;
Route::prefix('tenants/{tenant:codigo}') Route::prefix('tenants/{tenant:codigo}')
->middleware('auth:sanctum') ->middleware('auth:sanctum')
->group(function (): void { ->group(function (): void {
Route::get('tickets/events', [TicketController::class, 'events']);
Route::get('tickets', [TicketController::class, 'index']); Route::get('tickets', [TicketController::class, 'index']);
Route::post('tickets/pdf', [TicketController::class, 'downloadPdf']); Route::post('tickets/pdf', [TicketController::class, 'downloadPdf']);
}); });

View File

@@ -0,0 +1,52 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('tickets', function (Blueprint $table): void {
$table->foreignId('event_id')
->nullable()
->after('tenant_code')
->constrained('events')
->cascadeOnUpdate()
->restrictOnDelete();
$table->index(['tenant_code', 'user_id', 'event_id']);
});
DB::table('tickets')
->join('catalog_items', 'catalog_items.id', '=', 'tickets.source_catalog_item_id')
->whereNotNull('catalog_items.event_id')
->select(['tickets.id', 'catalog_items.event_id as source_event_id'])
->orderBy('tickets.id')
->chunk(500, function ($tickets): void {
foreach ($tickets as $ticket) {
DB::table('tickets')
->where('id', $ticket->id)
->update(['event_id' => $ticket->source_event_id]);
}
});
}
public function down(): void
{
if (DB::getDriverName() === 'sqlite') {
Schema::table('tickets', function (Blueprint $table): void {
$table->dropIndex(['tenant_code', 'user_id', 'event_id']);
$table->dropColumn('event_id');
});
return;
}
Schema::table('tickets', function (Blueprint $table): void {
$table->dropIndex(['tenant_code', 'user_id', 'event_id']);
$table->dropConstrainedForeignId('event_id');
});
}
};

View File

@@ -2,8 +2,15 @@
namespace Database\Seeders; namespace Database\Seeders;
use App\Domains\Commerce\Catalog\Enums\InventoryPolicy;
use App\Domains\Commerce\Catalog\Models\Attribute;
use App\Domains\Commerce\Catalog\Models\CatalogItem;
use App\Domains\Commerce\Catalog\Models\Category;
use App\Domains\Commerce\Catalog\Services\CatalogService;
use App\Domains\Core\Tenant\Models\Tenant; use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Ticketing\Event\Models\Event;
use App\Shared\Attachable\Services\AttachmentService; use App\Shared\Attachable\Services\AttachmentService;
use App\Shared\Enums\FieldType;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@@ -13,7 +20,10 @@ class TestEventsSeeder extends Seeder
{ {
private const IMAGE_DIRECTORY = 'images/tennants/onticket_otpimizado/test-events'; private const IMAGE_DIRECTORY = 'images/tennants/onticket_otpimizado/test-events';
public function __construct(private readonly AttachmentService $attachmentService) {} public function __construct(
private readonly AttachmentService $attachmentService,
private readonly CatalogService $catalogService,
) {}
public function run(): void public function run(): void
{ {
@@ -22,6 +32,7 @@ class TestEventsSeeder extends Seeder
$events = [ $events = [
[ [
'title' => '[Prueba] Noche de Música en Vivo', 'title' => '[Prueba] Noche de Música en Vivo',
'slug' => 'prueba-noche-de-musica-en-vivo',
'category' => 'Música', 'category' => 'Música',
'subtitle' => 'Una noche para cantar y bailar', 'subtitle' => 'Una noche para cantar y bailar',
'description' => 'Evento de prueba para explorar la cartelera de OnTicket.', 'description' => 'Evento de prueba para explorar la cartelera de OnTicket.',
@@ -30,9 +41,26 @@ class TestEventsSeeder extends Seeder
'weeks_from_now' => [2], 'weeks_from_now' => [2],
'time_start' => '21:00:00', 'time_start' => '21:00:00',
'time_end' => '23:30:00', 'time_end' => '23:30:00',
'catalog_items' => [
[
'slug' => 'prueba-noche-musica-entrada-general',
'name' => 'Entrada general',
'description' => 'Acceso general a la noche de música en vivo.',
'price' => 18000,
'stock' => 100,
],
[
'slug' => 'prueba-noche-musica-entrada-vip',
'name' => 'Entrada VIP',
'description' => 'Acceso preferencial y ubicación VIP.',
'price' => 32000,
'stock' => 40,
],
],
], ],
[ [
'title' => '[Prueba] Festival de Sabores', 'title' => '[Prueba] Festival de Sabores',
'slug' => 'prueba-festival-de-sabores',
'category' => 'Otros', 'category' => 'Otros',
'subtitle' => 'Gastronomía y música para toda la familia', 'subtitle' => 'Gastronomía y música para toda la familia',
'description' => 'Evento de prueba con dos jornadas disponibles.', 'description' => 'Evento de prueba con dos jornadas disponibles.',
@@ -42,9 +70,26 @@ class TestEventsSeeder extends Seeder
'day_offsets' => [0, 1], 'day_offsets' => [0, 1],
'time_start' => '12:00:00', 'time_start' => '12:00:00',
'time_end' => '20:00:00', 'time_end' => '20:00:00',
'catalog_items' => [
[
'slug' => 'prueba-festival-sabores-entrada-general',
'name' => 'Entrada general',
'description' => 'Acceso general para la jornada seleccionada.',
'price' => 12000,
'stock' => 150,
],
[
'slug' => 'prueba-festival-sabores-pase-premium',
'name' => 'Pase premium',
'description' => 'Acceso premium para la jornada seleccionada.',
'price' => 22000,
'stock' => 50,
],
],
], ],
[ [
'title' => '[Prueba] Stand Up en el Centro', 'title' => '[Prueba] Stand Up en el Centro',
'slug' => 'prueba-stand-up-en-el-centro',
'category' => 'Teatro', 'category' => 'Teatro',
'subtitle' => 'Humor para compartir', 'subtitle' => 'Humor para compartir',
'description' => 'Evento de prueba para visualizar distintas propuestas.', 'description' => 'Evento de prueba para visualizar distintas propuestas.',
@@ -53,14 +98,48 @@ class TestEventsSeeder extends Seeder
'weeks_from_now' => [4], 'weeks_from_now' => [4],
'time_start' => '20:30:00', 'time_start' => '20:30:00',
'time_end' => '22:00:00', 'time_end' => '22:00:00',
'catalog_items' => [
[
'slug' => 'prueba-stand-up-entrada-general',
'name' => 'Entrada general',
'description' => 'Acceso general al show de stand up.',
'price' => 14000,
'stock' => 120,
],
[
'slug' => 'prueba-stand-up-primera-fila',
'name' => 'Primera fila',
'description' => 'Ubicación preferencial en primera fila.',
'price' => 26000,
'stock' => 30,
],
],
], ],
]; ];
DB::transaction(function () use ($tenant, $events): void { DB::transaction(function () use ($tenant, $events): void {
Attribute::query()->firstOrCreate(
[
'tenant_codigo' => $tenant->codigo,
'codigo' => 'event_date',
],
[
'nombre' => 'Fecha',
'type' => FieldType::EventDate->value,
'is_required' => true,
],
);
$entryCategory = Category::query()->firstOrCreate([
'tenant_code' => $tenant->codigo,
'nombre' => 'Entradas',
]);
foreach ($events as $definition) { foreach ($events as $definition) {
$event = $tenant->events()->firstOrCreate( $event = $tenant->events()->firstOrCreate(
['title' => $definition['title']], ['title' => $definition['title']],
[ [
'slug' => $definition['slug'],
'subtitle' => $definition['subtitle'], 'subtitle' => $definition['subtitle'],
'description' => $definition['description'], 'description' => $definition['description'],
'location' => $definition['location'], 'location' => $definition['location'],
@@ -70,6 +149,10 @@ class TestEventsSeeder extends Seeder
], ],
); );
if ($event->slug !== $definition['slug']) {
$event->update(['slug' => $definition['slug']]);
}
if ($event->event_category_id === null) { if ($event->event_category_id === null) {
$event->update(['event_category_id' => $tenant->eventCategories() $event->update(['event_category_id' => $tenant->eventCategories()
->where('nombre', $definition['category'])->value('id')]); ->where('nombre', $definition['category'])->value('id')]);
@@ -89,10 +172,7 @@ class TestEventsSeeder extends Seeder
$event->update(['attachment_id' => $attachment->id]); $event->update(['attachment_id' => $attachment->id]);
} }
if ($event->dates()->exists()) { if (! $event->dates()->exists()) {
continue;
}
foreach ($definition['weeks_from_now'] as $index => $weeks) { foreach ($definition['weeks_from_now'] as $index => $weeks) {
$date = now()->startOfDay()->addWeeks($weeks) $date = now()->startOfDay()->addWeeks($weeks)
->addDays($definition['day_offsets'][$index] ?? 0); ->addDays($definition['day_offsets'][$index] ?? 0);
@@ -105,6 +185,52 @@ class TestEventsSeeder extends Seeder
]); ]);
} }
} }
$this->seedCatalogItems($tenant, $event, $entryCategory, $definition['catalog_items']);
}
}); });
} }
/** @param array<int, array<string, mixed>> $definitions */
private function seedCatalogItems(
Tenant $tenant,
Event $event,
Category $category,
array $definitions,
): void {
$eventDateIds = $event->dates()
->pluck('id')
->map(fn ($id): int => (int) $id)
->values();
foreach ($definitions as $order => $definition) {
$catalogItem = CatalogItem::query()
->where('tenant_code', $tenant->codigo)
->where('slug', $definition['slug'])
->first();
if ($catalogItem === null) {
$catalogItem = $this->catalogService->create([
'tenant_code' => $tenant->codigo,
'category_id' => $category->id,
'slug' => $definition['slug'],
'nombre' => $definition['name'],
'descripcion' => $definition['description'],
'precio' => $definition['price'],
'group_order' => $order,
'has_tickets' => true,
'inventory_policy' => InventoryPolicy::Tracked->value,
'attribute_codes' => ['event_date'],
'hidden_attribute_codes' => ['event_date'],
'images' => [$event->attachment->key],
'variants' => $eventDateIds->map(fn (int $eventDateId): array => [
'real_stock' => $definition['stock'],
'event_date_ids' => [$eventDateId],
])->all(),
]);
}
$catalogItem->forceFill(['event_id' => $event->id])->save();
}
}
} }

View File

@@ -29,6 +29,12 @@ class TestEventsSeederTest extends TestCase
$events = $tenant->events()->with('dates.validityTime')->get(); $events = $tenant->events()->with('dates.validityTime')->get();
$this->assertCount(3, $events); $this->assertCount(3, $events);
$this->assertSame(4, $events->sum(fn ($event): int => $event->dates->count())); $this->assertSame(4, $events->sum(fn ($event): int => $event->dates->count()));
$this->assertSame(6, $events->sum(fn ($event): int => $event->catalogItems()->count()));
$this->assertEqualsCanonicalizing([
'prueba-noche-de-musica-en-vivo',
'prueba-festival-de-sabores',
'prueba-stand-up-en-el-centro',
], $events->pluck('slug')->all());
$this->assertEqualsCanonicalizing( $this->assertEqualsCanonicalizing(
['live-music.avif', 'food-festival.avif', 'stand-up.avif'], ['live-music.avif', 'food-festival.avif', 'stand-up.avif'],
$events->map(fn ($event): string => $event->attachment->filename)->all(), $events->map(fn ($event): string => $event->attachment->filename)->all(),
@@ -41,6 +47,19 @@ class TestEventsSeederTest extends TestCase
$this->assertSame('image/avif', $event->attachment->mime_type); $this->assertSame('image/avif', $event->attachment->mime_type);
Storage::disk('s3')->assertExists($event->attachment->path); Storage::disk('s3')->assertExists($event->attachment->path);
$this->assertCount(2, $event->catalogItems);
foreach ($event->catalogItems as $catalogItem) {
$this->assertTrue($catalogItem->has_tickets);
$this->assertSame('Entradas', $catalogItem->category->nombre);
$this->assertSame($event->id, $catalogItem->event_id);
$this->assertSame($event->dates->count(), $catalogItem->variants->count());
$this->assertTrue($catalogItem->variants->every(
fn ($variant): bool => $variant->inventory->real_stock > 0
&& $variant->eventDates->count() === 1
));
}
foreach ($event->dates as $date) { foreach ($event->dates as $date) {
$this->assertTrue($date->startsAt()->isFuture()); $this->assertTrue($date->startsAt()->isFuture());
$this->assertNotNull($date->validityTime); $this->assertNotNull($date->validityTime);
@@ -51,6 +70,7 @@ class TestEventsSeederTest extends TestCase
$this->assertCount(3, $tenant->events()->get()); $this->assertCount(3, $tenant->events()->get());
$this->assertCount(4, $tenant->eventDates()->get()); $this->assertCount(4, $tenant->eventDates()->get());
$this->assertCount(6, $tenant->catalogItems()->get());
$this->assertSame( $this->assertSame(
$events->pluck('attachment_id')->all(), $events->pluck('attachment_id')->all(),
$tenant->events()->orderBy('id')->pluck('attachment_id')->all(), $tenant->events()->orderBy('id')->pluck('attachment_id')->all(),

View File

@@ -2,18 +2,20 @@
namespace Tests\Feature\Ticket; namespace Tests\Feature\Ticket;
use App\Shared\Attachable\Enums\AttachmentType;
use App\Shared\Attachable\Models\Attachment;
use App\Domains\Core\Auth\Models\User;
use App\Domains\Commerce\Catalog\Models\Attribute; use App\Domains\Commerce\Catalog\Models\Attribute;
use App\Domains\Commerce\Catalog\Models\CatalogItem; use App\Domains\Commerce\Catalog\Models\CatalogItem;
use App\Domains\Commerce\Catalog\Models\Inventory; use App\Domains\Commerce\Catalog\Models\Inventory;
use App\Domains\Core\Auth\Models\User;
use App\Domains\Core\Tenant\Models\StorefrontWebsiteType;
use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Ticketing\Event\Events\EventDateRescheduled; use App\Domains\Ticketing\Event\Events\EventDateRescheduled;
use App\Domains\Ticketing\Event\Events\EventDateSuspended; use App\Domains\Ticketing\Event\Events\EventDateSuspended;
use App\Domains\Ticketing\Event\Models\Event as TicketEvent;
use App\Domains\Ticketing\Event\Models\EventDate; use App\Domains\Ticketing\Event\Models\EventDate;
use App\Domains\Ticketing\Event\Services\EventService; use App\Domains\Ticketing\Event\Services\EventService;
use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Ticketing\Ticket\Models\Ticket; use App\Domains\Ticketing\Ticket\Models\Ticket;
use App\Shared\Attachable\Enums\AttachmentType;
use App\Shared\Attachable\Models\Attachment;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Event;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@@ -164,6 +166,59 @@ class TicketControllerTest extends TestCase
->assertJsonPath('data.0.id', $visibleTicket->id); ->assertJsonPath('data.0.id', $visibleTicket->id);
} }
public function test_a_multi_event_user_can_list_only_events_with_their_tickets(): void
{
StorefrontWebsiteType::query()->create([
'codigo' => 'onticket_multi_event',
'nombre' => 'OnTicket multi event',
'header_type' => 'standard',
]);
$tenant = $this->createTenant('multi');
$tenant->update(['storefront_website_type_code' => 'onticket_multi_event']);
$otherTenant = $this->createTenant('other');
$user = User::factory()->create();
$otherUser = User::factory()->create();
$firstEvent = $this->createEvent($tenant, 'First event');
$secondEvent = $this->createEvent($tenant, 'Second event');
$eventWithoutTickets = $this->createEvent($tenant, 'No tickets');
$otherTenantEvent = $this->createEvent($otherTenant, 'Other tenant event');
$this->createTicket($tenant, $user, 'First ticket')->update(['event_id' => $firstEvent->id]);
$this->createTicket($tenant, $user, 'Second ticket')->update(['event_id' => $secondEvent->id]);
$this->createTicket($tenant, $otherUser, 'Other user')->update(['event_id' => $eventWithoutTickets->id]);
$this->createTicket($otherTenant, $user, 'Other tenant')->update(['event_id' => $otherTenantEvent->id]);
$this->actingAs($user, 'sanctum')
->getJson("/api/tenants/{$tenant->codigo}/tickets/events")
->assertOk()
->assertJsonCount(2, 'data')
->assertJsonPath('data.0.id', $secondEvent->id)
->assertJsonPath('data.0.title', 'Second event')
->assertJsonPath('data.1.id', $firstEvent->id)
->assertJsonMissing(['title' => 'No tickets'])
->assertJsonMissing(['title' => 'Other tenant event']);
}
public function test_a_user_can_filter_their_tickets_by_event(): void
{
$tenant = $this->createTenant('multi');
$user = User::factory()->create();
$firstEvent = $this->createEvent($tenant, 'First event');
$secondEvent = $this->createEvent($tenant, 'Second event');
$visibleTicket = $this->createTicket($tenant, $user, 'Visible ticket');
$visibleTicket->update(['event_id' => $firstEvent->id]);
$hiddenTicket = $this->createTicket($tenant, $user, 'Hidden ticket');
$hiddenTicket->update(['event_id' => $secondEvent->id]);
$this->actingAs($user, 'sanctum')
->getJson("/api/tenants/{$tenant->codigo}/tickets?event_id={$firstEvent->id}")
->assertOk()
->assertJsonCount(1, 'data')
->assertJsonPath('data.0.id', $visibleTicket->id)
->assertJsonPath('data.0.event_id', $firstEvent->id)
->assertJsonMissing(['id' => $hiddenTicket->id]);
}
public function test_authentication_is_required_to_list_tickets(): void public function test_authentication_is_required_to_list_tickets(): void
{ {
$tenant = $this->createTenant('current'); $tenant = $this->createTenant('current');
@@ -248,6 +303,16 @@ class TicketControllerTest extends TestCase
]); ]);
} }
private function createEvent(Tenant $tenant, string $title): TicketEvent
{
return TicketEvent::query()->create([
'tenant_code' => $tenant->codigo,
'slug' => Str::slug($title).'-'.Str::lower(Str::random(8)),
'title' => $title,
'published_at' => now(),
]);
}
private function createAttachment(string $name): Attachment private function createAttachment(string $name): Attachment
{ {
return Attachment::query()->create([ return Attachment::query()->create([

View File

@@ -2,22 +2,23 @@
namespace Tests\Feature\Ticket; namespace Tests\Feature\Ticket;
use App\Shared\Attachable\Enums\AttachmentType;
use App\Shared\Attachable\Models\Attachment;
use App\Domains\Core\Auth\Models\User;
use App\Domains\Commerce\Catalog\Enums\CatalogItemType; use App\Domains\Commerce\Catalog\Enums\CatalogItemType;
use App\Domains\Commerce\Catalog\Models\Attribute; use App\Domains\Commerce\Catalog\Models\Attribute;
use App\Domains\Commerce\Catalog\Models\CatalogItem; use App\Domains\Commerce\Catalog\Models\CatalogItem;
use App\Domains\Commerce\Catalog\Models\Inventory; use App\Domains\Commerce\Catalog\Models\Inventory;
use App\Domains\Commerce\Catalog\Services\CatalogService; use App\Domains\Commerce\Catalog\Services\CatalogService;
use App\Domains\Ticketing\Event\Models\EventDate;
use App\Domains\Commerce\Purchase\Models\Purchase; use App\Domains\Commerce\Purchase\Models\Purchase;
use App\Shared\Enums\FieldType; use App\Domains\Core\Auth\Models\User;
use App\Domains\Core\Tenant\Models\Tenant; use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Ticketing\Event\Models\Event as TicketEvent;
use App\Domains\Ticketing\Event\Models\EventDate;
use App\Domains\Ticketing\Ticket\Enums\ValidityTimeType; use App\Domains\Ticketing\Ticket\Enums\ValidityTimeType;
use App\Domains\Ticketing\Ticket\Exceptions\TicketGenerationException; use App\Domains\Ticketing\Ticket\Exceptions\TicketGenerationException;
use App\Domains\Ticketing\Ticket\Models\ValidityTime; use App\Domains\Ticketing\Ticket\Models\ValidityTime;
use App\Domains\Ticketing\Ticket\Services\TicketGeneratorService; use App\Domains\Ticketing\Ticket\Services\TicketGeneratorService;
use App\Shared\Attachable\Enums\AttachmentType;
use App\Shared\Attachable\Models\Attachment;
use App\Shared\Enums\FieldType;
use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Database\QueryException; use Illuminate\Database\QueryException;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
@@ -79,6 +80,26 @@ class TicketGeneratorServiceTest extends TestCase
]); ]);
} }
public function test_it_preserves_the_catalog_items_event_on_the_ticket(): void
{
$event = TicketEvent::query()->create([
'tenant_code' => $this->tenant->codigo,
'slug' => 'original-event',
'title' => 'Original event',
'published_at' => now(),
]);
$item = $this->createTicketableItem('event-ticket');
$item->update(['event_id' => $event->id]);
$ticket = $this->service->generate($item->fresh(), $this->user)->sole();
$this->assertSame($event->id, $ticket->event_id);
$this->assertDatabaseHas('tickets', [
'id' => $ticket->id,
'event_id' => $event->id,
]);
}
public function test_it_rejects_an_item_without_tickets_enabled(): void public function test_it_rejects_an_item_without_tickets_enabled(): void
{ {
$item = $this->createTicketableItem('disabled'); $item = $this->createTicketableItem('disabled');