credit_card_support 1.0.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|