Credit Card Validator
Iām learning Elixir and looking to experiment with different kind of scripts
I follow the specification defined here : https://www.ibm.com/docs/en/order-management-sw/9.3.0?topic=cpms-handling-credit-cards And used the card number from stripe: https://stripe.com/docs/testing?testing-method=card-numbers
The Code
defmodule Card.Validator do
@moduledoc """
Validate visa and mastercard cards
"""
@doc """
Check based on set of rules if the provided card number is a visa or mastercard
## Examples
iex> card_type("4624748233249780")
{:ok, :visa, "4624748233249780"}
iex> card_type("5105105105105100")
{:ok, :mastercard, "5105105105105100"}
iex> card_type("6105105105105100")
{:error, :invalid_card_number}
"""
@spec card_type(credit_card_number :: String.t()) ::
{:error, :invalid_card_number}
| {:ok, :mastercard | :visa, credit_card_number :: String.t()}
def card_type(credit_card_number) do
first = credit_card_number |> String.first() |> String.to_integer()
first_two = credit_card_number |> String.slice(0..1) |> String.to_integer()
cond do
visa?(first) &&
(len13?(credit_card_number) || len16?(credit_card_number)) ->
{:ok, :visa, credit_card_number}
mastercard?(first_two) && len16?(credit_card_number) ->
{:ok, :mastercard, credit_card_number}
true ->
{:error, :invalid_card_number}
end
end
@doc """
Check length of credit card number, expecting 13 characters
"""
@spec len13?(credit_card_number :: String.t()) :: boolean()
def len13?(credit_card_number) when byte_size(credit_card_number) == 13, do: true
def len13?(_), do: false
@doc """
Check length of credit card number, expecting 16 characters
"""
@spec len16?(credit_card_number :: String.t()) :: boolean()
def len16?(credit_card_number) when byte_size(credit_card_number) == 16, do: true
def len16?(_), do: false
@doc """
Check credit card number if it is mastercard, prefix must be between 51 through 55
"""
@spec mastercard?(first_two :: integer()) :: boolean()
def mastercard?(first_two) when first_two in 51..55, do: true
def mastercard?(_), do: false
@doc """
Check credit card number if it is visa, prefix must be 4
"""
@spec visa?(first_two :: integer()) :: boolean()
def visa?(first) when first == 4, do: true
def visa?(_), do: false
@doc """
Apply Luhn's algorithm on the credit card number
## Examples
iex> Card.Validator.verify_digit("4624748233249080")
{50, 23}
"""
@spec verify_digit(credit_card_number :: String.t(), integer(), integer()) ::
{integer(), integer()} | {credit_card_number :: String.t(), integer(), integer()}
def verify_digit(credit_card_number, acc \\ 0, remaining \\ 0)
def verify_digit(credit_card_number, acc, remaining) when byte_size(credit_card_number) > 0 do
acc =
credit_card_number
|> String.at(-2)
|> String.to_integer()
|> Kernel.*(2)
|> Integer.digits()
|> Enum.sum()
|> Kernel.+(acc)
remaining =
credit_card_number
|> String.at(-1)
|> String.to_integer()
|> Kernel.+(remaining)
verify_digit(String.slice(credit_card_number, 0..-3//1), acc, remaining)
end
def verify_digit(_, acc, remaining), do: {acc, remaining}
@doc """
Check mod 10 on sum
"""
@spec mod?(integer()) :: boolean()
def mod?(total) when rem(total, 10) == 0, do: true
def mod?(total) when rem(total, 10) != 0, do: false
@doc """
Verify Credit card number and determine card type
## Examples
iex> Card.Validator.verify_digits("4624748233249080")
{:error, :invalid_card_number}
iex> Card.Validator.verify_digits("4624748233249780")
{:ok, "4624748233249780", :visa}
iex> Card.Validator.verify_digits("4242424242424242")
{:ok, "4242424242424242", :visa}
iex> Card.Validator.verify_digits("4000056655665556")
{:ok, "4000056655665556", :visa}
iex> Card.Validator.verify_digits("5555555555554444")
{:ok, "5555555555554444", :mastercard}
iex> Card.Validator.verify_digits("5105105105105100")
{:ok, "5105105105105100", :mastercard}
iex> Card.Validator.verify_digits("5200828282828210")
{:ok, "5200828282828210", :mastercard}
"""
@spec verify_digits(credit_card_number :: String.t()) ::
{:error, atom()} | {:ok, credit_card_number :: String.t(), type :: :visa | :mastercard}
def verify_digits(credit_card_number) do
credit_card_number
|> card_type()
|> case do
{:ok, type, _} ->
{acc, remaining} = verify_digit(credit_card_number)
sum = acc + remaining
if(mod?(sum)) do
{:ok, credit_card_number, type}
else
{:error, :invalid_card_number}
end
{:error, reason} ->
{:error, reason}
end
end
end
mix docs
mix test
Erlang/OTP 25 [erts-13.0] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]
Mix 1.14.0 (compiled with Erlang/OTP 25)
Let me know how to improve this code !