From 1f87583c4b615ae0dc13729f45809c7d5e831746 Mon Sep 17 00:00:00 2001 From: sinach Date: Thu, 24 Apr 2025 14:01:20 +0100 Subject: [PATCH] feat: world countries and worl regions --- database/factories/WorldCountryFactory.php | 20 ++++ database/factories/WorldRegionFactory.php | 26 +++++ ..._create_world_countries_table.php copy.php | 23 +++++ ...region_id_to_world_countries_table.php.php | 23 +++++ ..._world_country_in_special_region_table.php | 25 +++++ ...4_11_000004_create_world_regions_table.php | 26 +++++ database/seeders/SeedEURegionSeeder.php | 32 ++++++ database/testing.sqlite | Bin 0 -> 229376 bytes database/testing.sqlite-journal | Bin 0 -> 8720 bytes phpunit.xml.dist | 4 +- src/Filament/Pages/CreateCountry.php | 11 ++ src/Filament/Pages/CreateWorldRegion.php | 10 ++ src/Filament/Pages/EditCountry.php | 11 ++ src/Filament/Pages/EditWorldRegion.php | 10 ++ src/Filament/Pages/ListCountries.php | 11 ++ src/Filament/Pages/ListWorldRegions.php | 11 ++ src/Filament/Resources/CountryResource.php | 95 ++++++++++++++++++ .../Resources/WorldRegionResource.php | 76 ++++++++++++++ src/Models/WorldCountry.php | 67 ++++++++++++ src/Models/WorldRegion.php | 53 ++++++++++ .../Resources/CountryPermissionTest.php | 28 ++++++ .../Filament/Resources/CountryRegionTest.php | 55 ++++++++++ .../Resources/ImportCountriesJobTest.php | 9 ++ .../Filament/Resources/WorldRegionTest.php | 58 +++++++++++ tests/Feature/bootstrap.php | 6 ++ tests/TestCase.php | 6 +- 26 files changed, 694 insertions(+), 2 deletions(-) create mode 100644 database/factories/WorldCountryFactory.php create mode 100644 database/factories/WorldRegionFactory.php create mode 100644 database/migrations/2025_04_11_000001_create_world_countries_table.php copy.php create mode 100644 database/migrations/2025_04_11_000002_add_region_id_to_world_countries_table.php.php create mode 100644 database/migrations/2025_04_11_000003_create_world_country_in_special_region_table.php create mode 100644 database/migrations/2025_04_11_000004_create_world_regions_table.php create mode 100644 database/seeders/SeedEURegionSeeder.php create mode 100644 database/testing.sqlite create mode 100644 database/testing.sqlite-journal create mode 100644 src/Filament/Pages/CreateCountry.php create mode 100644 src/Filament/Pages/CreateWorldRegion.php create mode 100644 src/Filament/Pages/EditCountry.php create mode 100644 src/Filament/Pages/EditWorldRegion.php create mode 100644 src/Filament/Pages/ListCountries.php create mode 100644 src/Filament/Pages/ListWorldRegions.php create mode 100644 src/Filament/Resources/CountryResource.php create mode 100644 src/Filament/Resources/WorldRegionResource.php create mode 100644 src/Models/WorldCountry.php create mode 100644 src/Models/WorldRegion.php create mode 100644 tests/Feature/Filament/Resources/CountryPermissionTest.php create mode 100644 tests/Feature/Filament/Resources/CountryRegionTest.php create mode 100644 tests/Feature/Filament/Resources/ImportCountriesJobTest.php create mode 100644 tests/Feature/Filament/Resources/WorldRegionTest.php create mode 100644 tests/Feature/bootstrap.php diff --git a/database/factories/WorldCountryFactory.php b/database/factories/WorldCountryFactory.php new file mode 100644 index 0000000..1a6f48e --- /dev/null +++ b/database/factories/WorldCountryFactory.php @@ -0,0 +1,20 @@ + $this->faker->country, + 'code' => $this->faker->unique()->countryCode, + 'region_id' => null, // Will be set manually or in test + ]; + } +} diff --git a/database/factories/WorldRegionFactory.php b/database/factories/WorldRegionFactory.php new file mode 100644 index 0000000..186e18a --- /dev/null +++ b/database/factories/WorldRegionFactory.php @@ -0,0 +1,26 @@ + $this->faker->unique()->country, + 'code' => $this->faker->unique()->countryCode, + 'parent_id' => null, + 'is_special' => false, + ]; + } + + public function special(): self + { + return $this->state(fn () => ['is_special' => true]); + } +} diff --git a/database/migrations/2025_04_11_000001_create_world_countries_table.php copy.php b/database/migrations/2025_04_11_000001_create_world_countries_table.php copy.php new file mode 100644 index 0000000..d7c90c3 --- /dev/null +++ b/database/migrations/2025_04_11_000001_create_world_countries_table.php copy.php @@ -0,0 +1,23 @@ +id(); + $table->string('code')->nullable(); + $table->foreignId('region_id')->nullable()->constrained('world_regions')->nullOnDelete(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('world_countries'); + } +}; diff --git a/database/migrations/2025_04_11_000002_add_region_id_to_world_countries_table.php.php b/database/migrations/2025_04_11_000002_add_region_id_to_world_countries_table.php.php new file mode 100644 index 0000000..1e7cf36 --- /dev/null +++ b/database/migrations/2025_04_11_000002_add_region_id_to_world_countries_table.php.php @@ -0,0 +1,23 @@ +id(); + $table->string('code')->nullable(); + $table->foreignId('region_id')->nullable()->constrained('world_regions')->nullOnDelete(); + // $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('world_countries'); + } +}; diff --git a/database/migrations/2025_04_11_000003_create_world_country_in_special_region_table.php b/database/migrations/2025_04_11_000003_create_world_country_in_special_region_table.php new file mode 100644 index 0000000..69f660e --- /dev/null +++ b/database/migrations/2025_04_11_000003_create_world_country_in_special_region_table.php @@ -0,0 +1,25 @@ +id(); + $table->foreignId('country_id')->constrained('world_countries')->cascadeOnDelete(); + $table->foreignId('region_id')->constrained('world_regions')->cascadeOnDelete(); + $table->date('start_date')->default(DB::raw('CURRENT_DATE')); + $table->date('end_date')->nullable(); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists('world_country_in_special_region'); + } +}; diff --git a/database/migrations/2025_04_11_000004_create_world_regions_table.php b/database/migrations/2025_04_11_000004_create_world_regions_table.php new file mode 100644 index 0000000..26801d1 --- /dev/null +++ b/database/migrations/2025_04_11_000004_create_world_regions_table.php @@ -0,0 +1,26 @@ +id(); + $table->string('name'); + $table->string('code')->nullable()->unique(); + $table->foreignId('parent_id')->nullable()->constrained('world_regions')->nullOnDelete(); + $table->boolean('is_special')->default(false); + $table->softDeletes(); // For soft delete functionality + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists('world_regions'); + } +}; diff --git a/database/seeders/SeedEURegionSeeder.php b/database/seeders/SeedEURegionSeeder.php new file mode 100644 index 0000000..2c15904 --- /dev/null +++ b/database/seeders/SeedEURegionSeeder.php @@ -0,0 +1,32 @@ + 'EU'], + ['name' => 'European Union', 'is_special' => true] + ); + + $euCountryCodes = [ + 'FR', 'DE', 'IT', 'ES', 'NL', 'BE', 'PL', 'SE', 'AT', 'FI', 'DK', + 'IE', 'PT', 'CZ', 'SK', 'HU', 'RO', 'BG', 'HR', 'GR', 'SI', + 'LT', 'LV', 'EE', 'CY', 'LU', 'MT' + ]; + + $countries = WorldCountry::whereIn('code', $euCountryCodes)->get(); + + foreach ($countries as $country) { + $country->specialRegions()->syncWithoutDetaching([ + $eu->id => ['start_date' => now()] + ]); + } + } +} diff --git a/database/testing.sqlite b/database/testing.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..fa61b1ede490836a259a882c225ba8d3b09e0f0f GIT binary patch literal 229376 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 literal 0 HcmV?d00001 diff --git a/phpunit.xml.dist b/phpunit.xml.dist index ab1ad48..3a55237 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -16,13 +16,15 @@ + - + + diff --git a/src/Filament/Pages/CreateCountry.php b/src/Filament/Pages/CreateCountry.php new file mode 100644 index 0000000..9f9baae --- /dev/null +++ b/src/Filament/Pages/CreateCountry.php @@ -0,0 +1,11 @@ +schema([ + Forms\Components\TextInput::make('name') + ->required() + ->maxLength(255), + Forms\Components\TextInput::make('code') + ->required() + ->maxLength(10), + Forms\Components\Select::make('region_id') + ->relationship('geoRegion', 'name') + ->label('Geo Region') + ->searchable() + ->nullable(), + Forms\Components\Select::make('specialRegions') + ->multiple() + ->relationship('specialRegions', 'name') + ->label('Special Regions') + ->searchable(), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('name')->sortable()->searchable(), + Tables\Columns\TextColumn::make('code')->sortable()->searchable(), + Tables\Columns\TextColumn::make('geoRegion.name') + ->label('Geo Region') + ->sortable() + ->searchable(), + Tables\Columns\TextColumn::make('specialRegions.name') + ->label('Special Regions') + ->badge() + ->separator(', ') + ]) + ->filters([ + SelectFilter::make('region_id') + ->relationship('geoRegion', 'name') + ->label('Geo Region'), + SelectFilter::make('specialRegions') + ->relationship('specialRegions', 'name') + ->label('Special Region'), + ]) + ->actions([ + Tables\Actions\ViewAction::make(), + Tables\Actions\EditAction::make(), + Tables\Actions\DeleteAction::make(), + ]) + ->bulkActions([ + Tables\Actions\DeleteBulkAction::make(), + ]); + } + + public static function getPages(): array + { + return [ + 'index' => ListCountries::route('/'), + 'create' => CreateCountry::route('/create'), + 'edit' => EditCountry::route('/{record}/edit'), + ]; + } +} diff --git a/src/Filament/Resources/WorldRegionResource.php b/src/Filament/Resources/WorldRegionResource.php new file mode 100644 index 0000000..d9f4712 --- /dev/null +++ b/src/Filament/Resources/WorldRegionResource.php @@ -0,0 +1,76 @@ +schema([ + Forms\Components\TextInput::make('name')->required()->maxLength(255), + Forms\Components\TextInput::make('code')->nullable()->maxLength(20), + Forms\Components\Toggle::make('is_special')->label('Special region'), + Forms\Components\Select::make('parent_id') + ->relationship('parent', 'name') + ->label('Parent Region') + ->nullable(), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('name')->sortable()->searchable(), + Tables\Columns\TextColumn::make('code')->sortable()->searchable(), + Tables\Columns\IconColumn::make('is_special')->boolean(), + Tables\Columns\TextColumn::make('parent.name')->label('Parent'), + ]) + ->filters([ + TrashedFilter::make() + ]) + ->actions([ + Tables\Actions\ViewAction::make(), + Tables\Actions\EditAction::make(), + Tables\Actions\DeleteAction::make(), + Tables\Actions\RestoreAction::make(), + Tables\Actions\ForceDeleteAction::make(), + ]) + ->bulkActions([ + Tables\Actions\DeleteBulkAction::make(), + Tables\Actions\ForceDeleteBulkAction::make(), + Tables\Actions\RestoreBulkAction::make(), + ]); + } + + public static function getPages(): array + { + return [ + 'index' => ListWorldRegions::route('/'), + 'create' => CreateWorldRegion::route('/create'), + 'edit' => EditWorldRegion::route('/{record}/edit'), + ]; + } +} diff --git a/src/Models/WorldCountry.php b/src/Models/WorldCountry.php new file mode 100644 index 0000000..7debed2 --- /dev/null +++ b/src/Models/WorldCountry.php @@ -0,0 +1,67 @@ +belongsTo(WorldRegion::class, 'region_id'); + } + + /** + * Special regions (e.g., EU, EEA) the country is part of. + */ + public function specialRegions() + { + return $this->belongsToMany( + WorldRegion::class, + 'world_country_in_special_region', + 'country_id', + 'region_id' + )->withPivot('start_date', 'end_date') + ->withTimestamps(); + } + + /** + * Check if country is currently in a special region based on dates. + */ + public function isInSpecialRegion(string $regionCode): bool + { + return $this->specialRegions() + ->where('code', $regionCode) + ->where(function ($query) { + $query->whereNull('end_date')->orWhere('end_date', '>', now()); + }) + ->where('start_date', '<=', now()) + ->exists(); + } + + public static function newFactory(): WorldCountryFactory +{ + return WorldCountryFactory::new(); +} +} diff --git a/src/Models/WorldRegion.php b/src/Models/WorldRegion.php new file mode 100644 index 0000000..fcb5dbc --- /dev/null +++ b/src/Models/WorldRegion.php @@ -0,0 +1,53 @@ +hasMany(WorldRegion::class, 'parent_id'); + } + + /** + * Parent region (if any). + */ + public function parent() + { + return $this->belongsTo(WorldRegion::class, 'parent_id'); + } + + /** + * Countries that belong to this region (geo). + */ + public function countries() + { + return $this->hasMany(WorldCountry::class, 'region_id'); + } + + public static function newFactory(): WorldRegionFactory +{ + return WorldRegionFactory::new(); +} + +} diff --git a/tests/Feature/Filament/Resources/CountryPermissionTest.php b/tests/Feature/Filament/Resources/CountryPermissionTest.php new file mode 100644 index 0000000..0a6caaa --- /dev/null +++ b/tests/Feature/Filament/Resources/CountryPermissionTest.php @@ -0,0 +1,28 @@ +set_up_super_admin_and_tenant(); +}); + +test('authorized user can access country index', function () { + Auth::login($this->superAdmin); + $this->get('/admin/countries')->assertOk(); +}); + +test('authorized user can access create country page', function () { + Auth::login($this->superAdmin); + $this->get('/admin/countries/create')->assertOk(); +}); + +test('country index shows expected structure', function () { + Auth::login($this->superAdmin); + $this->get('/admin/countries') + ->assertSee('Countries') // Adjust based on actual blade titles + ->assertStatus(200); +}); diff --git a/tests/Feature/Filament/Resources/CountryRegionTest.php b/tests/Feature/Filament/Resources/CountryRegionTest.php new file mode 100644 index 0000000..19f0020 --- /dev/null +++ b/tests/Feature/Filament/Resources/CountryRegionTest.php @@ -0,0 +1,55 @@ +set_up_super_admin_and_tenant(); +}); + +test('can assign a geo region to a country', function () { + Auth::login($this->superAdmin); + + $region = WorldRegion::factory()->create(); + $country = WorldCountry::factory()->create(); + + $country->region()->associate($region); + $country->save(); + + $this->assertEquals($region->id, $country->region_id); +}); + +test('can attach special regions to a country with dates', function () { + Auth::login($this->superAdmin); + + $region = WorldRegion::factory()->create(['is_special' => true]); + $country = WorldCountry::factory()->create(); + + $country->specialRegions()->attach($region->id, [ + 'start_date' => now()->subYear(), + 'end_date' => null, + ]); + + $this->assertDatabaseHas('world_country_in_special_region', [ + 'country_id' => $country->id, + 'region_id' => $region->id, + ]); +}); + +test('can determine if country is in special region at date', function () { + $region = WorldRegion::factory()->create(['is_special' => true]); + $country = WorldCountry::factory()->create(); + + $country->specialRegions()->attach($region->id, [ + 'start_date' => now()->subMonth(), + 'end_date' => now()->addMonth(), + ]); + + $inRegion = $country->isInSpecialRegion('EU'); // assuming such method exists + expect($inRegion)->toBeTrue(); +}); diff --git a/tests/Feature/Filament/Resources/ImportCountriesJobTest.php b/tests/Feature/Filament/Resources/ImportCountriesJobTest.php new file mode 100644 index 0000000..e745cf4 --- /dev/null +++ b/tests/Feature/Filament/Resources/ImportCountriesJobTest.php @@ -0,0 +1,9 @@ +artisan('world:import') +// ->assertSuccessful(); + +// $this->assertDatabaseHas('world_regions', ['name' => 'Africa']); +// $this->assertDatabaseHas('world_regions', ['parent_id' => fn ($id) => !is_null($id)]); +// }); diff --git a/tests/Feature/Filament/Resources/WorldRegionTest.php b/tests/Feature/Filament/Resources/WorldRegionTest.php new file mode 100644 index 0000000..0bd85f3 --- /dev/null +++ b/tests/Feature/Filament/Resources/WorldRegionTest.php @@ -0,0 +1,58 @@ +set_up_super_admin_and_tenant(); +}); + +test('authorized user can access region index', function () { + Auth::login($this->superAdmin); + $this->get('/admin/world-regions')->assertOk(); +}); + +test('authorized user can access create region page', function () { + Auth::login($this->superAdmin); + $this->get('/admin/world-regions/create')->assertOk(); +}); + +test('region index shows expected structure', function () { + Auth::login($this->superAdmin); + $this->get('/admin/world-regions') + ->assertSee('World Regions') // Adjust based on actual blade content + ->assertStatus(200); +}); + +test('can create a geo region and sub-region', function () { + Auth::login($this->superAdmin); + + $parent = \Eclipse\Core\Models\WorldRegion::factory()->create(); + $childData = [ + 'name' => 'West Africa', + 'is_special' => false, + 'parent_id' => $parent->id, + ]; + + $this->post('/admin/world-regions', $childData) + ->assertRedirect('/admin/world-regions'); + + $this->assertDatabaseHas('world_regions', $childData); +}); + +test('can create a special region', function () { + Auth::login($this->superAdmin); + + $data = [ + 'name' => 'EU', + 'is_special' => true, + ]; + + $this->post('/admin/world-regions', $data) + ->assertRedirect('/admin/world-regions'); + + $this->assertDatabaseHas('world_regions', ['name' => 'EU', 'is_special' => true]); +}); \ No newline at end of file diff --git a/tests/Feature/bootstrap.php b/tests/Feature/bootstrap.php new file mode 100644 index 0000000..c96e83d --- /dev/null +++ b/tests/Feature/bootstrap.php @@ -0,0 +1,6 @@ +set('database.default', 'sqlite'); + // config()->set('database.connections.sqlite.database', database_path('testing.sqlite')); + parent::setUp(); $this->withoutVite();