credit_card_support 1.0.2 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/HISTORY.txt +5 -0
- data/README.md +57 -49
- data/lib/credit_card_support.rb +2 -2
- data/lib/credit_card_support/credit_card_number.rb +86 -0
- data/lib/credit_card_support/locale/en.yml +8 -9
- data/lib/credit_card_support/luhn_number.rb +4 -26
- data/lib/credit_card_support/validators/credit_card_number_validator.rb +48 -0
- data/lib/credit_card_support/version.rb +2 -2
- data/spec/lib/credit_card_support/credit_card_number_spec.rb +36 -0
- data/spec/lib/credit_card_support/luhn_number_spec.rb +26 -47
- data/spec/lib/credit_card_support/validators/credit_card_number_validator_spec.rb +81 -0
- metadata +5 -5
- data/lib/credit_card_support/credit_card_validator.rb +0 -87
- data/lib/credit_card_support/instrument.rb +0 -164
- data/spec/lib/credit_card_support/credit_card_validator_spec.rb +0 -127
- data/spec/lib/credit_card_support/instrument_spec.rb +0 -125
data/HISTORY.txt
CHANGED
data/README.md
CHANGED
@@ -12,18 +12,19 @@ Basic usage
|
|
12
12
|
-----------
|
13
13
|
|
14
14
|
```ruby
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
)
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
15
|
+
# for a String object
|
16
|
+
credit_card_number = "5500005555555559"
|
17
|
+
credit_card_number.extend(CreditCardSupport::CreditCardNumber)
|
18
|
+
|
19
|
+
# or for all the Strings
|
20
|
+
class String
|
21
|
+
include CreditCardSupport::CreditCardNumber)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Usage
|
25
|
+
credit_card_number.luhn? # true
|
26
|
+
credit_card_number.issuer # :visa
|
27
|
+
credit_card_number.test? # true
|
27
28
|
```
|
28
29
|
|
29
30
|
For better understanding read the source and RSpec.
|
@@ -32,28 +33,36 @@ For better understanding read the source and RSpec.
|
|
32
33
|
Validations support
|
33
34
|
-------------------
|
34
35
|
|
36
|
+
* validates Luhn (http://en.wikipedia.org/wiki/Luhn_algorithm)
|
37
|
+
* validates issuer
|
38
|
+
* validates that number is not a test number
|
39
|
+
* for presence, length etc, please use Rails already existing validators :)
|
40
|
+
|
35
41
|
### Define
|
36
42
|
|
37
|
-
|
43
|
+
#### ActiveRecord or Mongoid
|
38
44
|
|
39
45
|
```ruby
|
40
46
|
class CreditCard < ActiveRecord::Base
|
41
47
|
|
42
|
-
attr_accessible :number
|
48
|
+
attr_accessible :number
|
43
49
|
|
44
|
-
validates :number,
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
+
validates :number, presence: true,
|
51
|
+
length: {
|
52
|
+
minimum: 13,
|
53
|
+
maximum: 19
|
54
|
+
},
|
55
|
+
credit_card_number: {
|
56
|
+
allow_testcards: true,
|
57
|
+
allow_issuers: [:visa, :master_card]
|
58
|
+
}
|
50
59
|
|
51
60
|
end
|
52
61
|
```
|
53
62
|
|
54
|
-
|
63
|
+
#### Only ActiveModel
|
55
64
|
|
56
|
-
|
65
|
+
##### Rails 3
|
57
66
|
|
58
67
|
```ruby
|
59
68
|
class CreditCard
|
@@ -62,23 +71,27 @@ class CreditCard
|
|
62
71
|
include ActiveModel::Validations
|
63
72
|
include ActiveModel::Conversion
|
64
73
|
|
65
|
-
attr_accessor :number, :expiry_year, :expiry_month
|
74
|
+
attr_accessor :number, :expiry_year, :expiry_month, :errors
|
66
75
|
|
67
76
|
def initialize(opts={})
|
77
|
+
@errors = ActiveModel::Errors.new(self)
|
68
78
|
opts.each {|key, value| send(:"#{key}=", value) }
|
69
79
|
end
|
70
80
|
|
71
|
-
validates :number,
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
81
|
+
validates :number, presence: true,
|
82
|
+
length: {
|
83
|
+
minimum: 13,
|
84
|
+
maximum: 19
|
85
|
+
},
|
86
|
+
credit_card_number: {
|
87
|
+
allow_testcards: true,
|
88
|
+
allow_issuers: [:visa, :master_card]
|
89
|
+
}
|
77
90
|
|
78
91
|
end
|
79
92
|
```
|
80
93
|
|
81
|
-
|
94
|
+
##### Rails 4
|
82
95
|
|
83
96
|
```ruby
|
84
97
|
class CreditCard
|
@@ -86,12 +99,15 @@ class CreditCard
|
|
86
99
|
|
87
100
|
attr_accessor :number, :expiry_year, :expiry_month
|
88
101
|
|
89
|
-
validates :number,
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
102
|
+
validates :number, presence: true,
|
103
|
+
length: {
|
104
|
+
minimum: 13,
|
105
|
+
maximum: 19
|
106
|
+
},
|
107
|
+
credit_card_number: {
|
108
|
+
allow_testcards: true,
|
109
|
+
allow_issuers: [:visa, :master_card]
|
110
|
+
}
|
95
111
|
|
96
112
|
end
|
97
113
|
```
|
@@ -105,20 +121,12 @@ today = Date.today
|
|
105
121
|
```
|
106
122
|
|
107
123
|
```ruby
|
108
|
-
|
109
|
-
creditcard
|
110
|
-
creditcard.valid? # True
|
111
|
-
|
112
|
-
# RAILS_ENV == 'production'
|
113
|
-
creditcard = CreditCard.new(number: '4000111111111115', expiry_year: today.year, expiry_month: today.month)
|
114
|
-
creditcard.valid? # False
|
115
|
-
creditcard.errors # {number: ["is a test number"]}
|
116
|
-
|
117
|
-
# RAILS_ENV == 'test'
|
118
|
-
creditcard = CreditCard.new(number: '3566003566003566', expiry_year: today.year-1, expiry_month: today.month)
|
119
|
-
creditcard.valid? # False
|
120
|
-
creditcard.errors # {number: ["is a JCB card number and is not supported", expiry_year: ["is in the past"], expiry_month: ["is in the past"]]}
|
124
|
+
creditcard = CreditCard.new(number: '4000111111111115')
|
125
|
+
creditcard.valid? # true
|
121
126
|
|
127
|
+
# in case of allow_testcards is set to false
|
128
|
+
creditcard.valid? # false
|
129
|
+
creditcard.errors # <ActiveModel::Errors: ... @messages={:number=>["testcards not supported"]}>
|
122
130
|
```
|
123
131
|
|
124
132
|
|
data/lib/credit_card_support.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
1
|
require 'credit_card_support/luhn_number'
|
2
|
-
require 'credit_card_support/
|
3
|
-
require 'credit_card_support/
|
2
|
+
require 'credit_card_support/credit_card_number'
|
3
|
+
require 'credit_card_support/validators/credit_card_number_validator' if Object.const_defined?(:ActiveModel)
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module CreditCardSupport
|
4
|
+
module CreditCardNumber
|
5
|
+
include CreditCardSupport::LuhnNumber
|
6
|
+
|
7
|
+
ISSUERS = {
|
8
|
+
amex: /^3[47][0-9]{13}$/,
|
9
|
+
diners_club: /^3(?:0[0-5]|[68][0-9])[0-9]{11}$/,
|
10
|
+
discover: /^6(?:011|5[0-9]{2})[0-9]{12}$/,
|
11
|
+
enroute: /^2(014|149)\d{11}$/,
|
12
|
+
maestro: /(^6759[0-9]{2}([0-9]{10})$)|(^6759[0-9]{2}([0-9]{12})$)|(^6759[0-9]{2}([0-9]{13})$)/,
|
13
|
+
master_card: /^5[1-5][0-9]{14}$/,
|
14
|
+
jcb: /^35[0-9]{14}$/,
|
15
|
+
solo: /^6(3(34[5-9][0-9])|767[0-9]{2})\d{10}(\d{2,3})?$/,
|
16
|
+
visa: /^4[0-9]{12}(?:[0-9]{3})?$/
|
17
|
+
}.freeze
|
18
|
+
|
19
|
+
TESTCARDS = {
|
20
|
+
amex: [
|
21
|
+
'378282246310005',
|
22
|
+
'371449635398431',
|
23
|
+
'378734493671000',
|
24
|
+
'343434343434343',
|
25
|
+
'371144371144376',
|
26
|
+
'341134113411347'
|
27
|
+
],
|
28
|
+
diners_club: [
|
29
|
+
'30569309025904',
|
30
|
+
'38520000023237',
|
31
|
+
'36438936438936'
|
32
|
+
],
|
33
|
+
discover: [
|
34
|
+
'6011000990139424',
|
35
|
+
'6011111111111117',
|
36
|
+
'6011016011016011',
|
37
|
+
'6011000000000004',
|
38
|
+
'6011000995500000',
|
39
|
+
'6500000000000002'
|
40
|
+
],
|
41
|
+
master_card: [
|
42
|
+
'5555555555554444',
|
43
|
+
'5105105105105100',
|
44
|
+
'5500005555555559',
|
45
|
+
'5555555555555557',
|
46
|
+
'5454545454545454',
|
47
|
+
'5555515555555551',
|
48
|
+
'5405222222222226',
|
49
|
+
'5478050000000007',
|
50
|
+
'5111005111051128',
|
51
|
+
'5112345112345114'
|
52
|
+
],
|
53
|
+
visa: [
|
54
|
+
'4111111111111111',
|
55
|
+
'4012888888881881',
|
56
|
+
'4222222222222',
|
57
|
+
'4005519200000004',
|
58
|
+
'4009348888881881',
|
59
|
+
'4012000033330026',
|
60
|
+
'4012000077777777',
|
61
|
+
'4217651111111119',
|
62
|
+
'4500600000000061',
|
63
|
+
'4000111111111115'
|
64
|
+
],
|
65
|
+
jcb: [
|
66
|
+
'3566003566003566',
|
67
|
+
'3528000000000007'
|
68
|
+
]
|
69
|
+
}.freeze
|
70
|
+
|
71
|
+
def issuer
|
72
|
+
ISSUERS.each do |issuer, number_match|
|
73
|
+
return issuer if to_s.match(number_match)
|
74
|
+
end
|
75
|
+
nil
|
76
|
+
end
|
77
|
+
|
78
|
+
def testcard?
|
79
|
+
TESTCARDS.each do |issuer, numbers|
|
80
|
+
return true if numbers.include?(to_s)
|
81
|
+
end
|
82
|
+
false
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
@@ -1,10 +1,9 @@
|
|
1
1
|
en:
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
cant_be_so_far_in_the_future: "can't be so far in the future"
|
2
|
+
credit_card_support:
|
3
|
+
validators:
|
4
|
+
number:
|
5
|
+
messages:
|
6
|
+
luhn_not_valid: "is not valid"
|
7
|
+
issuer_not_known: "not known issuer"
|
8
|
+
issuer_not_supported: "%{issuer} not supported"
|
9
|
+
testcard_not_supported: "testcards not supported"
|
@@ -1,27 +1,10 @@
|
|
1
1
|
module CreditCardSupport
|
2
|
+
module LuhnNumber
|
2
3
|
|
3
|
-
|
4
|
-
|
5
|
-
attr_accessor :full_number
|
6
|
-
|
7
|
-
def initialize(full_number)
|
8
|
-
self.full_number = full_number
|
9
|
-
end
|
10
|
-
|
11
|
-
def full_number=(full_number)
|
12
|
-
@full_number = full_number.gsub(/[^0-9]/, '').to_s
|
13
|
-
end
|
14
|
-
|
15
|
-
def number_part
|
16
|
-
full_number[0, full_number.length-1] if full_number
|
17
|
-
end
|
18
|
-
|
19
|
-
def check_digit_part
|
20
|
-
full_number[-1, 1] if full_number
|
21
|
-
end
|
22
|
-
|
23
|
-
def valid?
|
4
|
+
def luhn?
|
5
|
+
full_number = to_s.gsub(/[^0-9]/, '')
|
24
6
|
return false unless full_number and full_number.length > 2
|
7
|
+
|
25
8
|
parts = full_number.split(//).map(&:to_i)
|
26
9
|
|
27
10
|
double = parts.length % 2 == 0
|
@@ -35,10 +18,5 @@ module CreditCardSupport
|
|
35
18
|
sum % 10 == 0
|
36
19
|
end
|
37
20
|
|
38
|
-
def invalid?
|
39
|
-
!valid?
|
40
|
-
end
|
41
|
-
|
42
21
|
end
|
43
|
-
|
44
22
|
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
I18n.load_path << File.dirname(__FILE__) + '/../locale/en.yml'
|
2
|
+
|
3
|
+
class CreditCardNumberValidator < ActiveModel::EachValidator
|
4
|
+
|
5
|
+
def validate_each(record, attribute, value)
|
6
|
+
@record = record
|
7
|
+
@attribute = attribute
|
8
|
+
@value = value
|
9
|
+
|
10
|
+
@value.extend(CreditCardSupport::CreditCardNumber)
|
11
|
+
|
12
|
+
validates_luhn &&
|
13
|
+
validates_testcard &&
|
14
|
+
validates_issuer_allowed
|
15
|
+
end
|
16
|
+
|
17
|
+
def validates_luhn
|
18
|
+
if !@value.luhn?
|
19
|
+
@record.errors.add(@attribute, t(:luhn_not_valid))
|
20
|
+
false
|
21
|
+
else
|
22
|
+
true
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def validates_testcard
|
27
|
+
if !options[:allow_testcards] && @value.testcard?
|
28
|
+
@record.errors.add(@attribute, t(:testcard_not_supported))
|
29
|
+
false
|
30
|
+
else
|
31
|
+
true
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def validates_issuer_allowed
|
36
|
+
if options[:allow_issuers] && !options[:allow_issuers].include?(@value.issuer)
|
37
|
+
@record.errors.add(@attribute, t(:issuer_not_supported, issuer: @value.issuer))
|
38
|
+
false
|
39
|
+
else
|
40
|
+
true
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def t(code, *args)
|
45
|
+
I18n.t("credit_card_support.validators.number.messages.#{code}", *args)
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
describe CreditCardSupport::CreditCardNumber do
|
5
|
+
|
6
|
+
describe "#testcard?" do
|
7
|
+
context "a testcard" do
|
8
|
+
it "returns true" do
|
9
|
+
number = "378282246310005"
|
10
|
+
number.extend(described_class)
|
11
|
+
number.should be_testcard
|
12
|
+
end
|
13
|
+
end
|
14
|
+
context "not a testcard" do
|
15
|
+
it "returns false" do
|
16
|
+
number = "4916119578780242"
|
17
|
+
number.extend(described_class)
|
18
|
+
number.should_not be_testcard
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "#issuer" do
|
24
|
+
described_class::TESTCARDS.each do |issuer, numbers|
|
25
|
+
context "#{issuer}" do
|
26
|
+
it "is recognized" do
|
27
|
+
numbers.each do |number|
|
28
|
+
number.extend(described_class)
|
29
|
+
number.issuer.should == issuer
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -2,59 +2,38 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe CreditCardSupport::LuhnNumber do
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
subject.number_part.should == "448507135960836"
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
describe "#check_digit_part" do
|
28
|
-
it "returns number part of full_number" do
|
29
|
-
subject.check_digit_part.should == "8"
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
describe "#valid?" do
|
34
|
-
it "returns if luhn algorytm % 10 is true" do
|
35
|
-
subject.should be_valid
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
describe "#valid?" do
|
5
|
+
VALID_LUHNS = [
|
6
|
+
"5500005555555559",
|
7
|
+
"5555555555555557",
|
8
|
+
"5454545454545454",
|
9
|
+
"5555515555555551",
|
10
|
+
"5405222222222226",
|
11
|
+
"5478050000000007"
|
12
|
+
]
|
13
|
+
INVALID_LUHNS = [
|
14
|
+
"5500005555555558",
|
15
|
+
"5555555555555556",
|
16
|
+
"5454545454545453",
|
17
|
+
"5555515555555550",
|
18
|
+
"5405222222222225",
|
19
|
+
"5478050000000006"
|
20
|
+
]
|
21
|
+
|
22
|
+
describe "#luhn?" do
|
40
23
|
context "valid luhn" do
|
41
24
|
it "returns true" do
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
described_class.new("5405222222222226").should be_valid
|
47
|
-
described_class.new("5478050000000007").should be_valid
|
25
|
+
VALID_LUHNS.each do |number|
|
26
|
+
number.extend(described_class)
|
27
|
+
number.should be_luhn
|
28
|
+
end
|
48
29
|
end
|
49
30
|
end
|
50
31
|
context "invalid luhn" do
|
51
32
|
it "returns false" do
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
described_class.new("5405222222222225").should be_invalid
|
57
|
-
described_class.new("5478050000000006").should be_invalid
|
33
|
+
INVALID_LUHNS.each do |number|
|
34
|
+
number.extend(described_class)
|
35
|
+
number.should_not be_luhn
|
36
|
+
end
|
58
37
|
end
|
59
38
|
end
|
60
39
|
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'active_model'
|
2
|
+
require 'spec_helper'
|
3
|
+
require 'credit_card_support/validators/credit_card_number_validator' if Object.const_defined?(:ActiveModel)
|
4
|
+
|
5
|
+
class CreditCard
|
6
|
+
extend ActiveModel::Naming
|
7
|
+
extend ActiveModel::Translation
|
8
|
+
include ActiveModel::Validations
|
9
|
+
include ActiveModel::Conversion
|
10
|
+
|
11
|
+
attr_accessor :number
|
12
|
+
|
13
|
+
def errors
|
14
|
+
@errors ||= ActiveModel::Errors.new(self)
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(opts={})
|
18
|
+
opts.each do |k,v|
|
19
|
+
send(:"#{k}=", v)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
class CreditCardTest < CreditCard
|
26
|
+
validates :number, presence: true,
|
27
|
+
length: {
|
28
|
+
minimum: 13,
|
29
|
+
maximum: 19
|
30
|
+
},
|
31
|
+
credit_card_number: {
|
32
|
+
allow_testcards: true,
|
33
|
+
allow_issuers: [:visa, :master_card]
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
class CreditCardProduction < CreditCard
|
38
|
+
validates :number, presence: true,
|
39
|
+
length: {
|
40
|
+
minimum: 13,
|
41
|
+
maximum: 19
|
42
|
+
},
|
43
|
+
credit_card_number: {
|
44
|
+
allow_testcards: false,
|
45
|
+
allow_issuers: [:visa, :master_card]
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
describe CreditCardNumberValidator do
|
51
|
+
subject { CreditCardTest.new(number: '4012888888881881') }
|
52
|
+
|
53
|
+
it "is valid" do
|
54
|
+
subject.should be_valid
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "#number" do
|
58
|
+
it "must exist" do
|
59
|
+
subject.number = nil
|
60
|
+
subject.should_not be_valid
|
61
|
+
end
|
62
|
+
it "must be luhn" do
|
63
|
+
subject.number = '4012888888881882'
|
64
|
+
subject.should_not be_valid
|
65
|
+
end
|
66
|
+
context "production" do
|
67
|
+
subject { CreditCardProduction.new(number: '4485071359608368') }
|
68
|
+
context "testnumber" do
|
69
|
+
it "is invalid" do
|
70
|
+
subject.number = '4012888888881881'
|
71
|
+
subject.should be_invalid
|
72
|
+
end
|
73
|
+
end
|
74
|
+
context "valid number" do
|
75
|
+
it "is valid" do
|
76
|
+
subject.should be_valid
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: credit_card_support
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -22,15 +22,15 @@ files:
|
|
22
22
|
- README.md
|
23
23
|
- LICENSE.txt
|
24
24
|
- HISTORY.txt
|
25
|
-
- lib/credit_card_support/
|
26
|
-
- lib/credit_card_support/instrument.rb
|
25
|
+
- lib/credit_card_support/credit_card_number.rb
|
27
26
|
- lib/credit_card_support/locale/en.yml
|
28
27
|
- lib/credit_card_support/luhn_number.rb
|
28
|
+
- lib/credit_card_support/validators/credit_card_number_validator.rb
|
29
29
|
- lib/credit_card_support/version.rb
|
30
30
|
- lib/credit_card_support.rb
|
31
|
-
- spec/lib/credit_card_support/
|
32
|
-
- spec/lib/credit_card_support/instrument_spec.rb
|
31
|
+
- spec/lib/credit_card_support/credit_card_number_spec.rb
|
33
32
|
- spec/lib/credit_card_support/luhn_number_spec.rb
|
33
|
+
- spec/lib/credit_card_support/validators/credit_card_number_validator_spec.rb
|
34
34
|
- spec/spec_helper.rb
|
35
35
|
homepage: https://github.com/tione/credit_card_support
|
36
36
|
licenses: []
|
@@ -1,87 +0,0 @@
|
|
1
|
-
I18n.load_path << File.dirname(__FILE__) + '/locale/en.yml'
|
2
|
-
|
3
|
-
class CreditCardValidator < ActiveModel::EachValidator
|
4
|
-
|
5
|
-
def validate_each(record, attribute, value)
|
6
|
-
@record = record
|
7
|
-
|
8
|
-
opts = options.dup
|
9
|
-
opts[:number] ||= attribute
|
10
|
-
|
11
|
-
@number_name = opts[:number] || :number
|
12
|
-
@expiry_year_name = opts[:expiry_year] || :expiry_year
|
13
|
-
@expiry_month_name = opts[:expiry_month] || :expiry_month
|
14
|
-
|
15
|
-
@number = record.send(@number_name)
|
16
|
-
@expiry_year = record.send(@expiry_year_name)
|
17
|
-
@expiry_month = record.send(@expiry_month_name)
|
18
|
-
@allowed_issuers = opts[:allowed_issuers]
|
19
|
-
@allow_testcards = opts[:allow_testcards]
|
20
|
-
|
21
|
-
@instrument = CreditCardSupport::Instrument.new(number: @number, expiry_year: @expiry_year, expiry_month: @expiry_month)
|
22
|
-
|
23
|
-
validate_number
|
24
|
-
validate_expiration
|
25
|
-
end
|
26
|
-
|
27
|
-
|
28
|
-
private
|
29
|
-
|
30
|
-
def errors
|
31
|
-
@record.errors
|
32
|
-
end
|
33
|
-
|
34
|
-
def instrument
|
35
|
-
@instrument
|
36
|
-
end
|
37
|
-
|
38
|
-
def validate_number
|
39
|
-
validate_number_luhn
|
40
|
-
validate_number_issuer
|
41
|
-
validate_number_testcard
|
42
|
-
end
|
43
|
-
|
44
|
-
def validate_expiration
|
45
|
-
validate_expiry_year
|
46
|
-
validate_expiry_month
|
47
|
-
validate_expiration_date rescue nil
|
48
|
-
end
|
49
|
-
|
50
|
-
def validate_number_luhn
|
51
|
-
errors.add(@number_name, t(:luhn_not_valid)) unless @instrument.has_valid_luhn?
|
52
|
-
end
|
53
|
-
|
54
|
-
def validate_number_issuer
|
55
|
-
errors.add(@number_name, t(:issuer_not_known)) unless @instrument.issuer
|
56
|
-
if @allowed_issuers and @instrument.issuer and !@allowed_issuers.map(&:to_sym).include?(@instrument.issuer)
|
57
|
-
errors.add(@number_name, t(:issuer_not_supported, issuer: @instrument.issuer.to_s.upcase))
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
def validate_number_testcard
|
62
|
-
errors.add(@number_name, t(:testcard_not_supported)) if !@allow_testcards && @instrument.is_testcard?
|
63
|
-
end
|
64
|
-
|
65
|
-
def validate_expiry_year
|
66
|
-
errors.add(@expiry_year_name, t(:cant_be_blank)) unless @instrument.expiry_year
|
67
|
-
end
|
68
|
-
|
69
|
-
def validate_expiry_month
|
70
|
-
errors.add(@expiry_month_name, t(:cant_be_blank)) unless @instrument.expiry_month
|
71
|
-
end
|
72
|
-
|
73
|
-
def validate_expiration_date
|
74
|
-
if @instrument.is_expired?
|
75
|
-
errors.add(@expiry_year_name, t(:cant_be_expired))
|
76
|
-
errors.add(@expiry_month_name, t(:cant_be_expired))
|
77
|
-
end
|
78
|
-
if @instrument.expiration_date > Date.new(Date.today.year+10)
|
79
|
-
errors.add(@expiry_year_name, t(:cant_be_so_far_in_the_future))
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
def t(code, *args)
|
84
|
-
I18n.t("credit_card_validator.messages.#{code}", *args)
|
85
|
-
end
|
86
|
-
|
87
|
-
end
|
@@ -1,164 +0,0 @@
|
|
1
|
-
require 'date'
|
2
|
-
|
3
|
-
module CreditCardSupport
|
4
|
-
|
5
|
-
# Usage:
|
6
|
-
#
|
7
|
-
# credit_card = CreditCardSupport.Instrument.new(
|
8
|
-
# number: '4222222222222',
|
9
|
-
# expiry_year: 13,
|
10
|
-
# expiry_month: 11,
|
11
|
-
# holder_name: 'A B'
|
12
|
-
# verification: '1234' # optional!
|
13
|
-
# )
|
14
|
-
#
|
15
|
-
# credit_card.expired? # returns false
|
16
|
-
# credit_card.expiration_date # Date (last day of the month for expiry month)
|
17
|
-
# credit_card.issuer # VISA
|
18
|
-
# credit_card.is_testcard? # true
|
19
|
-
|
20
|
-
|
21
|
-
class Instrument
|
22
|
-
|
23
|
-
NUMBERS = {
|
24
|
-
amex: /^3[47][0-9]{13}$/,
|
25
|
-
diners_club: /^3(?:0[0-5]|[68][0-9])[0-9]{11}$/,
|
26
|
-
discover: /^6(?:011|5[0-9]{2})[0-9]{12}$/,
|
27
|
-
enroute: /^2(014|149)\d{11}$/,
|
28
|
-
maestro: /(^6759[0-9]{2}([0-9]{10})$)|(^6759[0-9]{2}([0-9]{12})$)|(^6759[0-9]{2}([0-9]{13})$)/,
|
29
|
-
master_card: /^5[1-5][0-9]{14}$/,
|
30
|
-
jcb: /^35[0-9]{14}$/,
|
31
|
-
solo: /^6(3(34[5-9][0-9])|767[0-9]{2})\d{10}(\d{2,3})?$/,
|
32
|
-
visa: /^4[0-9]{12}(?:[0-9]{3})?$/
|
33
|
-
}.freeze
|
34
|
-
|
35
|
-
TESTCARD_NUMBERS = {
|
36
|
-
amex: [
|
37
|
-
'378282246310005',
|
38
|
-
'371449635398431',
|
39
|
-
'378734493671000',
|
40
|
-
'343434343434343',
|
41
|
-
'371144371144376',
|
42
|
-
'341134113411347'
|
43
|
-
],
|
44
|
-
diners_club: [
|
45
|
-
'30569309025904',
|
46
|
-
'38520000023237',
|
47
|
-
'36438936438936'
|
48
|
-
],
|
49
|
-
discover: [
|
50
|
-
'6011000990139424',
|
51
|
-
'6011111111111117',
|
52
|
-
'6011016011016011',
|
53
|
-
'6011000000000004',
|
54
|
-
'6011000995500000',
|
55
|
-
'6500000000000002'
|
56
|
-
],
|
57
|
-
master_card: [
|
58
|
-
'5555555555554444',
|
59
|
-
'5105105105105100',
|
60
|
-
'5500005555555559',
|
61
|
-
'5555555555555557',
|
62
|
-
'5454545454545454',
|
63
|
-
'5555515555555551',
|
64
|
-
'5405222222222226',
|
65
|
-
'5478050000000007',
|
66
|
-
'5111005111051128',
|
67
|
-
'5112345112345114'
|
68
|
-
],
|
69
|
-
visa: [
|
70
|
-
'4111111111111111',
|
71
|
-
'4012888888881881',
|
72
|
-
'4222222222222',
|
73
|
-
'4005519200000004',
|
74
|
-
'4009348888881881',
|
75
|
-
'4012000033330026',
|
76
|
-
'4012000077777777',
|
77
|
-
'4217651111111119',
|
78
|
-
'4500600000000061',
|
79
|
-
'4000111111111115'
|
80
|
-
],
|
81
|
-
jcb: [
|
82
|
-
'3566003566003566',
|
83
|
-
'3528000000000007'
|
84
|
-
]
|
85
|
-
}.freeze
|
86
|
-
|
87
|
-
def self.numbers
|
88
|
-
self::NUMBERS
|
89
|
-
end
|
90
|
-
|
91
|
-
def self.testcard_numbers
|
92
|
-
self::TESTCARD_NUMBERS
|
93
|
-
end
|
94
|
-
|
95
|
-
def self.determine_issuer(number)
|
96
|
-
numbers.each do |issuer, issuer_regexp|
|
97
|
-
return issuer if number.match(issuer_regexp)
|
98
|
-
end
|
99
|
-
nil
|
100
|
-
end
|
101
|
-
|
102
|
-
def self.is_testcard?(number)
|
103
|
-
testcard_numbers.values.flatten.include?(number)
|
104
|
-
end
|
105
|
-
|
106
|
-
def self.has_valid_luhn?(number)
|
107
|
-
CreditCardSupport::LuhnNumber.new(number).valid?
|
108
|
-
end
|
109
|
-
|
110
|
-
attr_accessor :number,
|
111
|
-
:expiry_year,
|
112
|
-
:expiry_month,
|
113
|
-
:holder_name,
|
114
|
-
:verification
|
115
|
-
|
116
|
-
attr_reader :issuer
|
117
|
-
|
118
|
-
def initialize(opts={}, &block)
|
119
|
-
self.number = opts[:number]
|
120
|
-
self.expiry_year = opts[:expiry_year]
|
121
|
-
self.expiry_month = opts[:expiry_month]
|
122
|
-
self.holder_name = opts[:holder_name]
|
123
|
-
self.verification = opts[:verification]
|
124
|
-
|
125
|
-
block.call(self) if block
|
126
|
-
end
|
127
|
-
|
128
|
-
def number=(number)
|
129
|
-
@number = number.to_s.gsub(/[^0-9]/, '')
|
130
|
-
@issuer = self.class.determine_issuer(self.number)
|
131
|
-
@is_testcard = self.class.is_testcard?(self.number)
|
132
|
-
@has_valid_luhn = self.class.has_valid_luhn?(self.number)
|
133
|
-
end
|
134
|
-
|
135
|
-
def expiry_year=(expiry_year)
|
136
|
-
@expiry_year = expiry_year.to_i if expiry_year
|
137
|
-
end
|
138
|
-
|
139
|
-
def expiry_month=(expiry_month)
|
140
|
-
@expiry_month = expiry_month.to_i if expiry_month
|
141
|
-
end
|
142
|
-
|
143
|
-
def expiry_year
|
144
|
-
(@expiry_year < 1900 ? 2000 + @expiry_year : @expiry_year) if @expiry_year
|
145
|
-
end
|
146
|
-
|
147
|
-
def expiration_date
|
148
|
-
Date.new(expiry_year, expiry_month, -1) rescue nil
|
149
|
-
end
|
150
|
-
|
151
|
-
def is_expired?
|
152
|
-
expiration_date < Date.today
|
153
|
-
end
|
154
|
-
|
155
|
-
def is_testcard?
|
156
|
-
@is_testcard
|
157
|
-
end
|
158
|
-
|
159
|
-
def has_valid_luhn?
|
160
|
-
@has_valid_luhn
|
161
|
-
end
|
162
|
-
|
163
|
-
end
|
164
|
-
end
|
@@ -1,127 +0,0 @@
|
|
1
|
-
require 'active_model'
|
2
|
-
require 'spec_helper'
|
3
|
-
|
4
|
-
|
5
|
-
class CreditCard
|
6
|
-
extend ActiveModel::Naming
|
7
|
-
extend ActiveModel::Translation
|
8
|
-
include ActiveModel::Validations
|
9
|
-
include ActiveModel::Conversion
|
10
|
-
|
11
|
-
attr_accessor :number, :expiry_year, :expiry_month
|
12
|
-
|
13
|
-
def initialize(opts={})
|
14
|
-
@errors = ActiveModel::Errors.new(self)
|
15
|
-
opts.each do |k,v|
|
16
|
-
send(:"#{k}=", v)
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
end
|
21
|
-
|
22
|
-
class CreditCardTest < CreditCard
|
23
|
-
validates :number, credit_card: {
|
24
|
-
expiry_year: :expiry_year,
|
25
|
-
expiry_month: :expiry_month,
|
26
|
-
allowed_issuers: [:visa, :mastercard],
|
27
|
-
allow_testcards: true
|
28
|
-
}
|
29
|
-
end
|
30
|
-
|
31
|
-
class CreditCardProduction < CreditCard
|
32
|
-
validates :number, credit_card: {
|
33
|
-
expiry_year: :expiry_year,
|
34
|
-
expiry_month: :expiry_month,
|
35
|
-
allowed_issuers: [:visa, :mastercard],
|
36
|
-
allow_testcards: false
|
37
|
-
}
|
38
|
-
end
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
describe CreditCardValidator do
|
43
|
-
let(:today) { Date.today }
|
44
|
-
|
45
|
-
describe "validatable" do
|
46
|
-
|
47
|
-
subject { CreditCardTest.new(number: '4012888888881881', expiry_year: today.year, expiry_month: today.month) }
|
48
|
-
|
49
|
-
it "is valid" do
|
50
|
-
subject.should be_valid
|
51
|
-
end
|
52
|
-
|
53
|
-
describe "#number" do
|
54
|
-
it "must exist" do
|
55
|
-
subject.number = nil
|
56
|
-
subject.should_not be_valid
|
57
|
-
end
|
58
|
-
it "must be luhn" do
|
59
|
-
subject.number = '4000111111111116'
|
60
|
-
subject.should_not be_valid
|
61
|
-
end
|
62
|
-
context "production" do
|
63
|
-
subject { CreditCardProduction.new(number: '4485071359608368', expiry_year: today.year, expiry_month: today.month) }
|
64
|
-
context "testnumber" do
|
65
|
-
it "is invalid" do
|
66
|
-
subject.number = '4012888888881881'
|
67
|
-
subject.should be_invalid
|
68
|
-
end
|
69
|
-
end
|
70
|
-
context "valid number" do
|
71
|
-
it "is valid" do
|
72
|
-
subject.should be_valid
|
73
|
-
end
|
74
|
-
end
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
describe "#expiry_year" do
|
79
|
-
it "must exist" do
|
80
|
-
subject.expiry_year = nil
|
81
|
-
subject.should_not be_valid
|
82
|
-
end
|
83
|
-
context "in the future" do
|
84
|
-
it "is valid" do
|
85
|
-
subject.expiry_year = today.year + 1
|
86
|
-
subject.should be_valid
|
87
|
-
end
|
88
|
-
end
|
89
|
-
context "in the future too much" do
|
90
|
-
it "is valid" do
|
91
|
-
subject.expiry_year = today.year + 11
|
92
|
-
subject.should be_invalid
|
93
|
-
end
|
94
|
-
end
|
95
|
-
context "in the past" do
|
96
|
-
it "is invalid" do
|
97
|
-
subject.expiry_year = today.year - 1
|
98
|
-
subject.should be_invalid
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
describe "#expiry_month" do
|
104
|
-
it "must exist" do
|
105
|
-
subject.expiry_month = nil
|
106
|
-
subject.should_not be_valid
|
107
|
-
end
|
108
|
-
context "in the future" do
|
109
|
-
it "is valid" do
|
110
|
-
subject.expiry_month = today.month + 1
|
111
|
-
subject.valid?
|
112
|
-
puts subject.errors.inspect
|
113
|
-
subject.should be_valid
|
114
|
-
end
|
115
|
-
end
|
116
|
-
context "in the past" do
|
117
|
-
it "is invalid" do
|
118
|
-
subject.expiry_month = today.month - 1
|
119
|
-
subject.should be_invalid
|
120
|
-
end
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
end
|
125
|
-
|
126
|
-
end
|
127
|
-
|
@@ -1,125 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
require 'date'
|
3
|
-
|
4
|
-
describe CreditCardSupport::Instrument do
|
5
|
-
let(:today) { Date.today }
|
6
|
-
subject { described_class.new(number: 4222222222222, expiry_year: "#{today.year}", expiry_month: "12", holder_name: 'A B', verification: '1234') }
|
7
|
-
|
8
|
-
describe ".numbers" do
|
9
|
-
it "returns hash containing of issuer: number_regexp" do
|
10
|
-
described_class.numbers.should be_a(Hash)
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
describe ".testcard_numbers" do
|
15
|
-
it "returns hash containing of testcard numbers" do
|
16
|
-
described_class.testcard_numbers.should be_a(Hash)
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
describe ".determine_issuer" do
|
21
|
-
described_class.testcard_numbers.each do |issuer, numbers|
|
22
|
-
context "#{issuer}" do
|
23
|
-
it "knows the numbers" do
|
24
|
-
numbers.each do |number|
|
25
|
-
described_class.determine_issuer(number).should == issuer
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
describe ".is_testcard?" do
|
33
|
-
it "determines if number is testnumber" do
|
34
|
-
described_class.is_testcard?("4000111111111115").should be_true
|
35
|
-
described_class.is_testcard?("4000111111111116").should be_false
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
describe "#initialize" do
|
40
|
-
it "supports a hash" do
|
41
|
-
instrument = described_class.new(number: "123")
|
42
|
-
instrument.number.should == "123"
|
43
|
-
end
|
44
|
-
it "supports a block" do
|
45
|
-
instrument = described_class.new do |instrument_object|
|
46
|
-
instrument_object.number = "123"
|
47
|
-
end
|
48
|
-
instrument.number.should == "123"
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
describe "#number" do
|
53
|
-
it "returns number as a string" do
|
54
|
-
subject.number = "1-2-3-412345"
|
55
|
-
subject.number.should == "123412345"
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
describe "#expiry_year" do
|
60
|
-
it "returns expiry year as NNNN integer" do
|
61
|
-
subject.expiry_year = "#{today.year-2000}"
|
62
|
-
subject.expiry_year.should == today.year
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
describe "#expiry_month" do
|
67
|
-
it "returns expiry month as NN integer" do
|
68
|
-
subject.expiry_month = "#{today.month}"
|
69
|
-
subject.expiry_month.should == today.month
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
describe "#expiration_date" do
|
74
|
-
it "returns last day of the month when expiring" do
|
75
|
-
subject.expiration_date.should == Date.new(today.year, 12, 31)
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
describe "#is_expired?" do
|
80
|
-
context "not expired" do
|
81
|
-
it "returns false" do
|
82
|
-
subject.is_expired?.should be_false
|
83
|
-
end
|
84
|
-
end
|
85
|
-
context "expired" do
|
86
|
-
it "returns true" do
|
87
|
-
subject.expiry_year = today.year
|
88
|
-
subject.expiry_month = today.month-1
|
89
|
-
subject.is_expired?.should be_true
|
90
|
-
end
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
describe "#is_testcard?" do
|
95
|
-
context "not a testcard" do
|
96
|
-
it "returns false" do
|
97
|
-
subject.number = 4222222222223
|
98
|
-
subject.is_testcard?.should be_false
|
99
|
-
end
|
100
|
-
end
|
101
|
-
context "a testcard" do
|
102
|
-
it "returns true" do
|
103
|
-
subject.number = 4222222222222
|
104
|
-
subject.is_testcard?.should be_true
|
105
|
-
end
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
describe "has_valid_luhn?" do
|
110
|
-
context "valid luhn" do
|
111
|
-
it "returns true" do
|
112
|
-
subject.number = 4222222222222
|
113
|
-
subject.is_testcard?.should be_true
|
114
|
-
end
|
115
|
-
end
|
116
|
-
context "invalid luhn" do
|
117
|
-
it "returns false" do
|
118
|
-
subject.number = 4222222222223
|
119
|
-
subject.is_testcard?.should be_false
|
120
|
-
end
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
|
125
|
-
end
|