From d10ca668a995dc80efb9f7973a7f18a3e578401c Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Mon, 30 Jun 2025 22:08:34 +0200 Subject: [PATCH] Map country codes instead of guessing --- app/services/countries/iso_code_mapper.rb | 397 ++++++++++++++++++ app/services/imports/create.rb | 2 +- .../reverse_geocoding/points/fetch_data.rb | 20 +- app/services/users/import_data/points.rb | 4 +- spec/factories/points.rb | 10 +- .../countries/iso_code_mapper_spec.rb | 245 +++++++++++ spec/services/imports/create_spec.rb | 32 ++ .../points/fetch_data_spec.rb | 59 ++- 8 files changed, 756 insertions(+), 13 deletions(-) create mode 100644 app/services/countries/iso_code_mapper.rb create mode 100644 spec/services/countries/iso_code_mapper_spec.rb diff --git a/app/services/countries/iso_code_mapper.rb b/app/services/countries/iso_code_mapper.rb new file mode 100644 index 00000000..017133c0 --- /dev/null +++ b/app/services/countries/iso_code_mapper.rb @@ -0,0 +1,397 @@ +# frozen_string_literal: true + +class Countries::IsoCodeMapper + # Comprehensive country data with name, ISO codes, and flag emoji + # Based on ISO 3166-1 standard + COUNTRIES = { + 'AF' => { name: 'Afghanistan', iso2: 'AF', iso3: 'AFG', flag: '๐Ÿ‡ฆ๐Ÿ‡ซ' }, + 'AL' => { name: 'Albania', iso2: 'AL', iso3: 'ALB', flag: '๐Ÿ‡ฆ๐Ÿ‡ฑ' }, + 'DZ' => { name: 'Algeria', iso2: 'DZ', iso3: 'DZA', flag: '๐Ÿ‡ฉ๐Ÿ‡ฟ' }, + 'AS' => { name: 'American Samoa', iso2: 'AS', iso3: 'ASM', flag: '๐Ÿ‡ฆ๐Ÿ‡ธ' }, + 'AD' => { name: 'Andorra', iso2: 'AD', iso3: 'AND', flag: '๐Ÿ‡ฆ๐Ÿ‡ฉ' }, + 'AO' => { name: 'Angola', iso2: 'AO', iso3: 'AGO', flag: '๐Ÿ‡ฆ๐Ÿ‡ด' }, + 'AI' => { name: 'Anguilla', iso2: 'AI', iso3: 'AIA', flag: '๐Ÿ‡ฆ๐Ÿ‡ฎ' }, + 'AQ' => { name: 'Antarctica', iso2: 'AQ', iso3: 'ATA', flag: '๐Ÿ‡ฆ๐Ÿ‡ถ' }, + 'AG' => { name: 'Antigua and Barbuda', iso2: 'AG', iso3: 'ATG', flag: '๐Ÿ‡ฆ๐Ÿ‡ฌ' }, + 'AR' => { name: 'Argentina', iso2: 'AR', iso3: 'ARG', flag: '๐Ÿ‡ฆ๐Ÿ‡ท' }, + 'AM' => { name: 'Armenia', iso2: 'AM', iso3: 'ARM', flag: '๐Ÿ‡ฆ๐Ÿ‡ฒ' }, + 'AW' => { name: 'Aruba', iso2: 'AW', iso3: 'ABW', flag: '๐Ÿ‡ฆ๐Ÿ‡ผ' }, + 'AU' => { name: 'Australia', iso2: 'AU', iso3: 'AUS', flag: '๐Ÿ‡ฆ๐Ÿ‡บ' }, + 'AT' => { name: 'Austria', iso2: 'AT', iso3: 'AUT', flag: '๐Ÿ‡ฆ๐Ÿ‡น' }, + 'AZ' => { name: 'Azerbaijan', iso2: 'AZ', iso3: 'AZE', flag: '๐Ÿ‡ฆ๐Ÿ‡ฟ' }, + 'BS' => { name: 'Bahamas', iso2: 'BS', iso3: 'BHS', flag: '๐Ÿ‡ง๐Ÿ‡ธ' }, + 'BH' => { name: 'Bahrain', iso2: 'BH', iso3: 'BHR', flag: '๐Ÿ‡ง๐Ÿ‡ญ' }, + 'BD' => { name: 'Bangladesh', iso2: 'BD', iso3: 'BGD', flag: '๐Ÿ‡ง๐Ÿ‡ฉ' }, + 'BB' => { name: 'Barbados', iso2: 'BB', iso3: 'BRB', flag: '๐Ÿ‡ง๐Ÿ‡ง' }, + 'BY' => { name: 'Belarus', iso2: 'BY', iso3: 'BLR', flag: '๐Ÿ‡ง๐Ÿ‡พ' }, + 'BE' => { name: 'Belgium', iso2: 'BE', iso3: 'BEL', flag: '๐Ÿ‡ง๐Ÿ‡ช' }, + 'BZ' => { name: 'Belize', iso2: 'BZ', iso3: 'BLZ', flag: '๐Ÿ‡ง๐Ÿ‡ฟ' }, + 'BJ' => { name: 'Benin', iso2: 'BJ', iso3: 'BEN', flag: '๐Ÿ‡ง๐Ÿ‡ฏ' }, + 'BM' => { name: 'Bermuda', iso2: 'BM', iso3: 'BMU', flag: '๐Ÿ‡ง๐Ÿ‡ฒ' }, + 'BT' => { name: 'Bhutan', iso2: 'BT', iso3: 'BTN', flag: '๐Ÿ‡ง๐Ÿ‡น' }, + 'BO' => { name: 'Bolivia', iso2: 'BO', iso3: 'BOL', flag: '๐Ÿ‡ง๐Ÿ‡ด' }, + 'BA' => { name: 'Bosnia and Herzegovina', iso2: 'BA', iso3: 'BIH', flag: '๐Ÿ‡ง๐Ÿ‡ฆ' }, + 'BW' => { name: 'Botswana', iso2: 'BW', iso3: 'BWA', flag: '๐Ÿ‡ง๐Ÿ‡ผ' }, + 'BR' => { name: 'Brazil', iso2: 'BR', iso3: 'BRA', flag: '๐Ÿ‡ง๐Ÿ‡ท' }, + 'BN' => { name: 'Brunei Darussalam', iso2: 'BN', iso3: 'BRN', flag: '๐Ÿ‡ง๐Ÿ‡ณ' }, + 'BG' => { name: 'Bulgaria', iso2: 'BG', iso3: 'BGR', flag: '๐Ÿ‡ง๐Ÿ‡ฌ' }, + 'BF' => { name: 'Burkina Faso', iso2: 'BF', iso3: 'BFA', flag: '๐Ÿ‡ง๐Ÿ‡ซ' }, + 'BI' => { name: 'Burundi', iso2: 'BI', iso3: 'BDI', flag: '๐Ÿ‡ง๐Ÿ‡ฎ' }, + 'KH' => { name: 'Cambodia', iso2: 'KH', iso3: 'KHM', flag: '๐Ÿ‡ฐ๐Ÿ‡ญ' }, + 'CM' => { name: 'Cameroon', iso2: 'CM', iso3: 'CMR', flag: '๐Ÿ‡จ๐Ÿ‡ฒ' }, + 'CA' => { name: 'Canada', iso2: 'CA', iso3: 'CAN', flag: '๐Ÿ‡จ๐Ÿ‡ฆ' }, + 'CV' => { name: 'Cape Verde', iso2: 'CV', iso3: 'CPV', flag: '๐Ÿ‡จ๐Ÿ‡ป' }, + 'KY' => { name: 'Cayman Islands', iso2: 'KY', iso3: 'CYM', flag: '๐Ÿ‡ฐ๐Ÿ‡พ' }, + 'CF' => { name: 'Central African Republic', iso2: 'CF', iso3: 'CAF', flag: '๐Ÿ‡จ๐Ÿ‡ซ' }, + 'TD' => { name: 'Chad', iso2: 'TD', iso3: 'TCD', flag: '๐Ÿ‡น๐Ÿ‡ฉ' }, + 'CL' => { name: 'Chile', iso2: 'CL', iso3: 'CHL', flag: '๐Ÿ‡จ๐Ÿ‡ฑ' }, + 'CN' => { name: 'China', iso2: 'CN', iso3: 'CHN', flag: '๐Ÿ‡จ๐Ÿ‡ณ' }, + 'CO' => { name: 'Colombia', iso2: 'CO', iso3: 'COL', flag: '๐Ÿ‡จ๐Ÿ‡ด' }, + 'KM' => { name: 'Comoros', iso2: 'KM', iso3: 'COM', flag: '๐Ÿ‡ฐ๐Ÿ‡ฒ' }, + 'CG' => { name: 'Congo', iso2: 'CG', iso3: 'COG', flag: '๐Ÿ‡จ๐Ÿ‡ฌ' }, + 'CD' => { name: 'Congo, Democratic Republic of the', iso2: 'CD', iso3: 'COD', flag: '๐Ÿ‡จ๐Ÿ‡ฉ' }, + 'CK' => { name: 'Cook Islands', iso2: 'CK', iso3: 'COK', flag: '๐Ÿ‡จ๐Ÿ‡ฐ' }, + 'CR' => { name: 'Costa Rica', iso2: 'CR', iso3: 'CRI', flag: '๐Ÿ‡จ๐Ÿ‡ท' }, + 'CI' => { name: 'Cรดte d\'Ivoire', iso2: 'CI', iso3: 'CIV', flag: '๐Ÿ‡จ๐Ÿ‡ฎ' }, + 'HR' => { name: 'Croatia', iso2: 'HR', iso3: 'HRV', flag: '๐Ÿ‡ญ๐Ÿ‡ท' }, + 'CU' => { name: 'Cuba', iso2: 'CU', iso3: 'CUB', flag: '๐Ÿ‡จ๐Ÿ‡บ' }, + 'CY' => { name: 'Cyprus', iso2: 'CY', iso3: 'CYP', flag: '๐Ÿ‡จ๐Ÿ‡พ' }, + 'CZ' => { name: 'Czech Republic', iso2: 'CZ', iso3: 'CZE', flag: '๐Ÿ‡จ๐Ÿ‡ฟ' }, + 'DK' => { name: 'Denmark', iso2: 'DK', iso3: 'DNK', flag: '๐Ÿ‡ฉ๐Ÿ‡ฐ' }, + 'DJ' => { name: 'Djibouti', iso2: 'DJ', iso3: 'DJI', flag: '๐Ÿ‡ฉ๐Ÿ‡ฏ' }, + 'DM' => { name: 'Dominica', iso2: 'DM', iso3: 'DMA', flag: '๐Ÿ‡ฉ๐Ÿ‡ฒ' }, + 'DO' => { name: 'Dominican Republic', iso2: 'DO', iso3: 'DOM', flag: '๐Ÿ‡ฉ๐Ÿ‡ด' }, + 'EC' => { name: 'Ecuador', iso2: 'EC', iso3: 'ECU', flag: '๐Ÿ‡ช๐Ÿ‡จ' }, + 'EG' => { name: 'Egypt', iso2: 'EG', iso3: 'EGY', flag: '๐Ÿ‡ช๐Ÿ‡ฌ' }, + 'SV' => { name: 'El Salvador', iso2: 'SV', iso3: 'SLV', flag: '๐Ÿ‡ธ๐Ÿ‡ป' }, + 'GQ' => { name: 'Equatorial Guinea', iso2: 'GQ', iso3: 'GNQ', flag: '๐Ÿ‡ฌ๐Ÿ‡ถ' }, + 'ER' => { name: 'Eritrea', iso2: 'ER', iso3: 'ERI', flag: '๐Ÿ‡ช๐Ÿ‡ท' }, + 'EE' => { name: 'Estonia', iso2: 'EE', iso3: 'EST', flag: '๐Ÿ‡ช๐Ÿ‡ช' }, + 'ET' => { name: 'Ethiopia', iso2: 'ET', iso3: 'ETH', flag: '๐Ÿ‡ช๐Ÿ‡น' }, + 'FK' => { name: 'Falkland Islands (Malvinas)', iso2: 'FK', iso3: 'FLK', flag: '๐Ÿ‡ซ๐Ÿ‡ฐ' }, + 'FO' => { name: 'Faroe Islands', iso2: 'FO', iso3: 'FRO', flag: '๐Ÿ‡ซ๐Ÿ‡ด' }, + 'FJ' => { name: 'Fiji', iso2: 'FJ', iso3: 'FJI', flag: '๐Ÿ‡ซ๐Ÿ‡ฏ' }, + 'FI' => { name: 'Finland', iso2: 'FI', iso3: 'FIN', flag: '๐Ÿ‡ซ๐Ÿ‡ฎ' }, + 'FR' => { name: 'France', iso2: 'FR', iso3: 'FRA', flag: '๐Ÿ‡ซ๐Ÿ‡ท' }, + 'GF' => { name: 'French Guiana', iso2: 'GF', iso3: 'GUF', flag: '๐Ÿ‡ฌ๐Ÿ‡ซ' }, + 'PF' => { name: 'French Polynesia', iso2: 'PF', iso3: 'PYF', flag: '๐Ÿ‡ต๐Ÿ‡ซ' }, + 'GA' => { name: 'Gabon', iso2: 'GA', iso3: 'GAB', flag: '๐Ÿ‡ฌ๐Ÿ‡ฆ' }, + 'GM' => { name: 'Gambia', iso2: 'GM', iso3: 'GMB', flag: '๐Ÿ‡ฌ๐Ÿ‡ฒ' }, + 'GE' => { name: 'Georgia', iso2: 'GE', iso3: 'GEO', flag: '๐Ÿ‡ฌ๐Ÿ‡ช' }, + 'DE' => { name: 'Germany', iso2: 'DE', iso3: 'DEU', flag: '๐Ÿ‡ฉ๐Ÿ‡ช' }, + 'GH' => { name: 'Ghana', iso2: 'GH', iso3: 'GHA', flag: '๐Ÿ‡ฌ๐Ÿ‡ญ' }, + 'GI' => { name: 'Gibraltar', iso2: 'GI', iso3: 'GIB', flag: '๐Ÿ‡ฌ๐Ÿ‡ฎ' }, + 'GR' => { name: 'Greece', iso2: 'GR', iso3: 'GRC', flag: '๐Ÿ‡ฌ๐Ÿ‡ท' }, + 'GL' => { name: 'Greenland', iso2: 'GL', iso3: 'GRL', flag: '๐Ÿ‡ฌ๐Ÿ‡ฑ' }, + 'GD' => { name: 'Grenada', iso2: 'GD', iso3: 'GRD', flag: '๐Ÿ‡ฌ๐Ÿ‡ฉ' }, + 'GP' => { name: 'Guadeloupe', iso2: 'GP', iso3: 'GLP', flag: '๐Ÿ‡ฌ๐Ÿ‡ต' }, + 'GU' => { name: 'Guam', iso2: 'GU', iso3: 'GUM', flag: '๐Ÿ‡ฌ๐Ÿ‡บ' }, + 'GT' => { name: 'Guatemala', iso2: 'GT', iso3: 'GTM', flag: '๐Ÿ‡ฌ๐Ÿ‡น' }, + 'GG' => { name: 'Guernsey', iso2: 'GG', iso3: 'GGY', flag: '๐Ÿ‡ฌ๐Ÿ‡ฌ' }, + 'GN' => { name: 'Guinea', iso2: 'GN', iso3: 'GIN', flag: '๐Ÿ‡ฌ๐Ÿ‡ณ' }, + 'GW' => { name: 'Guinea-Bissau', iso2: 'GW', iso3: 'GNB', flag: '๐Ÿ‡ฌ๐Ÿ‡ผ' }, + 'GY' => { name: 'Guyana', iso2: 'GY', iso3: 'GUY', flag: '๐Ÿ‡ฌ๐Ÿ‡พ' }, + 'HT' => { name: 'Haiti', iso2: 'HT', iso3: 'HTI', flag: '๐Ÿ‡ญ๐Ÿ‡น' }, + 'VA' => { name: 'Holy See (Vatican City State)', iso2: 'VA', iso3: 'VAT', flag: '๐Ÿ‡ป๐Ÿ‡ฆ' }, + 'HN' => { name: 'Honduras', iso2: 'HN', iso3: 'HND', flag: '๐Ÿ‡ญ๐Ÿ‡ณ' }, + 'HK' => { name: 'Hong Kong', iso2: 'HK', iso3: 'HKG', flag: '๐Ÿ‡ญ๐Ÿ‡ฐ' }, + 'HU' => { name: 'Hungary', iso2: 'HU', iso3: 'HUN', flag: '๐Ÿ‡ญ๐Ÿ‡บ' }, + 'IS' => { name: 'Iceland', iso2: 'IS', iso3: 'ISL', flag: '๐Ÿ‡ฎ๐Ÿ‡ธ' }, + 'IN' => { name: 'India', iso2: 'IN', iso3: 'IND', flag: '๐Ÿ‡ฎ๐Ÿ‡ณ' }, + 'ID' => { name: 'Indonesia', iso2: 'ID', iso3: 'IDN', flag: '๐Ÿ‡ฎ๐Ÿ‡ฉ' }, + 'IR' => { name: 'Iran, Islamic Republic of', iso2: 'IR', iso3: 'IRN', flag: '๐Ÿ‡ฎ๐Ÿ‡ท' }, + 'IQ' => { name: 'Iraq', iso2: 'IQ', iso3: 'IRQ', flag: '๐Ÿ‡ฎ๐Ÿ‡ถ' }, + 'IE' => { name: 'Ireland', iso2: 'IE', iso3: 'IRL', flag: '๐Ÿ‡ฎ๐Ÿ‡ช' }, + 'IM' => { name: 'Isle of Man', iso2: 'IM', iso3: 'IMN', flag: '๐Ÿ‡ฎ๐Ÿ‡ฒ' }, + 'IL' => { name: 'Israel', iso2: 'IL', iso3: 'ISR', flag: '๐Ÿ‡ฎ๐Ÿ‡ฑ' }, + 'IT' => { name: 'Italy', iso2: 'IT', iso3: 'ITA', flag: '๐Ÿ‡ฎ๐Ÿ‡น' }, + 'JM' => { name: 'Jamaica', iso2: 'JM', iso3: 'JAM', flag: '๐Ÿ‡ฏ๐Ÿ‡ฒ' }, + 'JP' => { name: 'Japan', iso2: 'JP', iso3: 'JPN', flag: '๐Ÿ‡ฏ๐Ÿ‡ต' }, + 'JE' => { name: 'Jersey', iso2: 'JE', iso3: 'JEY', flag: '๐Ÿ‡ฏ๐Ÿ‡ช' }, + 'JO' => { name: 'Jordan', iso2: 'JO', iso3: 'JOR', flag: '๐Ÿ‡ฏ๐Ÿ‡ด' }, + 'KZ' => { name: 'Kazakhstan', iso2: 'KZ', iso3: 'KAZ', flag: '๐Ÿ‡ฐ๐Ÿ‡ฟ' }, + 'KE' => { name: 'Kenya', iso2: 'KE', iso3: 'KEN', flag: '๐Ÿ‡ฐ๐Ÿ‡ช' }, + 'KI' => { name: 'Kiribati', iso2: 'KI', iso3: 'KIR', flag: '๐Ÿ‡ฐ๐Ÿ‡ฎ' }, + 'KP' => { name: 'Korea, Democratic People\'s Republic of', iso2: 'KP', iso3: 'PRK', flag: '๐Ÿ‡ฐ๐Ÿ‡ต' }, + 'KR' => { name: 'Korea, Republic of', iso2: 'KR', iso3: 'KOR', flag: '๐Ÿ‡ฐ๐Ÿ‡ท' }, + 'KW' => { name: 'Kuwait', iso2: 'KW', iso3: 'KWT', flag: '๐Ÿ‡ฐ๐Ÿ‡ผ' }, + 'KG' => { name: 'Kyrgyzstan', iso2: 'KG', iso3: 'KGZ', flag: '๐Ÿ‡ฐ๐Ÿ‡ฌ' }, + 'LA' => { name: 'Lao People\'s Democratic Republic', iso2: 'LA', iso3: 'LAO', flag: '๐Ÿ‡ฑ๐Ÿ‡ฆ' }, + 'LV' => { name: 'Latvia', iso2: 'LV', iso3: 'LVA', flag: '๐Ÿ‡ฑ๐Ÿ‡ป' }, + 'LB' => { name: 'Lebanon', iso2: 'LB', iso3: 'LBN', flag: '๐Ÿ‡ฑ๐Ÿ‡ง' }, + 'LS' => { name: 'Lesotho', iso2: 'LS', iso3: 'LSO', flag: '๐Ÿ‡ฑ๐Ÿ‡ธ' }, + 'LR' => { name: 'Liberia', iso2: 'LR', iso3: 'LBR', flag: '๐Ÿ‡ฑ๐Ÿ‡ท' }, + 'LY' => { name: 'Libya', iso2: 'LY', iso3: 'LBY', flag: '๐Ÿ‡ฑ๐Ÿ‡พ' }, + 'LI' => { name: 'Liechtenstein', iso2: 'LI', iso3: 'LIE', flag: '๐Ÿ‡ฑ๐Ÿ‡ฎ' }, + 'LT' => { name: 'Lithuania', iso2: 'LT', iso3: 'LTU', flag: '๐Ÿ‡ฑ๐Ÿ‡น' }, + 'LU' => { name: 'Luxembourg', iso2: 'LU', iso3: 'LUX', flag: '๐Ÿ‡ฑ๐Ÿ‡บ' }, + 'MO' => { name: 'Macao', iso2: 'MO', iso3: 'MAC', flag: '๐Ÿ‡ฒ๐Ÿ‡ด' }, + 'MK' => { name: 'North Macedonia', iso2: 'MK', iso3: 'MKD', flag: '๐Ÿ‡ฒ๐Ÿ‡ฐ' }, + 'MG' => { name: 'Madagascar', iso2: 'MG', iso3: 'MDG', flag: '๐Ÿ‡ฒ๐Ÿ‡ฌ' }, + 'MW' => { name: 'Malawi', iso2: 'MW', iso3: 'MWI', flag: '๐Ÿ‡ฒ๐Ÿ‡ผ' }, + 'MY' => { name: 'Malaysia', iso2: 'MY', iso3: 'MYS', flag: '๐Ÿ‡ฒ๐Ÿ‡พ' }, + 'MV' => { name: 'Maldives', iso2: 'MV', iso3: 'MDV', flag: '๐Ÿ‡ฒ๐Ÿ‡ป' }, + 'ML' => { name: 'Mali', iso2: 'ML', iso3: 'MLI', flag: '๐Ÿ‡ฒ๐Ÿ‡ฑ' }, + 'MT' => { name: 'Malta', iso2: 'MT', iso3: 'MLT', flag: '๐Ÿ‡ฒ๐Ÿ‡น' }, + 'MH' => { name: 'Marshall Islands', iso2: 'MH', iso3: 'MHL', flag: '๐Ÿ‡ฒ๐Ÿ‡ญ' }, + 'MQ' => { name: 'Martinique', iso2: 'MQ', iso3: 'MTQ', flag: '๐Ÿ‡ฒ๐Ÿ‡ถ' }, + 'MR' => { name: 'Mauritania', iso2: 'MR', iso3: 'MRT', flag: '๐Ÿ‡ฒ๐Ÿ‡ท' }, + 'MU' => { name: 'Mauritius', iso2: 'MU', iso3: 'MUS', flag: '๐Ÿ‡ฒ๐Ÿ‡บ' }, + 'YT' => { name: 'Mayotte', iso2: 'YT', iso3: 'MYT', flag: '๐Ÿ‡พ๐Ÿ‡น' }, + 'MX' => { name: 'Mexico', iso2: 'MX', iso3: 'MEX', flag: '๐Ÿ‡ฒ๐Ÿ‡ฝ' }, + 'FM' => { name: 'Micronesia, Federated States of', iso2: 'FM', iso3: 'FSM', flag: '๐Ÿ‡ซ๐Ÿ‡ฒ' }, + 'MD' => { name: 'Moldova, Republic of', iso2: 'MD', iso3: 'MDA', flag: '๐Ÿ‡ฒ๐Ÿ‡ฉ' }, + 'MC' => { name: 'Monaco', iso2: 'MC', iso3: 'MCO', flag: '๐Ÿ‡ฒ๐Ÿ‡จ' }, + 'MN' => { name: 'Mongolia', iso2: 'MN', iso3: 'MNG', flag: '๐Ÿ‡ฒ๐Ÿ‡ณ' }, + 'ME' => { name: 'Montenegro', iso2: 'ME', iso3: 'MNE', flag: '๐Ÿ‡ฒ๐Ÿ‡ช' }, + 'MS' => { name: 'Montserrat', iso2: 'MS', iso3: 'MSR', flag: '๐Ÿ‡ฒ๐Ÿ‡ธ' }, + 'MA' => { name: 'Morocco', iso2: 'MA', iso3: 'MAR', flag: '๐Ÿ‡ฒ๐Ÿ‡ฆ' }, + 'MZ' => { name: 'Mozambique', iso2: 'MZ', iso3: 'MOZ', flag: '๐Ÿ‡ฒ๐Ÿ‡ฟ' }, + 'MM' => { name: 'Myanmar', iso2: 'MM', iso3: 'MMR', flag: '๐Ÿ‡ฒ๐Ÿ‡ฒ' }, + 'NA' => { name: 'Namibia', iso2: 'NA', iso3: 'NAM', flag: '๐Ÿ‡ณ๐Ÿ‡ฆ' }, + 'NR' => { name: 'Nauru', iso2: 'NR', iso3: 'NRU', flag: '๐Ÿ‡ณ๐Ÿ‡ท' }, + 'NP' => { name: 'Nepal', iso2: 'NP', iso3: 'NPL', flag: '๐Ÿ‡ณ๐Ÿ‡ต' }, + 'NL' => { name: 'Netherlands', iso2: 'NL', iso3: 'NLD', flag: '๐Ÿ‡ณ๐Ÿ‡ฑ' }, + 'NC' => { name: 'New Caledonia', iso2: 'NC', iso3: 'NCL', flag: '๐Ÿ‡ณ๐Ÿ‡จ' }, + 'NZ' => { name: 'New Zealand', iso2: 'NZ', iso3: 'NZL', flag: '๐Ÿ‡ณ๐Ÿ‡ฟ' }, + 'NI' => { name: 'Nicaragua', iso2: 'NI', iso3: 'NIC', flag: '๐Ÿ‡ณ๐Ÿ‡ฎ' }, + 'NE' => { name: 'Niger', iso2: 'NE', iso3: 'NER', flag: '๐Ÿ‡ณ๐Ÿ‡ช' }, + 'NG' => { name: 'Nigeria', iso2: 'NG', iso3: 'NGA', flag: '๐Ÿ‡ณ๐Ÿ‡ฌ' }, + 'NU' => { name: 'Niue', iso2: 'NU', iso3: 'NIU', flag: '๐Ÿ‡ณ๐Ÿ‡บ' }, + 'NF' => { name: 'Norfolk Island', iso2: 'NF', iso3: 'NFK', flag: '๐Ÿ‡ณ๐Ÿ‡ซ' }, + 'MP' => { name: 'Northern Mariana Islands', iso2: 'MP', iso3: 'MNP', flag: '๐Ÿ‡ฒ๐Ÿ‡ต' }, + 'NO' => { name: 'Norway', iso2: 'NO', iso3: 'NOR', flag: '๐Ÿ‡ณ๐Ÿ‡ด' }, + 'OM' => { name: 'Oman', iso2: 'OM', iso3: 'OMN', flag: '๐Ÿ‡ด๐Ÿ‡ฒ' }, + 'PK' => { name: 'Pakistan', iso2: 'PK', iso3: 'PAK', flag: '๐Ÿ‡ต๐Ÿ‡ฐ' }, + 'PW' => { name: 'Palau', iso2: 'PW', iso3: 'PLW', flag: '๐Ÿ‡ต๐Ÿ‡ผ' }, + 'PS' => { name: 'Palestine, State of', iso2: 'PS', iso3: 'PSE', flag: '๐Ÿ‡ต๐Ÿ‡ธ' }, + 'PA' => { name: 'Panama', iso2: 'PA', iso3: 'PAN', flag: '๐Ÿ‡ต๐Ÿ‡ฆ' }, + 'PG' => { name: 'Papua New Guinea', iso2: 'PG', iso3: 'PNG', flag: '๐Ÿ‡ต๐Ÿ‡ฌ' }, + 'PY' => { name: 'Paraguay', iso2: 'PY', iso3: 'PRY', flag: '๐Ÿ‡ต๐Ÿ‡พ' }, + 'PE' => { name: 'Peru', iso2: 'PE', iso3: 'PER', flag: '๐Ÿ‡ต๐Ÿ‡ช' }, + 'PH' => { name: 'Philippines', iso2: 'PH', iso3: 'PHL', flag: '๐Ÿ‡ต๐Ÿ‡ญ' }, + 'PN' => { name: 'Pitcairn', iso2: 'PN', iso3: 'PCN', flag: '๐Ÿ‡ต๐Ÿ‡ณ' }, + 'PL' => { name: 'Poland', iso2: 'PL', iso3: 'POL', flag: '๐Ÿ‡ต๐Ÿ‡ฑ' }, + 'PT' => { name: 'Portugal', iso2: 'PT', iso3: 'PRT', flag: '๐Ÿ‡ต๐Ÿ‡น' }, + 'PR' => { name: 'Puerto Rico', iso2: 'PR', iso3: 'PRI', flag: '๐Ÿ‡ต๐Ÿ‡ท' }, + 'QA' => { name: 'Qatar', iso2: 'QA', iso3: 'QAT', flag: '๐Ÿ‡ถ๐Ÿ‡ฆ' }, + 'RE' => { name: 'Rรฉunion', iso2: 'RE', iso3: 'REU', flag: '๐Ÿ‡ท๐Ÿ‡ช' }, + 'RO' => { name: 'Romania', iso2: 'RO', iso3: 'ROU', flag: '๐Ÿ‡ท๐Ÿ‡ด' }, + 'RU' => { name: 'Russian Federation', iso2: 'RU', iso3: 'RUS', flag: '๐Ÿ‡ท๐Ÿ‡บ' }, + 'RW' => { name: 'Rwanda', iso2: 'RW', iso3: 'RWA', flag: '๐Ÿ‡ท๐Ÿ‡ผ' }, + 'BL' => { name: 'Saint Barthรฉlemy', iso2: 'BL', iso3: 'BLM', flag: '๐Ÿ‡ง๐Ÿ‡ฑ' }, + 'SH' => { name: 'Saint Helena, Ascension and Tristan da Cunha', iso2: 'SH', iso3: 'SHN', flag: '๐Ÿ‡ธ๐Ÿ‡ญ' }, + 'KN' => { name: 'Saint Kitts and Nevis', iso2: 'KN', iso3: 'KNA', flag: '๐Ÿ‡ฐ๐Ÿ‡ณ' }, + 'LC' => { name: 'Saint Lucia', iso2: 'LC', iso3: 'LCA', flag: '๐Ÿ‡ฑ๐Ÿ‡จ' }, + 'MF' => { name: 'Saint Martin (French part)', iso2: 'MF', iso3: 'MAF', flag: '๐Ÿ‡ฒ๐Ÿ‡ซ' }, + 'PM' => { name: 'Saint Pierre and Miquelon', iso2: 'PM', iso3: 'SPM', flag: '๐Ÿ‡ต๐Ÿ‡ฒ' }, + 'VC' => { name: 'Saint Vincent and the Grenadines', iso2: 'VC', iso3: 'VCT', flag: '๐Ÿ‡ป๐Ÿ‡จ' }, + 'WS' => { name: 'Samoa', iso2: 'WS', iso3: 'WSM', flag: '๐Ÿ‡ผ๐Ÿ‡ธ' }, + 'SM' => { name: 'San Marino', iso2: 'SM', iso3: 'SMR', flag: '๐Ÿ‡ธ๐Ÿ‡ฒ' }, + 'ST' => { name: 'Sao Tome and Principe', iso2: 'ST', iso3: 'STP', flag: '๐Ÿ‡ธ๐Ÿ‡น' }, + 'SA' => { name: 'Saudi Arabia', iso2: 'SA', iso3: 'SAU', flag: '๐Ÿ‡ธ๐Ÿ‡ฆ' }, + 'SN' => { name: 'Senegal', iso2: 'SN', iso3: 'SEN', flag: '๐Ÿ‡ธ๐Ÿ‡ณ' }, + 'RS' => { name: 'Serbia', iso2: 'RS', iso3: 'SRB', flag: '๐Ÿ‡ท๐Ÿ‡ธ' }, + 'SC' => { name: 'Seychelles', iso2: 'SC', iso3: 'SYC', flag: '๐Ÿ‡ธ๐Ÿ‡จ' }, + 'SL' => { name: 'Sierra Leone', iso2: 'SL', iso3: 'SLE', flag: '๐Ÿ‡ธ๐Ÿ‡ฑ' }, + 'SG' => { name: 'Singapore', iso2: 'SG', iso3: 'SGP', flag: '๐Ÿ‡ธ๐Ÿ‡ฌ' }, + 'SX' => { name: 'Sint Maarten (Dutch part)', iso2: 'SX', iso3: 'SXM', flag: '๐Ÿ‡ธ๐Ÿ‡ฝ' }, + 'SK' => { name: 'Slovakia', iso2: 'SK', iso3: 'SVK', flag: '๐Ÿ‡ธ๐Ÿ‡ฐ' }, + 'SI' => { name: 'Slovenia', iso2: 'SI', iso3: 'SVN', flag: '๐Ÿ‡ธ๐Ÿ‡ฎ' }, + 'SB' => { name: 'Solomon Islands', iso2: 'SB', iso3: 'SLB', flag: '๐Ÿ‡ธ๐Ÿ‡ง' }, + 'SO' => { name: 'Somalia', iso2: 'SO', iso3: 'SOM', flag: '๐Ÿ‡ธ๐Ÿ‡ด' }, + 'ZA' => { name: 'South Africa', iso2: 'ZA', iso3: 'ZAF', flag: '๐Ÿ‡ฟ๐Ÿ‡ฆ' }, + 'GS' => { name: 'South Georgia and the South Sandwich Islands', iso2: 'GS', iso3: 'SGS', flag: '๐Ÿ‡ฌ๐Ÿ‡ธ' }, + 'SS' => { name: 'South Sudan', iso2: 'SS', iso3: 'SSD', flag: '๐Ÿ‡ธ๐Ÿ‡ธ' }, + 'ES' => { name: 'Spain', iso2: 'ES', iso3: 'ESP', flag: '๐Ÿ‡ช๐Ÿ‡ธ' }, + 'LK' => { name: 'Sri Lanka', iso2: 'LK', iso3: 'LKA', flag: '๐Ÿ‡ฑ๐Ÿ‡ฐ' }, + 'SD' => { name: 'Sudan', iso2: 'SD', iso3: 'SDN', flag: '๐Ÿ‡ธ๐Ÿ‡ฉ' }, + 'SR' => { name: 'Suriname', iso2: 'SR', iso3: 'SUR', flag: '๐Ÿ‡ธ๐Ÿ‡ท' }, + 'SJ' => { name: 'Svalbard and Jan Mayen', iso2: 'SJ', iso3: 'SJM', flag: '๐Ÿ‡ธ๐Ÿ‡ฏ' }, + 'SE' => { name: 'Sweden', iso2: 'SE', iso3: 'SWE', flag: '๐Ÿ‡ธ๐Ÿ‡ช' }, + 'CH' => { name: 'Switzerland', iso2: 'CH', iso3: 'CHE', flag: '๐Ÿ‡จ๐Ÿ‡ญ' }, + 'SY' => { name: 'Syrian Arab Republic', iso2: 'SY', iso3: 'SYR', flag: '๐Ÿ‡ธ๐Ÿ‡พ' }, + 'TW' => { name: 'Taiwan, Province of China', iso2: 'TW', iso3: 'TWN', flag: '๐Ÿ‡น๐Ÿ‡ผ' }, + 'TJ' => { name: 'Tajikistan', iso2: 'TJ', iso3: 'TJK', flag: '๐Ÿ‡น๐Ÿ‡ฏ' }, + 'TZ' => { name: 'Tanzania, United Republic of', iso2: 'TZ', iso3: 'TZA', flag: '๐Ÿ‡น๐Ÿ‡ฟ' }, + 'TH' => { name: 'Thailand', iso2: 'TH', iso3: 'THA', flag: '๐Ÿ‡น๐Ÿ‡ญ' }, + 'TL' => { name: 'Timor-Leste', iso2: 'TL', iso3: 'TLS', flag: '๐Ÿ‡น๐Ÿ‡ฑ' }, + 'TG' => { name: 'Togo', iso2: 'TG', iso3: 'TGO', flag: '๐Ÿ‡น๐Ÿ‡ฌ' }, + 'TK' => { name: 'Tokelau', iso2: 'TK', iso3: 'TKL', flag: '๐Ÿ‡น๐Ÿ‡ฐ' }, + 'TO' => { name: 'Tonga', iso2: 'TO', iso3: 'TON', flag: '๐Ÿ‡น๐Ÿ‡ด' }, + 'TT' => { name: 'Trinidad and Tobago', iso2: 'TT', iso3: 'TTO', flag: '๐Ÿ‡น๐Ÿ‡น' }, + 'TN' => { name: 'Tunisia', iso2: 'TN', iso3: 'TUN', flag: '๐Ÿ‡น๐Ÿ‡ณ' }, + 'TR' => { name: 'Turkey', iso2: 'TR', iso3: 'TUR', flag: '๐Ÿ‡น๐Ÿ‡ท' }, + 'TM' => { name: 'Turkmenistan', iso2: 'TM', iso3: 'TKM', flag: '๐Ÿ‡น๐Ÿ‡ฒ' }, + 'TC' => { name: 'Turks and Caicos Islands', iso2: 'TC', iso3: 'TCA', flag: '๐Ÿ‡น๐Ÿ‡จ' }, + 'TV' => { name: 'Tuvalu', iso2: 'TV', iso3: 'TUV', flag: '๐Ÿ‡น๐Ÿ‡ป' }, + 'UG' => { name: 'Uganda', iso2: 'UG', iso3: 'UGA', flag: '๐Ÿ‡บ๐Ÿ‡ฌ' }, + 'UA' => { name: 'Ukraine', iso2: 'UA', iso3: 'UKR', flag: '๐Ÿ‡บ๐Ÿ‡ฆ' }, + 'AE' => { name: 'United Arab Emirates', iso2: 'AE', iso3: 'ARE', flag: '๐Ÿ‡ฆ๐Ÿ‡ช' }, + 'GB' => { name: 'United Kingdom', iso2: 'GB', iso3: 'GBR', flag: '๐Ÿ‡ฌ๐Ÿ‡ง' }, + 'US' => { name: 'United States', iso2: 'US', iso3: 'USA', flag: '๐Ÿ‡บ๐Ÿ‡ธ' }, + 'UM' => { name: 'United States Minor Outlying Islands', iso2: 'UM', iso3: 'UMI', flag: '๐Ÿ‡บ๐Ÿ‡ฒ' }, + 'UY' => { name: 'Uruguay', iso2: 'UY', iso3: 'URY', flag: '๐Ÿ‡บ๐Ÿ‡พ' }, + 'UZ' => { name: 'Uzbekistan', iso2: 'UZ', iso3: 'UZB', flag: '๐Ÿ‡บ๐Ÿ‡ฟ' }, + 'VU' => { name: 'Vanuatu', iso2: 'VU', iso3: 'VUT', flag: '๐Ÿ‡ป๐Ÿ‡บ' }, + 'VE' => { name: 'Venezuela, Bolivarian Republic of', iso2: 'VE', iso3: 'VEN', flag: '๐Ÿ‡ป๐Ÿ‡ช' }, + 'VN' => { name: 'Viet Nam', iso2: 'VN', iso3: 'VNM', flag: '๐Ÿ‡ป๐Ÿ‡ณ' }, + 'VG' => { name: 'Virgin Islands, British', iso2: 'VG', iso3: 'VGB', flag: '๐Ÿ‡ป๐Ÿ‡ฌ' }, + 'VI' => { name: 'Virgin Islands, U.S.', iso2: 'VI', iso3: 'VIR', flag: '๐Ÿ‡ป๐Ÿ‡ฎ' }, + 'WF' => { name: 'Wallis and Futuna', iso2: 'WF', iso3: 'WLF', flag: '๐Ÿ‡ผ๐Ÿ‡ซ' }, + 'EH' => { name: 'Western Sahara', iso2: 'EH', iso3: 'ESH', flag: '๐Ÿ‡ช๐Ÿ‡ญ' }, + 'YE' => { name: 'Yemen', iso2: 'YE', iso3: 'YEM', flag: '๐Ÿ‡พ๐Ÿ‡ช' }, + 'ZM' => { name: 'Zambia', iso2: 'ZM', iso3: 'ZMB', flag: '๐Ÿ‡ฟ๐Ÿ‡ฒ' }, + 'ZW' => { name: 'Zimbabwe', iso2: 'ZW', iso3: 'ZWE', flag: '๐Ÿ‡ฟ๐Ÿ‡ผ' } + }.freeze + + # Country name aliases and variations for better matching + COUNTRY_ALIASES = { + 'Russia' => 'Russian Federation', + 'South Korea' => 'Korea, Republic of', + 'North Korea' => 'Korea, Democratic People\'s Republic of', + 'United States of America' => 'United States', + 'USA' => 'United States', + 'UK' => 'United Kingdom', + 'Britain' => 'United Kingdom', + 'Great Britain' => 'United Kingdom', + 'England' => 'United Kingdom', + 'Scotland' => 'United Kingdom', + 'Wales' => 'United Kingdom', + 'Northern Ireland' => 'United Kingdom', + 'Macedonia' => 'North Macedonia', + 'Czech Republic' => 'Czech Republic', + 'Czechia' => 'Czech Republic', + 'Vatican' => 'Holy See (Vatican City State)', + 'Vatican City' => 'Holy See (Vatican City State)', + 'Taiwan' => 'Taiwan, Province of China', + 'Hong Kong SAR' => 'Hong Kong', + 'Macao SAR' => 'Macao', + 'Moldova' => 'Moldova, Republic of', + 'Bolivia' => 'Bolivia', + 'Venezuela' => 'Venezuela, Bolivarian Republic of', + 'Iran' => 'Iran, Islamic Republic of', + 'Syria' => 'Syrian Arab Republic', + 'Tanzania' => 'Tanzania, United Republic of', + 'Laos' => 'Lao People\'s Democratic Republic', + 'Vietnam' => 'Viet Nam', + 'Palestine' => 'Palestine, State of', + 'Congo' => 'Congo', + 'Democratic Republic of Congo' => 'Congo, Democratic Republic of the', + 'DRC' => 'Congo, Democratic Republic of the', + 'Ivory Coast' => 'Cรดte d\'Ivoire', + 'Cape Verde' => 'Cape Verde', + 'East Timor' => 'Timor-Leste', + 'Burma' => 'Myanmar', + 'Swaziland' => 'Eswatini' + }.freeze + + def self.iso_a3_from_a2(iso_a2) + return nil if iso_a2.blank? + + country_data = COUNTRIES[iso_a2.upcase] + country_data&.dig(:iso3) + end + + def self.iso_codes_from_country_name(country_name) + return [nil, nil] if country_name.blank? + + # Try exact match first + country_data = find_country_by_name(country_name) + return [country_data[:iso2], country_data[:iso3]] if country_data + + # Try aliases + standard_name = COUNTRY_ALIASES[country_name] + if standard_name + country_data = find_country_by_name(standard_name) + return [country_data[:iso2], country_data[:iso3]] if country_data + end + + # Try case-insensitive match + country_data = COUNTRIES.values.find { |data| data[:name].downcase == country_name.downcase } + return [country_data[:iso2], country_data[:iso3]] if country_data + + # Try partial match (country name contains or is contained in a known name) + country_data = COUNTRIES.values.find do |data| + data[:name].downcase.include?(country_name.downcase) || + country_name.downcase.include?(data[:name].downcase) + end + return [country_data[:iso2], country_data[:iso3]] if country_data + + # No match found + [nil, nil] + end + + def self.fallback_codes_from_country_name(country_name) + return [nil, nil] if country_name.blank? + + # First try to find proper ISO codes from country name + iso_a2, iso_a3 = iso_codes_from_country_name(country_name) + return [iso_a2, iso_a3] if iso_a2 && iso_a3 + + # Only use character-based fallback as a last resort + # This is still not ideal but better than nothing + fallback_a2 = country_name[0..1].upcase + fallback_a3 = country_name[0..2].upcase + + [fallback_a2, fallback_a3] + end + + def self.standardize_country_name(country_name) + return nil if country_name.blank? + + # Try exact match first + country_data = find_country_by_name(country_name) + return country_data[:name] if country_data + + # Try aliases + standard_name = COUNTRY_ALIASES[country_name] + return standard_name if standard_name + + # Try case-insensitive match + country_data = COUNTRIES.values.find { |data| data[:name].downcase == country_name.downcase } + return country_data[:name] if country_data + + # Try partial match + country_data = COUNTRIES.values.find do |data| + data[:name].downcase.include?(country_name.downcase) || + country_name.downcase.include?(data[:name].downcase) + end + return country_data[:name] if country_data + + nil + end + + def self.country_flag(iso_a2) + return nil if iso_a2.blank? + + country_data = COUNTRIES[iso_a2.upcase] + country_data&.dig(:flag) + end + + def self.country_by_iso2(iso_a2) + return nil if iso_a2.blank? + + COUNTRIES[iso_a2.upcase] + end + + def self.country_by_name(country_name) + return nil if country_name.blank? + + find_country_by_name(country_name) || + find_country_by_name(COUNTRY_ALIASES[country_name]) || + COUNTRIES.values.find { |data| data[:name].downcase == country_name.downcase } + end + + def self.all_countries + COUNTRIES.values + end + + private + + def self.find_country_by_name(name) + return nil if name.blank? + + COUNTRIES.values.find { |data| data[:name] == name } + end +end diff --git a/app/services/imports/create.rb b/app/services/imports/create.rb index d96ba38a..b2056663 100644 --- a/app/services/imports/create.rb +++ b/app/services/imports/create.rb @@ -21,7 +21,7 @@ class Imports::Create create_import_failed_notification(import, user, e) ensure - import.update!(status: :completed) if import.completed? + import.update!(status: :completed) if import.processing? end private diff --git a/app/services/reverse_geocoding/points/fetch_data.rb b/app/services/reverse_geocoding/points/fetch_data.rb index 7aae9e02..86fdc899 100644 --- a/app/services/reverse_geocoding/points/fetch_data.rb +++ b/app/services/reverse_geocoding/points/fetch_data.rb @@ -24,8 +24,9 @@ class ReverseGeocoding::Points::FetchData return if response.blank? || response.data['error'].present? country_record = Country.find_or_create_by(name: response.country) do |country| - country.iso_a2 = response.country[0..1].upcase if response.country - country.iso_a3 = response.country[0..2].upcase if response.country + iso_a2, iso_a3 = extract_iso_codes(response) + country.iso_a2 = iso_a2 + country.iso_a3 = iso_a3 country.geom = "MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)))" end if response.country @@ -36,4 +37,19 @@ class ReverseGeocoding::Points::FetchData reverse_geocoded_at: Time.current ) end + + def extract_iso_codes(response) + # First, try to get the ISO A2 code from the Geocoder response + iso_a2 = response.data.dig('properties', 'countrycode')&.upcase + + if iso_a2.present? + # If we have a valid ISO A2 code, get the corresponding ISO A3 code + iso_a3 = Countries::IsoCodeMapper.iso_a3_from_a2(iso_a2) + return [iso_a2, iso_a3] if iso_a3.present? + end + + # If no valid ISO code from Geocoder, try to match the country name + # This will return proper ISO codes if the country name is recognized + Countries::IsoCodeMapper.fallback_codes_from_country_name(response.country) + end end diff --git a/app/services/users/import_data/points.rb b/app/services/users/import_data/points.rb index 66de2048..41c9eaba 100644 --- a/app/services/users/import_data/points.rb +++ b/app/services/users/import_data/points.rb @@ -206,8 +206,8 @@ class Users::ImportData::Points def create_missing_country(country_info) Country.find_or_create_by(name: country_info['name']) do |new_country| - new_country.iso_a2 = country_info['iso_a2'] || country_info['name'][0..1].upcase - new_country.iso_a3 = country_info['iso_a3'] || country_info['name'][0..2].upcase + new_country.iso_a2 = country_info['iso_a2'] || Countries::IsoCodeMapper.fallback_codes_from_country_name(country_info['name'])[0] + new_country.iso_a3 = country_info['iso_a3'] || Countries::IsoCodeMapper.fallback_codes_from_country_name(country_info['name'])[1] new_country.geom = "MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)))" # Default geometry end rescue StandardError => e diff --git a/spec/factories/points.rb b/spec/factories/points.rb index d5b2cb35..4848250c 100644 --- a/spec/factories/points.rb +++ b/spec/factories/points.rb @@ -42,8 +42,9 @@ FactoryBot.define do if evaluator.country.is_a?(String) # Set both the country string attribute and the Country association country_obj = Country.find_or_create_by(name: evaluator.country) do |country| - country.iso_a2 = evaluator.country[0..1].upcase - country.iso_a3 = evaluator.country[0..2].upcase + iso_a2, iso_a3 = Countries::IsoCodeMapper.fallback_codes_from_country_name(evaluator.country) + country.iso_a2 = iso_a2 + country.iso_a3 = iso_a3 country.geom = "MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)))" end point.update_columns( @@ -95,8 +96,9 @@ FactoryBot.define do unless point.read_attribute(:country) country_name = FFaker::Address.country country_obj = Country.find_or_create_by(name: country_name) do |country| - country.iso_a2 = country_name[0..1].upcase - country.iso_a3 = country_name[0..2].upcase + iso_a2, iso_a3 = Countries::IsoCodeMapper.fallback_codes_from_country_name(country_name) + country.iso_a2 = iso_a2 + country.iso_a3 = iso_a3 country.geom = "MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)))" end point.write_attribute(:country, country_name) # Set the string attribute directly diff --git a/spec/services/countries/iso_code_mapper_spec.rb b/spec/services/countries/iso_code_mapper_spec.rb new file mode 100644 index 00000000..8b7d7f37 --- /dev/null +++ b/spec/services/countries/iso_code_mapper_spec.rb @@ -0,0 +1,245 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Countries::IsoCodeMapper do + describe '.iso_a3_from_a2' do + it 'returns correct ISO A3 code for valid ISO A2 code' do + expect(described_class.iso_a3_from_a2('DE')).to eq('DEU') + expect(described_class.iso_a3_from_a2('US')).to eq('USA') + expect(described_class.iso_a3_from_a2('GB')).to eq('GBR') + end + + it 'handles lowercase input' do + expect(described_class.iso_a3_from_a2('de')).to eq('DEU') + end + + it 'returns nil for invalid ISO A2 code' do + expect(described_class.iso_a3_from_a2('XX')).to be_nil + expect(described_class.iso_a3_from_a2('')).to be_nil + expect(described_class.iso_a3_from_a2(nil)).to be_nil + end + end + + describe '.iso_codes_from_country_name' do + it 'returns correct ISO codes for exact country name match' do + iso_a2, iso_a3 = described_class.iso_codes_from_country_name('Germany') + expect(iso_a2).to eq('DE') + expect(iso_a3).to eq('DEU') + end + + it 'returns correct ISO codes for country name aliases' do + iso_a2, iso_a3 = described_class.iso_codes_from_country_name('Russia') + expect(iso_a2).to eq('RU') + expect(iso_a3).to eq('RUS') + + iso_a2, iso_a3 = described_class.iso_codes_from_country_name('USA') + expect(iso_a2).to eq('US') + expect(iso_a3).to eq('USA') + end + + it 'handles case-insensitive matching' do + iso_a2, iso_a3 = described_class.iso_codes_from_country_name('GERMANY') + expect(iso_a2).to eq('DE') + expect(iso_a3).to eq('DEU') + + iso_a2, iso_a3 = described_class.iso_codes_from_country_name('germany') + expect(iso_a2).to eq('DE') + expect(iso_a3).to eq('DEU') + end + + it 'handles partial matching' do + # This should find "United States" when searching for "United States of America" + iso_a2, iso_a3 = described_class.iso_codes_from_country_name('United States of America') + expect(iso_a2).to eq('US') + expect(iso_a3).to eq('USA') + end + + it 'returns nil for unknown country names' do + iso_a2, iso_a3 = described_class.iso_codes_from_country_name('Atlantis') + expect(iso_a2).to be_nil + expect(iso_a3).to be_nil + end + + it 'returns nil for blank input' do + iso_a2, iso_a3 = described_class.iso_codes_from_country_name('') + expect(iso_a2).to be_nil + expect(iso_a3).to be_nil + + iso_a2, iso_a3 = described_class.iso_codes_from_country_name(nil) + expect(iso_a2).to be_nil + expect(iso_a3).to be_nil + end + end + + describe '.fallback_codes_from_country_name' do + it 'returns proper ISO codes when country name is recognized' do + iso_a2, iso_a3 = described_class.fallback_codes_from_country_name('Germany') + expect(iso_a2).to eq('DE') + expect(iso_a3).to eq('DEU') + end + + it 'falls back to character-based codes for unknown countries' do + iso_a2, iso_a3 = described_class.fallback_codes_from_country_name('Atlantis') + expect(iso_a2).to eq('AT') + expect(iso_a3).to eq('ATL') + end + + it 'returns nil for blank input' do + iso_a2, iso_a3 = described_class.fallback_codes_from_country_name('') + expect(iso_a2).to be_nil + expect(iso_a3).to be_nil + + iso_a2, iso_a3 = described_class.fallback_codes_from_country_name(nil) + expect(iso_a2).to be_nil + expect(iso_a3).to be_nil + end + end + + describe '.standardize_country_name' do + it 'returns standard name for exact match' do + expect(described_class.standardize_country_name('Germany')).to eq('Germany') + end + + it 'returns standard name for aliases' do + expect(described_class.standardize_country_name('Russia')).to eq('Russian Federation') + expect(described_class.standardize_country_name('USA')).to eq('United States') + end + + it 'handles case-insensitive matching' do + expect(described_class.standardize_country_name('GERMANY')).to eq('Germany') + expect(described_class.standardize_country_name('germany')).to eq('Germany') + end + + it 'returns nil for unknown country names' do + expect(described_class.standardize_country_name('Atlantis')).to be_nil + end + + it 'returns nil for blank input' do + expect(described_class.standardize_country_name('')).to be_nil + expect(described_class.standardize_country_name(nil)).to be_nil + end + end + + describe '.country_flag' do + it 'returns correct flag emoji for valid ISO A2 code' do + expect(described_class.country_flag('DE')).to eq('๐Ÿ‡ฉ๐Ÿ‡ช') + expect(described_class.country_flag('US')).to eq('๐Ÿ‡บ๐Ÿ‡ธ') + expect(described_class.country_flag('GB')).to eq('๐Ÿ‡ฌ๐Ÿ‡ง') + end + + it 'handles lowercase input' do + expect(described_class.country_flag('de')).to eq('๐Ÿ‡ฉ๐Ÿ‡ช') + end + + it 'returns nil for invalid ISO A2 code' do + expect(described_class.country_flag('XX')).to be_nil + expect(described_class.country_flag('')).to be_nil + expect(described_class.country_flag(nil)).to be_nil + end + end + + describe '.country_by_iso2' do + it 'returns complete country data for valid ISO A2 code' do + country = described_class.country_by_iso2('DE') + expect(country).to include( + name: 'Germany', + iso2: 'DE', + iso3: 'DEU', + flag: '๐Ÿ‡ฉ๐Ÿ‡ช' + ) + end + + it 'handles lowercase input' do + country = described_class.country_by_iso2('de') + expect(country[:name]).to eq('Germany') + end + + it 'returns nil for invalid ISO A2 code' do + expect(described_class.country_by_iso2('XX')).to be_nil + expect(described_class.country_by_iso2('')).to be_nil + expect(described_class.country_by_iso2(nil)).to be_nil + end + end + + describe '.country_by_name' do + it 'returns complete country data for exact name match' do + country = described_class.country_by_name('Germany') + expect(country).to include( + name: 'Germany', + iso2: 'DE', + iso3: 'DEU', + flag: '๐Ÿ‡ฉ๐Ÿ‡ช' + ) + end + + it 'returns country data for aliases' do + country = described_class.country_by_name('Russia') + expect(country).to include( + name: 'Russian Federation', + iso2: 'RU', + iso3: 'RUS', + flag: '๐Ÿ‡ท๐Ÿ‡บ' + ) + end + + it 'handles case-insensitive matching' do + country = described_class.country_by_name('GERMANY') + expect(country[:name]).to eq('Germany') + end + + it 'returns nil for unknown country names' do + expect(described_class.country_by_name('Atlantis')).to be_nil + end + + it 'returns nil for blank input' do + expect(described_class.country_by_name('')).to be_nil + expect(described_class.country_by_name(nil)).to be_nil + end + end + + describe '.all_countries' do + it 'returns all country data' do + countries = described_class.all_countries + expect(countries).to be_an(Array) + expect(countries.size).to be > 190 # There are 195+ countries + + # Check that each country has required fields + countries.each do |country| + expect(country).to have_key(:name) + expect(country).to have_key(:iso2) + expect(country).to have_key(:iso3) + expect(country).to have_key(:flag) + end + end + + it 'includes expected countries' do + countries = described_class.all_countries + country_names = countries.map { |c| c[:name] } + + expect(country_names).to include('Germany') + expect(country_names).to include('United States') + expect(country_names).to include('United Kingdom') + expect(country_names).to include('Russian Federation') + end + end + + describe 'data integrity' do + it 'has consistent data structure' do + described_class.all_countries.each do |country| + expect(country[:iso2]).to match(/\A[A-Z]{2}\z/) + expect(country[:iso3]).to match(/\A[A-Z]{3}\z/) + expect(country[:name]).to be_present + expect(country[:flag]).to be_present + end + end + + it 'has unique ISO codes' do + iso2_codes = described_class.all_countries.map { |c| c[:iso2] } + iso3_codes = described_class.all_countries.map { |c| c[:iso3] } + + expect(iso2_codes.uniq.size).to eq(iso2_codes.size) + expect(iso3_codes.uniq.size).to eq(iso3_codes.size) + end + end +end diff --git a/spec/services/imports/create_spec.rb b/spec/services/imports/create_spec.rb index 69634149..91fc643b 100644 --- a/spec/services/imports/create_spec.rb +++ b/spec/services/imports/create_spec.rb @@ -7,6 +7,38 @@ RSpec.describe Imports::Create do let(:service) { described_class.new(user, import) } describe '#call' do + describe 'status transitions' do + let(:import) { create(:import, source: 'owntracks', status: 'created') } + let(:file_path) { Rails.root.join('spec/fixtures/files/owntracks/2024-03.rec') } + + before do + import.file.attach(io: File.open(file_path), filename: '2024-03.rec', content_type: 'application/octet-stream') + end + + it 'sets status to processing at start' do + service.call + expect(import.reload.status).to eq('processing').or eq('completed') + end + + context 'when import succeeds' do + it 'sets status to completed' do + service.call + expect(import.reload.status).to eq('completed') + end + end + + context 'when import fails' do + before do + allow(OwnTracks::Importer).to receive(:new).with(import, user.id).and_raise(StandardError) + end + + it 'sets status to failed' do + service.call + expect(import.reload.status).to eq('failed') + end + end + end + context 'when source is google_semantic_history' do let(:import) { create(:import, source: 'google_semantic_history') } let(:file_path) { Rails.root.join('spec/fixtures/files/google/semantic_history.json') } diff --git a/spec/services/reverse_geocoding/points/fetch_data_spec.rb b/spec/services/reverse_geocoding/points/fetch_data_spec.rb index c26e82c9..249821a4 100644 --- a/spec/services/reverse_geocoding/points/fetch_data_spec.rb +++ b/spec/services/reverse_geocoding/points/fetch_data_spec.rb @@ -11,7 +11,14 @@ RSpec.describe ReverseGeocoding::Points::FetchData do before do allow(Geocoder).to receive(:search).and_return( [ - double(city: 'City', country: 'Country', data: { 'address' => 'Address' }) + double( + city: 'Berlin', + country: 'Germany', + data: { + 'address' => 'Address', + 'properties' => { 'countrycode' => 'DE' } + } + ) ] ) end @@ -19,12 +26,23 @@ RSpec.describe ReverseGeocoding::Points::FetchData do context 'when point does not have city and country' do it 'updates point with city and country' do expect { fetch_data }.to change { point.reload.city } - .from(nil).to('City') + .from(nil).to('Berlin') .and change { point.reload.country_id }.from(nil).to(be_present) end + it 'creates country with correct ISO codes' do + fetch_data + country = point.reload.country + expect(country.name).to eq('Germany') + expect(country.iso_a2).to eq('DE') + expect(country.iso_a3).to eq('DEU') + end + it 'updates point with geodata' do - expect { fetch_data }.to change { point.reload.geodata }.from({}).to('address' => 'Address') + expect { fetch_data }.to change { point.reload.geodata }.from({}).to( + 'address' => 'Address', + 'properties' => { 'countrycode' => 'DE' } + ) end it 'calls Geocoder' do @@ -40,7 +58,15 @@ RSpec.describe ReverseGeocoding::Points::FetchData do before do allow(Geocoder).to receive(:search).and_return( - [double(geodata: { 'address' => 'Address' }, city: 'City', country: 'Country')] + [double( + geodata: { 'address' => 'Address' }, + city: 'Berlin', + country: 'Germany', + data: { + 'address' => 'Address', + 'properties' => { 'countrycode' => 'DE' } + } + )] ) end @@ -56,6 +82,31 @@ RSpec.describe ReverseGeocoding::Points::FetchData do end end + context 'when Geocoder returns country name without ISO code' do + before do + allow(Geocoder).to receive(:search).and_return( + [ + double( + city: 'Paris', + country: 'France', + data: { + 'address' => 'Address', + 'properties' => { 'city' => 'Paris' } # No countrycode property + } + ) + ] + ) + end + + it 'creates country with correct ISO codes from country name mapping' do + fetch_data + country = point.reload.country + expect(country.name).to eq('France') + expect(country.iso_a2).to eq('FR') + expect(country.iso_a3).to eq('FRA') + end + end + context 'when Geocoder returns an error' do before do allow(Geocoder).to receive(:search).and_return([double(city: nil, country: nil, data: { 'error' => 'Error' })])