trip) adding support for lodgings
This commit is contained in:
@@ -5,6 +5,40 @@ import type { Database } from './types.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// Helper function to parse CSV line with proper quote handling
|
||||
function parseCSVLine(line: string): string[] {
|
||||
const fields: string[] = [];
|
||||
let current = '';
|
||||
let inQuotes = false;
|
||||
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
const char = line[i];
|
||||
const nextChar = line[i + 1];
|
||||
|
||||
if (char === '"') {
|
||||
if (inQuotes && nextChar === '"') {
|
||||
// Escaped quote
|
||||
current += '"';
|
||||
i++; // Skip next quote
|
||||
} else {
|
||||
// Toggle quote state
|
||||
inQuotes = !inQuotes;
|
||||
}
|
||||
} else if (char === ',' && !inQuotes) {
|
||||
// Field separator
|
||||
fields.push(current.trim());
|
||||
current = '';
|
||||
} else {
|
||||
current += char;
|
||||
}
|
||||
}
|
||||
|
||||
// Add last field
|
||||
fields.push(current.trim());
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
export function runMigrations(db: Database): void {
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS trips (
|
||||
@@ -51,6 +85,7 @@ export function runMigrations(db: Database): void {
|
||||
)
|
||||
`);
|
||||
|
||||
// Legacy table — superseded by `people` + `trip_travellers`. Kept for migration safety on existing DBs.
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS travellers (
|
||||
id TEXT PRIMARY KEY,
|
||||
@@ -86,11 +121,150 @@ export function runMigrations(db: Database): void {
|
||||
)
|
||||
`);
|
||||
|
||||
// Airports table
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS airports (
|
||||
id INTEGER PRIMARY KEY,
|
||||
iata_code TEXT UNIQUE,
|
||||
icao_code TEXT UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
city TEXT,
|
||||
country TEXT NOT NULL,
|
||||
country_code TEXT NOT NULL,
|
||||
latitude REAL,
|
||||
longitude REAL,
|
||||
timezone TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
|
||||
// Airlines table
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS airlines (
|
||||
id INTEGER PRIMARY KEY,
|
||||
iata_code TEXT,
|
||||
icao_code TEXT,
|
||||
name TEXT NOT NULL,
|
||||
country TEXT,
|
||||
country_code TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
|
||||
// Flight bookings table - links to a plan
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS flight_bookings (
|
||||
id TEXT PRIMARY KEY,
|
||||
plan_id TEXT NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
|
||||
confirmation_number TEXT,
|
||||
price REAL,
|
||||
currency TEXT DEFAULT 'USD',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
|
||||
// Flight segments table - individual flights within a booking
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS flight_segments (
|
||||
id TEXT PRIMARY KEY,
|
||||
flight_booking_id TEXT NOT NULL REFERENCES flight_bookings(id) ON DELETE CASCADE,
|
||||
departure_date TEXT NOT NULL,
|
||||
airline_id INTEGER REFERENCES airlines(id),
|
||||
airline_iata TEXT,
|
||||
airline_icao TEXT,
|
||||
airline_name TEXT,
|
||||
flight_number TEXT NOT NULL,
|
||||
position INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
|
||||
// Flight routes table - detailed route info for each segment
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS flight_routes (
|
||||
id TEXT PRIMARY KEY,
|
||||
flight_segment_id TEXT NOT NULL REFERENCES flight_segments(id) ON DELETE CASCADE,
|
||||
departure_airport_id INTEGER REFERENCES airports(id),
|
||||
departure_airport_code TEXT,
|
||||
departure_terminal TEXT,
|
||||
departure_gate TEXT,
|
||||
departure_datetime TEXT,
|
||||
departure_timezone TEXT,
|
||||
arrival_airport_id INTEGER REFERENCES airports(id),
|
||||
arrival_airport_code TEXT,
|
||||
arrival_terminal TEXT,
|
||||
arrival_gate TEXT,
|
||||
arrival_datetime TEXT,
|
||||
arrival_timezone TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
|
||||
// Flight booking passengers - links people to flight bookings
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS flight_booking_passengers (
|
||||
flight_booking_id TEXT NOT NULL REFERENCES flight_bookings(id) ON DELETE CASCADE,
|
||||
person_id TEXT NOT NULL REFERENCES people(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (flight_booking_id, person_id)
|
||||
)
|
||||
`);
|
||||
|
||||
// Lodgings table - links to a plan
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS lodgings (
|
||||
id TEXT PRIMARY KEY,
|
||||
plan_id TEXT NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
chain TEXT,
|
||||
check_in_date TEXT,
|
||||
check_in_time TEXT,
|
||||
check_in_timezone TEXT,
|
||||
check_out_date TEXT,
|
||||
check_out_time TEXT,
|
||||
check_out_timezone TEXT,
|
||||
address_line1 TEXT,
|
||||
address_line2 TEXT,
|
||||
city_name TEXT,
|
||||
country TEXT,
|
||||
country_code TEXT,
|
||||
postal_code TEXT,
|
||||
confirmation_number TEXT,
|
||||
website TEXT,
|
||||
phone TEXT,
|
||||
price REAL,
|
||||
currency TEXT NOT NULL DEFAULT 'USD',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
|
||||
// Lodging guests - links people to lodgings
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS lodging_guests (
|
||||
lodging_id TEXT NOT NULL REFERENCES lodgings(id) ON DELETE CASCADE,
|
||||
person_id TEXT NOT NULL REFERENCES people(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (lodging_id, person_id)
|
||||
)
|
||||
`);
|
||||
|
||||
// Seed cities table on first run
|
||||
const row = db.get<{ count: number }>('SELECT COUNT(*) as count FROM cities');
|
||||
if ((row?.count ?? 0) === 0) {
|
||||
seedCities(db);
|
||||
}
|
||||
|
||||
// Seed airports and airlines on first run only
|
||||
const airportsRow = db.get<{ count: number }>('SELECT COUNT(*) as count FROM airports');
|
||||
if ((airportsRow?.count ?? 0) === 0) {
|
||||
seedAirports(db);
|
||||
}
|
||||
|
||||
const airlinesRow = db.get<{ count: number }>('SELECT COUNT(*) as count FROM airlines');
|
||||
if ((airlinesRow?.count ?? 0) === 0) {
|
||||
seedAirlines(db);
|
||||
}
|
||||
}
|
||||
|
||||
function seedCities(db: Database): void {
|
||||
@@ -110,3 +284,504 @@ function seedCities(db: Database): void {
|
||||
console.error('[db] Failed to seed cities:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function seedAirports(db: Database): void {
|
||||
try {
|
||||
// Try to load from OpenFlights airports.dat file
|
||||
// Format: Airport ID, Name, City, Country, IATA, ICAO, Latitude, Longitude, Altitude, Timezone, DST, Tz database time zone, Type, Source
|
||||
const airportsPath = join(__dirname, '../data/airports.dat');
|
||||
try {
|
||||
const data = readFileSync(airportsPath, 'utf-8');
|
||||
const lines = data.split('\n').filter((line) => line.trim());
|
||||
let imported = 0;
|
||||
let skipped = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
// Skip comments
|
||||
if (line.startsWith('#')) continue;
|
||||
|
||||
// Parse CSV with proper quote handling
|
||||
const fields = parseCSVLine(line);
|
||||
|
||||
// OpenFlights format: ID, Name, City, Country, IATA, ICAO, Lat, Lon, Alt, TZ, DST, TZ_DB, Type, Source
|
||||
if (fields.length < 8) continue;
|
||||
|
||||
const [idStr, name, city, country, iata, icao, latStr, lonStr, , , , tzDb] = fields;
|
||||
|
||||
// Skip if missing essential data
|
||||
if (!name || !country) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const lat = latStr ? parseFloat(latStr) : null;
|
||||
const lon = lonStr ? parseFloat(lonStr) : null;
|
||||
|
||||
// Only import airports with valid coordinates and IATA/ICAO codes
|
||||
if ((!iata && !icao) || lat === null || lon === null || isNaN(lat) || isNaN(lon)) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get country code from country name (simplified - you might want a mapping)
|
||||
const countryCode = getCountryCode(country);
|
||||
|
||||
try {
|
||||
// Use IATA/ICAO code as unique identifier, let ID auto-increment
|
||||
db.run(
|
||||
`INSERT OR IGNORE INTO airports (iata_code, icao_code, name, city, country, country_code, latitude, longitude, timezone)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
iata || null,
|
||||
icao || null,
|
||||
name,
|
||||
city || null,
|
||||
country,
|
||||
countryCode,
|
||||
lat,
|
||||
lon,
|
||||
tzDb || null
|
||||
]
|
||||
);
|
||||
imported++;
|
||||
} catch (err) {
|
||||
// Skip duplicates or invalid data
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
console.log(`[db] Seeded ${imported} airports from airports.dat (${skipped} skipped)`);
|
||||
return;
|
||||
} catch (fileError) {
|
||||
// File doesn't exist, fall back to minimal seed only if table is empty
|
||||
const airportsRow = db.get<{ count: number }>('SELECT COUNT(*) as count FROM airports');
|
||||
if ((airportsRow?.count ?? 0) === 0) {
|
||||
console.log('[db] airports.dat not found, using minimal airport seed');
|
||||
} else {
|
||||
console.log('[db] airports.dat not found, skipping seed (table already has data)');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: minimal seed if file doesn't exist
|
||||
const majorAirports = [
|
||||
{
|
||||
iata: 'JFK',
|
||||
icao: 'KJFK',
|
||||
name: 'John F. Kennedy International Airport',
|
||||
city: 'New York',
|
||||
country: 'United States',
|
||||
country_code: 'US',
|
||||
lat: 40.6398,
|
||||
lon: -73.7789,
|
||||
tz: 'America/New_York'
|
||||
},
|
||||
{
|
||||
iata: 'LAX',
|
||||
icao: 'KLAX',
|
||||
name: 'Los Angeles International Airport',
|
||||
city: 'Los Angeles',
|
||||
country: 'United States',
|
||||
country_code: 'US',
|
||||
lat: 33.9425,
|
||||
lon: -118.4081,
|
||||
tz: 'America/Los_Angeles'
|
||||
},
|
||||
{
|
||||
iata: 'LHR',
|
||||
icao: 'EGLL',
|
||||
name: 'London Heathrow Airport',
|
||||
city: 'London',
|
||||
country: 'United Kingdom',
|
||||
country_code: 'GB',
|
||||
lat: 51.47,
|
||||
lon: -0.4543,
|
||||
tz: 'Europe/London'
|
||||
},
|
||||
{
|
||||
iata: 'CDG',
|
||||
icao: 'LFPG',
|
||||
name: 'Charles de Gaulle Airport',
|
||||
city: 'Paris',
|
||||
country: 'France',
|
||||
country_code: 'FR',
|
||||
lat: 49.0097,
|
||||
lon: 2.5479,
|
||||
tz: 'Europe/Paris'
|
||||
},
|
||||
{
|
||||
iata: 'DXB',
|
||||
icao: 'OMDB',
|
||||
name: 'Dubai International Airport',
|
||||
city: 'Dubai',
|
||||
country: 'United Arab Emirates',
|
||||
country_code: 'AE',
|
||||
lat: 25.2532,
|
||||
lon: 55.3657,
|
||||
tz: 'Asia/Dubai'
|
||||
},
|
||||
{
|
||||
iata: 'SYD',
|
||||
icao: 'YSSY',
|
||||
name: 'Sydney Kingsford Smith Airport',
|
||||
city: 'Sydney',
|
||||
country: 'Australia',
|
||||
country_code: 'AU',
|
||||
lat: -33.9399,
|
||||
lon: 151.1753,
|
||||
tz: 'Australia/Sydney'
|
||||
},
|
||||
{
|
||||
iata: 'NRT',
|
||||
icao: 'RJAA',
|
||||
name: 'Narita International Airport',
|
||||
city: 'Tokyo',
|
||||
country: 'Japan',
|
||||
country_code: 'JP',
|
||||
lat: 35.772,
|
||||
lon: 140.3929,
|
||||
tz: 'Asia/Tokyo'
|
||||
},
|
||||
{
|
||||
iata: 'SIN',
|
||||
icao: 'WSSS',
|
||||
name: 'Singapore Changi Airport',
|
||||
city: 'Singapore',
|
||||
country: 'Singapore',
|
||||
country_code: 'SG',
|
||||
lat: 1.3644,
|
||||
lon: 103.9915,
|
||||
tz: 'Asia/Singapore'
|
||||
}
|
||||
];
|
||||
|
||||
let id = 1;
|
||||
for (const airport of majorAirports) {
|
||||
db.run(
|
||||
`INSERT OR IGNORE INTO airports (id, iata_code, icao_code, name, city, country, country_code, latitude, longitude, timezone)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
id++,
|
||||
airport.iata,
|
||||
airport.icao,
|
||||
airport.name,
|
||||
airport.city,
|
||||
airport.country,
|
||||
airport.country_code,
|
||||
airport.lat,
|
||||
airport.lon,
|
||||
airport.tz
|
||||
]
|
||||
);
|
||||
}
|
||||
console.log(`[db] Seeded ${majorAirports.length} airports`);
|
||||
} catch (e) {
|
||||
console.error('[db] Failed to seed airports:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function seedAirlines(db: Database): void {
|
||||
try {
|
||||
// Try to load from OpenFlights airlines.dat file
|
||||
// Format: Airline ID, Name, Alias, IATA, ICAO, Callsign, Country, Active
|
||||
const airlinesPath = join(__dirname, '../data/airlines.dat');
|
||||
try {
|
||||
const data = readFileSync(airlinesPath, 'utf-8');
|
||||
const lines = data.split('\n').filter((line) => line.trim());
|
||||
let imported = 0;
|
||||
let skipped = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
// Skip comments
|
||||
if (line.startsWith('#')) continue;
|
||||
|
||||
// Parse CSV with proper quote handling
|
||||
const fields = parseCSVLine(line);
|
||||
|
||||
// OpenFlights format: ID, Name, Alias, IATA, ICAO, Callsign, Country, Active
|
||||
if (fields.length < 7) continue;
|
||||
|
||||
const [idStr, name, , iata, icao, , country, active] = fields;
|
||||
|
||||
// Skip if missing essential data or inactive
|
||||
if (!name || !country || active !== 'Y') {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only import airlines with IATA or ICAO codes
|
||||
if (!iata && !icao) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get country code from country name
|
||||
const countryCode = getCountryCode(country);
|
||||
|
||||
try {
|
||||
// Use IATA/ICAO code as unique identifier, let ID auto-increment
|
||||
db.run(
|
||||
`INSERT OR IGNORE INTO airlines (iata_code, icao_code, name, country, country_code)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
[iata || null, icao || null, name, country, countryCode]
|
||||
);
|
||||
imported++;
|
||||
} catch (err) {
|
||||
// Skip duplicates or invalid data
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
console.log(`[db] Seeded ${imported} airlines from airlines.dat (${skipped} skipped)`);
|
||||
return;
|
||||
} catch (fileError) {
|
||||
// File doesn't exist, fall back to minimal seed only if table is empty
|
||||
const airlinesRow = db.get<{ count: number }>('SELECT COUNT(*) as count FROM airlines');
|
||||
if ((airlinesRow?.count ?? 0) === 0) {
|
||||
console.log('[db] airlines.dat not found, using minimal airline seed');
|
||||
} else {
|
||||
console.log('[db] airlines.dat not found, skipping seed (table already has data)');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: minimal seed if file doesn't exist
|
||||
const majorAirlines = [
|
||||
{
|
||||
iata: 'AA',
|
||||
icao: 'AAL',
|
||||
name: 'American Airlines',
|
||||
country: 'United States',
|
||||
country_code: 'US'
|
||||
},
|
||||
{
|
||||
iata: 'UA',
|
||||
icao: 'UAL',
|
||||
name: 'United Airlines',
|
||||
country: 'United States',
|
||||
country_code: 'US'
|
||||
},
|
||||
{
|
||||
iata: 'DL',
|
||||
icao: 'DAL',
|
||||
name: 'Delta Air Lines',
|
||||
country: 'United States',
|
||||
country_code: 'US'
|
||||
},
|
||||
{
|
||||
iata: 'BA',
|
||||
icao: 'BAW',
|
||||
name: 'British Airways',
|
||||
country: 'United Kingdom',
|
||||
country_code: 'GB'
|
||||
},
|
||||
{ iata: 'AF', icao: 'AFR', name: 'Air France', country: 'France', country_code: 'FR' },
|
||||
{ iata: 'LH', icao: 'DLH', name: 'Lufthansa', country: 'Germany', country_code: 'DE' },
|
||||
{
|
||||
iata: 'EK',
|
||||
icao: 'UAE',
|
||||
name: 'Emirates',
|
||||
country: 'United Arab Emirates',
|
||||
country_code: 'AE'
|
||||
},
|
||||
{ iata: 'QF', icao: 'QFA', name: 'Qantas', country: 'Australia', country_code: 'AU' },
|
||||
{ iata: 'JL', icao: 'JAL', name: 'Japan Airlines', country: 'Japan', country_code: 'JP' },
|
||||
{
|
||||
iata: 'SQ',
|
||||
icao: 'SIA',
|
||||
name: 'Singapore Airlines',
|
||||
country: 'Singapore',
|
||||
country_code: 'SG'
|
||||
}
|
||||
];
|
||||
|
||||
let id = 1;
|
||||
for (const airline of majorAirlines) {
|
||||
db.run(
|
||||
`INSERT OR IGNORE INTO airlines (id, iata_code, icao_code, name, country, country_code)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
[id++, airline.iata, airline.icao, airline.name, airline.country, airline.country_code]
|
||||
);
|
||||
}
|
||||
console.log(`[db] Seeded ${majorAirlines.length} airlines`);
|
||||
} catch (e) {
|
||||
console.error('[db] Failed to seed airlines:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to map country names to ISO country codes
|
||||
// This is a simplified mapping - for production, consider using a comprehensive library
|
||||
function getCountryCode(countryName: string): string {
|
||||
// Common countries mapping (most frequently used in aviation)
|
||||
const countryMap: Record<string, string> = {
|
||||
'United States': 'US',
|
||||
'United Kingdom': 'GB',
|
||||
France: 'FR',
|
||||
Germany: 'DE',
|
||||
Japan: 'JP',
|
||||
China: 'CN',
|
||||
Canada: 'CA',
|
||||
Australia: 'AU',
|
||||
Brazil: 'BR',
|
||||
India: 'IN',
|
||||
Russia: 'RU',
|
||||
Mexico: 'MX',
|
||||
Spain: 'ES',
|
||||
Italy: 'IT',
|
||||
Netherlands: 'NL',
|
||||
Sweden: 'SE',
|
||||
Norway: 'NO',
|
||||
Denmark: 'DK',
|
||||
Finland: 'FI',
|
||||
Poland: 'PL',
|
||||
Turkey: 'TR',
|
||||
'South Korea': 'KR',
|
||||
Indonesia: 'ID',
|
||||
Thailand: 'TH',
|
||||
Malaysia: 'MY',
|
||||
Philippines: 'PH',
|
||||
Vietnam: 'VN',
|
||||
Singapore: 'SG',
|
||||
'United Arab Emirates': 'AE',
|
||||
'Saudi Arabia': 'SA',
|
||||
Israel: 'IL',
|
||||
'South Africa': 'ZA',
|
||||
Egypt: 'EG',
|
||||
Argentina: 'AR',
|
||||
Chile: 'CL',
|
||||
Colombia: 'CO',
|
||||
'New Zealand': 'NZ',
|
||||
Ireland: 'IE',
|
||||
Switzerland: 'CH',
|
||||
Austria: 'AT',
|
||||
Belgium: 'BE',
|
||||
Portugal: 'PT',
|
||||
Greece: 'GR',
|
||||
'Czech Republic': 'CZ',
|
||||
Hungary: 'HU',
|
||||
Romania: 'RO',
|
||||
Bulgaria: 'BG',
|
||||
Croatia: 'HR',
|
||||
Serbia: 'RS',
|
||||
Ukraine: 'UA',
|
||||
Belarus: 'BY',
|
||||
Kazakhstan: 'KZ',
|
||||
Pakistan: 'PK',
|
||||
Bangladesh: 'BD',
|
||||
'Sri Lanka': 'LK',
|
||||
Myanmar: 'MM',
|
||||
Cambodia: 'KH',
|
||||
Laos: 'LA',
|
||||
Nepal: 'NP',
|
||||
Afghanistan: 'AF',
|
||||
Iran: 'IR',
|
||||
Iraq: 'IQ',
|
||||
Jordan: 'JO',
|
||||
Lebanon: 'LB',
|
||||
Syria: 'SY',
|
||||
Yemen: 'YE',
|
||||
Oman: 'OM',
|
||||
Kuwait: 'KW',
|
||||
Qatar: 'QA',
|
||||
Bahrain: 'BH',
|
||||
Cyprus: 'CY',
|
||||
Morocco: 'MA',
|
||||
Algeria: 'DZ',
|
||||
Tunisia: 'TN',
|
||||
Libya: 'LY',
|
||||
Sudan: 'SD',
|
||||
Ethiopia: 'ET',
|
||||
Kenya: 'KE',
|
||||
Tanzania: 'TZ',
|
||||
Uganda: 'UG',
|
||||
Rwanda: 'RW',
|
||||
Ghana: 'GH',
|
||||
Nigeria: 'NG',
|
||||
Senegal: 'SN',
|
||||
'Ivory Coast': 'CI',
|
||||
Cameroon: 'CM',
|
||||
Angola: 'AO',
|
||||
Zambia: 'ZM',
|
||||
Zimbabwe: 'ZW',
|
||||
Botswana: 'BW',
|
||||
Namibia: 'NA',
|
||||
Mozambique: 'MZ',
|
||||
Madagascar: 'MG',
|
||||
Mauritius: 'MU',
|
||||
Peru: 'PE',
|
||||
Ecuador: 'EC',
|
||||
Venezuela: 'VE',
|
||||
Uruguay: 'UY',
|
||||
Paraguay: 'PY',
|
||||
Bolivia: 'BO',
|
||||
Panama: 'PA',
|
||||
'Costa Rica': 'CR',
|
||||
Nicaragua: 'NI',
|
||||
Honduras: 'HN',
|
||||
Guatemala: 'GT',
|
||||
Belize: 'BZ',
|
||||
'El Salvador': 'SV',
|
||||
Cuba: 'CU',
|
||||
Jamaica: 'JM',
|
||||
Haiti: 'HT',
|
||||
'Dominican Republic': 'DO',
|
||||
'Puerto Rico': 'PR',
|
||||
'Trinidad and Tobago': 'TT',
|
||||
Barbados: 'BB',
|
||||
Bahamas: 'BS',
|
||||
Iceland: 'IS',
|
||||
Luxembourg: 'LU',
|
||||
Malta: 'MT',
|
||||
Albania: 'AL',
|
||||
'Bosnia and Herzegovina': 'BA',
|
||||
'North Macedonia': 'MK',
|
||||
Montenegro: 'ME',
|
||||
Slovenia: 'SI',
|
||||
Slovakia: 'SK',
|
||||
Estonia: 'EE',
|
||||
Latvia: 'LV',
|
||||
Lithuania: 'LT',
|
||||
Moldova: 'MD',
|
||||
Armenia: 'AM',
|
||||
Georgia: 'GE',
|
||||
Azerbaijan: 'AZ',
|
||||
Kyrgyzstan: 'KG',
|
||||
Tajikistan: 'TJ',
|
||||
Turkmenistan: 'TM',
|
||||
Mongolia: 'MN',
|
||||
'North Korea': 'KP',
|
||||
Taiwan: 'TW',
|
||||
'Hong Kong': 'HK',
|
||||
Macau: 'MO',
|
||||
Brunei: 'BN',
|
||||
'Papua New Guinea': 'PG',
|
||||
Fiji: 'FJ',
|
||||
'New Caledonia': 'NC',
|
||||
'French Polynesia': 'PF',
|
||||
Samoa: 'WS',
|
||||
Tonga: 'TO',
|
||||
Palau: 'PW',
|
||||
Micronesia: 'FM',
|
||||
'Marshall Islands': 'MH',
|
||||
Kiribati: 'KI',
|
||||
Tuvalu: 'TV',
|
||||
Nauru: 'NR',
|
||||
'Cook Islands': 'CK',
|
||||
Niue: 'NU',
|
||||
Antarctica: 'AQ'
|
||||
};
|
||||
|
||||
// Try exact match first
|
||||
if (countryMap[countryName]) {
|
||||
return countryMap[countryName];
|
||||
}
|
||||
|
||||
// Try case-insensitive match
|
||||
const normalized = countryName.toLowerCase();
|
||||
for (const [key, value] of Object.entries(countryMap)) {
|
||||
if (key.toLowerCase() === normalized) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
// Unknown country — return sentinel rather than a misleading partial string
|
||||
return 'XX';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user