feat(ticketing): resolve validity in tenant timezone

This commit is contained in:
2026-09-21 15:19:09 -03:00
parent f4de2ff5b2
commit 3e3b3bfac0
17 changed files with 354 additions and 45 deletions

View File

@@ -2,23 +2,23 @@
namespace Tests\Feature\Event;
use App\Shared\Attachable\Enums\AttachmentType;
use App\Shared\Attachable\Models\Attachment;
use App\Domains\Core\Auth\Models\User;
use App\Domains\Core\Authorization\Enums\RoleCode;
use App\Domains\Commerce\Cart\Models\Cart;
use App\Domains\Commerce\Catalog\Models\CatalogItem;
use App\Domains\Commerce\Catalog\Models\Inventory;
use App\Domains\Commerce\Catalog\Models\StockReservation;
use App\Domains\Commerce\Catalog\Models\StockReservationLine;
use App\Domains\Commerce\Catalog\Models\Variant;
use App\Domains\Commerce\Purchase\Services\Checkout\CatalogSelectionResolver;
use App\Domains\Core\Auth\Models\User;
use App\Domains\Core\Authorization\Enums\RoleCode;
use App\Domains\Core\Tenant\Models\AdminWebsiteType;
use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Ticketing\Event\Events\EventDateRescheduled;
use App\Domains\Ticketing\Event\Events\EventDateSuspended;
use App\Domains\Commerce\Purchase\Services\Checkout\CatalogSelectionResolver;
use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Core\Tenant\Models\AdminWebsiteType;
use App\Domains\Ticketing\Ticket\Enums\ValidityTimeType;
use App\Domains\Ticketing\Ticket\Models\Ticket;
use App\Shared\Attachable\Enums\AttachmentType;
use App\Shared\Attachable\Models\Attachment;
use Database\Seeders\AuthorizationSeeder;
use Database\Seeders\SocialMediaSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
@@ -249,8 +249,8 @@ class AdminAppEventControllerTest extends TestCase
$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',
'fixed_starts_at' => '2026-10-01 11:00:00',
'fixed_expires_at' => '2026-10-01 15:00:00',
]);
$this->assertDatabaseHas('event_dates', ['id' => $removedDate->id]);
$this->assertSame('1 y 2 de Octubre 2026', $tenant->fresh()->event_date_text);
@@ -385,7 +385,7 @@ class AdminAppEventControllerTest extends TestCase
);
$this->assertDatabaseHas('validity_times', [
'id' => $original->validity_time_id,
'fixed_starts_at' => '2027-10-09 09:00:00',
'fixed_starts_at' => '2027-10-09 12:00:00',
]);
$this->postJson("/api/v1/adminapp/tenant/event-dates/{$destination->id}/reschedule", [

View File

@@ -0,0 +1,48 @@
<?php
namespace Tests\Feature\Migrations;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Tests\TestCase;
class AddTenantTimezoneTest extends TestCase
{
public function test_it_converts_legacy_event_windows_once_and_preserves_other_windows(): void
{
Schema::create('tenants', function (Blueprint $table): void {
$table->id();
$table->string('codigo');
});
Schema::create('event_dates', function (Blueprint $table): void {
$table->id();
$table->string('tenant_code');
$table->date('date');
$table->time('time_start');
$table->time('time_end');
$table->integer('validity_time_id');
});
Schema::create('validity_times', function (Blueprint $table): void {
$table->id();
$table->dateTime('fixed_starts_at');
$table->dateTime('fixed_expires_at');
});
DB::table('tenants')->insert(['codigo' => 'football']);
foreach ([1, 2] as $id) {
DB::table('validity_times')->insert([
'id' => $id, 'fixed_starts_at' => '2026-09-21 22:00:00', 'fixed_expires_at' => '2026-09-22 02:00:00',
]);
}
DB::table('event_dates')->insert([
'tenant_code' => 'football', 'date' => '2026-09-21', 'time_start' => '22:00:00', 'time_end' => '02:00:00', 'validity_time_id' => 1,
]);
$migration = require database_path('migrations/2026_09_21_040000_add_timezone_to_tenants.php');
$migration->up();
$this->assertDatabaseHas('tenants', ['codigo' => 'football', 'timezone' => 'America/Argentina/Buenos_Aires']);
$this->assertDatabaseHas('validity_times', ['id' => 1, 'fixed_starts_at' => '2026-09-22 01:00:00', 'fixed_expires_at' => '2026-09-22 05:00:00']);
$this->assertDatabaseHas('validity_times', ['id' => 2, 'fixed_starts_at' => '2026-09-21 22:00:00']);
$migration->down();
$this->assertFalse(Schema::hasColumn('tenants', 'timezone'));
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Tests\Feature\Tenant;
use App\Domains\Core\Tenant\Models\Tenant;
use App\Shared\Attachable\Enums\AttachmentType;
use App\Shared\Attachable\Models\Attachment;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
class TenantTimezoneTest extends TestCase
{
use RefreshDatabase;
public function test_timezone_is_exposed_by_the_api(): void
{
$tenant = $this->createTenant('timezone');
$this->getJson("/api/tenants/{$tenant->codigo}")->assertOk()
->assertJsonPath('data.timezone', 'America/Argentina/Buenos_Aires');
}
public function test_event_stores_utc_and_timezone_changes_preserve_stored_instants(): void
{
$tenant = $this->createTenant('event-zone');
$date = $tenant->eventDates()->create([
'date' => '2026-09-21', 'time_start' => '22:00', 'time_end' => '02:00',
]);
$this->assertDatabaseHas('validity_times', [
'id' => $date->validity_time_id,
'fixed_starts_at' => '2026-09-22 01:00:00',
'fixed_expires_at' => '2026-09-22 05:00:00',
]);
$tenant->update(['timezone' => 'Asia/Tokyo']);
$this->assertSame('2026-09-22 01:00:00', $date->validityTime->fresh()->fixed_starts_at->format('Y-m-d H:i:s'));
}
private function createTenant(string $code): Tenant
{
$attachmentIds = collect(['header', 'footer'])->map(function (string $name): int {
$key = (string) Str::uuid();
return Attachment::query()->create([
'key' => $key,
'path' => "tenants/{$key}.png",
'filename' => "{$name}.png",
'type' => AttachmentType::Image,
'mime_type' => 'image/png',
])->id;
});
$clientId = DB::table('clients')->insertGetId([
'code' => $code,
'name' => Str::headline($code),
]);
$tenantId = DB::table('tenants')->insertGetId([
'client_id' => $clientId,
'codigo' => $code,
'nombre' => Str::headline($code),
'dominio' => "{$code}.test",
'primary_color' => '#000000',
'secondary_color' => '#000000',
'danger_color' => '#000000',
'success_color' => '#000000',
'header_bg_color' => '#ffffff',
'footer_bg_color' => '#ffffff',
'header_logo_id' => $attachmentIds[0],
'footer_logo_id' => $attachmentIds[1],
]);
return Tenant::query()->findOrFail($tenantId);
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace Tests\Unit\Ticket;
use App\Domains\Commerce\Catalog\Models\CatalogItem;
use App\Domains\Commerce\Catalog\Models\Variant;
use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Ticketing\Event\Models\EventDate;
use App\Domains\Ticketing\Ticket\Enums\ValidityTimeType;
use App\Domains\Ticketing\Ticket\Models\ValidityTime;
use App\Domains\Ticketing\Ticket\Resources\ValidityTimeResource;
use App\Domains\Ticketing\Ticket\Services\ResolvedValidityGroup;
use App\Domains\Ticketing\Ticket\Services\TicketValidityResolver;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Collection;
use Tests\TestCase;
class TenantTimezoneValidityTest extends TestCase
{
public function test_recurring_windows_use_local_day_and_exclusive_end(): void
{
$window = new ValidityTime(['type' => ValidityTimeType::TimeWindow, 'start_time' => '22:00', 'end_time' => '02:00']);
$group = new ResolvedValidityGroup(collect([$window]), 'America/Argentina/Buenos_Aires');
foreach (['2026-09-22 01:00' => true, '2026-09-22 04:59' => true, '2026-09-22 05:00' => false, '2026-09-22 18:00' => false] as $time => $expected) {
$at = CarbonImmutable::parse($time, 'UTC');
$this->assertSame($expected, $group->isValid($at), $time);
$this->assertSame($expected, $window->isValid($at, 'America/Argentina/Buenos_Aires'), $time);
}
$this->assertFalse((new ResolvedValidityGroup(collect([$window]), 'Asia/Tokyo'))->isValid(CarbonImmutable::parse('2026-09-22 01:00', 'UTC')));
}
public function test_event_anchor_uses_local_date_when_utc_date_is_next_day(): void
{
$fixed = new ValidityTime(['type' => ValidityTimeType::FixedWindow,
'fixed_starts_at' => '2026-09-22 01:00', 'fixed_expires_at' => '2026-09-22 06:00']);
$window = new ValidityTime(['type' => ValidityTimeType::TimeWindow, 'start_time' => '22:00', 'end_time' => '02:00']);
$group = new ResolvedValidityGroup(collect([$fixed, $window]), 'America/Argentina/Buenos_Aires');
$this->assertTrue($group->isValid(CarbonImmutable::parse('2026-09-22 04:00', 'UTC')));
$this->assertFalse($group->isValid(CarbonImmutable::parse('2026-09-23 04:00', 'UTC')));
$this->assertSame('2026-09-22 01:00', $group->effectiveStartsAt()->utc()->format('Y-m-d H:i'));
$this->assertSame('2026-09-22 05:00', $group->effectiveExpiresAt()->utc()->format('Y-m-d H:i'));
}
public function test_local_window_compares_with_utc_now_in_resource(): void
{
$this->travelTo(CarbonImmutable::parse('2026-09-21 22:00', 'UTC'));
$window = new ValidityTime(['type' => ValidityTimeType::TimeWindow, 'start_time' => '18:00', 'end_time' => '20:00']);
$resource = ValidityTimeResource::make($window, 'America/Argentina/Buenos_Aires');
$this->assertTrue($resource->resolve()['is_valid']);
$this->assertFalse($window->isValid(now(), 'Asia/Tokyo'));
$this->travelBack();
}
public function test_daylight_saving_uses_date_specific_offset(): void
{
$window = new ValidityTime(['type' => ValidityTimeType::TimeWindow, 'start_time' => '18:00', 'end_time' => '20:00']);
$this->assertSame('2026-01-21 23:00', $window->startsAt(CarbonImmutable::parse('2026-01-21 12:00', 'UTC'), 'America/New_York')->utc()->format('Y-m-d H:i'));
$this->assertSame('2026-07-21 22:00', $window->startsAt(CarbonImmutable::parse('2026-07-21 12:00', 'UTC'), 'America/New_York')->utc()->format('Y-m-d H:i'));
}
public function test_resolver_passes_tenant_timezone_to_groups(): void
{
$tenant = new Tenant(['timezone' => 'Asia/Tokyo']);
$item = new CatalogItem;
$item->setRelation('tenant', $tenant);
$date = new EventDate;
$date->setRelation('validityTime', new ValidityTime([
'type' => ValidityTimeType::FixedWindow,
'fixed_starts_at' => '2026-09-21 13:00',
'fixed_expires_at' => '2026-09-21 17:00',
]));
$variant = new Variant;
$variant->setRelation('catalogItem', $item);
$variant->setRelation('eventDates', new Collection([$date]));
$variant->setRelation('eventDate', null);
$variant->setRelation('definitions', new Collection);
$resolved = (new TicketValidityResolver)->resolveVariant($variant);
$this->assertSame('Asia/Tokyo', $resolved->groups->first()->timezone);
}
public function test_event_hours_are_local_and_end_can_be_next_day(): void
{
$date = new EventDate(['date' => '2026-09-21', 'time_start' => '22:00', 'time_end' => '02:00']);
$date->setRelation('tenant', new Tenant(['timezone' => 'America/Argentina/Buenos_Aires']));
$this->assertSame('2026-09-22 01:00', $date->startsAt()->utc()->format('Y-m-d H:i'));
$this->assertSame('2026-09-22 05:00', $date->endsAt()->utc()->format('Y-m-d H:i'));
$date->setRelation('tenant', new Tenant(['timezone' => 'Asia/Tokyo']));
$this->assertSame('2026-09-21 13:00', $date->startsAt()->utc()->format('Y-m-d H:i'));
}
}

View File

@@ -8,10 +8,10 @@ use App\Domains\Commerce\Catalog\Models\ItemAttribute;
use App\Domains\Commerce\Catalog\Models\Variant;
use App\Domains\Commerce\Catalog\Models\VariantDefinition;
use App\Domains\Ticketing\Event\Models\EventDate;
use App\Shared\Enums\FieldType;
use App\Domains\Ticketing\Ticket\Enums\ValidityTimeType;
use App\Domains\Ticketing\Ticket\Models\ValidityTime;
use App\Domains\Ticketing\Ticket\Services\TicketValidityResolver;
use App\Shared\Enums\FieldType;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Support\Carbon;
use Tests\TestCase;
@@ -200,6 +200,7 @@ class TicketValidityResolverTest extends TestCase
private function variant(EloquentCollection $eventDates, EloquentCollection $definitions): Variant
{
$variant = new Variant;
$variant->setRelation('catalogItem', null);
$variant->setRelation('eventDates', $eventDates);
$variant->setRelation('eventDate', null);
$variant->setRelation('definitions', $definitions);