credit_card_validations 1.5.1 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +4 -1
- data/Changelog.md +85 -0
- data/README.md +32 -9
- data/Rakefile +4 -4
- data/credit_card_validations.gemspec +2 -2
- data/lib/active_model/credit_card_number_validator.rb +67 -2
- data/lib/credit_card_validations.rb +25 -17
- data/lib/credit_card_validations/detector.rb +110 -27
- data/lib/credit_card_validations/factory.rb +66 -0
- data/lib/credit_card_validations/luhn.rb +5 -0
- data/lib/credit_card_validations/mmi.rb +18 -13
- data/lib/credit_card_validations/string.rb +11 -1
- data/lib/credit_card_validations/version.rb +1 -1
- data/lib/data/brands.yaml +238 -0
- data/spec/active_model_spec.rb +69 -0
- data/spec/credit_card_validations_spec.rb +168 -0
- data/spec/factory_spec.rb +21 -0
- data/{test → spec}/fixtures/invalid_cards.yml +0 -0
- data/{test → spec}/fixtures/valid_cards.yml +1 -0
- data/spec/models/credit_card.rb +19 -0
- data/spec/string_spec.rb +43 -0
- data/{test → spec}/test_helper.rb +5 -4
- metadata +25 -17
- data/lib/credit_card_validations/card_rules.rb +0 -84
- data/test/credit_card_validations_test.rb +0 -151
- data/test/models/any_credit_card.rb +0 -6
- data/test/models/credit_card.rb +0 -5
@@ -0,0 +1,66 @@
|
|
1
|
+
# == CreditCardValidations Factory
|
2
|
+
# Generates card number that passes validation
|
3
|
+
#
|
4
|
+
# #random
|
5
|
+
# CreditCardValidations::Factory.random
|
6
|
+
# #or particular brand
|
7
|
+
# CreditCardValidations::Factory.random(:maestro) # "6010430241237266856"
|
8
|
+
#
|
9
|
+
#
|
10
|
+
#
|
11
|
+
module CreditCardValidations
|
12
|
+
class Factory
|
13
|
+
class << self
|
14
|
+
def random(brand = nil)
|
15
|
+
if brand.nil?
|
16
|
+
brand = Detector.brands.keys.sample
|
17
|
+
else
|
18
|
+
raise RuntimeError.new("Unsupported brand") if Detector.brands[brand].nil?
|
19
|
+
end
|
20
|
+
generate(Detector.brands[brand][:rules].sample)
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
def generate(rule)
|
25
|
+
number(rule[:prefixes].sample, rule[:length].sample, rule.fetch(:options, {})[:skip_luhn])
|
26
|
+
end
|
27
|
+
|
28
|
+
def number(prefix, length, skip_luhn = false)
|
29
|
+
number = prefix.dup
|
30
|
+
1.upto(length - (prefix.length + 1)) do
|
31
|
+
number << "#{rand(9)}"
|
32
|
+
end
|
33
|
+
#if skip luhn
|
34
|
+
if skip_luhn
|
35
|
+
number += "#{rand(9)}"
|
36
|
+
else
|
37
|
+
number += last_digit(number).to_s
|
38
|
+
end
|
39
|
+
number
|
40
|
+
end
|
41
|
+
|
42
|
+
#extracted from darkcoding-credit-card
|
43
|
+
|
44
|
+
def last_digit(number)
|
45
|
+
# Calculate sum
|
46
|
+
sum, pos = 0, 0
|
47
|
+
length = number.length + 1
|
48
|
+
|
49
|
+
reversed_number = number.reverse
|
50
|
+
while pos < length do
|
51
|
+
odd = reversed_number[pos].to_i * 2
|
52
|
+
odd -= 9 if odd > 9
|
53
|
+
|
54
|
+
sum += odd
|
55
|
+
|
56
|
+
sum += reversed_number[pos + 1].to_i if pos != (length - 2)
|
57
|
+
|
58
|
+
pos += 2
|
59
|
+
end
|
60
|
+
|
61
|
+
(((sum / 10).floor + 1) * 10 - sum) % 10
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -1,17 +1,22 @@
|
|
1
|
-
#
|
1
|
+
# == CreditCardValidations Mmi
|
2
2
|
#
|
3
|
-
#
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
3
|
+
# Implements Major Industry Identifier (MII) detection
|
4
|
+
#
|
5
|
+
# The first digit of a credit card number is the Major Industry Identifier (MII), which represents the category of entity which issued the card. MII digits represent the following issuer categories:
|
6
|
+
# 0 – ISO/TC 68 and other future industry assignments
|
7
|
+
# 1 – Airlines
|
8
|
+
# 2 – Airlines and other future industry assignments
|
9
|
+
# 3 – Travel and entertainment and banking/financial
|
10
|
+
# 4 – Banking and financial
|
11
|
+
# 5 – Banking and financial
|
12
|
+
# 6 – Merchandising and banking/financial
|
13
|
+
# 7 – Petroleum and other future industry assignments
|
14
|
+
# 8 – Healthcare, telecommunications and other future industry assignments
|
15
|
+
# 9 – National assignment
|
16
|
+
# For example, American Express, Diner's Club, Carte Blanche,
|
17
|
+
# and JCB are in the travel and entertainment category;
|
18
|
+
# VISA, MasterCard, and Discover are in the banking and financial category (Discover being in the Merchandising and banking/financial category);
|
19
|
+
# and Sun Oil and Exxon are in the petroleum category.
|
15
20
|
|
16
21
|
|
17
22
|
module CreditCardValidations
|
@@ -1,5 +1,10 @@
|
|
1
|
+
# String extension for brand detecting and number validation
|
1
2
|
#
|
2
|
-
#
|
3
|
+
# require 'credit_card_validations/string'
|
4
|
+
# '5274 5763 9425 9961'.credit_card_brand
|
5
|
+
# '5274 5763 9425 9961'.credit_card_brand_name
|
6
|
+
# '5274 5763 9425 9961'.valid_credit_card_brand?(:mastercard, :visa)
|
7
|
+
# '5274 5763 9425 9961'.valid_credit_card_brand?(:amex)
|
3
8
|
#
|
4
9
|
class String
|
5
10
|
def credit_card_brand
|
@@ -9,4 +14,9 @@ class String
|
|
9
14
|
def valid_credit_card_brand?(*brands)
|
10
15
|
CreditCardValidations::Detector.new(self).valid?(*brands)
|
11
16
|
end
|
17
|
+
|
18
|
+
def credit_card_brand_name
|
19
|
+
CreditCardValidations::Detector.new(self).brand_name
|
20
|
+
end
|
21
|
+
|
12
22
|
end
|
@@ -0,0 +1,238 @@
|
|
1
|
+
---
|
2
|
+
:visa:
|
3
|
+
:rules:
|
4
|
+
- :length:
|
5
|
+
- 13
|
6
|
+
- 16
|
7
|
+
:prefixes:
|
8
|
+
- '4'
|
9
|
+
:mastercard:
|
10
|
+
:rules:
|
11
|
+
- :length:
|
12
|
+
- 16
|
13
|
+
:prefixes:
|
14
|
+
- '51'
|
15
|
+
- '52'
|
16
|
+
- '53'
|
17
|
+
- '54'
|
18
|
+
- '55'
|
19
|
+
:options:
|
20
|
+
:brand_name: MasterCard
|
21
|
+
:amex:
|
22
|
+
:rules:
|
23
|
+
- :length:
|
24
|
+
- 15
|
25
|
+
:prefixes:
|
26
|
+
- '34'
|
27
|
+
- '37'
|
28
|
+
:options:
|
29
|
+
:brand_name: American Express
|
30
|
+
:diners:
|
31
|
+
:rules:
|
32
|
+
- :length:
|
33
|
+
- 14
|
34
|
+
:prefixes:
|
35
|
+
- '300'
|
36
|
+
- '301'
|
37
|
+
- '302'
|
38
|
+
- '303'
|
39
|
+
- '304'
|
40
|
+
- '305'
|
41
|
+
- '36'
|
42
|
+
- '38'
|
43
|
+
:options:
|
44
|
+
:brand_name: Diners Club
|
45
|
+
:diners_us:
|
46
|
+
:rules:
|
47
|
+
- :length:
|
48
|
+
- 16
|
49
|
+
:prefixes:
|
50
|
+
- '54'
|
51
|
+
- '55'
|
52
|
+
:options:
|
53
|
+
:brand_name: Diners Club US
|
54
|
+
:discover:
|
55
|
+
:rules:
|
56
|
+
- :length:
|
57
|
+
- 16
|
58
|
+
:prefixes:
|
59
|
+
- '6011'
|
60
|
+
- '644'
|
61
|
+
- '645'
|
62
|
+
- '646'
|
63
|
+
- '647'
|
64
|
+
- '648'
|
65
|
+
- '649'
|
66
|
+
- '65'
|
67
|
+
:jcb:
|
68
|
+
:rules:
|
69
|
+
- :length:
|
70
|
+
- 15
|
71
|
+
- 16
|
72
|
+
:prefixes:
|
73
|
+
- '3528'
|
74
|
+
- '3529'
|
75
|
+
- '353'
|
76
|
+
- '354'
|
77
|
+
- '355'
|
78
|
+
- '356'
|
79
|
+
- '357'
|
80
|
+
- '358'
|
81
|
+
- :length:
|
82
|
+
- 15
|
83
|
+
:prefixes:
|
84
|
+
- '1800'
|
85
|
+
- '2131'
|
86
|
+
- :length:
|
87
|
+
- 19
|
88
|
+
:prefixes:
|
89
|
+
- '357266'
|
90
|
+
:options:
|
91
|
+
:brand_name: JCB
|
92
|
+
:laser:
|
93
|
+
:rules:
|
94
|
+
- :length:
|
95
|
+
- 16
|
96
|
+
- 17
|
97
|
+
- 18
|
98
|
+
- 19
|
99
|
+
:prefixes:
|
100
|
+
- '6304'
|
101
|
+
- '6706'
|
102
|
+
- '6771'
|
103
|
+
:solo:
|
104
|
+
:rules:
|
105
|
+
- :length:
|
106
|
+
- 16
|
107
|
+
- 18
|
108
|
+
- 19
|
109
|
+
:prefixes:
|
110
|
+
- '6334'
|
111
|
+
- '6767'
|
112
|
+
:switch:
|
113
|
+
:rules:
|
114
|
+
- :length:
|
115
|
+
- 16
|
116
|
+
- 18
|
117
|
+
- 19
|
118
|
+
:prefixes:
|
119
|
+
- '633110'
|
120
|
+
- '633312'
|
121
|
+
- '633304'
|
122
|
+
- '633303'
|
123
|
+
- '633301'
|
124
|
+
- '633300'
|
125
|
+
:maestro:
|
126
|
+
:rules:
|
127
|
+
- :length:
|
128
|
+
- 12
|
129
|
+
- 13
|
130
|
+
- 14
|
131
|
+
- 15
|
132
|
+
- 16
|
133
|
+
- 17
|
134
|
+
- 18
|
135
|
+
- 19
|
136
|
+
:prefixes:
|
137
|
+
- '500'
|
138
|
+
- '5010'
|
139
|
+
- '5011'
|
140
|
+
- '5012'
|
141
|
+
- '5013'
|
142
|
+
- '5014'
|
143
|
+
- '5015'
|
144
|
+
- '5016'
|
145
|
+
- '5017'
|
146
|
+
- '5018'
|
147
|
+
- '502'
|
148
|
+
- '503'
|
149
|
+
- '504'
|
150
|
+
- '505'
|
151
|
+
- '506'
|
152
|
+
- '507'
|
153
|
+
- '508'
|
154
|
+
- '509'
|
155
|
+
- '56'
|
156
|
+
- '57'
|
157
|
+
- '58'
|
158
|
+
- '59'
|
159
|
+
- '6010'
|
160
|
+
- '6012'
|
161
|
+
- '6013'
|
162
|
+
- '6014'
|
163
|
+
- '6015'
|
164
|
+
- '6016'
|
165
|
+
- '6017'
|
166
|
+
- '6018'
|
167
|
+
- '6019'
|
168
|
+
- '602'
|
169
|
+
- '603'
|
170
|
+
- '604'
|
171
|
+
- '605'
|
172
|
+
- '6060'
|
173
|
+
- '621'
|
174
|
+
- '627'
|
175
|
+
- '629'
|
176
|
+
- '670'
|
177
|
+
- '671'
|
178
|
+
- '672'
|
179
|
+
- '673'
|
180
|
+
- '674'
|
181
|
+
- '675'
|
182
|
+
- '677'
|
183
|
+
- '6760'
|
184
|
+
- '6761'
|
185
|
+
- '6762'
|
186
|
+
- '6763'
|
187
|
+
- '6764'
|
188
|
+
- '6765'
|
189
|
+
- '6766'
|
190
|
+
- '6768'
|
191
|
+
- '6769'
|
192
|
+
- '679'
|
193
|
+
:unionpay:
|
194
|
+
:rules:
|
195
|
+
- :length:
|
196
|
+
- 16
|
197
|
+
- 17
|
198
|
+
- 18
|
199
|
+
- 19
|
200
|
+
:prefixes:
|
201
|
+
- '622'
|
202
|
+
- '624'
|
203
|
+
- '625'
|
204
|
+
- '626'
|
205
|
+
- '628'
|
206
|
+
:options:
|
207
|
+
:skip_luhn: true
|
208
|
+
:brand_name: China UnionPay
|
209
|
+
:dankrot:
|
210
|
+
:rules:
|
211
|
+
- :length:
|
212
|
+
- 16
|
213
|
+
:prefixes:
|
214
|
+
- '5019'
|
215
|
+
:rupay:
|
216
|
+
:rules:
|
217
|
+
- :length:
|
218
|
+
- 16
|
219
|
+
:prefixes:
|
220
|
+
- '6061'
|
221
|
+
- '6062'
|
222
|
+
- '6063'
|
223
|
+
- '6064'
|
224
|
+
- '6065'
|
225
|
+
- '6066'
|
226
|
+
- '6067'
|
227
|
+
- '6068'
|
228
|
+
- '6069'
|
229
|
+
- '607'
|
230
|
+
- '608'
|
231
|
+
:options:
|
232
|
+
:skip_luhn: true
|
233
|
+
:hipercard:
|
234
|
+
:rules:
|
235
|
+
- :length:
|
236
|
+
- 19
|
237
|
+
:prefixes:
|
238
|
+
- '384'
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
describe "ActiveModel Validator" do
|
4
|
+
|
5
|
+
let(:model) { CreditCard.new }
|
6
|
+
|
7
|
+
describe "Proc support" do
|
8
|
+
it "should be valid if brands from proc valid" do
|
9
|
+
card = model.dup
|
10
|
+
card.card_type = "Master Card"
|
11
|
+
card.number6 = CreditCardValidations::Factory.random(:visa)
|
12
|
+
card.valid?.must_equal false
|
13
|
+
card.number6 = CreditCardValidations::Factory.random(:mastercard)
|
14
|
+
card.valid?.must_equal true
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "Any Brand" do
|
21
|
+
it "should be valid for all prepared valid numbers" do
|
22
|
+
VALID_NUMBERS.each do |_, numbers|
|
23
|
+
|
24
|
+
numbers.each do |number|
|
25
|
+
card = model
|
26
|
+
card.number4 = number
|
27
|
+
card.number5 = number
|
28
|
+
card.valid?.must_equal true
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
describe "Except Amex and Maestro brand" do
|
37
|
+
it "should reject all other valid numbers" do
|
38
|
+
VALID_NUMBERS.except(:amex, :maestro) do |_, numbers|
|
39
|
+
card = model
|
40
|
+
card.number = numbers.first
|
41
|
+
card.valid?.must_equal false
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should accept using except options" do
|
46
|
+
VALID_NUMBERS.except(:amex, :maestro) do |_, numbers|
|
47
|
+
card = model
|
48
|
+
card.number3 = numbers.first
|
49
|
+
card.valid?.must_equal true
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "Only Amex and Mestro brands" do
|
56
|
+
|
57
|
+
|
58
|
+
it "should accept amex and maestro brand if valid" do
|
59
|
+
VALID_NUMBERS.slice(:amex, :maestro) do |_, numbers|
|
60
|
+
card = model
|
61
|
+
card.number = numbers.first
|
62
|
+
card.number2 = numbers.first
|
63
|
+
card.valid?.must_equal true
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|