id(); $table->string('codigo')->unique(); }); Schema::create('events', function (Blueprint $table): void { $table->id(); $table->string('tenant_code'); $table->string('title'); $table->string('location')->nullable(); $table->json('exact_location')->nullable(); $table->timestamps(); }); $addresses = require database_path('migrations/2026_09_30_000000_create_addresses_and_tenant_addresses_tables.php'); $addresses->up(); $this->schemaMigration()->up(); } public function test_it_copies_event_locations_and_coordinates_and_preserves_legacy_data(): void { $precise = $this->event('Predio de Doma', ['latitude' => -32.9431184, 'longitude' => -60.6437991]); $noMap = $this->event('Salón Luz y Fuerza'); $zero = $this->event('Punto cero', ['latitude' => 0, 'longitude' => 0]); $missing = $this->event(null); $blank = $this->event(' '); $before = DB::table('events')->orderBy('id')->get()->map(fn (object $event): array => [ 'id' => $event->id, 'tenant_code' => $event->tenant_code, 'title' => $event->title, 'location' => $event->location, 'exact_location' => $event->exact_location, 'created_at' => $event->created_at, 'updated_at' => $event->updated_at, ])->all(); $migration = $this->dataMigration(); $migration->up(); $migration->up(); $this->assertDatabaseCount('addresses', 3); $this->assertDatabaseCount('tenant_addresses', 0); $event = Event::findOrFail($precise); $this->assertSame('Ubicación del evento', $event->address->label); $this->assertSame('Predio de Doma', $event->address->address_text); $this->assertSame('-32.9431184', $event->address->latitude); $this->assertSame('-60.6437991', $event->address->longitude); $this->assertNull(Event::findOrFail($noMap)->address->latitude); $this->assertNull(Event::findOrFail($noMap)->address->longitude); $this->assertSame('0.0000000', Event::findOrFail($zero)->address->latitude); $this->assertSame('0.0000000', Event::findOrFail($zero)->address->longitude); $this->assertNull(Event::findOrFail($missing)->address); $this->assertNull(Event::findOrFail($blank)->address_id); foreach ($before as $original) { $this->assertDatabaseHas('events', $original); } $migration->down(); $this->assertDatabaseCount('addresses', 3); $this->assertSame($event->address_id, Event::findOrFail($precise)->address_id); } public function test_it_keeps_existing_links_and_creates_independent_copies_for_unlinked_events(): void { $original = Address::create(['label' => 'Existente', 'address_text' => 'Lugar elegido']); $linked = $this->event('Texto anterior'); DB::table('events')->where('id', $linked)->update(['address_id' => $original->id]); $first = $this->event('Mismo predio'); $second = $this->event('Mismo predio'); $this->dataMigration()->up(); $this->assertDatabaseCount('addresses', 3); $this->assertSame($original->id, Event::findOrFail($linked)->address_id); $this->assertNotSame(Event::findOrFail($first)->address_id, Event::findOrFail($second)->address_id); $this->assertSame('Lugar elegido', $original->fresh()->address_text); } public function test_multiple_events_can_share_one_address_through_the_model_relations(): void { $address = Address::create(['label' => 'Teatro', 'address_text' => 'San Martín 123']); $first = Event::findOrFail($this->event('Texto original')); $first->address()->associate($address); $first->save(); $second = Event::findOrFail($this->event('Otro evento')); $second->fill(['address_id' => (string) $address->id])->save(); $this->assertSame($address->id, $first->fresh()->address->id); $this->assertSame($address->id, $second->fresh()->address_id); $this->assertSame([$first->id, $second->id], $address->events()->orderBy('id')->pluck('id')->all()); $loaded = Event::with('address')->orderBy('id')->get(); $this->assertSame($address->id, $loaded->last()->address->id); $first->delete(); $this->assertDatabaseCount('addresses', 1); $address->delete(); $this->assertDatabaseHas('events', ['id' => $second->id, 'address_id' => null, 'location' => 'Otro evento']); } public function test_the_foreign_key_rejects_an_unknown_address(): void { $event = Event::findOrFail($this->event('Predio')); $this->expectException(QueryException::class); $event->update(['address_id' => 9999]); } #[DataProvider('invalidCoordinates')] public function test_invalid_coordinates_roll_back_all_copies(string $coordinates): void { $valid = $this->event('Primero', ['latitude' => -32, 'longitude' => -60]); $invalid = $this->event('Segundo'); DB::table('events')->where('id', $invalid)->update(['exact_location' => $coordinates]); try { $this->dataMigration()->up(); $this->fail('Invalid coordinates must stop the migration.'); } catch (\JsonException|RuntimeException) { $this->assertDatabaseCount('addresses', 0); $this->assertNull(Event::findOrFail($valid)->address_id); $this->assertNull(Event::findOrFail($invalid)->address_id); $this->assertSame($coordinates, DB::table('events')->where('id', $invalid)->value('exact_location')); } } public static function invalidCoordinates(): array { return [ 'latitude out of range' => ['{"latitude":-91,"longitude":-60}'], 'longitude out of range' => ['{"latitude":-32,"longitude":181}'], 'missing longitude' => ['{"latitude":-32}'], 'nonnumeric latitude' => ['{"latitude":"unknown","longitude":-60}'], 'invalid JSON' => ['not-json'], ]; } public function test_coordinates_without_address_text_stop_the_copy_instead_of_losing_data(): void { $id = $this->event(null, ['latitude' => -32, 'longitude' => -60]); try { $this->dataMigration()->up(); $this->fail('Coordinates without address text require correction.'); } catch (RuntimeException $exception) { $this->assertStringContainsString("Missing address text for event {$id}", $exception->getMessage()); $this->assertDatabaseCount('addresses', 0); $this->assertNull(Event::findOrFail($id)->address_id); } } public function test_schema_rollback_preserves_events_legacy_fields_and_copied_addresses(): void { $id = $this->event('Predio', ['latitude' => -32, 'longitude' => -60]); $this->dataMigration()->up(); $this->schemaMigration()->down(); $this->assertFalse(Schema::hasColumn('events', 'address_id')); $this->assertDatabaseHas('events', ['id' => $id, 'location' => 'Predio']); $this->assertDatabaseCount('addresses', 1); $this->assertTrue(Schema::hasColumn('events', 'exact_location')); } private function event(?string $location, ?array $coordinates = null): int { return DB::table('events')->insertGetId([ 'tenant_code' => 'alpha', 'title' => 'Evento', 'location' => $location, 'exact_location' => $coordinates === null ? null : json_encode($coordinates, JSON_THROW_ON_ERROR), 'created_at' => '2026-09-01 12:00:00', 'updated_at' => '2026-09-02 12:00:00', ]); } private function schemaMigration(): Migration { return require database_path('migrations/2026_09_30_000200_add_address_id_to_events.php'); } private function dataMigration(): Migration { return require database_path('migrations/2026_09_30_000300_copy_event_locations_to_addresses.php'); } }