Repo rebuild
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'name' => 'Countries',
|
||||
'allow_manual_create' => false,
|
||||
'allow_delete' => false,
|
||||
'default_active' => true,
|
||||
];
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Countries\Console;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class SyncIsoCountriesCommand extends Command
|
||||
{
|
||||
/** @var string */
|
||||
protected $signature = 'countries:sync-iso {--subdivisions : Also seed bundled ISO 3166-2 subdivision data}';
|
||||
|
||||
/** @var string */
|
||||
protected $description = 'Synchronise bundled ISO 3166 country reference data.';
|
||||
|
||||
/**
|
||||
* Execute the command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$this->call('db:seed', [
|
||||
'--class' => 'Modules\\Countries\\Database\\Seeders\\IsoCountriesSeeder',
|
||||
'--force' => true,
|
||||
]);
|
||||
|
||||
if ($this->option('subdivisions')) {
|
||||
$this->call('db:seed', [
|
||||
'--class' => 'Modules\\Countries\\Database\\Seeders\\IsoCountrySubdivisionsSeeder',
|
||||
'--force' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->components->info('ISO country reference data synchronised.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('countries', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->char('alpha2', 2)->unique()->comment('ISO 3166-1 alpha-2 country code.');
|
||||
$table->char('alpha3', 3)->unique()->comment('ISO 3166-1 alpha-3 country code.');
|
||||
$table->char('numeric', 3)->nullable()->unique()->comment('ISO 3166-1 numeric country code.');
|
||||
$table->string('name')->comment('Canonical ISO country name.');
|
||||
$table->string('official_name')->nullable()->comment('Official ISO country name where available.');
|
||||
$table->string('common_name')->nullable()->comment('Common country name where available.');
|
||||
$table->string('display_name')->nullable()->comment('Application-specific display name override.');
|
||||
$table->string('flag_emoji', 16)->nullable()->comment('Regional indicator flag emoji derived from alpha-2 code.');
|
||||
$table->boolean('is_active')->default(true)->index()->comment('Whether this country is available for selection in business workflows.');
|
||||
$table->unsignedInteger('sort_order')->default(0)->index()->comment('Optional UI ordering override.');
|
||||
$table->timestamp('last_synced_at')->nullable()->comment('Last time this row was synced from bundled ISO data.');
|
||||
$table->timestamps();
|
||||
$table->index(['is_active', 'sort_order', 'name']);
|
||||
$table->index(['name']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('countries');
|
||||
}
|
||||
};
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Add ISO 3166 governance metadata and ISO 3166-2 subdivision support.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('countries', function (Blueprint $table): void {
|
||||
if (! Schema::hasColumn('countries', 'iso_status')) {
|
||||
$table->string('iso_status', 32)->default('official')->index()->after('is_active')->comment('ISO 3166-1 lifecycle status: official, reserved, transitional, deleted, user_assigned, exceptionally_reserved.');
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('countries', 'independent')) {
|
||||
$table->boolean('independent')->nullable()->after('iso_status')->comment('Whether the ISO 3166-1 entry represents an independent state where known.');
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('countries', 'region')) {
|
||||
$table->string('region')->nullable()->index()->after('independent')->comment('UN M49-style broad region for filtering and reporting.');
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('countries', 'subregion')) {
|
||||
$table->string('subregion')->nullable()->index()->after('region')->comment('UN M49-style subregion for filtering and reporting.');
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('countries', 'source')) {
|
||||
$table->string('source')->nullable()->after('last_synced_at')->comment('Reference data source used for the last sync.');
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('countries', 'source_version')) {
|
||||
$table->string('source_version')->nullable()->after('source')->comment('Reference data version/release identifier.');
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('countries', 'metadata')) {
|
||||
$table->json('metadata')->nullable()->after('source_version')->comment('Additional non-authoritative metadata, never used as the primary ISO identity.');
|
||||
}
|
||||
});
|
||||
|
||||
if (! Schema::hasTable('country_subdivisions')) {
|
||||
Schema::create('country_subdivisions', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('country_id')->constrained('countries')->cascadeOnDelete();
|
||||
$table->char('country_alpha2', 2)->index()->comment('Parent ISO 3166-1 alpha-2 code, duplicated for fast lookup and imports.');
|
||||
$table->string('code', 16)->unique()->comment('Full ISO 3166-2 subdivision code, for example GB-ENG or US-CA.');
|
||||
$table->string('subdivision_code', 12)->comment('Subdivision part after the country prefix, for example ENG or CA.');
|
||||
$table->string('name')->comment('ISO 3166-2 subdivision name.');
|
||||
$table->string('local_name')->nullable()->comment('Optional local-language display name.');
|
||||
$table->string('type')->nullable()->index()->comment('Subdivision type, for example country, province, state, territory, council area.');
|
||||
$table->boolean('is_active')->default(true)->index()->comment('Whether this subdivision is currently available in business workflows.');
|
||||
$table->string('iso_status', 32)->default('official')->index()->comment('ISO 3166-2 lifecycle status.');
|
||||
$table->unsignedInteger('sort_order')->default(0)->index();
|
||||
$table->timestamp('last_synced_at')->nullable();
|
||||
$table->string('source')->nullable();
|
||||
$table->string('source_version')->nullable();
|
||||
$table->json('metadata')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['country_id', 'subdivision_code']);
|
||||
$table->index(['country_alpha2', 'is_active', 'sort_order', 'name']);
|
||||
$table->index(['type', 'is_active']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the ISO 3166 support additions.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('country_subdivisions');
|
||||
|
||||
Schema::table('countries', function (Blueprint $table): void {
|
||||
foreach (['metadata', 'source_version', 'source', 'subregion', 'region', 'independent', 'iso_status'] as $column) {
|
||||
if (Schema::hasColumn('countries', $column)) {
|
||||
$table->dropColumn($column);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Add enterprise ISO 3166 support fields used by platform administration,
|
||||
* address validation, logistics, billing, carrier and compliance workflows.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('countries', function (Blueprint $table): void {
|
||||
$columns = Schema::getColumnListing('countries');
|
||||
|
||||
if (! in_array('native_name', $columns, true)) {
|
||||
$table->string('native_name')->nullable()->after('display_name')->comment('Native/local country name where available.');
|
||||
}
|
||||
|
||||
if (! in_array('capital', $columns, true)) {
|
||||
$table->string('capital')->nullable()->after('native_name')->comment('Administrative capital city where applicable.');
|
||||
}
|
||||
|
||||
if (! in_array('continent', $columns, true)) {
|
||||
$table->string('continent', 64)->nullable()->index()->after('capital')->comment('Continent used for reporting and regional grouping.');
|
||||
}
|
||||
|
||||
if (! in_array('intermediate_region', $columns, true)) {
|
||||
$table->string('intermediate_region')->nullable()->index()->after('subregion')->comment('UN M49 intermediate region where applicable.');
|
||||
}
|
||||
|
||||
if (! in_array('sovereignty_status', $columns, true)) {
|
||||
$table->string('sovereignty_status', 64)->nullable()->index()->after('independent')->comment('Sovereignty or territory status.');
|
||||
}
|
||||
|
||||
if (! in_array('un_member', $columns, true)) {
|
||||
$table->boolean('un_member')->nullable()->index()->after('sovereignty_status')->comment('Whether the country is a United Nations member state.');
|
||||
}
|
||||
|
||||
if (! in_array('calling_code', $columns, true)) {
|
||||
$table->string('calling_code', 16)->nullable()->index()->after('flag_emoji')->comment('Primary international phone calling code, for example +44.');
|
||||
}
|
||||
|
||||
if (! in_array('tld', $columns, true)) {
|
||||
$table->string('tld', 16)->nullable()->index()->after('calling_code')->comment('Primary country-code top-level domain, for example .uk.');
|
||||
}
|
||||
|
||||
if (! in_array('currency_code', $columns, true)) {
|
||||
$table->char('currency_code', 3)->nullable()->index()->after('tld')->comment('Primary ISO 4217 currency code.');
|
||||
}
|
||||
|
||||
if (! in_array('currency_name', $columns, true)) {
|
||||
$table->string('currency_name')->nullable()->after('currency_code')->comment('Primary currency display name.');
|
||||
}
|
||||
|
||||
if (! in_array('currency_symbol', $columns, true)) {
|
||||
$table->string('currency_symbol', 16)->nullable()->after('currency_name')->comment('Primary currency symbol.');
|
||||
}
|
||||
|
||||
if (! in_array('languages', $columns, true)) {
|
||||
$table->json('languages')->nullable()->after('currency_symbol')->comment('Language metadata as JSON array/object.');
|
||||
}
|
||||
|
||||
if (! in_array('timezones', $columns, true)) {
|
||||
$table->json('timezones')->nullable()->after('languages')->comment('Timezone metadata as JSON array.');
|
||||
}
|
||||
|
||||
if (! in_array('latitude', $columns, true)) {
|
||||
$table->decimal('latitude', 10, 7)->nullable()->after('timezones')->comment('Approximate latitude for maps and geo grouping.');
|
||||
}
|
||||
|
||||
if (! in_array('longitude', $columns, true)) {
|
||||
$table->decimal('longitude', 10, 7)->nullable()->after('latitude')->comment('Approximate longitude for maps and geo grouping.');
|
||||
}
|
||||
|
||||
if (! in_array('flag_svg_url', $columns, true)) {
|
||||
$table->string('flag_svg_url')->nullable()->after('longitude')->comment('Optional externally hosted SVG flag URL.');
|
||||
}
|
||||
|
||||
if (! in_array('flag_png_url', $columns, true)) {
|
||||
$table->string('flag_png_url')->nullable()->after('flag_svg_url')->comment('Optional externally hosted PNG flag URL.');
|
||||
}
|
||||
|
||||
if (! in_array('ioc_code', $columns, true)) {
|
||||
$table->string('ioc_code', 8)->nullable()->index()->after('flag_png_url')->comment('IOC country code where applicable.');
|
||||
}
|
||||
|
||||
if (! in_array('fifa_code', $columns, true)) {
|
||||
$table->string('fifa_code', 8)->nullable()->index()->after('ioc_code')->comment('FIFA country code where applicable.');
|
||||
}
|
||||
|
||||
if (! in_array('postal_code_format', $columns, true)) {
|
||||
$table->string('postal_code_format')->nullable()->after('fifa_code')->comment('Human-readable postal-code format/pattern.');
|
||||
}
|
||||
|
||||
if (! in_array('address_format', $columns, true)) {
|
||||
$table->text('address_format')->nullable()->after('postal_code_format')->comment('Country-specific address formatting guidance.');
|
||||
}
|
||||
|
||||
if (! in_array('deleted_at', $columns, true)) {
|
||||
$table->softDeletes()->comment('Soft deletion for retired/custom country records.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse enterprise ISO 3166 support fields.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('countries', function (Blueprint $table): void {
|
||||
foreach ([
|
||||
'deleted_at',
|
||||
'address_format',
|
||||
'postal_code_format',
|
||||
'fifa_code',
|
||||
'ioc_code',
|
||||
'flag_png_url',
|
||||
'flag_svg_url',
|
||||
'longitude',
|
||||
'latitude',
|
||||
'timezones',
|
||||
'languages',
|
||||
'currency_symbol',
|
||||
'currency_name',
|
||||
'currency_code',
|
||||
'tld',
|
||||
'calling_code',
|
||||
'un_member',
|
||||
'sovereignty_status',
|
||||
'intermediate_region',
|
||||
'continent',
|
||||
'capital',
|
||||
'native_name',
|
||||
] as $column) {
|
||||
if (Schema::hasColumn('countries', $column)) {
|
||||
$table->dropColumn($column);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Countries\Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class CountriesDatabaseSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Seed ISO 3166 country reference data.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$this->command?->info('Running CountriesDatabaseSeeder');
|
||||
$this->call([
|
||||
IsoCountriesSeeder::class,
|
||||
IsoCountrySubdivisionsSeeder::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Countries\Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Modules\Countries\Models\Country;
|
||||
|
||||
class IsoCountriesSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$now = Carbon::now();
|
||||
$countries = [
|
||||
['alpha2' => 'AD', 'alpha3' => 'AND', 'numeric' => '020', 'name' => 'Andorra', 'official_name' => 'Principality of Andorra', 'common_name' => null, 'flag_emoji' => '🇦🇩'],
|
||||
['alpha2' => 'AE', 'alpha3' => 'ARE', 'numeric' => '784', 'name' => 'United Arab Emirates', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇦🇪'],
|
||||
['alpha2' => 'AF', 'alpha3' => 'AFG', 'numeric' => '004', 'name' => 'Afghanistan', 'official_name' => 'Islamic Republic of Afghanistan', 'common_name' => null, 'flag_emoji' => '🇦🇫'],
|
||||
['alpha2' => 'AG', 'alpha3' => 'ATG', 'numeric' => '028', 'name' => 'Antigua and Barbuda', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇦🇬'],
|
||||
['alpha2' => 'AI', 'alpha3' => 'AIA', 'numeric' => '660', 'name' => 'Anguilla', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇦🇮'],
|
||||
['alpha2' => 'AL', 'alpha3' => 'ALB', 'numeric' => '008', 'name' => 'Albania', 'official_name' => 'Republic of Albania', 'common_name' => null, 'flag_emoji' => '🇦🇱'],
|
||||
['alpha2' => 'AM', 'alpha3' => 'ARM', 'numeric' => '051', 'name' => 'Armenia', 'official_name' => 'Republic of Armenia', 'common_name' => null, 'flag_emoji' => '🇦🇲'],
|
||||
['alpha2' => 'AO', 'alpha3' => 'AGO', 'numeric' => '024', 'name' => 'Angola', 'official_name' => 'Republic of Angola', 'common_name' => null, 'flag_emoji' => '🇦🇴'],
|
||||
['alpha2' => 'AQ', 'alpha3' => 'ATA', 'numeric' => '010', 'name' => 'Antarctica', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇦🇶'],
|
||||
['alpha2' => 'AR', 'alpha3' => 'ARG', 'numeric' => '032', 'name' => 'Argentina', 'official_name' => 'Argentine Republic', 'common_name' => null, 'flag_emoji' => '🇦🇷'],
|
||||
['alpha2' => 'AS', 'alpha3' => 'ASM', 'numeric' => '016', 'name' => 'American Samoa', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇦🇸'],
|
||||
['alpha2' => 'AT', 'alpha3' => 'AUT', 'numeric' => '040', 'name' => 'Austria', 'official_name' => 'Republic of Austria', 'common_name' => null, 'flag_emoji' => '🇦🇹'],
|
||||
['alpha2' => 'AU', 'alpha3' => 'AUS', 'numeric' => '036', 'name' => 'Australia', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇦🇺'],
|
||||
['alpha2' => 'AW', 'alpha3' => 'ABW', 'numeric' => '533', 'name' => 'Aruba', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇦🇼'],
|
||||
['alpha2' => 'AX', 'alpha3' => 'ALA', 'numeric' => '248', 'name' => 'Åland Islands', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇦🇽'],
|
||||
['alpha2' => 'AZ', 'alpha3' => 'AZE', 'numeric' => '031', 'name' => 'Azerbaijan', 'official_name' => 'Republic of Azerbaijan', 'common_name' => null, 'flag_emoji' => '🇦🇿'],
|
||||
['alpha2' => 'BA', 'alpha3' => 'BIH', 'numeric' => '070', 'name' => 'Bosnia and Herzegovina', 'official_name' => 'Republic of Bosnia and Herzegovina', 'common_name' => null, 'flag_emoji' => '🇧🇦'],
|
||||
['alpha2' => 'BB', 'alpha3' => 'BRB', 'numeric' => '052', 'name' => 'Barbados', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇧🇧'],
|
||||
['alpha2' => 'BD', 'alpha3' => 'BGD', 'numeric' => '050', 'name' => 'Bangladesh', 'official_name' => 'People\'s Republic of Bangladesh', 'common_name' => null, 'flag_emoji' => '🇧🇩'],
|
||||
['alpha2' => 'BE', 'alpha3' => 'BEL', 'numeric' => '056', 'name' => 'Belgium', 'official_name' => 'Kingdom of Belgium', 'common_name' => null, 'flag_emoji' => '🇧🇪'],
|
||||
['alpha2' => 'BF', 'alpha3' => 'BFA', 'numeric' => '854', 'name' => 'Burkina Faso', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇧🇫'],
|
||||
['alpha2' => 'BG', 'alpha3' => 'BGR', 'numeric' => '100', 'name' => 'Bulgaria', 'official_name' => 'Republic of Bulgaria', 'common_name' => null, 'flag_emoji' => '🇧🇬'],
|
||||
['alpha2' => 'BH', 'alpha3' => 'BHR', 'numeric' => '048', 'name' => 'Bahrain', 'official_name' => 'Kingdom of Bahrain', 'common_name' => null, 'flag_emoji' => '🇧🇭'],
|
||||
['alpha2' => 'BI', 'alpha3' => 'BDI', 'numeric' => '108', 'name' => 'Burundi', 'official_name' => 'Republic of Burundi', 'common_name' => null, 'flag_emoji' => '🇧🇮'],
|
||||
['alpha2' => 'BJ', 'alpha3' => 'BEN', 'numeric' => '204', 'name' => 'Benin', 'official_name' => 'Republic of Benin', 'common_name' => null, 'flag_emoji' => '🇧🇯'],
|
||||
['alpha2' => 'BL', 'alpha3' => 'BLM', 'numeric' => '652', 'name' => 'Saint Barthélemy', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇧🇱'],
|
||||
['alpha2' => 'BM', 'alpha3' => 'BMU', 'numeric' => '060', 'name' => 'Bermuda', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇧🇲'],
|
||||
['alpha2' => 'BN', 'alpha3' => 'BRN', 'numeric' => '096', 'name' => 'Brunei Darussalam', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇧🇳'],
|
||||
['alpha2' => 'BO', 'alpha3' => 'BOL', 'numeric' => '068', 'name' => 'Bolivia, Plurinational State of', 'official_name' => 'Plurinational State of Bolivia', 'common_name' => 'Bolivia', 'flag_emoji' => '🇧🇴'],
|
||||
['alpha2' => 'BQ', 'alpha3' => 'BES', 'numeric' => '535', 'name' => 'Bonaire, Sint Eustatius and Saba', 'official_name' => 'Bonaire, Sint Eustatius and Saba', 'common_name' => null, 'flag_emoji' => '🇧🇶'],
|
||||
['alpha2' => 'BR', 'alpha3' => 'BRA', 'numeric' => '076', 'name' => 'Brazil', 'official_name' => 'Federative Republic of Brazil', 'common_name' => null, 'flag_emoji' => '🇧🇷'],
|
||||
['alpha2' => 'BS', 'alpha3' => 'BHS', 'numeric' => '044', 'name' => 'Bahamas', 'official_name' => 'Commonwealth of the Bahamas', 'common_name' => null, 'flag_emoji' => '🇧🇸'],
|
||||
['alpha2' => 'BT', 'alpha3' => 'BTN', 'numeric' => '064', 'name' => 'Bhutan', 'official_name' => 'Kingdom of Bhutan', 'common_name' => null, 'flag_emoji' => '🇧🇹'],
|
||||
['alpha2' => 'BV', 'alpha3' => 'BVT', 'numeric' => '074', 'name' => 'Bouvet Island', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇧🇻'],
|
||||
['alpha2' => 'BW', 'alpha3' => 'BWA', 'numeric' => '072', 'name' => 'Botswana', 'official_name' => 'Republic of Botswana', 'common_name' => null, 'flag_emoji' => '🇧🇼'],
|
||||
['alpha2' => 'BY', 'alpha3' => 'BLR', 'numeric' => '112', 'name' => 'Belarus', 'official_name' => 'Republic of Belarus', 'common_name' => null, 'flag_emoji' => '🇧🇾'],
|
||||
['alpha2' => 'BZ', 'alpha3' => 'BLZ', 'numeric' => '084', 'name' => 'Belize', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇧🇿'],
|
||||
['alpha2' => 'CA', 'alpha3' => 'CAN', 'numeric' => '124', 'name' => 'Canada', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇨🇦'],
|
||||
['alpha2' => 'CC', 'alpha3' => 'CCK', 'numeric' => '166', 'name' => 'Cocos (Keeling) Islands', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇨🇨'],
|
||||
['alpha2' => 'CD', 'alpha3' => 'COD', 'numeric' => '180', 'name' => 'Congo, The Democratic Republic of the', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇨🇩'],
|
||||
['alpha2' => 'CF', 'alpha3' => 'CAF', 'numeric' => '140', 'name' => 'Central African Republic', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇨🇫'],
|
||||
['alpha2' => 'CG', 'alpha3' => 'COG', 'numeric' => '178', 'name' => 'Congo', 'official_name' => 'Republic of the Congo', 'common_name' => null, 'flag_emoji' => '🇨🇬'],
|
||||
['alpha2' => 'CH', 'alpha3' => 'CHE', 'numeric' => '756', 'name' => 'Switzerland', 'official_name' => 'Swiss Confederation', 'common_name' => null, 'flag_emoji' => '🇨🇭'],
|
||||
['alpha2' => 'CI', 'alpha3' => 'CIV', 'numeric' => '384', 'name' => 'Côte d\'Ivoire', 'official_name' => 'Republic of Côte d\'Ivoire', 'common_name' => null, 'flag_emoji' => '🇨🇮'],
|
||||
['alpha2' => 'CK', 'alpha3' => 'COK', 'numeric' => '184', 'name' => 'Cook Islands', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇨🇰'],
|
||||
['alpha2' => 'CL', 'alpha3' => 'CHL', 'numeric' => '152', 'name' => 'Chile', 'official_name' => 'Republic of Chile', 'common_name' => null, 'flag_emoji' => '🇨🇱'],
|
||||
['alpha2' => 'CM', 'alpha3' => 'CMR', 'numeric' => '120', 'name' => 'Cameroon', 'official_name' => 'Republic of Cameroon', 'common_name' => null, 'flag_emoji' => '🇨🇲'],
|
||||
['alpha2' => 'CN', 'alpha3' => 'CHN', 'numeric' => '156', 'name' => 'China', 'official_name' => 'People\'s Republic of China', 'common_name' => null, 'flag_emoji' => '🇨🇳'],
|
||||
['alpha2' => 'CO', 'alpha3' => 'COL', 'numeric' => '170', 'name' => 'Colombia', 'official_name' => 'Republic of Colombia', 'common_name' => null, 'flag_emoji' => '🇨🇴'],
|
||||
['alpha2' => 'CR', 'alpha3' => 'CRI', 'numeric' => '188', 'name' => 'Costa Rica', 'official_name' => 'Republic of Costa Rica', 'common_name' => null, 'flag_emoji' => '🇨🇷'],
|
||||
['alpha2' => 'CU', 'alpha3' => 'CUB', 'numeric' => '192', 'name' => 'Cuba', 'official_name' => 'Republic of Cuba', 'common_name' => null, 'flag_emoji' => '🇨🇺'],
|
||||
['alpha2' => 'CV', 'alpha3' => 'CPV', 'numeric' => '132', 'name' => 'Cabo Verde', 'official_name' => 'Republic of Cabo Verde', 'common_name' => null, 'flag_emoji' => '🇨🇻'],
|
||||
['alpha2' => 'CW', 'alpha3' => 'CUW', 'numeric' => '531', 'name' => 'Curaçao', 'official_name' => 'Curaçao', 'common_name' => null, 'flag_emoji' => '🇨🇼'],
|
||||
['alpha2' => 'CX', 'alpha3' => 'CXR', 'numeric' => '162', 'name' => 'Christmas Island', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇨🇽'],
|
||||
['alpha2' => 'CY', 'alpha3' => 'CYP', 'numeric' => '196', 'name' => 'Cyprus', 'official_name' => 'Republic of Cyprus', 'common_name' => null, 'flag_emoji' => '🇨🇾'],
|
||||
['alpha2' => 'CZ', 'alpha3' => 'CZE', 'numeric' => '203', 'name' => 'Czechia', 'official_name' => 'Czech Republic', 'common_name' => null, 'flag_emoji' => '🇨🇿'],
|
||||
['alpha2' => 'DE', 'alpha3' => 'DEU', 'numeric' => '276', 'name' => 'Germany', 'official_name' => 'Federal Republic of Germany', 'common_name' => null, 'flag_emoji' => '🇩🇪'],
|
||||
['alpha2' => 'DJ', 'alpha3' => 'DJI', 'numeric' => '262', 'name' => 'Djibouti', 'official_name' => 'Republic of Djibouti', 'common_name' => null, 'flag_emoji' => '🇩🇯'],
|
||||
['alpha2' => 'DK', 'alpha3' => 'DNK', 'numeric' => '208', 'name' => 'Denmark', 'official_name' => 'Kingdom of Denmark', 'common_name' => null, 'flag_emoji' => '🇩🇰'],
|
||||
['alpha2' => 'DM', 'alpha3' => 'DMA', 'numeric' => '212', 'name' => 'Dominica', 'official_name' => 'Commonwealth of Dominica', 'common_name' => null, 'flag_emoji' => '🇩🇲'],
|
||||
['alpha2' => 'DO', 'alpha3' => 'DOM', 'numeric' => '214', 'name' => 'Dominican Republic', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇩🇴'],
|
||||
['alpha2' => 'DZ', 'alpha3' => 'DZA', 'numeric' => '012', 'name' => 'Algeria', 'official_name' => 'People\'s Democratic Republic of Algeria', 'common_name' => null, 'flag_emoji' => '🇩🇿'],
|
||||
['alpha2' => 'EC', 'alpha3' => 'ECU', 'numeric' => '218', 'name' => 'Ecuador', 'official_name' => 'Republic of Ecuador', 'common_name' => null, 'flag_emoji' => '🇪🇨'],
|
||||
['alpha2' => 'EE', 'alpha3' => 'EST', 'numeric' => '233', 'name' => 'Estonia', 'official_name' => 'Republic of Estonia', 'common_name' => null, 'flag_emoji' => '🇪🇪'],
|
||||
['alpha2' => 'EG', 'alpha3' => 'EGY', 'numeric' => '818', 'name' => 'Egypt', 'official_name' => 'Arab Republic of Egypt', 'common_name' => null, 'flag_emoji' => '🇪🇬'],
|
||||
['alpha2' => 'EH', 'alpha3' => 'ESH', 'numeric' => '732', 'name' => 'Western Sahara', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇪🇭'],
|
||||
['alpha2' => 'ER', 'alpha3' => 'ERI', 'numeric' => '232', 'name' => 'Eritrea', 'official_name' => 'the State of Eritrea', 'common_name' => null, 'flag_emoji' => '🇪🇷'],
|
||||
['alpha2' => 'ES', 'alpha3' => 'ESP', 'numeric' => '724', 'name' => 'Spain', 'official_name' => 'Kingdom of Spain', 'common_name' => null, 'flag_emoji' => '🇪🇸'],
|
||||
['alpha2' => 'ET', 'alpha3' => 'ETH', 'numeric' => '231', 'name' => 'Ethiopia', 'official_name' => 'Federal Democratic Republic of Ethiopia', 'common_name' => null, 'flag_emoji' => '🇪🇹'],
|
||||
['alpha2' => 'FI', 'alpha3' => 'FIN', 'numeric' => '246', 'name' => 'Finland', 'official_name' => 'Republic of Finland', 'common_name' => null, 'flag_emoji' => '🇫🇮'],
|
||||
['alpha2' => 'FJ', 'alpha3' => 'FJI', 'numeric' => '242', 'name' => 'Fiji', 'official_name' => 'Republic of Fiji', 'common_name' => null, 'flag_emoji' => '🇫🇯'],
|
||||
['alpha2' => 'FK', 'alpha3' => 'FLK', 'numeric' => '238', 'name' => 'Falkland Islands (Malvinas)', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇫🇰'],
|
||||
['alpha2' => 'FM', 'alpha3' => 'FSM', 'numeric' => '583', 'name' => 'Micronesia, Federated States of', 'official_name' => 'Federated States of Micronesia', 'common_name' => null, 'flag_emoji' => '🇫🇲'],
|
||||
['alpha2' => 'FO', 'alpha3' => 'FRO', 'numeric' => '234', 'name' => 'Faroe Islands', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇫🇴'],
|
||||
['alpha2' => 'FR', 'alpha3' => 'FRA', 'numeric' => '250', 'name' => 'France', 'official_name' => 'French Republic', 'common_name' => null, 'flag_emoji' => '🇫🇷'],
|
||||
['alpha2' => 'GA', 'alpha3' => 'GAB', 'numeric' => '266', 'name' => 'Gabon', 'official_name' => 'Gabonese Republic', 'common_name' => null, 'flag_emoji' => '🇬🇦'],
|
||||
['alpha2' => 'GB', 'alpha3' => 'GBR', 'numeric' => '826', 'name' => 'United Kingdom', 'official_name' => 'United Kingdom of Great Britain and Northern Ireland', 'common_name' => null, 'flag_emoji' => '🇬🇧'],
|
||||
['alpha2' => 'GD', 'alpha3' => 'GRD', 'numeric' => '308', 'name' => 'Grenada', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇬🇩'],
|
||||
['alpha2' => 'GE', 'alpha3' => 'GEO', 'numeric' => '268', 'name' => 'Georgia', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇬🇪'],
|
||||
['alpha2' => 'GF', 'alpha3' => 'GUF', 'numeric' => '254', 'name' => 'French Guiana', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇬🇫'],
|
||||
['alpha2' => 'GG', 'alpha3' => 'GGY', 'numeric' => '831', 'name' => 'Guernsey', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇬🇬'],
|
||||
['alpha2' => 'GH', 'alpha3' => 'GHA', 'numeric' => '288', 'name' => 'Ghana', 'official_name' => 'Republic of Ghana', 'common_name' => null, 'flag_emoji' => '🇬🇭'],
|
||||
['alpha2' => 'GI', 'alpha3' => 'GIB', 'numeric' => '292', 'name' => 'Gibraltar', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇬🇮'],
|
||||
['alpha2' => 'GL', 'alpha3' => 'GRL', 'numeric' => '304', 'name' => 'Greenland', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇬🇱'],
|
||||
['alpha2' => 'GM', 'alpha3' => 'GMB', 'numeric' => '270', 'name' => 'Gambia', 'official_name' => 'Republic of the Gambia', 'common_name' => null, 'flag_emoji' => '🇬🇲'],
|
||||
['alpha2' => 'GN', 'alpha3' => 'GIN', 'numeric' => '324', 'name' => 'Guinea', 'official_name' => 'Republic of Guinea', 'common_name' => null, 'flag_emoji' => '🇬🇳'],
|
||||
['alpha2' => 'GP', 'alpha3' => 'GLP', 'numeric' => '312', 'name' => 'Guadeloupe', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇬🇵'],
|
||||
['alpha2' => 'GQ', 'alpha3' => 'GNQ', 'numeric' => '226', 'name' => 'Equatorial Guinea', 'official_name' => 'Republic of Equatorial Guinea', 'common_name' => null, 'flag_emoji' => '🇬🇶'],
|
||||
['alpha2' => 'GR', 'alpha3' => 'GRC', 'numeric' => '300', 'name' => 'Greece', 'official_name' => 'Hellenic Republic', 'common_name' => null, 'flag_emoji' => '🇬🇷'],
|
||||
['alpha2' => 'GS', 'alpha3' => 'SGS', 'numeric' => '239', 'name' => 'South Georgia and the South Sandwich Islands', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇬🇸'],
|
||||
['alpha2' => 'GT', 'alpha3' => 'GTM', 'numeric' => '320', 'name' => 'Guatemala', 'official_name' => 'Republic of Guatemala', 'common_name' => null, 'flag_emoji' => '🇬🇹'],
|
||||
['alpha2' => 'GU', 'alpha3' => 'GUM', 'numeric' => '316', 'name' => 'Guam', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇬🇺'],
|
||||
['alpha2' => 'GW', 'alpha3' => 'GNB', 'numeric' => '624', 'name' => 'Guinea-Bissau', 'official_name' => 'Republic of Guinea-Bissau', 'common_name' => null, 'flag_emoji' => '🇬🇼'],
|
||||
['alpha2' => 'GY', 'alpha3' => 'GUY', 'numeric' => '328', 'name' => 'Guyana', 'official_name' => 'Republic of Guyana', 'common_name' => null, 'flag_emoji' => '🇬🇾'],
|
||||
['alpha2' => 'HK', 'alpha3' => 'HKG', 'numeric' => '344', 'name' => 'Hong Kong', 'official_name' => 'Hong Kong Special Administrative Region of China', 'common_name' => null, 'flag_emoji' => '🇭🇰'],
|
||||
['alpha2' => 'HM', 'alpha3' => 'HMD', 'numeric' => '334', 'name' => 'Heard Island and McDonald Islands', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇭🇲'],
|
||||
['alpha2' => 'HN', 'alpha3' => 'HND', 'numeric' => '340', 'name' => 'Honduras', 'official_name' => 'Republic of Honduras', 'common_name' => null, 'flag_emoji' => '🇭🇳'],
|
||||
['alpha2' => 'HR', 'alpha3' => 'HRV', 'numeric' => '191', 'name' => 'Croatia', 'official_name' => 'Republic of Croatia', 'common_name' => null, 'flag_emoji' => '🇭🇷'],
|
||||
['alpha2' => 'HT', 'alpha3' => 'HTI', 'numeric' => '332', 'name' => 'Haiti', 'official_name' => 'Republic of Haiti', 'common_name' => null, 'flag_emoji' => '🇭🇹'],
|
||||
['alpha2' => 'HU', 'alpha3' => 'HUN', 'numeric' => '348', 'name' => 'Hungary', 'official_name' => 'Hungary', 'common_name' => null, 'flag_emoji' => '🇭🇺'],
|
||||
['alpha2' => 'ID', 'alpha3' => 'IDN', 'numeric' => '360', 'name' => 'Indonesia', 'official_name' => 'Republic of Indonesia', 'common_name' => null, 'flag_emoji' => '🇮🇩'],
|
||||
['alpha2' => 'IE', 'alpha3' => 'IRL', 'numeric' => '372', 'name' => 'Ireland', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇮🇪'],
|
||||
['alpha2' => 'IL', 'alpha3' => 'ISR', 'numeric' => '376', 'name' => 'Israel', 'official_name' => 'State of Israel', 'common_name' => null, 'flag_emoji' => '🇮🇱'],
|
||||
['alpha2' => 'IM', 'alpha3' => 'IMN', 'numeric' => '833', 'name' => 'Isle of Man', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇮🇲'],
|
||||
['alpha2' => 'IN', 'alpha3' => 'IND', 'numeric' => '356', 'name' => 'India', 'official_name' => 'Republic of India', 'common_name' => null, 'flag_emoji' => '🇮🇳'],
|
||||
['alpha2' => 'IO', 'alpha3' => 'IOT', 'numeric' => '086', 'name' => 'British Indian Ocean Territory', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇮🇴'],
|
||||
['alpha2' => 'IQ', 'alpha3' => 'IRQ', 'numeric' => '368', 'name' => 'Iraq', 'official_name' => 'Republic of Iraq', 'common_name' => null, 'flag_emoji' => '🇮🇶'],
|
||||
['alpha2' => 'IR', 'alpha3' => 'IRN', 'numeric' => '364', 'name' => 'Iran, Islamic Republic of', 'official_name' => 'Islamic Republic of Iran', 'common_name' => 'Iran', 'flag_emoji' => '🇮🇷'],
|
||||
['alpha2' => 'IS', 'alpha3' => 'ISL', 'numeric' => '352', 'name' => 'Iceland', 'official_name' => 'Republic of Iceland', 'common_name' => null, 'flag_emoji' => '🇮🇸'],
|
||||
['alpha2' => 'IT', 'alpha3' => 'ITA', 'numeric' => '380', 'name' => 'Italy', 'official_name' => 'Italian Republic', 'common_name' => null, 'flag_emoji' => '🇮🇹'],
|
||||
['alpha2' => 'JE', 'alpha3' => 'JEY', 'numeric' => '832', 'name' => 'Jersey', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇯🇪'],
|
||||
['alpha2' => 'JM', 'alpha3' => 'JAM', 'numeric' => '388', 'name' => 'Jamaica', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇯🇲'],
|
||||
['alpha2' => 'JO', 'alpha3' => 'JOR', 'numeric' => '400', 'name' => 'Jordan', 'official_name' => 'Hashemite Kingdom of Jordan', 'common_name' => null, 'flag_emoji' => '🇯🇴'],
|
||||
['alpha2' => 'JP', 'alpha3' => 'JPN', 'numeric' => '392', 'name' => 'Japan', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇯🇵'],
|
||||
['alpha2' => 'KE', 'alpha3' => 'KEN', 'numeric' => '404', 'name' => 'Kenya', 'official_name' => 'Republic of Kenya', 'common_name' => null, 'flag_emoji' => '🇰🇪'],
|
||||
['alpha2' => 'KG', 'alpha3' => 'KGZ', 'numeric' => '417', 'name' => 'Kyrgyzstan', 'official_name' => 'Kyrgyz Republic', 'common_name' => null, 'flag_emoji' => '🇰🇬'],
|
||||
['alpha2' => 'KH', 'alpha3' => 'KHM', 'numeric' => '116', 'name' => 'Cambodia', 'official_name' => 'Kingdom of Cambodia', 'common_name' => null, 'flag_emoji' => '🇰🇭'],
|
||||
['alpha2' => 'KI', 'alpha3' => 'KIR', 'numeric' => '296', 'name' => 'Kiribati', 'official_name' => 'Republic of Kiribati', 'common_name' => null, 'flag_emoji' => '🇰🇮'],
|
||||
['alpha2' => 'KM', 'alpha3' => 'COM', 'numeric' => '174', 'name' => 'Comoros', 'official_name' => 'Union of the Comoros', 'common_name' => null, 'flag_emoji' => '🇰🇲'],
|
||||
['alpha2' => 'KN', 'alpha3' => 'KNA', 'numeric' => '659', 'name' => 'Saint Kitts and Nevis', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇰🇳'],
|
||||
['alpha2' => 'KP', 'alpha3' => 'PRK', 'numeric' => '408', 'name' => 'Korea, Democratic People\'s Republic of', 'official_name' => 'Democratic People\'s Republic of Korea', 'common_name' => 'North Korea', 'flag_emoji' => '🇰🇵'],
|
||||
['alpha2' => 'KR', 'alpha3' => 'KOR', 'numeric' => '410', 'name' => 'Korea, Republic of', 'official_name' => null, 'common_name' => 'South Korea', 'flag_emoji' => '🇰🇷'],
|
||||
['alpha2' => 'KW', 'alpha3' => 'KWT', 'numeric' => '414', 'name' => 'Kuwait', 'official_name' => 'State of Kuwait', 'common_name' => null, 'flag_emoji' => '🇰🇼'],
|
||||
['alpha2' => 'KY', 'alpha3' => 'CYM', 'numeric' => '136', 'name' => 'Cayman Islands', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇰🇾'],
|
||||
['alpha2' => 'KZ', 'alpha3' => 'KAZ', 'numeric' => '398', 'name' => 'Kazakhstan', 'official_name' => 'Republic of Kazakhstan', 'common_name' => null, 'flag_emoji' => '🇰🇿'],
|
||||
['alpha2' => 'LA', 'alpha3' => 'LAO', 'numeric' => '418', 'name' => 'Lao People\'s Democratic Republic', 'official_name' => null, 'common_name' => 'Laos', 'flag_emoji' => '🇱🇦'],
|
||||
['alpha2' => 'LB', 'alpha3' => 'LBN', 'numeric' => '422', 'name' => 'Lebanon', 'official_name' => 'Lebanese Republic', 'common_name' => null, 'flag_emoji' => '🇱🇧'],
|
||||
['alpha2' => 'LC', 'alpha3' => 'LCA', 'numeric' => '662', 'name' => 'Saint Lucia', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇱🇨'],
|
||||
['alpha2' => 'LI', 'alpha3' => 'LIE', 'numeric' => '438', 'name' => 'Liechtenstein', 'official_name' => 'Principality of Liechtenstein', 'common_name' => null, 'flag_emoji' => '🇱🇮'],
|
||||
['alpha2' => 'LK', 'alpha3' => 'LKA', 'numeric' => '144', 'name' => 'Sri Lanka', 'official_name' => 'Democratic Socialist Republic of Sri Lanka', 'common_name' => null, 'flag_emoji' => '🇱🇰'],
|
||||
['alpha2' => 'LR', 'alpha3' => 'LBR', 'numeric' => '430', 'name' => 'Liberia', 'official_name' => 'Republic of Liberia', 'common_name' => null, 'flag_emoji' => '🇱🇷'],
|
||||
['alpha2' => 'LS', 'alpha3' => 'LSO', 'numeric' => '426', 'name' => 'Lesotho', 'official_name' => 'Kingdom of Lesotho', 'common_name' => null, 'flag_emoji' => '🇱🇸'],
|
||||
['alpha2' => 'LT', 'alpha3' => 'LTU', 'numeric' => '440', 'name' => 'Lithuania', 'official_name' => 'Republic of Lithuania', 'common_name' => null, 'flag_emoji' => '🇱🇹'],
|
||||
['alpha2' => 'LU', 'alpha3' => 'LUX', 'numeric' => '442', 'name' => 'Luxembourg', 'official_name' => 'Grand Duchy of Luxembourg', 'common_name' => null, 'flag_emoji' => '🇱🇺'],
|
||||
['alpha2' => 'LV', 'alpha3' => 'LVA', 'numeric' => '428', 'name' => 'Latvia', 'official_name' => 'Republic of Latvia', 'common_name' => null, 'flag_emoji' => '🇱🇻'],
|
||||
['alpha2' => 'LY', 'alpha3' => 'LBY', 'numeric' => '434', 'name' => 'Libya', 'official_name' => 'Libya', 'common_name' => null, 'flag_emoji' => '🇱🇾'],
|
||||
['alpha2' => 'MA', 'alpha3' => 'MAR', 'numeric' => '504', 'name' => 'Morocco', 'official_name' => 'Kingdom of Morocco', 'common_name' => null, 'flag_emoji' => '🇲🇦'],
|
||||
['alpha2' => 'MC', 'alpha3' => 'MCO', 'numeric' => '492', 'name' => 'Monaco', 'official_name' => 'Principality of Monaco', 'common_name' => null, 'flag_emoji' => '🇲🇨'],
|
||||
['alpha2' => 'MD', 'alpha3' => 'MDA', 'numeric' => '498', 'name' => 'Moldova, Republic of', 'official_name' => 'Republic of Moldova', 'common_name' => 'Moldova', 'flag_emoji' => '🇲🇩'],
|
||||
['alpha2' => 'ME', 'alpha3' => 'MNE', 'numeric' => '499', 'name' => 'Montenegro', 'official_name' => 'Montenegro', 'common_name' => null, 'flag_emoji' => '🇲🇪'],
|
||||
['alpha2' => 'MF', 'alpha3' => 'MAF', 'numeric' => '663', 'name' => 'Saint Martin (French part)', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇲🇫'],
|
||||
['alpha2' => 'MG', 'alpha3' => 'MDG', 'numeric' => '450', 'name' => 'Madagascar', 'official_name' => 'Republic of Madagascar', 'common_name' => null, 'flag_emoji' => '🇲🇬'],
|
||||
['alpha2' => 'MH', 'alpha3' => 'MHL', 'numeric' => '584', 'name' => 'Marshall Islands', 'official_name' => 'Republic of the Marshall Islands', 'common_name' => null, 'flag_emoji' => '🇲🇭'],
|
||||
['alpha2' => 'MK', 'alpha3' => 'MKD', 'numeric' => '807', 'name' => 'North Macedonia', 'official_name' => 'Republic of North Macedonia', 'common_name' => null, 'flag_emoji' => '🇲🇰'],
|
||||
['alpha2' => 'ML', 'alpha3' => 'MLI', 'numeric' => '466', 'name' => 'Mali', 'official_name' => 'Republic of Mali', 'common_name' => null, 'flag_emoji' => '🇲🇱'],
|
||||
['alpha2' => 'MM', 'alpha3' => 'MMR', 'numeric' => '104', 'name' => 'Myanmar', 'official_name' => 'Republic of Myanmar', 'common_name' => null, 'flag_emoji' => '🇲🇲'],
|
||||
['alpha2' => 'MN', 'alpha3' => 'MNG', 'numeric' => '496', 'name' => 'Mongolia', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇲🇳'],
|
||||
['alpha2' => 'MO', 'alpha3' => 'MAC', 'numeric' => '446', 'name' => 'Macao', 'official_name' => 'Macao Special Administrative Region of China', 'common_name' => null, 'flag_emoji' => '🇲🇴'],
|
||||
['alpha2' => 'MP', 'alpha3' => 'MNP', 'numeric' => '580', 'name' => 'Northern Mariana Islands', 'official_name' => 'Commonwealth of the Northern Mariana Islands', 'common_name' => null, 'flag_emoji' => '🇲🇵'],
|
||||
['alpha2' => 'MQ', 'alpha3' => 'MTQ', 'numeric' => '474', 'name' => 'Martinique', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇲🇶'],
|
||||
['alpha2' => 'MR', 'alpha3' => 'MRT', 'numeric' => '478', 'name' => 'Mauritania', 'official_name' => 'Islamic Republic of Mauritania', 'common_name' => null, 'flag_emoji' => '🇲🇷'],
|
||||
['alpha2' => 'MS', 'alpha3' => 'MSR', 'numeric' => '500', 'name' => 'Montserrat', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇲🇸'],
|
||||
['alpha2' => 'MT', 'alpha3' => 'MLT', 'numeric' => '470', 'name' => 'Malta', 'official_name' => 'Republic of Malta', 'common_name' => null, 'flag_emoji' => '🇲🇹'],
|
||||
['alpha2' => 'MU', 'alpha3' => 'MUS', 'numeric' => '480', 'name' => 'Mauritius', 'official_name' => 'Republic of Mauritius', 'common_name' => null, 'flag_emoji' => '🇲🇺'],
|
||||
['alpha2' => 'MV', 'alpha3' => 'MDV', 'numeric' => '462', 'name' => 'Maldives', 'official_name' => 'Republic of Maldives', 'common_name' => null, 'flag_emoji' => '🇲🇻'],
|
||||
['alpha2' => 'MW', 'alpha3' => 'MWI', 'numeric' => '454', 'name' => 'Malawi', 'official_name' => 'Republic of Malawi', 'common_name' => null, 'flag_emoji' => '🇲🇼'],
|
||||
['alpha2' => 'MX', 'alpha3' => 'MEX', 'numeric' => '484', 'name' => 'Mexico', 'official_name' => 'United Mexican States', 'common_name' => null, 'flag_emoji' => '🇲🇽'],
|
||||
['alpha2' => 'MY', 'alpha3' => 'MYS', 'numeric' => '458', 'name' => 'Malaysia', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇲🇾'],
|
||||
['alpha2' => 'MZ', 'alpha3' => 'MOZ', 'numeric' => '508', 'name' => 'Mozambique', 'official_name' => 'Republic of Mozambique', 'common_name' => null, 'flag_emoji' => '🇲🇿'],
|
||||
['alpha2' => 'NA', 'alpha3' => 'NAM', 'numeric' => '516', 'name' => 'Namibia', 'official_name' => 'Republic of Namibia', 'common_name' => null, 'flag_emoji' => '🇳🇦'],
|
||||
['alpha2' => 'NC', 'alpha3' => 'NCL', 'numeric' => '540', 'name' => 'New Caledonia', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇳🇨'],
|
||||
['alpha2' => 'NE', 'alpha3' => 'NER', 'numeric' => '562', 'name' => 'Niger', 'official_name' => 'Republic of the Niger', 'common_name' => null, 'flag_emoji' => '🇳🇪'],
|
||||
['alpha2' => 'NF', 'alpha3' => 'NFK', 'numeric' => '574', 'name' => 'Norfolk Island', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇳🇫'],
|
||||
['alpha2' => 'NG', 'alpha3' => 'NGA', 'numeric' => '566', 'name' => 'Nigeria', 'official_name' => 'Federal Republic of Nigeria', 'common_name' => null, 'flag_emoji' => '🇳🇬'],
|
||||
['alpha2' => 'NI', 'alpha3' => 'NIC', 'numeric' => '558', 'name' => 'Nicaragua', 'official_name' => 'Republic of Nicaragua', 'common_name' => null, 'flag_emoji' => '🇳🇮'],
|
||||
['alpha2' => 'NL', 'alpha3' => 'NLD', 'numeric' => '528', 'name' => 'Netherlands', 'official_name' => 'Kingdom of the Netherlands', 'common_name' => null, 'flag_emoji' => '🇳🇱'],
|
||||
['alpha2' => 'NO', 'alpha3' => 'NOR', 'numeric' => '578', 'name' => 'Norway', 'official_name' => 'Kingdom of Norway', 'common_name' => null, 'flag_emoji' => '🇳🇴'],
|
||||
['alpha2' => 'NP', 'alpha3' => 'NPL', 'numeric' => '524', 'name' => 'Nepal', 'official_name' => 'Federal Democratic Republic of Nepal', 'common_name' => null, 'flag_emoji' => '🇳🇵'],
|
||||
['alpha2' => 'NR', 'alpha3' => 'NRU', 'numeric' => '520', 'name' => 'Nauru', 'official_name' => 'Republic of Nauru', 'common_name' => null, 'flag_emoji' => '🇳🇷'],
|
||||
['alpha2' => 'NU', 'alpha3' => 'NIU', 'numeric' => '570', 'name' => 'Niue', 'official_name' => 'Niue', 'common_name' => null, 'flag_emoji' => '🇳🇺'],
|
||||
['alpha2' => 'NZ', 'alpha3' => 'NZL', 'numeric' => '554', 'name' => 'New Zealand', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇳🇿'],
|
||||
['alpha2' => 'OM', 'alpha3' => 'OMN', 'numeric' => '512', 'name' => 'Oman', 'official_name' => 'Sultanate of Oman', 'common_name' => null, 'flag_emoji' => '🇴🇲'],
|
||||
['alpha2' => 'PA', 'alpha3' => 'PAN', 'numeric' => '591', 'name' => 'Panama', 'official_name' => 'Republic of Panama', 'common_name' => null, 'flag_emoji' => '🇵🇦'],
|
||||
['alpha2' => 'PE', 'alpha3' => 'PER', 'numeric' => '604', 'name' => 'Peru', 'official_name' => 'Republic of Peru', 'common_name' => null, 'flag_emoji' => '🇵🇪'],
|
||||
['alpha2' => 'PF', 'alpha3' => 'PYF', 'numeric' => '258', 'name' => 'French Polynesia', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇵🇫'],
|
||||
['alpha2' => 'PG', 'alpha3' => 'PNG', 'numeric' => '598', 'name' => 'Papua New Guinea', 'official_name' => 'Independent State of Papua New Guinea', 'common_name' => null, 'flag_emoji' => '🇵🇬'],
|
||||
['alpha2' => 'PH', 'alpha3' => 'PHL', 'numeric' => '608', 'name' => 'Philippines', 'official_name' => 'Republic of the Philippines', 'common_name' => null, 'flag_emoji' => '🇵🇭'],
|
||||
['alpha2' => 'PK', 'alpha3' => 'PAK', 'numeric' => '586', 'name' => 'Pakistan', 'official_name' => 'Islamic Republic of Pakistan', 'common_name' => null, 'flag_emoji' => '🇵🇰'],
|
||||
['alpha2' => 'PL', 'alpha3' => 'POL', 'numeric' => '616', 'name' => 'Poland', 'official_name' => 'Republic of Poland', 'common_name' => null, 'flag_emoji' => '🇵🇱'],
|
||||
['alpha2' => 'PM', 'alpha3' => 'SPM', 'numeric' => '666', 'name' => 'Saint Pierre and Miquelon', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇵🇲'],
|
||||
['alpha2' => 'PN', 'alpha3' => 'PCN', 'numeric' => '612', 'name' => 'Pitcairn', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇵🇳'],
|
||||
['alpha2' => 'PR', 'alpha3' => 'PRI', 'numeric' => '630', 'name' => 'Puerto Rico', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇵🇷'],
|
||||
['alpha2' => 'PS', 'alpha3' => 'PSE', 'numeric' => '275', 'name' => 'Palestine, State of', 'official_name' => 'the State of Palestine', 'common_name' => null, 'flag_emoji' => '🇵🇸'],
|
||||
['alpha2' => 'PT', 'alpha3' => 'PRT', 'numeric' => '620', 'name' => 'Portugal', 'official_name' => 'Portuguese Republic', 'common_name' => null, 'flag_emoji' => '🇵🇹'],
|
||||
['alpha2' => 'PW', 'alpha3' => 'PLW', 'numeric' => '585', 'name' => 'Palau', 'official_name' => 'Republic of Palau', 'common_name' => null, 'flag_emoji' => '🇵🇼'],
|
||||
['alpha2' => 'PY', 'alpha3' => 'PRY', 'numeric' => '600', 'name' => 'Paraguay', 'official_name' => 'Republic of Paraguay', 'common_name' => null, 'flag_emoji' => '🇵🇾'],
|
||||
['alpha2' => 'QA', 'alpha3' => 'QAT', 'numeric' => '634', 'name' => 'Qatar', 'official_name' => 'State of Qatar', 'common_name' => null, 'flag_emoji' => '🇶🇦'],
|
||||
['alpha2' => 'RE', 'alpha3' => 'REU', 'numeric' => '638', 'name' => 'Réunion', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇷🇪'],
|
||||
['alpha2' => 'RO', 'alpha3' => 'ROU', 'numeric' => '642', 'name' => 'Romania', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇷🇴'],
|
||||
['alpha2' => 'RS', 'alpha3' => 'SRB', 'numeric' => '688', 'name' => 'Serbia', 'official_name' => 'Republic of Serbia', 'common_name' => null, 'flag_emoji' => '🇷🇸'],
|
||||
['alpha2' => 'RU', 'alpha3' => 'RUS', 'numeric' => '643', 'name' => 'Russian Federation', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇷🇺'],
|
||||
['alpha2' => 'RW', 'alpha3' => 'RWA', 'numeric' => '646', 'name' => 'Rwanda', 'official_name' => 'Rwandese Republic', 'common_name' => null, 'flag_emoji' => '🇷🇼'],
|
||||
['alpha2' => 'SA', 'alpha3' => 'SAU', 'numeric' => '682', 'name' => 'Saudi Arabia', 'official_name' => 'Kingdom of Saudi Arabia', 'common_name' => null, 'flag_emoji' => '🇸🇦'],
|
||||
['alpha2' => 'SB', 'alpha3' => 'SLB', 'numeric' => '090', 'name' => 'Solomon Islands', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇸🇧'],
|
||||
['alpha2' => 'SC', 'alpha3' => 'SYC', 'numeric' => '690', 'name' => 'Seychelles', 'official_name' => 'Republic of Seychelles', 'common_name' => null, 'flag_emoji' => '🇸🇨'],
|
||||
['alpha2' => 'SD', 'alpha3' => 'SDN', 'numeric' => '729', 'name' => 'Sudan', 'official_name' => 'Republic of the Sudan', 'common_name' => null, 'flag_emoji' => '🇸🇩'],
|
||||
['alpha2' => 'SE', 'alpha3' => 'SWE', 'numeric' => '752', 'name' => 'Sweden', 'official_name' => 'Kingdom of Sweden', 'common_name' => null, 'flag_emoji' => '🇸🇪'],
|
||||
['alpha2' => 'SG', 'alpha3' => 'SGP', 'numeric' => '702', 'name' => 'Singapore', 'official_name' => 'Republic of Singapore', 'common_name' => null, 'flag_emoji' => '🇸🇬'],
|
||||
['alpha2' => 'SH', 'alpha3' => 'SHN', 'numeric' => '654', 'name' => 'Saint Helena, Ascension and Tristan da Cunha', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇸🇭'],
|
||||
['alpha2' => 'SI', 'alpha3' => 'SVN', 'numeric' => '705', 'name' => 'Slovenia', 'official_name' => 'Republic of Slovenia', 'common_name' => null, 'flag_emoji' => '🇸🇮'],
|
||||
['alpha2' => 'SJ', 'alpha3' => 'SJM', 'numeric' => '744', 'name' => 'Svalbard and Jan Mayen', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇸🇯'],
|
||||
['alpha2' => 'SK', 'alpha3' => 'SVK', 'numeric' => '703', 'name' => 'Slovakia', 'official_name' => 'Slovak Republic', 'common_name' => null, 'flag_emoji' => '🇸🇰'],
|
||||
['alpha2' => 'SL', 'alpha3' => 'SLE', 'numeric' => '694', 'name' => 'Sierra Leone', 'official_name' => 'Republic of Sierra Leone', 'common_name' => null, 'flag_emoji' => '🇸🇱'],
|
||||
['alpha2' => 'SM', 'alpha3' => 'SMR', 'numeric' => '674', 'name' => 'San Marino', 'official_name' => 'Republic of San Marino', 'common_name' => null, 'flag_emoji' => '🇸🇲'],
|
||||
['alpha2' => 'SN', 'alpha3' => 'SEN', 'numeric' => '686', 'name' => 'Senegal', 'official_name' => 'Republic of Senegal', 'common_name' => null, 'flag_emoji' => '🇸🇳'],
|
||||
['alpha2' => 'SO', 'alpha3' => 'SOM', 'numeric' => '706', 'name' => 'Somalia', 'official_name' => 'Federal Republic of Somalia', 'common_name' => null, 'flag_emoji' => '🇸🇴'],
|
||||
['alpha2' => 'SR', 'alpha3' => 'SUR', 'numeric' => '740', 'name' => 'Suriname', 'official_name' => 'Republic of Suriname', 'common_name' => null, 'flag_emoji' => '🇸🇷'],
|
||||
['alpha2' => 'SS', 'alpha3' => 'SSD', 'numeric' => '728', 'name' => 'South Sudan', 'official_name' => 'Republic of South Sudan', 'common_name' => null, 'flag_emoji' => '🇸🇸'],
|
||||
['alpha2' => 'ST', 'alpha3' => 'STP', 'numeric' => '678', 'name' => 'Sao Tome and Principe', 'official_name' => 'Democratic Republic of Sao Tome and Principe', 'common_name' => null, 'flag_emoji' => '🇸🇹'],
|
||||
['alpha2' => 'SV', 'alpha3' => 'SLV', 'numeric' => '222', 'name' => 'El Salvador', 'official_name' => 'Republic of El Salvador', 'common_name' => null, 'flag_emoji' => '🇸🇻'],
|
||||
['alpha2' => 'SX', 'alpha3' => 'SXM', 'numeric' => '534', 'name' => 'Sint Maarten (Dutch part)', 'official_name' => 'Sint Maarten (Dutch part)', 'common_name' => null, 'flag_emoji' => '🇸🇽'],
|
||||
['alpha2' => 'SY', 'alpha3' => 'SYR', 'numeric' => '760', 'name' => 'Syrian Arab Republic', 'official_name' => null, 'common_name' => 'Syria', 'flag_emoji' => '🇸🇾'],
|
||||
['alpha2' => 'SZ', 'alpha3' => 'SWZ', 'numeric' => '748', 'name' => 'Eswatini', 'official_name' => 'Kingdom of Eswatini', 'common_name' => null, 'flag_emoji' => '🇸🇿'],
|
||||
['alpha2' => 'TC', 'alpha3' => 'TCA', 'numeric' => '796', 'name' => 'Turks and Caicos Islands', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇹🇨'],
|
||||
['alpha2' => 'TD', 'alpha3' => 'TCD', 'numeric' => '148', 'name' => 'Chad', 'official_name' => 'Republic of Chad', 'common_name' => null, 'flag_emoji' => '🇹🇩'],
|
||||
['alpha2' => 'TF', 'alpha3' => 'ATF', 'numeric' => '260', 'name' => 'French Southern Territories', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇹🇫'],
|
||||
['alpha2' => 'TG', 'alpha3' => 'TGO', 'numeric' => '768', 'name' => 'Togo', 'official_name' => 'Togolese Republic', 'common_name' => null, 'flag_emoji' => '🇹🇬'],
|
||||
['alpha2' => 'TH', 'alpha3' => 'THA', 'numeric' => '764', 'name' => 'Thailand', 'official_name' => 'Kingdom of Thailand', 'common_name' => null, 'flag_emoji' => '🇹🇭'],
|
||||
['alpha2' => 'TJ', 'alpha3' => 'TJK', 'numeric' => '762', 'name' => 'Tajikistan', 'official_name' => 'Republic of Tajikistan', 'common_name' => null, 'flag_emoji' => '🇹🇯'],
|
||||
['alpha2' => 'TK', 'alpha3' => 'TKL', 'numeric' => '772', 'name' => 'Tokelau', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇹🇰'],
|
||||
['alpha2' => 'TL', 'alpha3' => 'TLS', 'numeric' => '626', 'name' => 'Timor-Leste', 'official_name' => 'Democratic Republic of Timor-Leste', 'common_name' => null, 'flag_emoji' => '🇹🇱'],
|
||||
['alpha2' => 'TM', 'alpha3' => 'TKM', 'numeric' => '795', 'name' => 'Turkmenistan', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇹🇲'],
|
||||
['alpha2' => 'TN', 'alpha3' => 'TUN', 'numeric' => '788', 'name' => 'Tunisia', 'official_name' => 'Republic of Tunisia', 'common_name' => null, 'flag_emoji' => '🇹🇳'],
|
||||
['alpha2' => 'TO', 'alpha3' => 'TON', 'numeric' => '776', 'name' => 'Tonga', 'official_name' => 'Kingdom of Tonga', 'common_name' => null, 'flag_emoji' => '🇹🇴'],
|
||||
['alpha2' => 'TR', 'alpha3' => 'TUR', 'numeric' => '792', 'name' => 'Türkiye', 'official_name' => 'Republic of Türkiye', 'common_name' => null, 'flag_emoji' => '🇹🇷'],
|
||||
['alpha2' => 'TT', 'alpha3' => 'TTO', 'numeric' => '780', 'name' => 'Trinidad and Tobago', 'official_name' => 'Republic of Trinidad and Tobago', 'common_name' => null, 'flag_emoji' => '🇹🇹'],
|
||||
['alpha2' => 'TV', 'alpha3' => 'TUV', 'numeric' => '798', 'name' => 'Tuvalu', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇹🇻'],
|
||||
['alpha2' => 'TW', 'alpha3' => 'TWN', 'numeric' => '158', 'name' => 'Taiwan, Province of China', 'official_name' => 'Taiwan, Province of China', 'common_name' => 'Taiwan', 'flag_emoji' => '🇹🇼'],
|
||||
['alpha2' => 'TZ', 'alpha3' => 'TZA', 'numeric' => '834', 'name' => 'Tanzania, United Republic of', 'official_name' => 'United Republic of Tanzania', 'common_name' => 'Tanzania', 'flag_emoji' => '🇹🇿'],
|
||||
['alpha2' => 'UA', 'alpha3' => 'UKR', 'numeric' => '804', 'name' => 'Ukraine', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇺🇦'],
|
||||
['alpha2' => 'UG', 'alpha3' => 'UGA', 'numeric' => '800', 'name' => 'Uganda', 'official_name' => 'Republic of Uganda', 'common_name' => null, 'flag_emoji' => '🇺🇬'],
|
||||
['alpha2' => 'UM', 'alpha3' => 'UMI', 'numeric' => '581', 'name' => 'United States Minor Outlying Islands', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇺🇲'],
|
||||
['alpha2' => 'US', 'alpha3' => 'USA', 'numeric' => '840', 'name' => 'United States', 'official_name' => 'United States of America', 'common_name' => null, 'flag_emoji' => '🇺🇸'],
|
||||
['alpha2' => 'UY', 'alpha3' => 'URY', 'numeric' => '858', 'name' => 'Uruguay', 'official_name' => 'Eastern Republic of Uruguay', 'common_name' => null, 'flag_emoji' => '🇺🇾'],
|
||||
['alpha2' => 'UZ', 'alpha3' => 'UZB', 'numeric' => '860', 'name' => 'Uzbekistan', 'official_name' => 'Republic of Uzbekistan', 'common_name' => null, 'flag_emoji' => '🇺🇿'],
|
||||
['alpha2' => 'VA', 'alpha3' => 'VAT', 'numeric' => '336', 'name' => 'Holy See (Vatican City State)', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇻🇦'],
|
||||
['alpha2' => 'VC', 'alpha3' => 'VCT', 'numeric' => '670', 'name' => 'Saint Vincent and the Grenadines', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇻🇨'],
|
||||
['alpha2' => 'VE', 'alpha3' => 'VEN', 'numeric' => '862', 'name' => 'Venezuela, Bolivarian Republic of', 'official_name' => 'Bolivarian Republic of Venezuela', 'common_name' => 'Venezuela', 'flag_emoji' => '🇻🇪'],
|
||||
['alpha2' => 'VG', 'alpha3' => 'VGB', 'numeric' => '092', 'name' => 'Virgin Islands, British', 'official_name' => 'British Virgin Islands', 'common_name' => null, 'flag_emoji' => '🇻🇬'],
|
||||
['alpha2' => 'VI', 'alpha3' => 'VIR', 'numeric' => '850', 'name' => 'Virgin Islands, U.S.', 'official_name' => 'Virgin Islands of the United States', 'common_name' => null, 'flag_emoji' => '🇻🇮'],
|
||||
['alpha2' => 'VN', 'alpha3' => 'VNM', 'numeric' => '704', 'name' => 'Viet Nam', 'official_name' => 'Socialist Republic of Viet Nam', 'common_name' => 'Vietnam', 'flag_emoji' => '🇻🇳'],
|
||||
['alpha2' => 'VU', 'alpha3' => 'VUT', 'numeric' => '548', 'name' => 'Vanuatu', 'official_name' => 'Republic of Vanuatu', 'common_name' => null, 'flag_emoji' => '🇻🇺'],
|
||||
['alpha2' => 'WF', 'alpha3' => 'WLF', 'numeric' => '876', 'name' => 'Wallis and Futuna', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇼🇫'],
|
||||
['alpha2' => 'WS', 'alpha3' => 'WSM', 'numeric' => '882', 'name' => 'Samoa', 'official_name' => 'Independent State of Samoa', 'common_name' => null, 'flag_emoji' => '🇼🇸'],
|
||||
['alpha2' => 'YE', 'alpha3' => 'YEM', 'numeric' => '887', 'name' => 'Yemen', 'official_name' => 'Republic of Yemen', 'common_name' => null, 'flag_emoji' => '🇾🇪'],
|
||||
['alpha2' => 'YT', 'alpha3' => 'MYT', 'numeric' => '175', 'name' => 'Mayotte', 'official_name' => null, 'common_name' => null, 'flag_emoji' => '🇾🇹'],
|
||||
['alpha2' => 'ZA', 'alpha3' => 'ZAF', 'numeric' => '710', 'name' => 'South Africa', 'official_name' => 'Republic of South Africa', 'common_name' => null, 'flag_emoji' => '🇿🇦'],
|
||||
['alpha2' => 'ZM', 'alpha3' => 'ZMB', 'numeric' => '894', 'name' => 'Zambia', 'official_name' => 'Republic of Zambia', 'common_name' => null, 'flag_emoji' => '🇿🇲'],
|
||||
['alpha2' => 'ZW', 'alpha3' => 'ZWE', 'numeric' => '716', 'name' => 'Zimbabwe', 'official_name' => 'Republic of Zimbabwe', 'common_name' => null, 'flag_emoji' => '🇿🇼'],
|
||||
];
|
||||
|
||||
foreach ($countries as $index => $country) {
|
||||
Country::query()->updateOrCreate(
|
||||
['alpha2' => $country['alpha2']],
|
||||
array_merge($country, [
|
||||
'is_active' => true,
|
||||
'sort_order' => $index + 1,
|
||||
'last_synced_at' => $now,
|
||||
'iso_status' => 'official',
|
||||
'source' => 'bundled-iso-3166-1',
|
||||
'source_version' => '2026-06-15',
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Countries\Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Modules\Countries\Models\Country;
|
||||
use Modules\Countries\Models\CountrySubdivision;
|
||||
|
||||
class IsoCountrySubdivisionsSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Seed high-value ISO 3166-2 subdivisions used by UK/US business workflows.
|
||||
*
|
||||
* This module is structured to import the full ISO 3166-2 data set from an approved licensed source.
|
||||
* The bundled seed keeps the module useful out of the box without pretending to be the complete ISO 3166-2 catalogue.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$now = Carbon::now();
|
||||
$rows = [
|
||||
['country' => 'GB', 'code' => 'GB-ENG', 'name' => 'England', 'type' => 'country'],
|
||||
['country' => 'GB', 'code' => 'GB-SCT', 'name' => 'Scotland', 'type' => 'country'],
|
||||
['country' => 'GB', 'code' => 'GB-WLS', 'name' => 'Wales', 'type' => 'country'],
|
||||
['country' => 'GB', 'code' => 'GB-NIR', 'name' => 'Northern Ireland', 'type' => 'province'],
|
||||
['country' => 'US', 'code' => 'US-AL', 'name' => 'Alabama', 'type' => 'state'],
|
||||
['country' => 'US', 'code' => 'US-AK', 'name' => 'Alaska', 'type' => 'state'],
|
||||
['country' => 'US', 'code' => 'US-AZ', 'name' => 'Arizona', 'type' => 'state'],
|
||||
['country' => 'US', 'code' => 'US-AR', 'name' => 'Arkansas', 'type' => 'state'],
|
||||
['country' => 'US', 'code' => 'US-CA', 'name' => 'California', 'type' => 'state'],
|
||||
['country' => 'US', 'code' => 'US-CO', 'name' => 'Colorado', 'type' => 'state'],
|
||||
['country' => 'US', 'code' => 'US-CT', 'name' => 'Connecticut', 'type' => 'state'],
|
||||
['country' => 'US', 'code' => 'US-DE', 'name' => 'Delaware', 'type' => 'state'],
|
||||
['country' => 'US', 'code' => 'US-FL', 'name' => 'Florida', 'type' => 'state'],
|
||||
['country' => 'US', 'code' => 'US-NY', 'name' => 'New York', 'type' => 'state'],
|
||||
['country' => 'US', 'code' => 'US-TX', 'name' => 'Texas', 'type' => 'state'],
|
||||
['country' => 'US', 'code' => 'US-WA', 'name' => 'Washington', 'type' => 'state'],
|
||||
['country' => 'CA', 'code' => 'CA-AB', 'name' => 'Alberta', 'type' => 'province'],
|
||||
['country' => 'CA', 'code' => 'CA-BC', 'name' => 'British Columbia', 'type' => 'province'],
|
||||
['country' => 'CA', 'code' => 'CA-ON', 'name' => 'Ontario', 'type' => 'province'],
|
||||
['country' => 'CA', 'code' => 'CA-QC', 'name' => 'Quebec', 'type' => 'province'],
|
||||
];
|
||||
|
||||
foreach ($rows as $index => $row) {
|
||||
$country = Country::query()->where('alpha2', $row['country'])->first();
|
||||
|
||||
if (! $country) {
|
||||
continue;
|
||||
}
|
||||
|
||||
CountrySubdivision::query()->updateOrCreate(
|
||||
['code' => $row['code']],
|
||||
[
|
||||
'country_id' => $country->id,
|
||||
'country_alpha2' => $country->alpha2,
|
||||
'subdivision_code' => substr($row['code'], 3),
|
||||
'name' => $row['name'],
|
||||
'local_name' => null,
|
||||
'type' => $row['type'],
|
||||
'is_active' => true,
|
||||
'iso_status' => 'official',
|
||||
'sort_order' => $index + 1,
|
||||
'last_synced_at' => $now,
|
||||
'source' => 'bundled-bootstrap',
|
||||
'source_version' => '2026-06-15',
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
use Modules\Countries\Support\ModuleInfo;
|
||||
|
||||
if (! function_exists('countries_feature_enabled')) {
|
||||
/**
|
||||
* Check if a feature is enabled in the config.
|
||||
*/
|
||||
function countries_feature_enabled(string $key): bool
|
||||
{
|
||||
return (bool) config(ModuleInfo::nameLower() . ".features.{$key}", false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Countries\Http\Controllers\Api;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Modules\Countries\Models\Country;
|
||||
use Modules\Countries\Models\CountrySubdivision;
|
||||
use Modules\Countries\Support\CountryRepository;
|
||||
|
||||
class CountriesApiController extends Controller
|
||||
{
|
||||
/**
|
||||
* List ISO 3166-1 countries for integrations and selectors.
|
||||
*/
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$activeOnly = $request->boolean('active_only', false);
|
||||
$search = trim((string) $request->query('search', ''));
|
||||
|
||||
$countries = Country::query()
|
||||
->when($activeOnly, fn ($query) => $query->where('is_active', true))
|
||||
->when($search !== '', function ($query) use ($search): void {
|
||||
$query->where(function ($nested) use ($search): void {
|
||||
$nested->where('name', 'ilike', "%{$search}%")
|
||||
->orWhere('official_name', 'ilike', "%{$search}%")
|
||||
->orWhere('common_name', 'ilike', "%{$search}%")
|
||||
->orWhere('display_name', 'ilike', "%{$search}%")
|
||||
->orWhere('native_name', 'ilike', "%{$search}%")
|
||||
->orWhere('alpha2', 'ilike', "%{$search}%")
|
||||
->orWhere('alpha3', 'ilike', "%{$search}%")
|
||||
->orWhere('numeric', 'ilike', "%{$search}%")
|
||||
->orWhere('region', 'ilike', "%{$search}%")
|
||||
->orWhere('subregion', 'ilike', "%{$search}%")
|
||||
->orWhere('currency_code', 'ilike', "%{$search}%")
|
||||
->orWhere('calling_code', 'ilike', "%{$search}%");
|
||||
});
|
||||
})
|
||||
->ordered()
|
||||
->get();
|
||||
|
||||
return response()->json(['data' => $countries]);
|
||||
}
|
||||
|
||||
/**
|
||||
* List active ISO 3166-1 countries for business selectors.
|
||||
*/
|
||||
public function active(): JsonResponse
|
||||
{
|
||||
$countries = Country::query()->active()->ordered()->get();
|
||||
|
||||
return response()->json(['data' => $countries]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show one country by alpha-2 route binding.
|
||||
*/
|
||||
public function show(Country $country): JsonResponse
|
||||
{
|
||||
return response()->json(['data' => $country->loadCount('subdivisions')]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve alpha-2, alpha-3 or numeric ISO 3166-1 country code.
|
||||
*/
|
||||
public function resolve(string $code, CountryRepository $countries): JsonResponse
|
||||
{
|
||||
$country = $countries->findByIsoCode($code);
|
||||
|
||||
abort_if($country === null, 404, 'Country code not found.');
|
||||
|
||||
return response()->json(['data' => $country]);
|
||||
}
|
||||
|
||||
/**
|
||||
* List ISO 3166-2 subdivisions for the supplied ISO 3166-1 alpha-2 country.
|
||||
*/
|
||||
public function subdivisions(string $alpha2, Request $request, CountryRepository $countries): JsonResponse
|
||||
{
|
||||
$activeOnly = $request->boolean('active_only', true);
|
||||
$country = $countries->findByAlpha2($alpha2);
|
||||
|
||||
abort_if($country === null, 404, 'Country not found.');
|
||||
|
||||
$subdivisions = CountrySubdivision::query()
|
||||
->where('country_id', $country->id)
|
||||
->when($activeOnly, fn ($query) => $query->where('is_active', true))
|
||||
->ordered()
|
||||
->get();
|
||||
|
||||
return response()->json(['data' => $subdivisions]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Countries\Http\Controllers;
|
||||
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Modules\Countries\Http\Requests\StoreCountryRequest;
|
||||
use Modules\Countries\Http\Requests\UpdateCountryRequest;
|
||||
use Modules\Countries\Models\Country;
|
||||
use Modules\Countries\Support\CountryUsageReport;
|
||||
|
||||
class CountriesController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request): View
|
||||
{
|
||||
$search = trim((string) $request->query('search', ''));
|
||||
$status = (string) $request->query('status', 'all');
|
||||
$region = (string) $request->query('region', 'all');
|
||||
|
||||
$countries = Country::query()
|
||||
->when($search !== '', function ($query) use ($search): void {
|
||||
$query->where(function ($nested) use ($search): void {
|
||||
$nested->where('name', 'ilike', "%{$search}%")
|
||||
->orWhere('official_name', 'ilike', "%{$search}%")
|
||||
->orWhere('common_name', 'ilike', "%{$search}%")
|
||||
->orWhere('display_name', 'ilike', "%{$search}%")
|
||||
->orWhere('native_name', 'ilike', "%{$search}%")
|
||||
->orWhere('alpha2', 'ilike', "%{$search}%")
|
||||
->orWhere('alpha3', 'ilike', "%{$search}%")
|
||||
->orWhere('numeric', 'ilike', "%{$search}%")
|
||||
->orWhere('currency_code', 'ilike', "%{$search}%")
|
||||
->orWhere('calling_code', 'ilike', "%{$search}%");
|
||||
});
|
||||
})
|
||||
->when($status === 'active', fn ($query) => $query->where('is_active', true))
|
||||
->when($status === 'inactive', fn ($query) => $query->where('is_active', false))
|
||||
->when($region !== 'all' && $region !== '', fn ($query) => $query->where('region', $region))
|
||||
->orderByRaw('LOWER(COALESCE(display_name, common_name, name)) ASC')
|
||||
->paginate(50)
|
||||
->withQueryString();
|
||||
|
||||
$activeCount = Country::query()->where('is_active', true)->count();
|
||||
$inactiveCount = Country::query()->where('is_active', false)->count();
|
||||
$totalCount = Country::query()->count();
|
||||
|
||||
$regions = Country::query()
|
||||
->whereNotNull('region')
|
||||
->distinct()
|
||||
->orderBy('region')
|
||||
->pluck('region');
|
||||
|
||||
return view('countries::index', compact(
|
||||
'countries',
|
||||
'activeCount',
|
||||
'inactiveCount',
|
||||
'totalCount',
|
||||
'search',
|
||||
'status',
|
||||
'region',
|
||||
'regions',
|
||||
));
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
return view('countries::create', ['country' => new Country]);
|
||||
}
|
||||
|
||||
public function store(StoreCountryRequest $request): RedirectResponse
|
||||
{
|
||||
$country = Country::query()->create($this->payload($request));
|
||||
|
||||
return redirect()
|
||||
->route('countries.show', $country)
|
||||
->with('success', 'Country created.');
|
||||
}
|
||||
|
||||
public function show(Country $country, CountryUsageReport $usageReport): View
|
||||
{
|
||||
$country->load(['subdivisions' => fn ($query) => $query->ordered()->limit(250)]);
|
||||
|
||||
$usageRows = $usageReport->forCountry($country);
|
||||
|
||||
return view('countries::show', compact('country', 'usageRows'));
|
||||
}
|
||||
|
||||
public function edit(Country $country): View
|
||||
{
|
||||
return view('countries::edit', compact('country'));
|
||||
}
|
||||
|
||||
public function update(UpdateCountryRequest $request, Country $country): RedirectResponse
|
||||
{
|
||||
$country->update($this->payload($request));
|
||||
|
||||
return redirect()
|
||||
->route('countries.show', $country)
|
||||
->with('success', 'Country updated.');
|
||||
}
|
||||
|
||||
public function destroy(Country $country): RedirectResponse
|
||||
{
|
||||
$country->delete();
|
||||
|
||||
return redirect()
|
||||
->route('countries.index')
|
||||
->with('success', 'Country archived.');
|
||||
}
|
||||
|
||||
private function payload(StoreCountryRequest|UpdateCountryRequest $request): array
|
||||
{
|
||||
$data = $request->validated();
|
||||
|
||||
foreach (['alpha2', 'alpha3', 'currency_code', 'ioc_code', 'fifa_code'] as $key) {
|
||||
if (isset($data[$key]) && is_string($data[$key])) {
|
||||
$data[$key] = strtoupper(trim($data[$key]));
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($data['numeric']) && is_string($data['numeric'])) {
|
||||
$data['numeric'] = str_pad(trim($data['numeric']), 3, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
foreach (['languages', 'timezones', 'metadata'] as $jsonField) {
|
||||
$value = $data[$jsonField] ?? null;
|
||||
|
||||
$data[$jsonField] = blank($value)
|
||||
? null
|
||||
: json_decode((string) $value, true, 512, JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
foreach (['is_active', 'independent', 'un_member'] as $booleanField) {
|
||||
$data[$booleanField] = $request->boolean($booleanField);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Countries\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
class CountrySyncController extends Controller
|
||||
{
|
||||
/**
|
||||
* Synchronise bundled ISO 3166 reference data.
|
||||
*/
|
||||
public function __invoke(): RedirectResponse
|
||||
{
|
||||
Artisan::call('countries:sync-iso', [
|
||||
'--subdivisions' => true,
|
||||
]);
|
||||
|
||||
return redirect()->route('countries.index')->with('success', 'ISO 3166 country reference data synchronised.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Countries\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Contracts\View\View;
|
||||
|
||||
class ReferenceDataController extends Controller
|
||||
{
|
||||
public function currencies(): View
|
||||
{
|
||||
return view('countries::reference.currencies');
|
||||
}
|
||||
|
||||
public function languages(): View
|
||||
{
|
||||
return view('countries::reference.languages');
|
||||
}
|
||||
|
||||
public function scripts(): View
|
||||
{
|
||||
return view('countries::reference.scripts');
|
||||
}
|
||||
|
||||
public function locodes(): View
|
||||
{
|
||||
return view('countries::reference.locodes');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Countries\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Modules\Countries\Support\Iso3166;
|
||||
|
||||
class StoreCountryRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Authorize country reference-data administration.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return (bool) $this->user()?->isMpbaPlatformAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise ISO code inputs before validation.
|
||||
*/
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'alpha2' => strtoupper(trim((string) $this->input('alpha2'))),
|
||||
'alpha3' => strtoupper(trim((string) $this->input('alpha3'))),
|
||||
'numeric' => str_pad(trim((string) $this->input('numeric')), 3, '0', STR_PAD_LEFT),
|
||||
'currency_code' => $this->filled('currency_code') ? strtoupper(trim((string) $this->input('currency_code'))) : null,
|
||||
'ioc_code' => $this->filled('ioc_code') ? strtoupper(trim((string) $this->input('ioc_code'))) : null,
|
||||
'fifa_code' => $this->filled('fifa_code') ? strtoupper(trim((string) $this->input('fifa_code'))) : null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'alpha2' => ['required', 'string', 'size:2', 'uppercase', 'unique:countries,alpha2'],
|
||||
'alpha3' => ['required', 'string', 'size:3', 'uppercase', 'unique:countries,alpha3'],
|
||||
'numeric' => ['required', 'string', 'size:3', 'unique:countries,numeric'],
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'official_name' => ['nullable', 'string', 'max:255'],
|
||||
'common_name' => ['nullable', 'string', 'max:255'],
|
||||
'display_name' => ['nullable', 'string', 'max:255'],
|
||||
'native_name' => ['nullable', 'string', 'max:255'],
|
||||
'capital' => ['nullable', 'string', 'max:255'],
|
||||
'flag_emoji' => ['nullable', 'string', 'max:16'],
|
||||
'calling_code' => ['nullable', 'string', 'max:16'],
|
||||
'tld' => ['nullable', 'string', 'max:16'],
|
||||
'currency_code' => ['nullable', 'string', 'size:3', 'uppercase'],
|
||||
'currency_name' => ['nullable', 'string', 'max:255'],
|
||||
'currency_symbol' => ['nullable', 'string', 'max:16'],
|
||||
'languages' => ['nullable', 'json'],
|
||||
'timezones' => ['nullable', 'json'],
|
||||
'latitude' => ['nullable', 'numeric', 'between:-90,90'],
|
||||
'longitude' => ['nullable', 'numeric', 'between:-180,180'],
|
||||
'flag_svg_url' => ['nullable', 'url', 'max:255'],
|
||||
'flag_png_url' => ['nullable', 'url', 'max:255'],
|
||||
'ioc_code' => ['nullable', 'string', 'max:8'],
|
||||
'fifa_code' => ['nullable', 'string', 'max:8'],
|
||||
'postal_code_format' => ['nullable', 'string', 'max:255'],
|
||||
'address_format' => ['nullable', 'string', 'max:5000'],
|
||||
'is_active' => ['required', 'boolean'],
|
||||
'iso_status' => ['required', 'string', Rule::in(Iso3166::statuses())],
|
||||
'independent' => ['nullable', 'boolean'],
|
||||
'sovereignty_status' => ['nullable', 'string', 'max:64'],
|
||||
'un_member' => ['nullable', 'boolean'],
|
||||
'continent' => ['nullable', 'string', 'max:64'],
|
||||
'region' => ['nullable', 'string', 'max:100'],
|
||||
'subregion' => ['nullable', 'string', 'max:100'],
|
||||
'intermediate_region' => ['nullable', 'string', 'max:255'],
|
||||
'sort_order' => ['nullable', 'integer', 'min:0', 'max:999999'],
|
||||
'source' => ['nullable', 'string', 'max:255'],
|
||||
'source_version' => ['nullable', 'string', 'max:255'],
|
||||
'last_synced_at' => ['nullable', 'date'],
|
||||
'metadata' => ['nullable', 'json'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Countries\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Modules\Countries\Support\Iso3166;
|
||||
|
||||
class UpdateCountryRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Authorize country reference-data administration.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return (bool) $this->user()?->isMpbaPlatformAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise ISO code inputs before validation.
|
||||
*/
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'alpha2' => strtoupper(trim((string) $this->input('alpha2'))),
|
||||
'alpha3' => strtoupper(trim((string) $this->input('alpha3'))),
|
||||
'numeric' => str_pad(trim((string) $this->input('numeric')), 3, '0', STR_PAD_LEFT),
|
||||
'currency_code' => $this->filled('currency_code') ? strtoupper(trim((string) $this->input('currency_code'))) : null,
|
||||
'ioc_code' => $this->filled('ioc_code') ? strtoupper(trim((string) $this->input('ioc_code'))) : null,
|
||||
'fifa_code' => $this->filled('fifa_code') ? strtoupper(trim((string) $this->input('fifa_code'))) : null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$countryId = $this->route('country')?->id;
|
||||
|
||||
return [
|
||||
'alpha2' => ['required', 'string', 'size:2', 'uppercase', Rule::unique('countries', 'alpha2')->ignore($countryId)],
|
||||
'alpha3' => ['required', 'string', 'size:3', 'uppercase', Rule::unique('countries', 'alpha3')->ignore($countryId)],
|
||||
'numeric' => ['required', 'string', 'size:3', Rule::unique('countries', 'numeric')->ignore($countryId)],
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'official_name' => ['nullable', 'string', 'max:255'],
|
||||
'common_name' => ['nullable', 'string', 'max:255'],
|
||||
'display_name' => ['nullable', 'string', 'max:255'],
|
||||
'native_name' => ['nullable', 'string', 'max:255'],
|
||||
'capital' => ['nullable', 'string', 'max:255'],
|
||||
'flag_emoji' => ['nullable', 'string', 'max:16'],
|
||||
'calling_code' => ['nullable', 'string', 'max:16'],
|
||||
'tld' => ['nullable', 'string', 'max:16'],
|
||||
'currency_code' => ['nullable', 'string', 'size:3', 'uppercase'],
|
||||
'currency_name' => ['nullable', 'string', 'max:255'],
|
||||
'currency_symbol' => ['nullable', 'string', 'max:16'],
|
||||
'languages' => ['nullable', 'json'],
|
||||
'timezones' => ['nullable', 'json'],
|
||||
'latitude' => ['nullable', 'numeric', 'between:-90,90'],
|
||||
'longitude' => ['nullable', 'numeric', 'between:-180,180'],
|
||||
'flag_svg_url' => ['nullable', 'url', 'max:255'],
|
||||
'flag_png_url' => ['nullable', 'url', 'max:255'],
|
||||
'ioc_code' => ['nullable', 'string', 'max:8'],
|
||||
'fifa_code' => ['nullable', 'string', 'max:8'],
|
||||
'postal_code_format' => ['nullable', 'string', 'max:255'],
|
||||
'address_format' => ['nullable', 'string', 'max:5000'],
|
||||
'is_active' => ['required', 'boolean'],
|
||||
'iso_status' => ['required', 'string', Rule::in(Iso3166::statuses())],
|
||||
'independent' => ['nullable', 'boolean'],
|
||||
'sovereignty_status' => ['nullable', 'string', 'max:64'],
|
||||
'un_member' => ['nullable', 'boolean'],
|
||||
'continent' => ['nullable', 'string', 'max:64'],
|
||||
'region' => ['nullable', 'string', 'max:100'],
|
||||
'subregion' => ['nullable', 'string', 'max:100'],
|
||||
'intermediate_region' => ['nullable', 'string', 'max:255'],
|
||||
'sort_order' => ['nullable', 'integer', 'min:0', 'max:999999'],
|
||||
'source' => ['nullable', 'string', 'max:255'],
|
||||
'source_version' => ['nullable', 'string', 'max:255'],
|
||||
'last_synced_at' => ['nullable', 'date'],
|
||||
'metadata' => ['nullable', 'json'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Countries\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Country extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use SoftDeletes;
|
||||
|
||||
/** @var list<string> */
|
||||
protected $fillable = [
|
||||
'alpha2',
|
||||
'alpha3',
|
||||
'numeric',
|
||||
'name',
|
||||
'official_name',
|
||||
'common_name',
|
||||
'display_name',
|
||||
'native_name',
|
||||
'capital',
|
||||
'flag_emoji',
|
||||
'calling_code',
|
||||
'tld',
|
||||
'currency_code',
|
||||
'currency_name',
|
||||
'currency_symbol',
|
||||
'languages',
|
||||
'timezones',
|
||||
'latitude',
|
||||
'longitude',
|
||||
'flag_svg_url',
|
||||
'flag_png_url',
|
||||
'ioc_code',
|
||||
'fifa_code',
|
||||
'postal_code_format',
|
||||
'address_format',
|
||||
'is_active',
|
||||
'iso_status',
|
||||
'independent',
|
||||
'sovereignty_status',
|
||||
'un_member',
|
||||
'continent',
|
||||
'region',
|
||||
'subregion',
|
||||
'intermediate_region',
|
||||
'sort_order',
|
||||
'last_synced_at',
|
||||
'source',
|
||||
'source_version',
|
||||
'metadata',
|
||||
];
|
||||
|
||||
/** @var array<string, string> */
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'independent' => 'boolean',
|
||||
'un_member' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
'last_synced_at' => 'datetime',
|
||||
'languages' => 'array',
|
||||
'timezones' => 'array',
|
||||
'metadata' => 'array',
|
||||
'latitude' => 'decimal:7',
|
||||
'longitude' => 'decimal:7',
|
||||
];
|
||||
|
||||
/**
|
||||
* Use ISO 3166-1 alpha-2 for route model binding.
|
||||
*/
|
||||
public function getRouteKeyName(): string
|
||||
{
|
||||
return 'alpha2';
|
||||
}
|
||||
|
||||
/**
|
||||
* Effective display name used throughout WMS selectors and pages.
|
||||
*/
|
||||
public function getEffectiveNameAttribute(): string
|
||||
{
|
||||
return $this->display_name ?: $this->common_name ?: $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* ISO 3166-2 subdivisions belonging to this country.
|
||||
*/
|
||||
public function subdivisions(): HasMany
|
||||
{
|
||||
return $this->hasMany(CountrySubdivision::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a country from alpha-2, alpha-3 or numeric ISO 3166-1 code.
|
||||
*/
|
||||
public function scopeForIsoCode(Builder $query, string $code): Builder
|
||||
{
|
||||
$normalised = strtoupper(trim($code));
|
||||
|
||||
return $query->where(function (Builder $nested) use ($normalised): void {
|
||||
$nested->where('alpha2', $normalised)
|
||||
->orWhere('alpha3', $normalised)
|
||||
->orWhere('numeric', str_pad($normalised, 3, '0', STR_PAD_LEFT));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope active countries for business selectors.
|
||||
*/
|
||||
public function scopeActive(Builder $query): Builder
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope ordered countries for UI lists.
|
||||
*/
|
||||
public function scopeOrdered(Builder $query): Builder
|
||||
{
|
||||
return $query->orderBy('sort_order')->orderBy('name');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Countries\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class CountrySubdivision extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/** @var list<string> */
|
||||
protected $fillable = [
|
||||
'country_id',
|
||||
'country_alpha2',
|
||||
'code',
|
||||
'subdivision_code',
|
||||
'name',
|
||||
'local_name',
|
||||
'type',
|
||||
'is_active',
|
||||
'iso_status',
|
||||
'sort_order',
|
||||
'last_synced_at',
|
||||
'source',
|
||||
'source_version',
|
||||
'metadata',
|
||||
];
|
||||
|
||||
/** @var array<string, string> */
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
'last_synced_at' => 'datetime',
|
||||
'metadata' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
* Use the ISO 3166-2 code for route model binding.
|
||||
*/
|
||||
public function getRouteKeyName(): string
|
||||
{
|
||||
return 'code';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parent ISO 3166-1 country.
|
||||
*/
|
||||
public function country(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Country::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope active subdivisions.
|
||||
*/
|
||||
public function scopeActive(Builder $query): Builder
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope ordered subdivisions for selectors and administration screens.
|
||||
*/
|
||||
public function scopeOrdered(Builder $query): Builder
|
||||
{
|
||||
return $query->orderBy('sort_order')->orderBy('name');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Countries\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Modules\Countries\Support\ModuleInfo;
|
||||
|
||||
class CountriesServiceProvider extends ServiceProvider
|
||||
{
|
||||
protected string $moduleName = 'Countries';
|
||||
|
||||
protected string $moduleNameLower = 'countries';
|
||||
|
||||
/**
|
||||
* Register module services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->register(RouteServiceProvider::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap module services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
$this->registerTranslations();
|
||||
$this->registerConfig();
|
||||
$this->registerViews();
|
||||
$this->registerHelpers();
|
||||
$this->registerCommands();
|
||||
$this->registerMigrations();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register module migrations using the real filesystem path.
|
||||
*/
|
||||
protected function registerMigrations(): void
|
||||
{
|
||||
$path = dirname(__DIR__) . '/Database/Migrations';
|
||||
|
||||
if (is_dir($path)) {
|
||||
$this->loadMigrationsFrom($path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register module helper files.
|
||||
*/
|
||||
protected function registerHelpers(): void
|
||||
{
|
||||
foreach (glob(dirname(__DIR__) . '/Helpers/*.php') ?: [] as $filename) {
|
||||
require_once $filename;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register console commands when present.
|
||||
*/
|
||||
protected function registerCommands(): void
|
||||
{
|
||||
$commandPath = dirname(__DIR__) . '/Console';
|
||||
|
||||
if (! is_dir($commandPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$commands = collect(File::allFiles($commandPath))
|
||||
->map(fn ($file): string => 'Modules\\' . $this->moduleName . '\\Console\\' . $file->getFilenameWithoutExtension())
|
||||
->filter(fn (string $class): bool => class_exists($class))
|
||||
->values()
|
||||
->all();
|
||||
|
||||
if ($commands !== []) {
|
||||
$this->commands($commands);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register module config.
|
||||
*/
|
||||
protected function registerConfig(): void
|
||||
{
|
||||
$sourcePath = dirname(__DIR__) . '/Config/config.php';
|
||||
|
||||
if (! is_file($sourcePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->publishes([
|
||||
$sourcePath => config_path(ModuleInfo::nameLower() . '.php'),
|
||||
], 'config');
|
||||
|
||||
$this->mergeConfigFrom($sourcePath, ModuleInfo::nameLower());
|
||||
}
|
||||
|
||||
/**
|
||||
* Register module views.
|
||||
*/
|
||||
protected function registerViews(): void
|
||||
{
|
||||
$viewPath = resource_path('views/modules/' . ModuleInfo::nameLower());
|
||||
$sourcePath = dirname(__DIR__) . '/Resources/views';
|
||||
|
||||
if (! is_dir($sourcePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->publishes([
|
||||
$sourcePath => $viewPath,
|
||||
], ['views', ModuleInfo::nameLower() . '-module-views']);
|
||||
|
||||
$this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), ModuleInfo::nameLower());
|
||||
}
|
||||
|
||||
/**
|
||||
* Register module translations.
|
||||
*/
|
||||
protected function registerTranslations(): void
|
||||
{
|
||||
$publishedPath = resource_path('lang/modules/' . ModuleInfo::nameLower());
|
||||
$sourcePath = dirname(__DIR__) . '/Resources/lang';
|
||||
|
||||
if (is_dir($publishedPath)) {
|
||||
$this->loadTranslationsFrom($publishedPath, ModuleInfo::nameLower());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_dir($sourcePath)) {
|
||||
$this->loadTranslationsFrom($sourcePath, ModuleInfo::nameLower());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get published view paths.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function getPublishableViewPaths(): array
|
||||
{
|
||||
$paths = [];
|
||||
|
||||
foreach (Config::get('view.paths', []) as $path) {
|
||||
if (is_dir($path . '/modules/' . ModuleInfo::nameLower())) {
|
||||
$paths[] = $path . '/modules/' . ModuleInfo::nameLower();
|
||||
}
|
||||
}
|
||||
|
||||
return $paths;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Countries\Providers;
|
||||
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register Countries module routes.
|
||||
*/
|
||||
public function map(): void
|
||||
{
|
||||
$this->mapWebRoutes();
|
||||
$this->mapApiRoutes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register web routes from the real module path.
|
||||
*/
|
||||
protected function mapWebRoutes(): void
|
||||
{
|
||||
$path = dirname(__DIR__) . '/Routes/web.php';
|
||||
|
||||
if (is_file($path)) {
|
||||
Route::middleware(['web', 'auth'])->group($path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register API routes from the real module path.
|
||||
*/
|
||||
protected function mapApiRoutes(): void
|
||||
{
|
||||
$path = dirname(__DIR__) . '/Routes/api.php';
|
||||
|
||||
if (is_file($path)) {
|
||||
Route::middleware(['api'])->prefix('api/v1')->group($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,387 @@
|
||||
@php
|
||||
$required = '<span class="text-red-600 dark:text-red-400">*</span>';
|
||||
$textInput =
|
||||
'mt-2 min-h-11 w-full rounded-xl border-zinc-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100';
|
||||
$textareaInput =
|
||||
'mt-2 min-h-28 w-full rounded-xl border-zinc-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100';
|
||||
$labelClass = 'text-sm font-semibold text-zinc-900 dark:text-zinc-100';
|
||||
@endphp
|
||||
|
||||
<div class="grid gap-5 md:grid-cols-3">
|
||||
<div>
|
||||
<label class="{{ $labelClass }}" for="alpha2">Alpha-2 {!! $required !!}</label>
|
||||
<input id="alpha2" name="alpha2" maxlength="2" value="{{ old('alpha2', $country->alpha2) }}"
|
||||
class="{{ $textInput }} uppercase" placeholder="GB" />
|
||||
@error('alpha2')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="{{ $labelClass }}" for="alpha3">Alpha-3 {!! $required !!}</label>
|
||||
<input id="alpha3" name="alpha3" maxlength="3" value="{{ old('alpha3', $country->alpha3) }}"
|
||||
class="{{ $textInput }} uppercase" placeholder="GBR" />
|
||||
@error('alpha3')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="{{ $labelClass }}" for="numeric">Numeric {!! $required !!}</label>
|
||||
<input id="numeric" name="numeric" maxlength="3" value="{{ old('numeric', $country->numeric) }}"
|
||||
class="{{ $textInput }}" placeholder="826" />
|
||||
@error('numeric')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="{{ $labelClass }}" for="name">Canonical ISO name {!! $required !!}</label>
|
||||
<input id="name" name="name" value="{{ old('name', $country->name) }}" class="{{ $textInput }}" />
|
||||
@error('name')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="{{ $labelClass }}" for="official_name">Official name</label>
|
||||
<input id="official_name" name="official_name" value="{{ old('official_name', $country->official_name) }}"
|
||||
class="{{ $textInput }}" />
|
||||
@error('official_name')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="{{ $labelClass }}" for="common_name">Common name</label>
|
||||
<input id="common_name" name="common_name" value="{{ old('common_name', $country->common_name) }}"
|
||||
class="{{ $textInput }}" />
|
||||
@error('common_name')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="{{ $labelClass }}" for="display_name">Display name override</label>
|
||||
<input id="display_name" name="display_name" value="{{ old('display_name', $country->display_name) }}"
|
||||
class="{{ $textInput }}" />
|
||||
@error('display_name')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="{{ $labelClass }}" for="native_name">Native name</label>
|
||||
<input id="native_name" name="native_name" value="{{ old('native_name', $country->native_name) }}"
|
||||
class="{{ $textInput }}" />
|
||||
@error('native_name')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="{{ $labelClass }}" for="capital">Capital</label>
|
||||
<input id="capital" name="capital" value="{{ old('capital', $country->capital) }}"
|
||||
class="{{ $textInput }}" />
|
||||
@error('capital')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 border-t border-zinc-200 pt-6 dark:border-zinc-800">
|
||||
<h2 class="text-base font-semibold text-zinc-900 dark:text-zinc-100">Regional and sovereignty metadata</h2>
|
||||
<div class="mt-4 grid gap-5 md:grid-cols-3">
|
||||
<div>
|
||||
<label class="{{ $labelClass }}" for="iso_status">ISO lifecycle status {!! $required !!}</label>
|
||||
<select id="iso_status" name="iso_status" class="{{ $textInput }}">
|
||||
@foreach (\Modules\Countries\Support\Iso3166::statuses() as $status)
|
||||
<option value="{{ $status }}" @selected(old('iso_status', $country->iso_status ?? 'official') === $status)>
|
||||
{{ str($status)->replace('_', ' ')->title() }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('iso_status')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="{{ $labelClass }}" for="sovereignty_status">Sovereignty status</label>
|
||||
<input id="sovereignty_status" name="sovereignty_status"
|
||||
value="{{ old('sovereignty_status', $country->sovereignty_status) }}" class="{{ $textInput }}" />
|
||||
@error('sovereignty_status')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="{{ $labelClass }}" for="continent">Continent</label>
|
||||
<input id="continent" name="continent" value="{{ old('continent', $country->continent) }}"
|
||||
class="{{ $textInput }}" />
|
||||
@error('continent')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="{{ $labelClass }}" for="region">Region</label>
|
||||
<input id="region" name="region" value="{{ old('region', $country->region) }}"
|
||||
class="{{ $textInput }}" />
|
||||
@error('region')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="{{ $labelClass }}" for="subregion">Subregion</label>
|
||||
<input id="subregion" name="subregion" value="{{ old('subregion', $country->subregion) }}"
|
||||
class="{{ $textInput }}" />
|
||||
@error('subregion')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="{{ $labelClass }}" for="intermediate_region">Intermediate region</label>
|
||||
<input id="intermediate_region" name="intermediate_region"
|
||||
value="{{ old('intermediate_region', $country->intermediate_region) }}"
|
||||
class="{{ $textInput }}" />
|
||||
@error('intermediate_region')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 border-t border-zinc-200 pt-6 dark:border-zinc-800">
|
||||
<h2 class="text-base font-semibold text-zinc-900 dark:text-zinc-100">Logistics, billing and web metadata</h2>
|
||||
<div class="mt-4 grid gap-5 md:grid-cols-3">
|
||||
<div>
|
||||
<label class="{{ $labelClass }}" for="flag_emoji">Flag emoji</label>
|
||||
<input id="flag_emoji" name="flag_emoji" value="{{ old('flag_emoji', $country->flag_emoji) }}"
|
||||
class="{{ $textInput }}" />
|
||||
@error('flag_emoji')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="{{ $labelClass }}" for="calling_code">Calling code</label>
|
||||
<input id="calling_code" name="calling_code" value="{{ old('calling_code', $country->calling_code) }}"
|
||||
class="{{ $textInput }}" placeholder="+44" />
|
||||
@error('calling_code')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="{{ $labelClass }}" for="tld">Top-level domain</label>
|
||||
<input id="tld" name="tld" value="{{ old('tld', $country->tld) }}"
|
||||
class="{{ $textInput }}" placeholder=".uk" />
|
||||
@error('tld')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="{{ $labelClass }}" for="currency_code">Currency code</label>
|
||||
<input id="currency_code" name="currency_code" maxlength="3"
|
||||
value="{{ old('currency_code', $country->currency_code) }}" class="{{ $textInput }} uppercase"
|
||||
placeholder="GBP" />
|
||||
@error('currency_code')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="{{ $labelClass }}" for="currency_name">Currency name</label>
|
||||
<input id="currency_name" name="currency_name"
|
||||
value="{{ old('currency_name', $country->currency_name) }}" class="{{ $textInput }}" />
|
||||
@error('currency_name')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="{{ $labelClass }}" for="currency_symbol">Currency symbol</label>
|
||||
<input id="currency_symbol" name="currency_symbol"
|
||||
value="{{ old('currency_symbol', $country->currency_symbol) }}" class="{{ $textInput }}" />
|
||||
@error('currency_symbol')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="{{ $labelClass }}" for="latitude">Latitude</label>
|
||||
<input id="latitude" name="latitude" type="number" step="0.0000001"
|
||||
value="{{ old('latitude', $country->latitude) }}" class="{{ $textInput }}" />
|
||||
@error('latitude')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="{{ $labelClass }}" for="longitude">Longitude</label>
|
||||
<input id="longitude" name="longitude" type="number" step="0.0000001"
|
||||
value="{{ old('longitude', $country->longitude) }}" class="{{ $textInput }}" />
|
||||
@error('longitude')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="{{ $labelClass }}" for="sort_order">Sort order</label>
|
||||
<input id="sort_order" name="sort_order" type="number" min="0"
|
||||
value="{{ old('sort_order', $country->sort_order ?? 0) }}" class="{{ $textInput }}" />
|
||||
@error('sort_order')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="{{ $labelClass }}" for="ioc_code">IOC code</label>
|
||||
<input id="ioc_code" name="ioc_code" value="{{ old('ioc_code', $country->ioc_code) }}"
|
||||
class="{{ $textInput }} uppercase" />
|
||||
@error('ioc_code')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="{{ $labelClass }}" for="fifa_code">FIFA code</label>
|
||||
<input id="fifa_code" name="fifa_code" value="{{ old('fifa_code', $country->fifa_code) }}"
|
||||
class="{{ $textInput }} uppercase" />
|
||||
@error('fifa_code')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="{{ $labelClass }}" for="postal_code_format">Postal-code format</label>
|
||||
<input id="postal_code_format" name="postal_code_format"
|
||||
value="{{ old('postal_code_format', $country->postal_code_format) }}"
|
||||
class="{{ $textInput }}" />
|
||||
@error('postal_code_format')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-3">
|
||||
<label class="{{ $labelClass }}" for="flag_svg_url">Flag SVG URL</label>
|
||||
<input id="flag_svg_url" name="flag_svg_url" value="{{ old('flag_svg_url', $country->flag_svg_url) }}"
|
||||
class="{{ $textInput }}" />
|
||||
@error('flag_svg_url')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-3">
|
||||
<label class="{{ $labelClass }}" for="flag_png_url">Flag PNG URL</label>
|
||||
<input id="flag_png_url" name="flag_png_url" value="{{ old('flag_png_url', $country->flag_png_url) }}"
|
||||
class="{{ $textInput }}" />
|
||||
@error('flag_png_url')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 border-t border-zinc-200 pt-6 dark:border-zinc-800">
|
||||
<h2 class="text-base font-semibold text-zinc-900 dark:text-zinc-100">JSON metadata</h2>
|
||||
<p class="mt-1 text-sm text-zinc-500 dark:text-zinc-400">Use valid JSON. Examples: languages as <code
|
||||
class="rounded bg-zinc-100 px-1 dark:bg-zinc-800">["en"]</code>, timezones as <code
|
||||
class="rounded bg-zinc-100 px-1 dark:bg-zinc-800">["Europe/London"]</code>.</p>
|
||||
<div class="mt-4 grid gap-5 md:grid-cols-3">
|
||||
<div>
|
||||
<label class="{{ $labelClass }}" for="languages">Languages JSON</label>
|
||||
<textarea id="languages" name="languages" class="{{ $textareaInput }}">{{ old('languages', $country->languages ? json_encode($country->languages, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) : '') }}</textarea>
|
||||
@error('languages')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="{{ $labelClass }}" for="timezones">Timezones JSON</label>
|
||||
<textarea id="timezones" name="timezones" class="{{ $textareaInput }}">{{ old('timezones', $country->timezones ? json_encode($country->timezones, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) : '') }}</textarea>
|
||||
@error('timezones')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="{{ $labelClass }}" for="metadata">Metadata JSON</label>
|
||||
<textarea id="metadata" name="metadata" class="{{ $textareaInput }}">{{ old('metadata', $country->metadata ? json_encode($country->metadata, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) : '') }}</textarea>
|
||||
@error('metadata')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 border-t border-zinc-200 pt-6 dark:border-zinc-800">
|
||||
<h2 class="text-base font-semibold text-zinc-900 dark:text-zinc-100">Operational support</h2>
|
||||
<div class="mt-4 grid gap-5 md:grid-cols-3">
|
||||
<label
|
||||
class="flex items-center gap-3 rounded-2xl border border-zinc-200 bg-zinc-50 p-4 dark:border-zinc-800 dark:bg-zinc-950">
|
||||
<input type="hidden" name="is_active" value="0" />
|
||||
<input type="checkbox" name="is_active" value="1" @checked(old('is_active', $country->exists ? $country->is_active : true))
|
||||
class="rounded border-zinc-300 text-blue-600 focus:ring-blue-500" />
|
||||
<span><span class="block text-sm font-semibold">Supported {!! $required !!}</span><span
|
||||
class="block text-xs text-zinc-500">Available in WMS selectors and workflows.</span></span>
|
||||
</label>
|
||||
|
||||
<label
|
||||
class="flex items-center gap-3 rounded-2xl border border-zinc-200 bg-zinc-50 p-4 dark:border-zinc-800 dark:bg-zinc-950">
|
||||
<input type="hidden" name="independent" value="0" />
|
||||
<input type="checkbox" name="independent" value="1" @checked(old('independent', $country->independent))
|
||||
class="rounded border-zinc-300 text-blue-600 focus:ring-blue-500" />
|
||||
<span><span class="block text-sm font-semibold">Independent state</span><span
|
||||
class="block text-xs text-zinc-500">Used for reporting and compliance filters.</span></span>
|
||||
</label>
|
||||
|
||||
<label
|
||||
class="flex items-center gap-3 rounded-2xl border border-zinc-200 bg-zinc-50 p-4 dark:border-zinc-800 dark:bg-zinc-950">
|
||||
<input type="hidden" name="un_member" value="0" />
|
||||
<input type="checkbox" name="un_member" value="1" @checked(old('un_member', $country->un_member))
|
||||
class="rounded border-zinc-300 text-blue-600 focus:ring-blue-500" />
|
||||
<span><span class="block text-sm font-semibold">UN member</span><span
|
||||
class="block text-xs text-zinc-500">Optional global reference attribute.</span></span>
|
||||
</label>
|
||||
|
||||
<div>
|
||||
<label class="{{ $labelClass }}" for="source">Source</label>
|
||||
<input id="source" name="source" value="{{ old('source', $country->source) }}"
|
||||
class="{{ $textInput }}" />
|
||||
@error('source')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="{{ $labelClass }}" for="source_version">Source version</label>
|
||||
<input id="source_version" name="source_version"
|
||||
value="{{ old('source_version', $country->source_version) }}" class="{{ $textInput }}" />
|
||||
@error('source_version')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="{{ $labelClass }}" for="last_synced_at">Last synced at</label>
|
||||
<input id="last_synced_at" name="last_synced_at" type="datetime-local"
|
||||
value="{{ old('last_synced_at', $country->last_synced_at?->format('Y-m-d\\TH:i')) }}"
|
||||
class="{{ $textInput }}" />
|
||||
@error('last_synced_at')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-3">
|
||||
<label class="{{ $labelClass }}" for="address_format">Address format</label>
|
||||
<textarea id="address_format" name="address_format" class="{{ $textareaInput }}">{{ old('address_format', $country->address_format) }}</textarea>
|
||||
@error('address_format')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,41 @@
|
||||
<x-layouts.app>
|
||||
<div class="mx-auto max-w-[1400px] space-y-6" dusk="countries-create">
|
||||
<div
|
||||
class="overflow-hidden rounded-3xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div class="bg-gradient-to-br from-slate-950 via-slate-900 to-blue-950 px-6 py-7 text-white">
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-blue-200">ISO 3166 country
|
||||
support</p>
|
||||
<h1 class="mt-2 text-3xl font-semibold tracking-tight">Create country</h1>
|
||||
<p class="mt-2 max-w-3xl text-sm text-blue-100/90">Create a governed country reference record for
|
||||
platform, address, billing, carrier, customs and integration workflows.</p>
|
||||
</div>
|
||||
<a href="{{ route('countries.index') }}" wire:navigate
|
||||
class="rounded-xl bg-white/10 px-4 py-2.5 text-sm font-semibold text-white ring-1 ring-white/20 hover:bg-white/20">Back
|
||||
to countries</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('countries.store') }}"
|
||||
class="rounded-2xl border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
@csrf
|
||||
<div
|
||||
class="mb-5 rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800 dark:border-red-900 dark:bg-red-950 dark:text-red-200">
|
||||
Fields marked with <span class="font-bold text-red-600 dark:text-red-300">*</span> are mandatory ISO/WMS
|
||||
support fields.
|
||||
</div>
|
||||
|
||||
@include('countries::_form', ['country' => $country])
|
||||
|
||||
<div class="mt-8 flex flex-wrap gap-2 border-t border-zinc-200 pt-6 dark:border-zinc-800">
|
||||
<button type="submit"
|
||||
class="rounded-xl bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">Create
|
||||
country</button>
|
||||
<a href="{{ route('countries.index') }}" wire:navigate
|
||||
class="rounded-xl bg-zinc-100 px-4 py-2.5 text-sm font-semibold text-zinc-900 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</x-layouts.app>
|
||||
@@ -0,0 +1,47 @@
|
||||
<x-layouts.app>
|
||||
<div class="mx-auto max-w-[1400px] space-y-6" dusk="countries-edit">
|
||||
<div
|
||||
class="overflow-hidden rounded-3xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div class="bg-gradient-to-br from-slate-950 via-slate-900 to-blue-950 px-6 py-7 text-white">
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-blue-200">ISO 3166 country
|
||||
support</p>
|
||||
<h1 class="mt-2 text-3xl font-semibold tracking-tight">Edit {{ $country->flag_emoji }}
|
||||
{{ $country->effective_name }}</h1>
|
||||
<p class="mt-2 max-w-3xl text-sm text-blue-100/90">Expose and manage every ISO, operational,
|
||||
regional, currency, logistics and WMS support parameter.</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a href="{{ route('countries.show', $country) }}" wire:navigate
|
||||
class="rounded-xl bg-white/10 px-4 py-2.5 text-sm font-semibold text-white ring-1 ring-white/20 hover:bg-white/20">View
|
||||
country</a>
|
||||
<a href="{{ route('countries.index') }}" wire:navigate
|
||||
class="rounded-xl bg-white/10 px-4 py-2.5 text-sm font-semibold text-white ring-1 ring-white/20 hover:bg-white/20">Back</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('countries.update', $country) }}"
|
||||
class="rounded-2xl border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
<div
|
||||
class="mb-5 rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800 dark:border-red-900 dark:bg-red-950 dark:text-red-200">
|
||||
Fields marked with <span class="font-bold text-red-600 dark:text-red-300">*</span> are mandatory ISO/WMS
|
||||
support fields.
|
||||
</div>
|
||||
|
||||
@include('countries::_form', ['country' => $country])
|
||||
|
||||
<div class="mt-8 flex flex-wrap gap-2 border-t border-zinc-200 pt-6 dark:border-zinc-800">
|
||||
<button type="submit"
|
||||
class="rounded-xl bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">Save
|
||||
country</button>
|
||||
<a href="{{ route('countries.show', $country) }}" wire:navigate
|
||||
class="rounded-xl bg-zinc-100 px-4 py-2.5 text-sm font-semibold text-zinc-900 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</x-layouts.app>
|
||||
@@ -0,0 +1,152 @@
|
||||
<x-layouts.app>
|
||||
<div class="mx-auto max-w-[1800px] space-y-6" dusk="countries-index">
|
||||
<div
|
||||
class="overflow-hidden rounded-3xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div class="bg-gradient-to-br from-slate-950 via-slate-900 to-blue-950 px-6 py-7 text-white">
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-blue-200">Reference data</p>
|
||||
<h1 class="mt-2 text-3xl font-semibold tracking-tight">Countries</h1>
|
||||
<p class="mt-2 max-w-3xl text-sm text-blue-100/90">Governed ISO 3166 country records for tenants,
|
||||
companies, warehouses, billing, shipping, carriers, customs and integrations.</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<form method="POST" action="{{ route('countries.sync') }}">
|
||||
@csrf
|
||||
<button type="submit"
|
||||
class="rounded-xl bg-white/10 px-4 py-2.5 text-sm font-semibold text-white ring-1 ring-white/20 hover:bg-white/20">Sync
|
||||
ISO data</button>
|
||||
</form>
|
||||
<a href="{{ route('countries.create') }}" wire:navigate
|
||||
class="rounded-xl bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">New
|
||||
country</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (session('success'))
|
||||
<div
|
||||
class="rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-800 dark:border-emerald-900 dark:bg-emerald-950 dark:text-emerald-200">
|
||||
{{ session('success') }}</div>
|
||||
@endif
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<div
|
||||
class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div class="text-sm text-zinc-500">Total countries</div>
|
||||
<div class="mt-2 text-3xl font-semibold">{{ number_format($totalCount) }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div class="text-sm text-zinc-500">Active</div>
|
||||
<div class="mt-2 text-3xl font-semibold text-emerald-700 dark:text-emerald-300">
|
||||
{{ number_format($activeCount) }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div class="text-sm text-zinc-500">Inactive</div>
|
||||
<div class="mt-2 text-3xl font-semibold text-amber-700 dark:text-amber-300">
|
||||
{{ number_format($inactiveCount) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="GET" action="{{ route('countries.index') }}"
|
||||
class="grid gap-3 rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-700 dark:bg-zinc-900 md:grid-cols-[1fr_220px_220px_auto] md:items-center">
|
||||
<input name="search" value="{{ $search }}"
|
||||
placeholder="Search by country, ISO code, currency or calling code"
|
||||
class="min-h-11 rounded-xl border-zinc-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100" />
|
||||
<select name="status"
|
||||
class="min-h-11 rounded-xl border-zinc-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100">
|
||||
<option value="all" @selected($status === 'all')>All statuses</option>
|
||||
<option value="active" @selected($status === 'active')>Active</option>
|
||||
<option value="inactive" @selected($status === 'inactive')>Inactive</option>
|
||||
</select>
|
||||
<select name="region"
|
||||
class="min-h-11 rounded-xl border-zinc-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100">
|
||||
<option value="all" @selected($region === 'all')>All regions</option>
|
||||
@foreach ($regions as $availableRegion)
|
||||
<option value="{{ $availableRegion }}" @selected($region === $availableRegion)>{{ $availableRegion }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<button type="submit"
|
||||
class="rounded-xl bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-700">Filter</button>
|
||||
</form>
|
||||
|
||||
<div
|
||||
class="overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-zinc-200 dark:divide-zinc-800">
|
||||
<thead class="bg-zinc-50 dark:bg-zinc-950">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-bold uppercase tracking-wide text-zinc-500">
|
||||
Country</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-bold uppercase tracking-wide text-zinc-500">ISO
|
||||
</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-bold uppercase tracking-wide text-zinc-500">
|
||||
Region</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-bold uppercase tracking-wide text-zinc-500">
|
||||
Currency</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-bold uppercase tracking-wide text-zinc-500">
|
||||
Support</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-bold uppercase tracking-wide text-zinc-500">
|
||||
Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-zinc-100 dark:divide-zinc-800">
|
||||
@forelse ($countries as $country)
|
||||
<tr class="hover:bg-blue-50/50 dark:hover:bg-blue-950/20">
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-xl">{{ $country->flag_emoji }}</span>
|
||||
<div>
|
||||
<div class="font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
{{ $country->effective_name }}</div>
|
||||
<div class="text-xs text-zinc-500">
|
||||
{{ $country->official_name ?: $country->name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="font-mono text-sm text-blue-700 dark:text-blue-300">
|
||||
{{ $country->alpha2 }} / {{ $country->alpha3 }} / {{ $country->numeric }}
|
||||
</div>
|
||||
<div class="text-xs text-zinc-500">
|
||||
{{ str($country->iso_status ?? 'official')->replace('_', ' ')->title() }}</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm text-zinc-600 dark:text-zinc-300">
|
||||
<div>{{ $country->region ?: '—' }}</div>
|
||||
<div class="text-xs text-zinc-500">{{ $country->subregion ?: '—' }}</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<div class="font-mono text-zinc-900 dark:text-zinc-100">
|
||||
{{ $country->currency_code ?: '—' }}</div>
|
||||
<div class="text-xs text-zinc-500">{{ $country->currency_name ?: '—' }}</div>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
@if ($country->is_active)
|
||||
<span
|
||||
class="rounded-full bg-emerald-100 px-2.5 py-1 text-xs font-bold text-emerald-800 dark:bg-emerald-950 dark:text-emerald-200">Active</span>@else<span
|
||||
class="rounded-full bg-amber-100 px-2.5 py-1 text-xs font-bold text-amber-800 dark:bg-amber-950 dark:text-amber-200">Inactive</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right"><a href="{{ route('countries.show', $country) }}"
|
||||
wire:navigate
|
||||
class="rounded-lg bg-blue-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-blue-700">Open</a>
|
||||
<a href="{{ route('countries.edit', $country) }}" wire:navigate
|
||||
class="rounded-lg bg-emerald-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-emerald-700">Edit</a>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="px-4 py-10 text-center text-sm text-zinc-500">No countries
|
||||
matched your search.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="border-t border-zinc-200 px-4 py-3 dark:border-zinc-800">{{ $countries->links() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-layouts.app>
|
||||
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Module Countries</title>
|
||||
|
||||
{{-- Laravel Mix - CSS File --}}
|
||||
{{-- <link rel="stylesheet" href="{{ mix('css/countries.css') }}"> --}}
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div data-dusk="page-ready" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);"
|
||||
aria-hidden="true"></div>
|
||||
@yield('content')
|
||||
|
||||
{{-- Laravel Mix - JS File --}}
|
||||
{{-- <script src="{{ mix('js/countries.js') }}"></script> --}}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,212 @@
|
||||
<x-layouts.app>
|
||||
<div class="mx-auto max-w-[1400px] space-y-6" dusk="countries-show">
|
||||
<div
|
||||
class="overflow-hidden rounded-3xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div class="bg-gradient-to-br from-slate-950 via-slate-900 to-blue-950 px-6 py-7 text-white">
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-blue-200">ISO 3166 country</p>
|
||||
<h1 class="mt-2 text-3xl font-semibold tracking-tight">{{ $country->flag_emoji }}
|
||||
{{ $country->effective_name }}</h1>
|
||||
<p class="mt-2 max-w-3xl text-sm text-blue-100/90">
|
||||
{{ $country->official_name ?: $country->name }}</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a href="{{ route('countries.edit', $country) }}" wire:navigate
|
||||
class="rounded-xl bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white hover:bg-blue-700">Edit
|
||||
country</a>
|
||||
<a href="{{ route('countries.index') }}" wire:navigate
|
||||
class="rounded-xl bg-white/10 px-4 py-2.5 text-sm font-semibold text-white ring-1 ring-white/20 hover:bg-white/20">Back</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (session('success'))
|
||||
<div
|
||||
class="rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-800 dark:border-emerald-900 dark:bg-emerald-950 dark:text-emerald-200">
|
||||
{{ session('success') }}</div>
|
||||
@endif
|
||||
|
||||
@php
|
||||
$fields = [
|
||||
'Alpha-2' => $country->alpha2,
|
||||
'Alpha-3' => $country->alpha3,
|
||||
'Numeric' => $country->numeric,
|
||||
'Canonical name' => $country->name,
|
||||
'Official name' => $country->official_name ?: '—',
|
||||
'Common name' => $country->common_name ?: '—',
|
||||
'Display override' => $country->display_name ?: '—',
|
||||
'Native name' => $country->native_name ?: '—',
|
||||
'Capital' => $country->capital ?: '—',
|
||||
'ISO status' => str($country->iso_status ?? 'official')
|
||||
->replace('_', ' ')
|
||||
->title(),
|
||||
'Sovereignty status' => $country->sovereignty_status ?: '—',
|
||||
'Independent' => is_null($country->independent) ? '—' : ($country->independent ? 'Yes' : 'No'),
|
||||
'UN member' => is_null($country->un_member) ? '—' : ($country->un_member ? 'Yes' : 'No'),
|
||||
'Continent' => $country->continent ?: '—',
|
||||
'Region' => $country->region ?: '—',
|
||||
'Subregion' => $country->subregion ?: '—',
|
||||
'Intermediate region' => $country->intermediate_region ?: '—',
|
||||
'Calling code' => $country->calling_code ?: '—',
|
||||
'TLD' => $country->tld ?: '—',
|
||||
'Currency code' => $country->currency_code ?: '—',
|
||||
'Currency name' => $country->currency_name ?: '—',
|
||||
'Currency symbol' => $country->currency_symbol ?: '—',
|
||||
'Latitude' => $country->latitude ?: '—',
|
||||
'Longitude' => $country->longitude ?: '—',
|
||||
'IOC code' => $country->ioc_code ?: '—',
|
||||
'FIFA code' => $country->fifa_code ?: '—',
|
||||
'Postal-code format' => $country->postal_code_format ?: '—',
|
||||
'Active' => $country->is_active ? 'Yes' : 'No',
|
||||
'Sort order' => $country->sort_order,
|
||||
'Source' => $country->source ?: '—',
|
||||
'Source version' => $country->source_version ?: '—',
|
||||
'Last synced' => $country->last_synced_at?->format('Y-m-d H:i') ?: '—',
|
||||
];
|
||||
@endphp
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
@foreach ($fields as $label => $value)
|
||||
<div
|
||||
class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div class="text-xs font-bold uppercase tracking-wide text-zinc-500">{{ $label }}</div>
|
||||
<div class="mt-2 break-words text-base font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
{{ $value }}</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 lg:grid-cols-3">
|
||||
@foreach (['Languages' => $country->languages, 'Timezones' => $country->timezones, 'Metadata' => $country->metadata] as $label => $value)
|
||||
<div
|
||||
class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<h2 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100">{{ $label }}</h2>
|
||||
<pre class="mt-3 max-h-80 overflow-auto rounded-xl bg-zinc-950 p-4 text-xs text-zinc-100">{{ $value ? json_encode($value, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) : '—' }}</pre>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@if ($country->address_format || $country->flag_svg_url || $country->flag_png_url)
|
||||
<div class="grid gap-4 lg:grid-cols-3">
|
||||
<div
|
||||
class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900 lg:col-span-2">
|
||||
<h2 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100">Address format</h2>
|
||||
<pre
|
||||
class="mt-3 whitespace-pre-wrap rounded-xl bg-zinc-100 p-4 text-sm text-zinc-700 dark:bg-zinc-950 dark:text-zinc-200">{{ $country->address_format ?: '—' }}</pre>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<h2 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100">Flag assets</h2>
|
||||
<div class="mt-3 space-y-2 text-sm text-zinc-600 dark:text-zinc-300">
|
||||
<div>SVG: {{ $country->flag_svg_url ?: '—' }}</div>
|
||||
<div>PNG: {{ $country->flag_png_url ?: '—' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
||||
<div
|
||||
class="overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div class="border-b border-zinc-200 px-5 py-4 dark:border-zinc-800">
|
||||
<h2 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100">WMS usage report</h2>
|
||||
<p class="text-xs text-zinc-500">Best-effort counts across known WMS tables. Missing optional tables are
|
||||
skipped safely.</p>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-zinc-200 dark:divide-zinc-800">
|
||||
<thead class="bg-zinc-50 dark:bg-zinc-950">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-bold uppercase tracking-wide text-zinc-500">Area
|
||||
</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-bold uppercase tracking-wide text-zinc-500">
|
||||
Table</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-bold uppercase tracking-wide text-zinc-500">
|
||||
Column</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-bold uppercase tracking-wide text-zinc-500">
|
||||
References</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-zinc-100 dark:divide-zinc-800">
|
||||
@forelse ($usageRows ?? [] as $row)
|
||||
<tr>
|
||||
<td class="px-4 py-3 text-sm font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
{{ $row['label'] }}</td>
|
||||
<td class="px-4 py-3 font-mono text-xs text-zinc-500">{{ $row['table'] }}</td>
|
||||
<td class="px-4 py-3 font-mono text-xs text-blue-700 dark:text-blue-300">
|
||||
{{ $row['column'] }}</td>
|
||||
<td class="px-4 py-3 text-right text-sm font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
{{ number_format($row['count']) }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="4" class="px-4 py-8 text-center text-sm text-zinc-500">No country-aware
|
||||
WMS usage tables were detected yet.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div class="border-b border-zinc-200 px-5 py-4 dark:border-zinc-800">
|
||||
<h2 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100">ISO 3166-2 subdivisions</h2>
|
||||
<p class="text-xs text-zinc-500">Subdivisions support address, compliance and shipping workflows.</p>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-zinc-200 dark:divide-zinc-800">
|
||||
<thead class="bg-zinc-50 dark:bg-zinc-950">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-bold uppercase tracking-wide text-zinc-500">Code
|
||||
</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-bold uppercase tracking-wide text-zinc-500">Name
|
||||
</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-bold uppercase tracking-wide text-zinc-500">Type
|
||||
</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-bold uppercase tracking-wide text-zinc-500">
|
||||
Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-zinc-100 dark:divide-zinc-800">
|
||||
@forelse ($country->subdivisions as $subdivision)
|
||||
<tr>
|
||||
<td class="px-4 py-3 font-mono text-sm text-blue-700 dark:text-blue-300">
|
||||
{{ $subdivision->code }}</td>
|
||||
<td class="px-4 py-3 text-sm font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
{{ $subdivision->name }}</td>
|
||||
<td class="px-4 py-3 text-sm text-zinc-500">{{ $subdivision->type ?: '—' }}</td>
|
||||
<td class="px-4 py-3 text-sm text-zinc-500">
|
||||
{{ str($subdivision->iso_status)->replace('_', ' ')->title() }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="4" class="px-4 py-8 text-center text-sm text-zinc-500">No subdivisions
|
||||
have been imported for this country yet.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('countries.destroy', $country) }}"
|
||||
class="rounded-2xl border border-red-200 bg-red-50 p-5 dark:border-red-900 dark:bg-red-950">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<h2 class="text-sm font-semibold text-red-900 dark:text-red-100">Archive country</h2>
|
||||
<p class="text-sm text-red-700 dark:text-red-200">Soft-deletes the record for administration. Do not
|
||||
archive countries referenced by active business data.</p>
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="rounded-xl bg-red-600 px-4 py-2.5 text-sm font-semibold text-white hover:bg-red-700"
|
||||
onclick="return confirm('Archive this country?')">Archive</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</x-layouts.app>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Countries\Http\Controllers\CountriesController;
|
||||
use Modules\Countries\Http\Controllers\CountrySyncController;
|
||||
use Modules\Countries\Http\Controllers\ReferenceDataController;
|
||||
|
||||
Route::prefix('countries')->name('countries.')->middleware(['web', 'auth'])->group(function (): void {
|
||||
Route::get('/', CountriesController::class)->name('index');
|
||||
Route::get('/create', [CountriesController::class, 'create'])->name('create');
|
||||
Route::post('/', [CountriesController::class, 'store'])->name('store');
|
||||
Route::post('/sync', CountrySyncController::class)->name('sync');
|
||||
|
||||
Route::get('/reference/currencies', [ReferenceDataController::class, 'currencies'])->name('currencies.index');
|
||||
Route::get('/reference/languages', [ReferenceDataController::class, 'languages'])->name('languages.index');
|
||||
Route::get('/reference/scripts', [ReferenceDataController::class, 'scripts'])->name('scripts.index');
|
||||
Route::get('/reference/locodes', [ReferenceDataController::class, 'locodes'])->name('locodes.index');
|
||||
Route::get('/{country:alpha2}', [CountriesController::class, 'show'])->name('show');
|
||||
Route::get('/{country:alpha2}/edit', [CountriesController::class, 'edit'])->name('edit');
|
||||
Route::put('/{country:alpha2}', [CountriesController::class, 'update'])->name('update');
|
||||
Route::delete('/{country:alpha2}', [CountriesController::class, 'destroy'])->name('destroy');
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Countries\Rules;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
|
||||
class ValidCountryCode implements ValidationRule
|
||||
{
|
||||
public function __construct(private readonly bool $activeOnly = true) {}
|
||||
|
||||
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||
{
|
||||
(new ValidIso3166CountryCode($this->activeOnly, 'alpha2'))->validate($attribute, $value, $fail);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Countries\Rules;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Modules\Countries\Models\Country;
|
||||
|
||||
class ValidIso3166CountryCode implements ValidationRule
|
||||
{
|
||||
public function __construct(
|
||||
private readonly bool $activeOnly = true,
|
||||
private readonly string $format = 'alpha2',
|
||||
) {}
|
||||
|
||||
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||
{
|
||||
if (! is_string($value) && ! is_int($value)) {
|
||||
$fail('The :attribute must be a valid ISO 3166 country code.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$code = strtoupper(trim((string) $value));
|
||||
$query = Country::query();
|
||||
|
||||
match ($this->format) {
|
||||
'alpha2' => $query->where('alpha2', $code),
|
||||
'alpha3' => $query->where('alpha3', $code),
|
||||
'numeric' => $query->where('numeric', str_pad($code, 3, '0', STR_PAD_LEFT)),
|
||||
'any' => $query->forIsoCode($code),
|
||||
default => $query->whereRaw('1 = 0'),
|
||||
};
|
||||
|
||||
if ($this->activeOnly) {
|
||||
$query->where('is_active', true);
|
||||
}
|
||||
|
||||
if (! $query->exists()) {
|
||||
$fail('The :attribute must be a supported ISO 3166 country code.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Countries\Rules;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Modules\Countries\Models\CountrySubdivision;
|
||||
|
||||
class ValidIso3166SubdivisionCode implements ValidationRule
|
||||
{
|
||||
public function __construct(private readonly bool $activeOnly = true) {}
|
||||
|
||||
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||
{
|
||||
if (! is_string($value) || ! preg_match('/^[A-Z]{2}-[A-Z0-9]{1,6}$/', strtoupper($value))) {
|
||||
$fail('The :attribute must be a valid ISO 3166-2 subdivision code.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$query = CountrySubdivision::query()->where('code', strtoupper($value));
|
||||
|
||||
if ($this->activeOnly) {
|
||||
$query->where('is_active', true);
|
||||
}
|
||||
|
||||
if (! $query->exists()) {
|
||||
$fail('The :attribute must be a supported ISO 3166-2 subdivision code.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Countries\Support;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Modules\Countries\Models\Country;
|
||||
use Modules\Countries\Models\CountrySubdivision;
|
||||
|
||||
class CountryRepository
|
||||
{
|
||||
/**
|
||||
* Active countries for form selects.
|
||||
*
|
||||
* @return Collection<int, Country>
|
||||
*/
|
||||
public function activeForSelect(): Collection
|
||||
{
|
||||
return Country::query()->active()->ordered()
|
||||
->get(['id', 'alpha2', 'alpha3', 'numeric', 'name', 'display_name', 'common_name', 'flag_emoji', 'region', 'subregion']);
|
||||
}
|
||||
|
||||
public function findByAlpha2(string $alpha2): ?Country
|
||||
{
|
||||
return Country::query()->where('alpha2', Iso3166::normaliseAlpha2($alpha2))->first();
|
||||
}
|
||||
|
||||
public function findByIsoCode(string $code): ?Country
|
||||
{
|
||||
return Country::query()->forIsoCode($code)->first();
|
||||
}
|
||||
|
||||
public function findSubdivision(string $code): ?CountrySubdivision
|
||||
{
|
||||
return CountrySubdivision::query()->where('code', strtoupper(trim($code)))->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Active subdivisions for a country select.
|
||||
*
|
||||
* @return Collection<int, CountrySubdivision>
|
||||
*/
|
||||
public function activeSubdivisionsForCountry(string $alpha2): Collection
|
||||
{
|
||||
$alpha2 = Iso3166::normaliseAlpha2($alpha2);
|
||||
|
||||
return CountrySubdivision::query()
|
||||
->where('country_alpha2', $alpha2)
|
||||
->active()
|
||||
->ordered()
|
||||
->get(['id', 'country_alpha2', 'code', 'subdivision_code', 'name', 'local_name', 'type']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Countries\Support;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Modules\Countries\Models\Country;
|
||||
|
||||
class CountryUsageReport
|
||||
{
|
||||
/**
|
||||
* Tables commonly using country_id in WMS and SaaS workflows.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private array $countryIdTables = [
|
||||
'companies' => 'Companies',
|
||||
'warehouses' => 'Warehouses',
|
||||
'users' => 'Users',
|
||||
'suppliers' => 'Suppliers',
|
||||
'customers' => 'Customers',
|
||||
'carriers' => 'Carriers',
|
||||
'addresses' => 'Addresses',
|
||||
'billing_addresses' => 'Billing addresses',
|
||||
'shipping_addresses' => 'Shipping addresses',
|
||||
];
|
||||
|
||||
/**
|
||||
* Tables commonly storing ISO alpha-2 country codes.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private array $alpha2Tables = [
|
||||
'companies' => 'Companies by ISO code',
|
||||
'warehouses' => 'Warehouses by ISO code',
|
||||
'addresses' => 'Addresses by ISO code',
|
||||
'billing_addresses' => 'Billing addresses by ISO code',
|
||||
'shipping_addresses' => 'Shipping addresses by ISO code',
|
||||
];
|
||||
|
||||
/**
|
||||
* Return usage counts without failing when optional business tables do not exist.
|
||||
*
|
||||
* @return array<int, array{label: string, table: string, column: string, count: int}>
|
||||
*/
|
||||
public function forCountry(Country $country): array
|
||||
{
|
||||
$rows = [];
|
||||
|
||||
foreach ($this->countryIdTables as $table => $label) {
|
||||
if (! Schema::hasTable($table) || ! Schema::hasColumn($table, 'country_id')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rows[] = [
|
||||
'label' => $label,
|
||||
'table' => $table,
|
||||
'column' => 'country_id',
|
||||
'count' => (int) DB::table($table)->where('country_id', $country->id)->count(),
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($this->alpha2Tables as $table => $label) {
|
||||
foreach (['country_code', 'country_alpha2', 'iso_country_code'] as $column) {
|
||||
if (! Schema::hasTable($table) || ! Schema::hasColumn($table, $column)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rows[] = [
|
||||
'label' => $label,
|
||||
'table' => $table,
|
||||
'column' => $column,
|
||||
'count' => (int) DB::table($table)->where($column, $country->alpha2)->count(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Countries\Support;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
final class Iso3166
|
||||
{
|
||||
public const STATUS_OFFICIAL = 'official';
|
||||
|
||||
public const STATUS_RESERVED = 'reserved';
|
||||
|
||||
public const STATUS_TRANSITIONAL = 'transitional';
|
||||
|
||||
public const STATUS_DELETED = 'deleted';
|
||||
|
||||
public const STATUS_USER_ASSIGNED = 'user_assigned';
|
||||
|
||||
public const STATUS_EXCEPTIONALLY_RESERVED = 'exceptionally_reserved';
|
||||
|
||||
/** @return list<string> */
|
||||
public static function statuses(): array
|
||||
{
|
||||
return [
|
||||
self::STATUS_OFFICIAL,
|
||||
self::STATUS_RESERVED,
|
||||
self::STATUS_TRANSITIONAL,
|
||||
self::STATUS_DELETED,
|
||||
self::STATUS_USER_ASSIGNED,
|
||||
self::STATUS_EXCEPTIONALLY_RESERVED,
|
||||
];
|
||||
}
|
||||
|
||||
public static function normaliseAlpha2(string $code): string
|
||||
{
|
||||
$normalised = strtoupper(trim($code));
|
||||
|
||||
if (! preg_match('/^[A-Z]{2}$/', $normalised)) {
|
||||
throw new InvalidArgumentException('ISO 3166-1 alpha-2 codes must contain exactly two letters.');
|
||||
}
|
||||
|
||||
return $normalised;
|
||||
}
|
||||
|
||||
public static function normaliseAlpha3(string $code): string
|
||||
{
|
||||
$normalised = strtoupper(trim($code));
|
||||
|
||||
if (! preg_match('/^[A-Z]{3}$/', $normalised)) {
|
||||
throw new InvalidArgumentException('ISO 3166-1 alpha-3 codes must contain exactly three letters.');
|
||||
}
|
||||
|
||||
return $normalised;
|
||||
}
|
||||
|
||||
public static function normaliseNumeric(string|int $code): string
|
||||
{
|
||||
$normalised = str_pad((string) $code, 3, '0', STR_PAD_LEFT);
|
||||
|
||||
if (! preg_match('/^[0-9]{3}$/', $normalised)) {
|
||||
throw new InvalidArgumentException('ISO 3166-1 numeric codes must contain exactly three digits.');
|
||||
}
|
||||
|
||||
return $normalised;
|
||||
}
|
||||
|
||||
public static function normaliseSubdivisionCode(string $countryAlpha2, string $subdivisionCode): string
|
||||
{
|
||||
$country = self::normaliseAlpha2($countryAlpha2);
|
||||
$code = strtoupper(trim($subdivisionCode));
|
||||
|
||||
if (str_starts_with($code, $country . '-')) {
|
||||
return $code;
|
||||
}
|
||||
|
||||
if (! preg_match('/^[A-Z0-9]{1,6}$/', $code)) {
|
||||
throw new InvalidArgumentException('ISO 3166-2 subdivision codes must contain 1 to 6 letters or numbers after the country prefix.');
|
||||
}
|
||||
|
||||
return $country . '-' . $code;
|
||||
}
|
||||
|
||||
public static function flagEmoji(string $alpha2): string
|
||||
{
|
||||
$alpha2 = self::normaliseAlpha2($alpha2);
|
||||
$base = 127397;
|
||||
|
||||
return mb_chr($base + ord($alpha2[0]), 'UTF-8') . mb_chr($base + ord($alpha2[1]), 'UTF-8');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Countries\Support;
|
||||
|
||||
class ModuleInfo
|
||||
{
|
||||
protected static string $moduleName = 'Countries';
|
||||
|
||||
/**
|
||||
* defines the Module Name
|
||||
*/
|
||||
public static function name(): string
|
||||
{
|
||||
return static::$moduleName;
|
||||
}
|
||||
|
||||
/**
|
||||
* define the lowercase version
|
||||
*/
|
||||
public static function nameLower(): string
|
||||
{
|
||||
return strtolower(static::$moduleName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Countries\Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Countries\Database\Seeders\IsoCountriesSeeder;
|
||||
use Modules\Countries\Models\Country;
|
||||
use Tests\TestCase;
|
||||
|
||||
class CountriesModuleTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_countries_module_reference_pages_are_registered(): void
|
||||
{
|
||||
$this->assertTrue(Route::has('countries.currencies.index'));
|
||||
$this->assertTrue(Route::has('countries.languages.index'));
|
||||
$this->assertTrue(Route::has('countries.scripts.index'));
|
||||
$this->assertTrue(Route::has('countries.locodes.index'));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_seeds_iso_countries(): void
|
||||
{
|
||||
$this->seed(IsoCountriesSeeder::class);
|
||||
$this->assertDatabaseHas('countries', ['alpha2' => 'GB', 'alpha3' => 'GBR', 'numeric' => '826']);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_update_country_support_settings(): void
|
||||
{
|
||||
$this->seed(IsoCountriesSeeder::class);
|
||||
$country = Country::query()->where('alpha2', 'GB')->firstOrFail();
|
||||
$country->update(['display_name' => 'United Kingdom', 'is_active' => false, 'sort_order' => 1]);
|
||||
$this->assertDatabaseHas('countries', ['alpha2' => 'GB', 'display_name' => 'United Kingdom', 'is_active' => false, 'sort_order' => 1]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Countries\Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Modules\Countries\Database\Seeders\CountriesDatabaseSeeder;
|
||||
use Modules\Countries\Rules\ValidIso3166CountryCode;
|
||||
use Modules\Countries\Rules\ValidIso3166SubdivisionCode;
|
||||
use Tests\TestCase;
|
||||
|
||||
class Iso3166SupportTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_it_seeds_iso_3166_country_and_subdivision_reference_data(): void
|
||||
{
|
||||
$this->seed(CountriesDatabaseSeeder::class);
|
||||
|
||||
$this->assertDatabaseHas('countries', [
|
||||
'alpha2' => 'GB',
|
||||
'alpha3' => 'GBR',
|
||||
'numeric' => '826',
|
||||
'iso_status' => 'official',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('country_subdivisions', [
|
||||
'country_alpha2' => 'GB',
|
||||
'code' => 'GB-ENG',
|
||||
'name' => 'England',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_it_resolves_country_by_any_iso_3166_1_code(): void
|
||||
{
|
||||
$this->seed(CountriesDatabaseSeeder::class);
|
||||
|
||||
$this->getJson('/api/countries/resolve/GBR')
|
||||
->assertOk()
|
||||
->assertJsonPath('data.alpha2', 'GB');
|
||||
|
||||
$this->getJson('/api/countries/resolve/826')
|
||||
->assertOk()
|
||||
->assertJsonPath('data.alpha3', 'GBR');
|
||||
}
|
||||
|
||||
public function test_it_lists_iso_3166_2_subdivisions_for_country(): void
|
||||
{
|
||||
$this->seed(CountriesDatabaseSeeder::class);
|
||||
|
||||
$this->getJson('/api/countries/GB/subdivisions')
|
||||
->assertOk()
|
||||
->assertJsonFragment(['code' => 'GB-ENG'])
|
||||
->assertJsonFragment(['code' => 'GB-SCT']);
|
||||
}
|
||||
|
||||
public function test_validation_rules_support_country_and_subdivision_codes(): void
|
||||
{
|
||||
$this->seed(CountriesDatabaseSeeder::class);
|
||||
|
||||
$countryRule = new ValidIso3166CountryCode(activeOnly: true, format: 'any');
|
||||
$subdivisionRule = new ValidIso3166SubdivisionCode(activeOnly: true);
|
||||
|
||||
$countryFailed = false;
|
||||
$subdivisionFailed = false;
|
||||
|
||||
$countryRule->validate('country', 'GBR', function () use (&$countryFailed): void {
|
||||
$countryFailed = true;
|
||||
});
|
||||
|
||||
$subdivisionRule->validate('subdivision', 'GB-ENG', function () use (&$subdivisionFailed): void {
|
||||
$subdivisionFailed = true;
|
||||
});
|
||||
|
||||
$this->assertFalse($countryFailed);
|
||||
$this->assertFalse($subdivisionFailed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
latest: 1.5.0
|
||||
entries:
|
||||
- version: 1.5.0
|
||||
date: 2026-06-15
|
||||
changes:
|
||||
- Restored full enterprise Countries module after provider/route repair.
|
||||
- Preserved full ISO 3166-1 CRUD with all editable parameters exposed.
|
||||
- Preserved mandatory red asterisk markers for required ISO/WMS fields.
|
||||
- Restored boot-safe CountriesServiceProvider and RouteServiceProvider using real module paths instead of module_path lowercase lookups.
|
||||
- Added ISO sync command and platform sync action for bundled ISO data.
|
||||
- Added WMS country usage report support for companies, warehouses, users, addresses, billing, shipping, suppliers, customers and carriers where those tables exist.
|
||||
- Preserved ISO 3166-2 subdivision support and API endpoints.
|
||||
- Preserved WMS/Platform visual design using slate-blue hero, zinc cards, blue actions, emerald success states and red danger states.
|
||||
- version: 1.4.0
|
||||
date: 2026-06-15
|
||||
changes:
|
||||
- Added enterprise ISO fields and full CRUD support.
|
||||
- version: 1.3.0
|
||||
date: 2026-06-15
|
||||
changes:
|
||||
- Added editable ISO fields and professional CRUD views.
|
||||
- version: 1.2.0
|
||||
date: 2026-06-15
|
||||
changes:
|
||||
- Added ISO 3166-1 and ISO 3166-2 support.
|
||||
- version: 1.1.0
|
||||
date: 2026-06-15
|
||||
changes:
|
||||
- Added platform administration sidebar integration.
|
||||
- version: 1.0.0
|
||||
date: 2026-06-15
|
||||
changes:
|
||||
- Initial Countries reference-data module.
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "mpba/countries",
|
||||
"description": "ISO 3166 country reference module for the WMS platform.",
|
||||
"type": "laravel-module",
|
||||
"authors": [{"name": "Mark Gregory", "email": "mark.gregory@mpba.co.uk"}],
|
||||
"autoload": {"psr-4": {"Modules\\Countries\\": ""}},
|
||||
"autoload-dev": {"psr-4": {"Modules\\Countries\\Tests\\": "Tests/"}},
|
||||
"extra": {"laravel": {"providers": []}}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "Countries",
|
||||
"alias": "countries",
|
||||
"version": "1.5.0",
|
||||
"description": "Enterprise ISO 3166 country reference module with full CRUD, ISO 3166-2 subdivisions, sync, usage reporting and WMS platform administration integration.",
|
||||
"keywords": [
|
||||
"countries",
|
||||
"iso-3166",
|
||||
"iso-3166-1",
|
||||
"iso-3166-2",
|
||||
"reference-data",
|
||||
"localisation",
|
||||
"platform-administration"
|
||||
],
|
||||
"priority": 0,
|
||||
"providers": [
|
||||
"Modules\\Countries\\Providers\\CountriesServiceProvider"
|
||||
],
|
||||
"aliases": {},
|
||||
"files": [],
|
||||
"requires": []
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "npm run development",
|
||||
"development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
|
||||
"watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
|
||||
"watch-poll": "npm run watch -- --watch-poll",
|
||||
"hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
|
||||
"prod": "npm run production",
|
||||
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0",
|
||||
"laravel-mix": "^5.0.1",
|
||||
"laravel-mix-merge-manifest": "^0.1.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
const dotenvExpand = require('dotenv-expand');
|
||||
dotenvExpand(require('dotenv').config({ path: '../../.env'/*, debug: true*/}));
|
||||
|
||||
const mix = require('laravel-mix');
|
||||
|
||||
mix.js(__dirname + '/Resources/assets/js/app.js', 'js/countries.js')
|
||||
.sass( __dirname + '/Resources/assets/sass/app.scss', 'css/countries.css');
|
||||
|
||||
if (mix.inProduction()) {
|
||||
mix.version();
|
||||
}
|
||||
Reference in New Issue
Block a user