seed([AuthorizationSeeder::class, SocialMediaSeeder::class]); WebsiteType::query()->create([ 'codigo' => 'onticket', 'nombre' => 'OnTicket', ]); } public function test_authentication_is_required(): void { $this->getJson('/api/v1/adminapp/tenant/event')->assertUnauthorized(); $this->putJson('/api/v1/adminapp/tenant/event', $this->eventPayload())->assertUnauthorized(); $this->postJson('/api/v1/adminapp/tenant/event-dates', $this->datePayload())->assertUnauthorized(); } public function test_an_adminapp_user_can_update_event_and_contact_information_without_synchronizing_dates(): void { $tenant = $this->createTenant('acme'); Sanctum::actingAs($this->createAdminAppUser($tenant)); $response = $this->putJson('/api/v1/adminapp/tenant/event', $this->eventPayload()) ->assertOk() ->assertJsonPath('data.title', 'Festival Acme') ->assertJsonPath('data.location', 'Predio Ferial, Rosario') ->assertJsonCount(0, 'data.dates') ->assertJsonPath('data.contact.whatsapp_url', 'https://wa.me/5493415550101') ->assertJsonPath('data.contact.instagram_url', 'https://instagram.com/acme') ->assertJsonPath('data.contact.facebook_url', null); $this->assertSame($tenant->id, $response->json('data.id')); $this->assertDatabaseHas('tenants', [ 'id' => $tenant->id, 'event_title' => 'Festival Acme', 'event_location' => 'Predio Ferial, Rosario', 'event_date_text' => null, ]); $this->assertDatabaseCount('event_dates', 0); $this->assertDatabaseHas('tenant_social_media', [ 'tenant_code' => $tenant->codigo, 'social_media_code' => 'whatsapp', 'url' => 'https://wa.me/5493415550101', ]); } public function test_an_adminapp_user_can_read_only_its_tenant_active_event(): void { $tenant = $this->createTenant('acme'); $otherTenant = $this->createTenant('other'); $this->createActiveEvent($tenant, 'Acme Event'); $this->createActiveEvent($otherTenant, 'Other Event'); Sanctum::actingAs($this->createAdminAppUser($tenant)); $this->getJson('/api/v1/adminapp/tenant/event') ->assertOk() ->assertJsonPath('data.title', 'Acme Event') ->assertJsonMissing(['title' => 'Other Event']); } public function test_reading_a_tenant_without_event_configuration_returns_empty_values(): void { $tenant = $this->createTenant('acme'); Sanctum::actingAs($this->createAdminAppUser($tenant)); $this->getJson('/api/v1/adminapp/tenant/event') ->assertOk() ->assertJsonPath('data.title', null) ->assertJsonCount(0, 'data.dates'); } public function test_updating_event_does_not_change_or_delete_existing_dates(): void { $tenant = $this->createTenant('acme'); $eventTenant = $this->createActiveEvent($tenant, 'Old Event'); $firstDate = $eventTenant->eventDates()->create([ 'date' => '2026-10-01', 'time_start' => '08:00', 'time_end' => '12:00', ]); $removedDate = $eventTenant->eventDates()->create([ 'date' => '2026-10-02', 'time_start' => '08:00', 'time_end' => '12:00', ]); $firstValidityTimeId = $firstDate->validity_time_id; $tenant->socialMedia()->attach('facebook', [ 'url' => 'https://facebook.com/old', 'orden' => 2, ]); $tenant->socialMedia()->attach('linkedin', [ 'url' => 'https://linkedin.com/company/acme', 'orden' => 3, ]); Sanctum::actingAs($this->createAdminAppUser($tenant)); $payload = $this->eventPayload(); $this->putJson('/api/v1/adminapp/tenant/event', $payload) ->assertOk() ->assertJsonPath('data.id', $tenant->id) ->assertJsonPath('data.dates.0.id', $firstDate->id) ->assertJsonPath('data.contact.facebook_url', null); $this->assertDatabaseHas('event_dates', [ 'id' => $firstDate->id, 'validity_time_id' => $firstValidityTimeId, 'date' => '2026-10-01', ]); $this->assertDatabaseHas('validity_times', [ 'id' => $firstValidityTimeId, 'type' => ValidityTimeType::FixedWindow->value, 'fixed_starts_at' => '2026-10-01 08:00:00', 'fixed_expires_at' => '2026-10-01 12:00:00', ]); $this->assertDatabaseHas('event_dates', ['id' => $removedDate->id]); $this->assertSame('1 y 2 de Octubre 2026', $tenant->fresh()->event_date_text); $this->assertDatabaseMissing('tenant_social_media', [ 'tenant_code' => $tenant->codigo, 'social_media_code' => 'facebook', ]); $this->assertDatabaseHas('tenant_social_media', [ 'tenant_code' => $tenant->codigo, 'social_media_code' => 'linkedin', 'url' => 'https://linkedin.com/company/acme', ]); } public function test_dates_are_created_independently_and_recalculate_the_tenant_date_text(): void { $tenant = $this->createTenant('acme'); Sanctum::actingAs($this->createAdminAppUser($tenant)); foreach ([9, 10, 11, 12] as $day) { $this->postJson('/api/v1/adminapp/tenant/event-dates', [ 'date' => sprintf('2026-10-%02d', $day), 'start_time' => '09:00', 'end_time' => '18:30', ])->assertCreated()->assertJsonPath('data.status', 'scheduled'); } $this->assertSame( '9, 10, 11 y 12 de Octubre 2026', $tenant->fresh()->event_date_text ); } public function test_rescheduling_reuses_an_existing_date_and_tickets_resolve_its_validity(): void { $tenant = $this->createTenant('acme'); $admin = $this->createAdminAppUser($tenant); $original = $tenant->eventDates()->create([ 'date' => '2027-10-09', 'time_start' => '09:00', 'time_end' => '18:30', ]); $destination = $tenant->eventDates()->create([ 'date' => '2027-10-20', 'time_start' => '11:00', 'time_end' => '20:00', ]); $variant = $this->createVariant($tenant, $original->id); $ticket = $this->createTicket($tenant, $admin, $variant); Sanctum::actingAs($admin); $this->postJson("/api/v1/adminapp/tenant/event-dates/{$original->id}/reschedule", [ 'date' => '2027-10-20', ]) ->assertOk() ->assertJsonPath('data.status', 'rescheduled') ->assertJsonPath('data.rescheduled_to_event_date_id', $destination->id); $this->assertDatabaseCount('event_dates', 2); $this->assertSame( '2027-10-20 11:00:00', $ticket->resolvedValidity()->effectiveStartsAt()?->format('Y-m-d H:i:s'), ); $this->assertDatabaseHas('validity_times', [ 'id' => $original->validity_time_id, 'fixed_starts_at' => '2027-10-09 09:00:00', ]); $this->postJson("/api/v1/adminapp/tenant/event-dates/{$destination->id}/reschedule", [ 'date' => '2027-10-25', ])->assertOk()->assertJsonPath('data.status', 'rescheduled'); $this->assertDatabaseCount('event_dates', 3); $this->assertSame( '2027-10-25 11:00:00', $ticket->fresh()->resolvedValidity()->effectiveStartsAt()?->format('Y-m-d H:i:s'), ); } public function test_suspending_disables_only_tickets_without_another_usable_date(): void { $tenant = $this->createTenant('acme'); $admin = $this->createAdminAppUser($tenant); $suspendedDate = $tenant->eventDates()->create([ 'date' => '2027-10-09', 'time_start' => '09:00', 'time_end' => '18:30', ]); $otherDate = $tenant->eventDates()->create([ 'date' => '2027-10-10', 'time_start' => '09:00', 'time_end' => '18:30', ]); $singleDateVariant = $this->createVariant($tenant, $suspendedDate->id); $multipleDateVariant = $this->createVariant($tenant); $multipleDateVariant->eventDates()->sync([$suspendedDate->id, $otherDate->id]); $singleDateTicket = $this->createTicket($tenant, $admin, $singleDateVariant); $multipleDateTicket = $this->createTicket($tenant, $admin, $multipleDateVariant); Sanctum::actingAs($admin); $this->postJson("/api/v1/adminapp/tenant/event-dates/{$suspendedDate->id}/suspend") ->assertOk() ->assertJsonPath('data.status', 'suspended') ->assertJsonPath('data.suspended_at', fn ($value) => is_string($value)); $this->assertNotNull($singleDateTicket->fresh()->disabled_at); $this->assertNull($multipleDateTicket->fresh()->disabled_at); $this->assertSame( '2027-10-10 09:00:00', $multipleDateTicket->fresh()->resolvedValidity()->effectiveStartsAt()?->format('Y-m-d H:i:s'), ); } public function test_suspending_a_reschedule_destination_disables_tickets_from_predecessor_dates(): void { $tenant = $this->createTenant('acme'); $admin = $this->createAdminAppUser($tenant); $original = $tenant->eventDates()->create([ 'date' => '2027-10-09', 'time_start' => '09:00', 'time_end' => '18:30', ]); $destination = $tenant->eventDates()->create([ 'date' => '2027-10-20', 'time_start' => '09:00', 'time_end' => '18:30', ]); $original->update(['rescheduled_to_event_date_id' => $destination->id]); $ticket = $this->createTicket( $tenant, $admin, $this->createVariant($tenant, $original->id), ); Sanctum::actingAs($admin); $this->postJson("/api/v1/adminapp/tenant/event-dates/{$destination->id}/suspend") ->assertOk(); $this->assertNotNull($ticket->fresh()->disabled_at); $this->assertFalse($ticket->fresh()->resolvedValidity()->isResolvable); } public function test_update_and_date_creation_validate_their_own_payloads(): void { $tenant = $this->createTenant('acme'); Sanctum::actingAs($this->createAdminAppUser($tenant)); $this->putJson('/api/v1/adminapp/tenant/event', [ 'title' => '', 'location' => '', 'contact' => [ 'whatsapp_url' => 'not-a-url', 'instagram_url' => null, 'facebook_url' => null, ], ]) ->assertUnprocessable() ->assertJsonValidationErrors([ 'title', 'location', 'contact.whatsapp_url', ]); $this->postJson('/api/v1/adminapp/tenant/event-dates', [ 'date' => '09/10/2026', 'start_time' => '9am', 'end_time' => '18:00', ])->assertUnprocessable()->assertJsonValidationErrors(['date', 'start_time']); $this->assertNull($tenant->fresh()->event_title); } public function test_social_media_accepts_any_registered_code_and_rejects_unknown_codes(): void { $tenant = $this->createTenant('acme'); Sanctum::actingAs($this->createAdminAppUser($tenant)); $payload = $this->eventPayload(); unset($payload['contact']); $payload['social_media'] = [[ 'code' => 'linkedin', 'url' => 'https://linkedin.com/company/acme', 'orden' => 5, ]]; $this->putJson('/api/v1/adminapp/tenant/event', $payload) ->assertOk() ->assertJsonPath('data.social_media.0.code', 'linkedin') ->assertJsonPath('data.social_media.0.url', 'https://linkedin.com/company/acme') ->assertJsonPath('data.social_media.0.orden', 5); $this->assertDatabaseHas('tenant_social_media', [ 'tenant_code' => $tenant->codigo, 'social_media_code' => 'linkedin', 'url' => 'https://linkedin.com/company/acme', 'orden' => 5, ]); $payload['social_media'][0]['code'] = 'unknown'; $this->putJson('/api/v1/adminapp/tenant/event', $payload) ->assertUnprocessable() ->assertJsonValidationErrors(['social_media.0.code']); } public function test_a_customer_cannot_manage_an_event(): void { Sanctum::actingAs(User::factory()->create([ 'rol_codigo' => RoleCode::User->value, 'tenant_codigo' => null, ])); $this->getJson('/api/v1/adminapp/tenant/event')->assertForbidden(); } /** @return array */ private function eventPayload(): array { return [ 'title' => 'Festival Acme', 'location' => 'Predio Ferial, Rosario', 'contact' => [ 'whatsapp_url' => 'https://wa.me/5493415550101', 'instagram_url' => 'https://instagram.com/acme', 'facebook_url' => null, ], ]; } /** @return array{date: string, start_time: string, end_time: string} */ private function datePayload(): array { return [ 'date' => '2026-10-09', 'start_time' => '09:00', 'end_time' => '18:30', ]; } private function createVariant(Tenant $tenant, ?int $eventDateId = null): Variant { $item = CatalogItem::query()->create([ 'tenant_code' => $tenant->codigo, 'slug' => 'item-'.Str::uuid(), 'nombre' => 'Entrada', 'precio' => '1000.00', ]); return Variant::query()->create([ 'catalog_item_id' => $item->id, 'inventory_id' => Inventory::query()->create()->id, 'event_date_id' => $eventDateId, ]); } private function createTicket(Tenant $tenant, User $user, Variant $variant): Ticket { return Ticket::query()->create([ 'tenant_code' => $tenant->codigo, 'ticket' => (string) Str::uuid(), 'user_id' => $user->id, 'source_catalog_item_id' => $variant->catalog_item_id, 'source_variant_id' => $variant->id, ]); } private function createTenant(string $code): Tenant { $headerLogo = $this->createAttachment("{$code}-header"); $footerLogo = $this->createAttachment("{$code}-footer"); return Tenant::query()->create([ 'codigo' => $code, 'nombre' => ucfirst($code), 'dominio' => "{$code}.test", 'website_type_code' => 'onticket', 'primary_color' => '#000000', 'secondary_color' => '#000000', 'danger_color' => '#000000', 'success_color' => '#000000', 'header_bg_color' => '#000000', 'footer_bg_color' => '#000000', 'header_logo_id' => $headerLogo->id, 'footer_logo_id' => $footerLogo->id, ]); } private function createAttachment(string $name): Attachment { return Attachment::query()->create([ 'path' => "test/{$name}.png", 'filename' => "{$name}.png", 'type' => AttachmentType::Image, 'mime_type' => 'image/png', ]); } private function createAdminAppUser(Tenant $tenant): User { return User::factory()->create([ 'rol_codigo' => RoleCode::AdminApp->value, 'tenant_codigo' => $tenant->codigo, ]); } private function createActiveEvent(Tenant $tenant, string $name): Tenant { $tenant->update([ 'event_title' => $name, 'event_location' => 'Rosario', ]); return $tenant->fresh(); } }