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();