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 0000000..fa61b1e Binary files /dev/null and b/database/testing.sqlite differ diff --git a/database/testing.sqlite-journal b/database/testing.sqlite-journal new file mode 100644 index 0000000..25b8e54 Binary files /dev/null and b/database/testing.sqlite-journal differ diff --git a/phpunit.xml.dist b/phpunit.xml.dist index ab1ad48..1dc7df0 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -26,3 +26,34 @@ + + + + + + ./tests + + + + + ./src + + + + + + + + + + + + + + + + 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();