Skip to content

Commit afd14e7

Browse files
author
Mawuli Adzaku
committed
Make it so
1 parent d2734a8 commit afd14e7

File tree

9 files changed

+388
-0
lines changed

9 files changed

+388
-0
lines changed

.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/_build
2+
/cover
3+
/deps
4+
/docs
5+
erl_crash.dump
6+
*.ez

.travis.yml

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
sudo: false
2+
language: elixir
3+
script:
4+
- mix test
5+
before_script:
6+
- mix local.hex --force
7+
- mix deps.get
8+
after_script:
9+
- mix deps.get --only docs
10+
- MIX_ENV=docs mix inch.report

README.md

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# CreditCard
2+
3+
[![Build Status](https://travis-ci.org/abakhi/credit_card.svg?branch=master)](https://travis-ci.org/abakhi/credit_card)
4+
5+
6+
Elixir library for validating credit card numbers (a port of [credit_card_validator](https://github.com/tobias/credit_card_validator) Ruby gem)
7+
8+
9+
## Documentation
10+
11+
API documentation can be found at [http://hexdocs.pm/credit_card](http://hexdocs.pm/credit_card)
12+
13+
## TODO
14+
15+
Incorporate some ideas from [Card](https://github.com/jessepollak/card)
16+
17+
* Check CVC length and number length
18+
* Differentiate between VISA and VISA electron
19+
20+
## Installation
21+
22+
The package can be installed as:
23+
24+
1. Add credit_card to your list of dependencies in `mix.exs`:
25+
26+
def deps do
27+
[{:credit_card, "~> 0.0.1"}]
28+
end

config/config.exs

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# This file is responsible for configuring your application
2+
# and its dependencies with the aid of the Mix.Config module.
3+
use Mix.Config
4+
5+
# This configuration is loaded before any dependency and is restricted
6+
# to this project. If another project depends on this project, this
7+
# file won't be loaded nor affect the parent project. For this reason,
8+
# if you want to provide default values for your application for
9+
# 3rd-party users, it should be done in your "mix.exs" file.
10+
11+
# You can configure for your application as:
12+
#
13+
# config :credit_card, key: :value
14+
#
15+
# And access this configuration in your application as:
16+
#
17+
# Application.get_env(:credit_card, :key)
18+
#
19+
# Or configure a 3rd-party app:
20+
#
21+
# config :logger, level: :info
22+
#
23+
24+
# It is also possible to import configuration files, relative to this
25+
# directory. For example, you can emulate configuration per environment
26+
# by uncommenting the line below and defining dev.exs, test.exs and such.
27+
# Configuration from the imported file will override the ones defined
28+
# here (which is why it is important to import them last).
29+
#
30+
# import_config "#{Mix.env}.exs"

lib/credit_card.ex

+177
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
defmodule CreditCard do
2+
@moduledoc """
3+
Credit card validations library
4+
"""
5+
@card_types [
6+
visa: ~r/^4[0-9]{12}(?:[0-9]{3})?$/,
7+
master_card: ~r/^5[1-5][0-9]{14}$/,
8+
maestro: ~r/(^6759[0-9]{2}([0-9]{10})$)|(^6759[0-9]{2}([0-9]{12})$)|(^6759[0-9]{2}([0-9]{13})$)/,
9+
diners_club: ~r/^3(?:0[0-5]|[68][0-9])[0-9]{11}$/,
10+
amex: ~r/^3[47][0-9]{13}$/,
11+
discover: ~r/^6(?:011|5[0-9]{2})[0-9]{12}$/,
12+
jcb: ~r/^(?:2131|1800|35\d{3})\d{11}$/
13+
]
14+
15+
@test_numbers [
16+
amex: ~w(378282246310005 371449635398431 378734493671000 ),
17+
diners_club: ~w(30569309025904 38520000023237 ),
18+
discover: ~w(6011000990139424 6011111111111117 ),
19+
master_card: ~w(5555555555554444 5105105105105100 ),
20+
visa: ~w(
21+
4111111111111111 4012888888881881 4222222222222
22+
4005519200000004 4009348888881881 4012000033330026
23+
4012000077777777 4217651111111119 4500600000000061
24+
4000111111111115 ),
25+
jcb: ~w(3530111333300000 3566002020360505)
26+
] |> Keyword.values |> List.flatten
27+
28+
@opts %{
29+
allowed_card_types: Keyword.keys(@card_types),
30+
test_numbers_are_valid: false
31+
}
32+
33+
@type opts :: %{atom => String.t}
34+
@type validation_error :: {:error, :unrecognized_card_type}
35+
36+
@doc """
37+
Returns CreditCard options map.
38+
"""
39+
@spec new :: opts
40+
def new, do: @opts
41+
42+
@doc """
43+
Returns True if card matches the allowed card patterns. By default,
44+
the set of cards matched against are Amex, VISA, MasterCard, Discover,
45+
Diners Club and JCB. Otherwise, false
46+
47+
## Examples
48+
49+
iex> CreditCard.valid?("4000111111111115")
50+
false
51+
"""
52+
@spec valid?(String.t, opts) :: boolean
53+
def valid?(number, opts \\ @opts) do
54+
case card_type(number) do
55+
{:error, _err_msg} ->
56+
false
57+
type when is_atom(type) ->
58+
valid_card? = (is_allowed_card_type?(number, opts)
59+
&& verify_luhn(number))
60+
valid_card? &&
61+
(if opts[:test_numbers_are_valid] do
62+
true
63+
else
64+
!is_test_number(number)
65+
end)
66+
end
67+
end
68+
69+
@doc """
70+
Returns true if the validation options is set to recognize the card's class.
71+
Otherwise, false.
72+
73+
## Examples
74+
75+
iex> CreditCard.is_allowed_card_type?("5555555555554444")
76+
true
77+
"""
78+
@spec is_allowed_card_type?(String.t, opts) :: boolean
79+
def is_allowed_card_type?(number, opts \\ @opts) do
80+
case card_type(number) do
81+
{:error, _err_msg} ->
82+
false
83+
type when is_atom(type) ->
84+
allowed? = (opts[:allowed_card_types]
85+
&& (type in opts[:allowed_card_types]))
86+
if allowed?, do: true, else: false
87+
end
88+
end
89+
90+
@doc """
91+
Returns true if number is among the list of test credit card numbers
92+
;otherwise, it returns false.
93+
"""
94+
@spec is_test_number(String.t) :: boolean
95+
def is_test_number(number) do
96+
strip(number) in @test_numbers
97+
end
98+
99+
@doc """
100+
Performs luhn check on the credit card number
101+
"""
102+
@spec verify_luhn(String.t) :: boolean
103+
def verify_luhn(number) do
104+
total =
105+
number |> strip |> String.reverse |> String.split("")
106+
|> Enum.reject(&(&1 == ""))
107+
|> Enum.reduce([0,0], fn x, acc ->
108+
n = String.to_integer(x)
109+
[acc_h, acc_t] = Enum.slice(acc, 0, 2)
110+
val = (if((rem(acc_t,2) == 1), do: rotate(n*2), else: n))
111+
[acc_h + val, acc_t + 1|acc]
112+
end)
113+
114+
rem(Enum.at(total,0),10) == 0
115+
end
116+
117+
@doc """
118+
Returns the type of card
119+
120+
## Examples
121+
122+
iex> CreditCard.card_type("3566002020360505")
123+
:jcb
124+
"""
125+
@spec card_type(String.t) :: atom | validation_error
126+
def card_type(number) do
127+
card_type = (Keyword.keys(@card_types)
128+
|> Enum.filter(&(card_is(strip(number), &1))))
129+
case card_type do
130+
[h|_] -> h
131+
_ -> {:error, :unrecognized_card_type}
132+
end
133+
end
134+
135+
@spec strip(String.t) :: String.t
136+
defp strip(number) do
137+
number
138+
|> String.split("")
139+
|> Enum.reject(&(&1 in ["", " "]))
140+
|> Enum.join("")
141+
end
142+
143+
@spec card_is(String.t, atom) :: boolean
144+
def card_is(number, :visa) do
145+
@card_types[:visa] |> Regex.match?(number)
146+
end
147+
148+
def card_is(number, :master_card) do
149+
@card_types[:master_card] |> Regex.match?(number)
150+
end
151+
152+
def card_is(number, :maestro) do
153+
@card_types[:maestro] |> Regex.match?(number)
154+
end
155+
156+
def card_is(number, :diners_club) do
157+
@card_types[:diners_club] |> Regex.match?(number)
158+
end
159+
160+
def card_is(number, :discover) do
161+
@card_types[:discover] |> Regex.match?(number)
162+
end
163+
164+
def card_is(number, :amex) do
165+
@card_types[:amex] |> Regex.match?(number)
166+
end
167+
168+
def card_is(number, :jcb) do
169+
@card_types[:jcb] |> Regex.match?(number)
170+
end
171+
172+
@spec rotate(String.t) :: String.t
173+
defp rotate(number) do
174+
if number > 9, do: number = rem(number,10) + 1
175+
number
176+
end
177+
end

mix.exs

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
defmodule CreditCard.Mixfile do
2+
use Mix.Project
3+
4+
def project do
5+
[app: :credit_card,
6+
version: "0.0.1",
7+
description: "A library for validating credit card numbers",
8+
source_url: "https://github.com/abakhi/credit_card",
9+
package: package,
10+
elixir: "~> 1.0",
11+
build_embedded: Mix.env == :prod,
12+
start_permanent: Mix.env == :prod,
13+
deps: deps]
14+
end
15+
16+
# Configuration for the OTP application
17+
#
18+
# Type "mix help compile.app" for more information
19+
def application do
20+
[applications: [:logger]]
21+
end
22+
23+
# Dependencies can be Hex packages:
24+
#
25+
# {:mydep, "~> 0.3.0"}
26+
#
27+
# Or git/path repositories:
28+
#
29+
# {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
30+
#
31+
# Type "mix help deps" for more examples and options
32+
defp deps do
33+
[{:earmark, "~> 0.1", only: :docs},
34+
{:ex_doc, "~> 0.10", only: :docs},
35+
{:inch_ex, "~> 0.2", only: :docs},]
36+
end
37+
38+
defp package do
39+
[
40+
maintainers: ["Mawuli Adzaku", "Kirk S. Agbenyegah"],
41+
licenses: ["MIT"],
42+
links: %{
43+
"GitHub" => "https://github.com/abakhi/credit_card",
44+
"Documentation" => "http://hexdocs.pm/credit_card"
45+
}
46+
]
47+
end
48+
end

mix.lock

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
%{"earmark": {:hex, :earmark, "0.1.19"},
2+
"ex_doc": {:hex, :ex_doc, "0.10.0"},
3+
"inch_ex": {:hex, :inch_ex, "0.4.0"},
4+
"poison": {:hex, :poison, "1.5.0"}}

test/credit_card_test.exs

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
defmodule CreditCardTest do
2+
use ExUnit.Case
3+
doctest CreditCard
4+
5+
@card_opts CreditCard.new
6+
7+
test "recognize card type" do
8+
assert CreditCard.card_type("4111111111111111") == :visa
9+
assert CreditCard.card_type("5555555555554444") == :master_card
10+
assert CreditCard.card_type("30569309025904") == :diners_club
11+
assert CreditCard.card_type("371449635398431") == :amex
12+
assert CreditCard.card_type("6011000990139424") == :discover
13+
assert CreditCard.card_type("6759671431256542") == :maestro
14+
assert CreditCard.card_type("3530111333300000") == :jcb
15+
end
16+
17+
test "detect specific types" do
18+
assert CreditCard.card_is("4111111111111111", :visa)
19+
assert !CreditCard.card_is("5555555555554444", :visa)
20+
assert !CreditCard.card_is("30569309025904", :visa)
21+
assert !CreditCard.card_is("371449635398431", :visa)
22+
assert !CreditCard.card_is("6011000990139424", :visa)
23+
assert CreditCard.card_is("5555555555554444", :master_card)
24+
assert CreditCard.card_is("30569309025904", :diners_club)
25+
assert CreditCard.card_is("371449635398431", :amex)
26+
assert CreditCard.card_is("6011000990139424", :discover)
27+
assert CreditCard.card_is("6759671431256542", :maestro)
28+
assert !CreditCard.card_is("5555555555554444", :maestro)
29+
assert !CreditCard.card_is("30569309025904", :maestro)
30+
assert CreditCard.card_is("3530111333300000", :jcb)
31+
assert !CreditCard.card_is("6759671431256542", :jcb)
32+
end
33+
34+
test "luhn verification" do
35+
assert CreditCard.verify_luhn("49927398716")
36+
assert CreditCard.verify_luhn("049927398716")
37+
assert CreditCard.verify_luhn("0049927398716")
38+
assert !CreditCard.verify_luhn("49927398715")
39+
assert !CreditCard.verify_luhn("49927398717")
40+
end
41+
42+
test "ignore whitespace" do
43+
assert :visa == CreditCard.card_type("4111 1111 1111 1111 ")
44+
assert :visa == CreditCard.card_type(" 4111 1111 1111 1111 ")
45+
assert CreditCard.verify_luhn(" 004 992739 87 16")
46+
assert CreditCard.is_test_number("601 11111111111 17")
47+
end
48+
49+
test "should recognize test numbers" do
50+
~w(
51+
378282246310005 371449635398431 378734493671000
52+
30569309025904 38520000023237 6011111111111117
53+
6011000990139424 5555555555554444 5105105105105100
54+
4111111111111111 4012888888881881 4222222222222
55+
3530111333300000 3566002020360505
56+
) |> Enum.each(&(assert CreditCard.is_test_number(&1)))
57+
58+
assert !CreditCard.is_test_number("1234")
59+
end
60+
61+
test "test number validity cases" do
62+
assert !CreditCard.valid?("378282246310005")
63+
opts = %{@card_opts|test_numbers_are_valid: true}
64+
assert CreditCard.valid?("378282246310005", opts)
65+
end
66+
67+
test "is allowed card type" do
68+
assert CreditCard.is_allowed_card_type?("378282246310005")
69+
opts = %{@card_opts|allowed_card_types: [:visa]}
70+
assert CreditCard.is_allowed_card_type?("4012888888881881", opts)
71+
assert !CreditCard.is_allowed_card_type?("378282246310005", opts)
72+
73+
end
74+
75+
test "card type allowance" do
76+
opts = %{@card_opts|test_numbers_are_valid: true}
77+
assert CreditCard.valid?("378282246310005", opts)
78+
opts = %{opts|allowed_card_types: [:visa]}
79+
assert CreditCard.valid?("4012888888881881", opts)
80+
assert !CreditCard.valid?("378282246310005", opts)
81+
82+
end
83+
84+
end

test/test_helper.exs

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ExUnit.start()

0 commit comments

Comments
 (0)