Webux Lab

By Studio Webux

Learning Elixir - Card Validator

TG
Tommy Gingras Studio Webux 2023-09-03

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 !


Search