defmodule SimpleCardBrand.Guards do
@moduledoc """
Guard macros.
NOTE: defguard is a macro so the doc tests do not run.
"""
@moduledoc since: "0.1.1"
@doc """
Check the PAN length is in the minimum to maximum allowed length (inclusive).
Use in guards where `pan_length` is known to be an integer.
## Examples
iex> SimpleCardBrand.Guards.pan_range(16, 12, 19)
true
iex> SimpleCardBrand.Guards.pan_range(16, 19, 19)
false
"""
defguard pan_range(pan_length, minimum, maximum) when pan_length in minimum..maximum
@doc """
Compare the first eight (8) digits of the `pan` against the UkrCard range.
Use in guards where `pan` is known to be bytes.
## Examples
iex> SimpleCardBrand.Guards.is_ukrcard("60420099123212")
true
iex> SimpleCardBrand.Guards.is_ukrcard("6042010022222")
false
"""
defguard is_ukrcard(pan)
when binary_part(pan, 0, 8) >= "60400100" and binary_part(pan, 0, 8) <= "60420099"
@doc """
Compare the first six (6) digits of the `pan` against the Verve brand range.
Use in guards where `pan` is known to be bytes.
## Examples
iex> SimpleCardBrand.Guards.is_verve("506099212121")
true
iex> SimpleCardBrand.Guards.is_verve("50609123")
false
"""
defguard is_verve(pan)
when (binary_part(pan, 0, 6) >= "506099" and binary_part(pan, 0, 6) <= "506198") or
(binary_part(pan, 0, 6) >= "507865" and binary_part(pan, 0, 6) <= "507964") or
(binary_part(pan, 0, 6) >= "650002" and binary_part(pan, 0, 6) <= "650027")
end
defmodule SimpleCardBrand do
@moduledoc """
Identify the card brand from the PAN or the first six or eight (UkrCard) PAN digits.
The PAN must contain only digits without leading or trailing spaces.
Supports:
- American Express (:americanexpress)
- BORICA (:borica)
- China T-Union (:chinatunion)
- China UnionPay (:chinaunionpay)
- Dankort (:dankort)
- Diners Club (:dinersclub)
- Diners Club International (:dinersclubinternational)
- Discover (:discover)
- GPN (:gpn)
- Humo (:humo)
- InstaPayment (:instapayment)
- InterPayment (:interpayment)
- JCB: (:jcb)
- LankaPay: (:lankapay)
- Maestro (:maestro)
- Maestro UK (:maestrouk)
- Mastercard (:mastercard)
- Mir (:mir)
- Napas (:napas)
- RuPay (:rupay)
- Troy (:troy)
- UATP (:uatp)
- UkrCard (:ukrcard)
- UzCard (:uzcard)
- Verve (:verve)
- Visa (:visa)
- Visa Electron (:visaelectron)
Rules as per: https://en.wikipedia.org/wiki/Payment_card_number
"""
@moduledoc since: "0.1.1"
import SimpleCardBrand.Guards
@minimum_pan_length 12
@maximum_pan_length 19
@spec card_brand(String.t()) ::
{:error,
{:pan_too_long, String.t()}
| {:pan_too_short, String.t()}
| {:pan_unknown, String.t()}}
| {:ok, atom}
@doc ~S"""
Identify the card brand from the `pan`.
## Examples
iex> SimpleCardBrand.card_brand("4111111111111111")
{:ok, :visa}
iex> SimpleCardBrand.card_brand("6040010012121161819")
{:ok, :ukrcard}
iex> SimpleCardBrand.card_brand("5060994444444416")
{:ok, :verve}
iex> SimpleCardBrand.card_brand("41111111111")
{:error, {:pan_too_short, "Minimum PAN length is 12, found 11."}}
iex> SimpleCardBrand.card_brand("41111111111111111120")
{:error, {:pan_too_long, "Maximum PAN length is 19, found 20."}}
"""
def card_brand(pan) when is_binary(pan) do
pan
|> card_brand(String.length(pan))
end
@spec card_brand(String.t(), integer) ::
{:error,
{:pan_too_long, String.t()}
| {:pan_too_short, String.t()}
| {:pan_unknown, String.t()}}
| {:ok, atom}
@doc ~S"""
Identify the card brand from a full or partial `pan` and the actual PAN length.
Useful when identifying brands from previously-stored PANs.
Check for UkrCard and Verve early.
## Examples
iex> SimpleCardBrand.card_brand("411111", 16)
{:ok, :visa}
iex> SimpleCardBrand.card_brand("6040010012", 18)
{:ok, :ukrcard}
iex> SimpleCardBrand.card_brand("50609944444", 19)
{:ok, :verve}
iex> SimpleCardBrand.card_brand("4111111111111111", 10)
{:error, {:pan_too_short,"Minimum PAN length is 12, found 10."}}
iex> SimpleCardBrand.card_brand("0123456789012345678")
{:error, {:pan_unknown,"Unknown card brand."}}
"""
def card_brand(pan, pan_length)
when is_binary(pan) and is_integer(pan_length) and pan_length in [16, 18, 19] and
is_verve(pan) do
{:ok, :verve}
end
def card_brand(pan, pan_length)
when is_binary(pan) and is_integer(pan_length) and pan_length in [16, 18, 19] and
is_ukrcard(pan) do
{:ok, :ukrcard}
end
def card_brand(pan, pan_length) when is_binary(pan) and is_integer(pan_length) do
cond do
pan_length < @minimum_pan_length ->
{:error,
{:pan_too_short,
"Minimum PAN length is " <>
Integer.to_string(@minimum_pan_length) <>
", found " <> Integer.to_string(pan_length) <> "."}}
pan_length > @maximum_pan_length ->
{:error,
{:pan_too_long,
"Maximum PAN length is " <>
Integer.to_string(@maximum_pan_length) <>
", found " <> Integer.to_string(pan_length) <> "."}}
true ->
_card_brand(String.codepoints(pan), pan_length)
end
end
# China T-Union
defp _card_brand(["3", "1" | _], 19) do
{:ok, :chinatunion}
end
# American Express
defp _card_brand(["3", second | _], 15) when second in ["4", "7"] do
{:ok, :americanexpress}
end
# LankaPay
defp _card_brand(["3", "5", "7", "1", "1", "1" | _], 16) do
{:ok, :lankapay}
end
# RuPay
defp _card_brand(["3", "5", "3" | _], 16) do
{:ok, :rupay}
end
# RuPay
defp _card_brand(["3", "5", "6" | _], 16) do
{:ok, :rupay}
end
# JCB
defp _card_brand(["3", "5" | pan], pan_length) when pan_range(pan_length, 16, 19) do
sub_pan =
Enum.slice(pan, 0, 2)
|> Enum.join()
if "28" <= sub_pan and sub_pan <= "89" do
{:ok, :jcb}
else
{:error, {:pan_unknown, "Unknown card brand."}}
end
end
# Diners Club International
defp _card_brand(["3", "6" | _], pan_length) when pan_range(pan_length, 14, 19) do
{:ok, :dinersclubinternational}
end
# Diners Club International
# 54 is in the Mastercard range. Branded as Diners in US and Canada
defp _card_brand(["5", "4" | _], 16) do
{:ok, :dinersclub}
end
# BORICA
defp _card_brand(["2", "2", "0", "5" | _], 16) do
{:ok, :borica}
end
# Mir
defp _card_brand(["2", "2", "0", fourth | _], pan_length)
when fourth in ["0", "1", "2", "3", "4"] and pan_range(pan_length, 16, 19) do
{:ok, :mir}
end
# Mastercard and GPN starting with 2
defp _card_brand(["2" | pan], 16) do
sub_pan =
Enum.slice(pan, 0, 3)
|> Enum.join()
if "221" <= sub_pan and sub_pan <= "720" do
{:ok, :mastercard}
else
{:ok, :gpn}
end
end
# Mastercard
# 54 cards are branded as Diners in US and Canada.
defp _card_brand(["5", second | _], 16) when second in ["1", "2", "3", "5"] do
{:ok, :mastercard}
end
# Visa Electron
defp _card_brand(["4", "0", "2", "6" | _], 16) do
{:ok, :visaelectron}
end
# Visa Electron
defp _card_brand(["4", "1", "7", "5", "0", "0" | _], 16) do
{:ok, :visaelectron}
end
# Visa Electron
defp _card_brand(["4", "5", "0", "8" | _], 16) do
{:ok, :visaelectron}
end
# Visa Electron
defp _card_brand(["4", "8", "4", "4" | _], 16) do
{:ok, :visaelectron}
end
# Visa Electron
defp _card_brand(["4", "9", "1", fourth | _], 16) when fourth in ["3", "7"] do
{:ok, :visaelectron}
end
# Visa
defp _card_brand(["4" | _], pan_length) when pan_length in [13, 16, 19] do
{:ok, :visa}
end
# Discover
defp _card_brand(["6", "0", "1", "1" | _], pan_length) when pan_range(pan_length, 16, 19) do
{:ok, :discover}
end
# Discover
defp _card_brand(["6", "4", third | _], pan_length)
when third in ["4", "5", "6", "7", "8", "9"] and pan_range(pan_length, 16, 19) do
{:ok, :discover}
end
# Discover
defp _card_brand(["6", "2", "2" | tail], pan_length) when pan_range(pan_length, 16, 19) do
sub_pan =
Enum.slice(tail, 0, 3)
|> Enum.join()
if "126" <= sub_pan and sub_pan <= "925" do
{:ok, :discover}
else
# 62 is ChinaUnionPay
# Both Discover and ChinaUnionPay length can be 16 to 19.
{:ok, :chinaunionpay}
end
end
# Discover
defp _card_brand(["6", "5" | _], pan_length) when pan_range(pan_length, 16, 19) do
{:ok, :discover}
end
# China UnionPay: After Discover 622
defp _card_brand(["6", "2" | _], pan_length) when pan_range(pan_length, 16, 19) do
{:ok, :chinaunionpay}
end
# Maestro UK
# Conflicts with Maestro range.
defp _card_brand(["6", "7", "5", "9" | _], pan_length) when pan_range(pan_length, 12, 19) do
{:ok, :maestrouk}
end
# Maestro UK
defp _card_brand(["6", "7", "6", "7", "7", sixth | _], pan_length)
when sixth in ["0", "4"] and pan_range(pan_length, 12, 19) do
{:ok, :maestrouk}
end
# Maestro
defp _card_brand(["5", "0", "1", "8" | _], pan_length) when pan_range(pan_length, 12, 19) do
{:ok, :maestro}
end
# Maestro
defp _card_brand(["5", "0", "2", "0" | _], pan_length) when pan_range(pan_length, 12, 19) do
{:ok, :maestro}
end
# Maestro
defp _card_brand(["5", "0", "3", "8" | _], pan_length) when pan_range(pan_length, 12, 19) do
{:ok, :maestro}
end
# Maestro
defp _card_brand(["5", "8", "9", "3" | _], pan_length) when pan_range(pan_length, 12, 19) do
{:ok, :maestro}
end
# Maestro
defp _card_brand(["6", "3", "0", "4" | _], pan_length) when pan_range(pan_length, 12, 19) do
{:ok, :maestro}
end
# Maestro
defp _card_brand(["6", "7", "6", fourth | _], pan_length)
when fourth in ["1", "2", "3"] and pan_range(pan_length, 12, 19) do
{:ok, :maestro}
end
# UATP
defp _card_brand(["1" | _], 15) do
{:ok, :uatp}
end
# Dankort
defp _card_brand(["5", "0", "1", "9" | _], 16) do
{:ok, :dankort}
end
# RuPay
defp _card_brand(["6", "0" | _], 16) do
{:ok, :rupay}
end
# RuPay
defp _card_brand(["8", "1" | _], 16) do
{:ok, :rupay}
end
# RuPay
defp _card_brand(["8", "2" | _], 16) do
{:ok, :rupay}
end
# RuPay
defp _card_brand(["5", "0", "8" | _], 16) do
{:ok, :rupay}
end
# InstaPayment
defp _card_brand(["6", "3", third | _], pan_length)
when third in ["7", "8", "9"] and pan_range(pan_length, 16, 19) do
{:ok, :instapayment}
end
# InterPayment
defp _card_brand(["6", "3", "6" | _], pan_length) when pan_range(pan_length, 16, 19) do
{:ok, :interpayment}
end
# UzCard
defp _card_brand(["8", "6", "0", "0" | _], 16) do
{:ok, :uzcard}
end
# Napas
defp _card_brand(["9", "7", "0", "4" | _], 16) do
{:ok, :napas}
end
# Napas
defp _card_brand(["9", "7", "9", "2" | _], 16) do
{:ok, :troy}
end
# Humo
defp _card_brand(["9", "8", "6", "0" | _], 16) do
{:ok, :humo}
end
# GPN starting with 2 handled by Mastercard pattern match.
defp _card_brand([first | _], 16) when first in ["1", "6", "7", "8", "9"] do
{:ok, :gpn}
end
# Error
defp _card_brand(_, _) do
{:error, {:pan_unknown, "Unknown card brand."}}
end
end