defmodule ZambiaMobileNetworks do
@moduledoc """
Identify Zambian mobile networks (Airtel, MTN, Zamtel, Zed Mobile) from a phone number.
The operator prefix is the two digits after the leading `0` or `260` country code.
| Network | Local | With country code |
|------------|--------------------|---------------------------|
| Airtel | 097, 077, 057 | 26097, 26077, 26057 |
| MTN | 096, 076, 056 | 26096, 26076, 26056 |
| Zamtel | 095, 075, 055 | 26095, 26075, 26055 |
| Zed Mobile | 098, 078\\*, 058\\* | 26098, 26078\\*, 26058\\* |
\\* `078`/`058` are reserved for Zed Mobile but not yet officially enabled by the
network provider; they are detected pre-emptively.
"""
@prefixes %{
airtel: ~w(97 77 57),
mtn: ~w(96 76 56),
zamtel: ~w(95 75 55),
# 78/58 reserved but not yet officially enabled by Zed Mobile; detected pre-emptively.
zed: ~w(98 78 58)
}
@type network :: :airtel | :mtn | :zamtel | :zed
@doc "Returns the supported networks."
@spec networks() :: [network()]
def networks, do: Map.keys(@prefixes)
@doc "Operator prefixes for a network."
@spec prefixes(network()) :: [String.t()]
def prefixes(network), do: Map.get(@prefixes, network, [])
@doc "Human-readable label for a network."
@spec label(network()) :: String.t()
def label(:airtel), do: "Airtel"
def label(:mtn), do: "MTN"
def label(:zamtel), do: "Zamtel"
def label(:zed), do: "Zed Mobile"
@doc """
Detects the network for an MSISDN, or `nil` if unrecognised.
Accepts local (`0XY…`) and country-code (`260XY…`, `+260 XY…`) formats.
## Examples
iex> ZambiaMobileNetworks.detect("0971234567")
:airtel
iex> ZambiaMobileNetworks.detect("260761234567")
:mtn
iex> ZambiaMobileNetworks.detect("+260 95 123 4567")
:zamtel
iex> ZambiaMobileNetworks.detect("0981234567")
:zed
iex> ZambiaMobileNetworks.detect("0911234567")
nil
"""
@spec detect(String.t()) :: network() | nil
def detect(msisdn) when is_binary(msisdn) do
prefix =
msisdn
|> String.replace(~r/\D/, "")
|> strip_country_code()
|> String.slice(0, 2)
Enum.find_value(@prefixes, fn {network, prefixes} ->
if prefix in prefixes, do: network
end)
end
def detect(_), do: nil
@doc """
Whether the MSISDN belongs to the given network.
## Examples
iex> ZambiaMobileNetworks.supports?("0961234567", :mtn)
true
iex> ZambiaMobileNetworks.supports?("0961234567", :airtel)
false
"""
@spec supports?(String.t(), network()) :: boolean()
def supports?(msisdn, network), do: detect(msisdn) == network
@doc """
Returns the network logo as a base64 `data:` URI (200x200 PNG), or `nil` for an
unknown network.
Pass `custom` (a file path or raw image binary) to use your own logo instead of
the bundled one. The custom image is returned encoded as-is — it is not resized.
iex> "data:image/png;base64," <> _ = ZambiaMobileNetworks.logo(:mtn)
"""
@spec logo(network(), Path.t() | binary() | nil) :: String.t() | nil
def logo(network, custom \\ nil)
def logo(_network, custom) when is_binary(custom) do
bin = if File.regular?(custom), do: File.read!(custom), else: custom
"data:image/png;base64," <> Base.encode64(bin)
end
def logo(network, nil) when network in [:airtel, :mtn, :zamtel, :zed] do
path = Application.app_dir(:zambia_mobile_networks, "priv/logos/#{network}.png")
"data:image/png;base64," <> Base.encode64(File.read!(path))
end
def logo(_network, nil), do: nil
defp strip_country_code("260" <> rest), do: rest
defp strip_country_code("0" <> rest), do: rest
defp strip_country_code(digits), do: digits
end