PhoenixKit.Utils.CountryData (phoenix_kit v2.3.0)

Copy Markdown View Source

Wrapper for BeamLabCountries with country data utility functions.

Provides a convenient API for working with country data: country selection, tax rates, EU membership.

Includes workaround for charlist bug in VAT rates until fixed upstream.

Examples

# Get list of countries for dropdown
countries = CountryData.countries_for_select()
# [{"🇦🇩 Andorra", "AD"}, {"🇦🇪 United Arab Emirates", "AE"}, ...]

# Get standard VAT rate
rate = CountryData.get_standard_vat_rate("EE")
# #Decimal<0.20>

# Check EU membership
CountryData.eu_member?("EE")
# true

# Get country information
country = CountryData.get_country("DE")
# %BeamLabCountries.Country{name: "Germany", ...}

# Format company address from Settings
address = CountryData.format_company_address()
# "123 Business Street\nTallinn 10115\nEstonia"

Summary

Functions

Get list of countries for select dropdown.

Get list of EEA countries (EU + Norway, Iceland, Liechtenstein).

Check if country is an EEA (European Economic Area) member.

Get list of EU countries.

Get list of EU countries for select dropdown.

Check if country is an EU member.

Check if country with given code exists.

Format company address from Settings for document printing.

Get bank details from consolidated Settings.

Get company information from consolidated Settings.

Get country by alpha-2 code.

Get country name in the active locale.

Get country currency code.

Get country flag (emoji).

Get standard VAT rate as percentage (integer).

Get standard VAT rate for a country as Decimal.

Get the subdivision label for a country.

Get the unified tax configuration from Organization settings.

Get all VAT rates with workaround for charlist bug.

Keep only the codes that name a real country, in the order given.

Get all countries sorted by name.

Split an operator-entered priority string into alpha-2 codes.

Suggest a starting priority list for a host based in country_code.

Validate IBAN format (length based on bank country, not company country).

Validate SWIFT/BIC format (8 or 11 characters).

Functions

countries_for_select(opts \\ [])

Get list of countries for select dropdown.

Returns list of tuples {display_name, alpha2_code} for use in Phoenix form selects, sorted by the country name in the active locale.

Names come from BeamLabCountries.Translations, not the country struct's :name field — the two differ even in English, for 20 of the 250 countries (as of beamlab_countries 1.1.0). For example: GB "United Kingdom of Great Britain and Northern Ireland" -> "United Kingdom", US "United States of America" -> "United States", CZ "Czech Republic" -> "Czechia", TR "Turkey" -> "Türkiye", KP "Korea (Democratic People's Republic of)" -> "North Korea" (which also moves its place in the sorted list, from the K's to the N's). A host that never passes :locale still gets different strings, and a different order, than a version of this function that read .name directly.

Sorting folds accented letters to their base form before comparing (e.g. "ü" sorts with "u"), which is a deliberate approximation, not proper collation — the BEAM has no ICU. It is correct for most Latin-script locales, but wrong for locales that give diacritics their own place in the alphabet: in Estonian, Ü belongs at the very end (after W, Õ, Ä, Ö), and in Swedish, Å, Ä, Ö belong after Z; folding moves those names out of that position instead of leaving them there. A host that needs strict local collation should sort the returned list itself.

Options

  • :locale — locale for the country names. Defaults to the active PhoenixKitWeb.Gettext locale, reduced to its base code ("ru-RU" normalizes to "ru"). BeamLabCountries 1.1.0 ships translations for ar, de, en, es, fr, it, ja, ko, nl, pl, pt, ru, sv, uk, zh (BeamLabCountries.Translations.supported_locales/0); any other locale falls back to the country's English name, and so does an unsupported value such as an atom (locale: :ru) — a host only ever loses the translation, never the entry.

  • :priority — alpha-2 codes pinned to the top of the list, in the order given; everything else follows alphabetically. A non-list value is treated as []. The default is the country_select_priority setting (Admin → Settings → Organization), and nothing else: there is no compile-time config, deliberately. A default baked into the library would pin whatever countries its author serves, so every other host would install it and find the dropdown already reordered before anyone chose anything. Until an operator stores a list, nothing is pinned and the order is plain alphabetical; suggested_priority/2 is what the settings UI offers them as a starting point, derived from their own country rather than from a constant.

opts itself must be a keyword list — a map or a bare string raises FunctionClauseError naming this function rather than Keyword.

Examples

iex> countries = CountryData.countries_for_select(locale: "en")
iex> {"🇦🇫 Afghanistan", "AF"} in countries
true

iex> CountryData.countries_for_select(locale: "ru", priority: ["EE", "FI"])
...> |> Enum.take(2)
[{"🇪🇪 Эстония", "EE"}, {"🇫🇮 Финляндия", "FI"}]

eea_countries()

Get list of EEA countries (EU + Norway, Iceland, Liechtenstein).

eea_member?(country_code)

Check if country is an EEA (European Economic Area) member.

EEA includes EU + Norway, Iceland, Liechtenstein.

Examples

iex> CountryData.eea_member?("EE")
true

iex> CountryData.eea_member?("NO")
true

iex> CountryData.eea_member?("CH")
false

eu_countries()

Get list of EU countries.

Examples

iex> eu = CountryData.eu_countries()
iex> length(eu)
27
iex> Enum.map(eu, & &1.alpha2) |> Enum.sort() |> Enum.take(5)
["AT", "BE", "BG", "CY", "CZ"]

eu_countries_for_select(opts \\ [])

Get list of EU countries for select dropdown.

Takes the same :locale and :priority options as countries_for_select/1, including its fallback, leniency, and sorting caveats.

eu_member?(country_code)

Check if country is an EU member.

Examples

iex> CountryData.eu_member?("EE")
true

iex> CountryData.eu_member?("GB")
false

iex> CountryData.eu_member?("US")
false

exists?(country_code)

Check if country with given code exists.

Examples

iex> CountryData.exists?("EE")
true

iex> CountryData.exists?("XX")
false

format_company_address()

Format company address from Settings for document printing.

Assembles address from individual fields (address_line1, address_line2, city, state, postal_code, country) into a single string with line breaks.

Returns

Formatted address as string, for example:

123 Business Street
Suite 100
Tallinn 10115
Estonia

Examples

iex> CountryData.format_company_address()
"123 Business Street\nTallinn 10115\nEstonia"

get_bank_details()

Get bank details from consolidated Settings.

Reads from company_bank_details JSONB with fallback to legacy billing_bank_* keys.

get_company_info()

Get company information from consolidated Settings.

Reads from company_info JSONB with fallback to legacy billing_company_* keys.

get_country(code)

Get country by alpha-2 code.

Examples

iex> country = CountryData.get_country("EE")
iex> country.name
"Estonia"

iex> CountryData.get_country("XX")
nil

get_country_name(country_code, opts \\ [])

Get country name in the active locale.

Takes the same :locale option as countries_for_select/1 — including its supported-locale set and its fallback/leniency rules — and falls back to the English name when that locale has no translation for the country.

opts must be a keyword list. get_country_name("EE", "ru") is a realistic slip (the option is :locale), and raises FunctionClauseError naming this function rather than Keyword.

Examples

iex> CountryData.get_country_name("EE", locale: "en")
"Estonia"

iex> CountryData.get_country_name("EE", locale: "ru")
"Эстония"

iex> CountryData.get_country_name("XX")
nil

get_currency_code(country_code)

Get country currency code.

Examples

iex> CountryData.get_currency_code("EE")
"EUR"

iex> CountryData.get_currency_code("GB")
"GBP"

iex> CountryData.get_currency_code("US")
"USD"

get_flag(country_code)

Get country flag (emoji).

Examples

iex> CountryData.get_flag("EE")
"🇪🇪"

get_standard_vat_percent(country_code)

Get standard VAT rate as percentage (integer).

Returns rate as percentage (20 = 20%).

Examples

iex> CountryData.get_standard_vat_percent("EE")
20

iex> CountryData.get_standard_vat_percent("DE")
19

iex> CountryData.get_standard_vat_percent("US")
0

get_standard_vat_rate(country_code)

Get standard VAT rate for a country as Decimal.

Returns rate in decimal format (0.20 = 20%). If country not found or has no VAT rates, returns 0.

Examples

iex> CountryData.get_standard_vat_rate("EE")
#Decimal<0.20>

iex> CountryData.get_standard_vat_rate("DE")
#Decimal<0.19>

iex> CountryData.get_standard_vat_rate("US")
#Decimal<0>

get_subdivision_label(alpha2)

Get the subdivision label for a country.

Returns appropriate label like "State", "Province", "Region", etc. based on what the country uses for administrative divisions.

Examples

iex> CountryData.get_subdivision_label("US")
"State"

iex> CountryData.get_subdivision_label("CA")
"Province"

iex> CountryData.get_subdivision_label("EE")
"County"

get_tax_config()

Get the unified tax configuration from Organization settings.

Returns a map with:

  • :enabled - boolean, whether tax is enabled
  • :rate - string percentage (e.g. "20")
  • :rate_decimal - Decimal fraction (e.g. Decimal.new("0.20"))

Tax rate is stored in the company_info JSON setting under "tax_rate" and "tax_enabled" keys. Falls back to billing_default_tax_rate / billing_tax_enabled for backward compatibility.

get_vat_rates(country_code)

Get all VAT rates with workaround for charlist bug.

Returns map with normalized rates:

  • :standard - standard rate (integer)
  • :reduced - reduced rates (list of integers)
  • :super_reduced - super reduced rate (integer or nil)
  • :parking - parking rate (integer or nil)

Examples

iex> CountryData.get_vat_rates("EE")
%{standard: 20, reduced: [9], super_reduced: nil, parking: nil}

iex> CountryData.get_vat_rates("FR")
%{standard: 20, reduced: [5.5, 10], super_reduced: 2.1, parking: nil}

iex> CountryData.get_vat_rates("US")
nil

known_country_codes(codes)

Keep only the codes that name a real country, in the order given.

The counterpart of parse_priority/1 for a settings form: it tells the operator which of the codes they typed will actually pin something.

Examples

iex> CountryData.known_country_codes(["EE", "ZZ", "FI"])
["EE", "FI"]

list_countries()

Get all countries sorted by name.

Examples

iex> countries = CountryData.list_countries()
iex> length(countries)
250
iex> hd(countries).name
"Afghanistan"

parse_priority(value)

Split an operator-entered priority string into alpha-2 codes.

Accepts the separators a human actually types — commas, spaces, semicolons, newlines — so "EE, FI", "ee fi" and "EE;FI" all parse. Unknown codes are kept here, not dropped — normalize_priority/1 only filters non-binaries, upcases, and dedupes. They are dropped later, when split_priority/2 looks each one up against the real country list and finds no match; use known_country_codes/1 to report them to the operator before that happens.

Examples

iex> CountryData.parse_priority("EE, FI ; lv")
["EE", "FI", "LV"]

iex> CountryData.parse_priority("")
[]

suggested_priority(country_code, opts \\ [])

Suggest a starting priority list for a host based in country_code.

Returns that country first, then its nearest neighbours by great-circle distance between country centroids — so a host in Estonia is offered Latvia, Åland, Finland and Lithuania, one in Germany gets Luxembourg, the Netherlands, Czechia and Belgium, and one in Singapore gets Malaysia, Indonesia, Cambodia and Brunei. The point is that it is derived from the host's own data rather than from a constant baked in by whoever wrote the library.

This is a suggestion for the settings UI to offer, never applied on its own: nothing is pinned until an operator stores a list. The result can include dependent territories (Åland is the second-nearest thing to Estonia) — the data has no "sovereign state" flag — so the operator is expected to prune it.

Dissolved countries are excluded. Countries with no coordinates cannot be ranked and are skipped.

Options

  • :limit — how many neighbours to add after the country itself. Defaults to 4.

Examples

iex> CountryData.suggested_priority("EE", limit: 2)
["EE", "LV", "AX"]

iex> CountryData.suggested_priority("XX")
[]

validate_iban_format(iban, arg2)

Validate IBAN format (length based on bank country, not company country).

Bank can be in a different country than the company - this is legal. Validates format and length based on IBAN's country prefix.

Returns :ok or {:error, reason}.

Examples

iex> CountryData.validate_iban_format("EE382200221020145685", "EE")
:ok

iex> CountryData.validate_iban_format("DE89370400440532013000", "EE")
:ok  # German bank for Estonian company is valid

iex> CountryData.validate_iban_format("DE123", "EE")
{:error, "IBAN must be 22 characters for DE"}

validate_swift_format(swift)

Validate SWIFT/BIC format (8 or 11 characters).

SWIFT codes structure:

  • 4 letters: bank code
  • 2 letters: country code (ISO 3166)
  • 2 characters: location code
  • 3 characters (optional): branch code

Examples

iex> CountryData.validate_swift_format("HABAEE2X")
:ok

iex> CountryData.validate_swift_format("HABAEE2XXXX")
:ok

iex> CountryData.validate_swift_format("INVALID")
{:error, "SWIFT/BIC must be 8 or 11 characters"}