credit_card_support 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +7 -0
- data/LICENSE.txt +20 -0
- data/README.md +119 -0
- data/lib/credit_card_support/credit_card_validator.rb +78 -0
- data/lib/credit_card_support/instrument.rb +162 -0
- data/lib/credit_card_support/locale/en.yml +9 -0
- data/lib/credit_card_support/luhn_number.rb +44 -0
- data/lib/credit_card_support/version.rb +9 -0
- data/lib/credit_card_support.rb +2 -0
- data/spec/lib/credit_card_support/credit_card_validator_spec.rb +120 -0
- data/spec/lib/credit_card_support/instrument_spec.rb +125 -0
- data/spec/lib/credit_card_support/luhn_number_spec.rb +62 -0
- data/spec/spec_helper.rb +1 -0
- metadata +59 -0
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2012 mxrguspxrt
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
CreditCardSupport
|
2
|
+
=================
|
3
|
+
|
4
|
+
Intro
|
5
|
+
-----
|
6
|
+
|
7
|
+
* Internationalization and localization support for validations
|
8
|
+
* At the moment knows AMEX, DinersClub, Discover, Enroute, Maestro, MasterCard, JCB, Solo, VISA (if you need more, create an issue and request for it :)
|
9
|
+
|
10
|
+
|
11
|
+
Basic usage
|
12
|
+
-----------
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
credit_card = CreditCardSupport.Instrument.new(
|
16
|
+
number: '4222222222222',
|
17
|
+
expire_year: 13,
|
18
|
+
expire_month: 11,
|
19
|
+
holder_name: 'A B' # optional!
|
20
|
+
verification: '1234' # optional!
|
21
|
+
)
|
22
|
+
|
23
|
+
credit_card.expired? # returns false
|
24
|
+
credit_card.expire_date # Date (last day of the month for expire month)
|
25
|
+
credit_card.issuer # VISA
|
26
|
+
credit_card.is_testcard? # true
|
27
|
+
```
|
28
|
+
|
29
|
+
For better understanding read the source and RSpec.
|
30
|
+
|
31
|
+
|
32
|
+
Validations support
|
33
|
+
-------------------
|
34
|
+
|
35
|
+
### Define
|
36
|
+
|
37
|
+
##### ActiveRecord or Mongoid
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
class CreditCard < ActiveRecord::Base
|
41
|
+
|
42
|
+
attr_accessable :number, :expiry_year, :expiry_month
|
43
|
+
|
44
|
+
validates :number, credit_card: {
|
45
|
+
expiry_year: :expiry_year, # default: :expiry_year
|
46
|
+
expiry_month: :expiry_month, # default: :expiry_month
|
47
|
+
allowed_issuers: [:visa, :mastercard], # default: all known issuers
|
48
|
+
allow_testcards: !Rails.env.production? # default: False
|
49
|
+
}
|
50
|
+
|
51
|
+
end
|
52
|
+
```
|
53
|
+
|
54
|
+
##### Only ActiveModel
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
class CreditCard
|
58
|
+
extend ActiveModel::Naming
|
59
|
+
extend ActiveModel::Translation
|
60
|
+
include ActiveModel::Validations
|
61
|
+
include ActiveModel::Conversion
|
62
|
+
|
63
|
+
attr_accessor :number, :expiry_year, :expiry_month
|
64
|
+
attr_accessable :number, :expiry_year, :expiry_month
|
65
|
+
|
66
|
+
validates :number, credit_card: {
|
67
|
+
expiry_year: :expiry_year, # default: :expiry_year
|
68
|
+
expiry_month: :expiry_month, # default: :expiry_month
|
69
|
+
allowed_issuers: [:visa, :mastercard], # default: all known issuers
|
70
|
+
allow_testcards: !Rails.env.production? # default: False
|
71
|
+
}
|
72
|
+
|
73
|
+
end
|
74
|
+
```
|
75
|
+
|
76
|
+
### Use
|
77
|
+
|
78
|
+
After you have defined your CreditCard
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
today = Date.today
|
82
|
+
```
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
# RAILS_ENV == 'test'
|
86
|
+
creditcard = CreditCard.new(number: '4000111111111115', expiry_year: today.year, expiry_month: today.month)
|
87
|
+
creditcard.valid? # True
|
88
|
+
|
89
|
+
# RAILS_ENV == 'production'
|
90
|
+
creditcard = CreditCard.new(number: '4000111111111115', expiry_year: today.year, expiry_month: today.month)
|
91
|
+
creditcard.valid? # False
|
92
|
+
creditcard.errors # {number: ["is a test number"]}
|
93
|
+
|
94
|
+
# RAILS_ENV == 'test'
|
95
|
+
creditcard = CreditCard.new(number: '3566003566003566', expiry_year: today.year-1, expiry_month: today.month)
|
96
|
+
creditcard.valid? # False
|
97
|
+
creditcard.errors # {number: ["is a JCB card number and is not supported", expiry_year: ["is in the past"], expiry_month: ["is in the past"]]}
|
98
|
+
|
99
|
+
```
|
100
|
+
|
101
|
+
|
102
|
+
Problems?
|
103
|
+
=========
|
104
|
+
|
105
|
+
Create an issue or submit a patch.
|
106
|
+
|
107
|
+
|
108
|
+
Inspired by
|
109
|
+
===========
|
110
|
+
|
111
|
+
* https://github.com/tobias/credit_card_validator
|
112
|
+
* https://github.com/pda/am_credit_card
|
113
|
+
* http://rubygems.org/gems/creditcard
|
114
|
+
|
115
|
+
|
116
|
+
Author
|
117
|
+
======
|
118
|
+
|
119
|
+
Margus Pärt
|
@@ -0,0 +1,78 @@
|
|
1
|
+
raise "ActiveModel not included" unless Object.const_defined?(:ActiveModel)
|
2
|
+
I18n.load_path << File.dirname(__FILE__) + '/locale/en.yml'
|
3
|
+
|
4
|
+
class CreditCardValidator < ActiveModel::EachValidator
|
5
|
+
|
6
|
+
def validate_each(record, attribute, value)
|
7
|
+
@record = record
|
8
|
+
|
9
|
+
opts = options.dup
|
10
|
+
opts[:number] ||= attribute
|
11
|
+
|
12
|
+
number = record.send(opts[:number]||:number) rescue (raise ":number not defined")
|
13
|
+
expiry_year = record.send(opts[:expiry_year]||:expiry_year) rescue (raise ":expiry_year not defined")
|
14
|
+
expiry_month = record.send(opts[:expiry_month]||:expiry_month) rescue (raise ":expiry_month not defined")
|
15
|
+
@allowed_issuers = opts[:allowed_issuers]
|
16
|
+
@allow_testcards = opts[:allow_testcards]
|
17
|
+
|
18
|
+
@opts = opts
|
19
|
+
@instrument = CreditCardSupport::Instrument.new(number: number, expiry_year: expiry_year, expiry_month: expiry_month)
|
20
|
+
|
21
|
+
validate_number
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def errors
|
28
|
+
@record.errors
|
29
|
+
end
|
30
|
+
|
31
|
+
def instrument
|
32
|
+
@instrument
|
33
|
+
end
|
34
|
+
|
35
|
+
def validate_number
|
36
|
+
validate_luhn
|
37
|
+
validate_issuer
|
38
|
+
validate_testcard
|
39
|
+
validate_expiry_year
|
40
|
+
validate_expiry_month
|
41
|
+
validate_expiration_date rescue false
|
42
|
+
end
|
43
|
+
|
44
|
+
def validate_luhn
|
45
|
+
errors.add(@opts[:number], t(:luhn_not_valid)) unless @instrument.has_valid_luhn?
|
46
|
+
end
|
47
|
+
|
48
|
+
def validate_issuer
|
49
|
+
errors.add(@opts[:number], t(:issuer_not_known)) unless @instrument.issuer
|
50
|
+
if @allowed_issuers and @instrument.issuer and !@allowed_issuers.map(&:to_sym).include?(@instrument.issuer)
|
51
|
+
errors.add(@opts[:number], t(:issuer_not_supported, issuer: @instrument.issuer.to_s.upcase))
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def validate_testcard
|
56
|
+
errors.add(@opts[:number], t(:testcard_not_supported)) if !@allow_testcards && @instrument.is_testcard?
|
57
|
+
end
|
58
|
+
|
59
|
+
def validate_expiry_year
|
60
|
+
errors.add(@opts[:expiry_year], t(:cant_be_blank)) unless @instrument.expiry_year
|
61
|
+
end
|
62
|
+
|
63
|
+
def validate_expiry_month
|
64
|
+
errors.add(@opts[:expiry_month], t(:cant_be_blank)) unless @instrument.expiry_month
|
65
|
+
end
|
66
|
+
|
67
|
+
def validate_expiration_date
|
68
|
+
if @instrument.expired?
|
69
|
+
errors.add(@opts[:expiry_year], t(:cant_be_expired))
|
70
|
+
errors.add(@opts[:expiry_month], t(:cant_be_expired))
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def t(code, *args)
|
75
|
+
I18n.t("credit_card_validator.messages.#{code}", *args)
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
module CreditCardSupport
|
2
|
+
|
3
|
+
# Usage:
|
4
|
+
#
|
5
|
+
# credit_card = CreditCardSupport.Instrument.new(
|
6
|
+
# number: '4222222222222',
|
7
|
+
# expiry_year: 13,
|
8
|
+
# expiry_month: 11,
|
9
|
+
# holder_name: 'A B'
|
10
|
+
# verification: '1234' # optional!
|
11
|
+
# )
|
12
|
+
#
|
13
|
+
# credit_card.expired? # returns false
|
14
|
+
# credit_card.expiration_date # Date (last day of the month for expiry month)
|
15
|
+
# credit_card.issuer # VISA
|
16
|
+
# credit_card.is_testcard? # true
|
17
|
+
|
18
|
+
|
19
|
+
class Instrument
|
20
|
+
|
21
|
+
NUMBERS = {
|
22
|
+
amex: /^3[47][0-9]{13}$/,
|
23
|
+
diners_club: /^3(?:0[0-5]|[68][0-9])[0-9]{11}$/,
|
24
|
+
discover: /^6(?:011|5[0-9]{2})[0-9]{12}$/,
|
25
|
+
enroute: /^2(014|149)\d{11}$/,
|
26
|
+
maestro: /(^6759[0-9]{2}([0-9]{10})$)|(^6759[0-9]{2}([0-9]{12})$)|(^6759[0-9]{2}([0-9]{13})$)/,
|
27
|
+
master_card: /^5[1-5][0-9]{14}$/,
|
28
|
+
jcb: /^35[0-9]{14}$/,
|
29
|
+
solo: /^6(3(34[5-9][0-9])|767[0-9]{2})\d{10}(\d{2,3})?$/,
|
30
|
+
visa: /^4[0-9]{12}(?:[0-9]{3})?$/
|
31
|
+
}.freeze
|
32
|
+
|
33
|
+
TESTCARD_NUMBERS = {
|
34
|
+
amex: [
|
35
|
+
'378282246310005',
|
36
|
+
'371449635398431',
|
37
|
+
'378734493671000',
|
38
|
+
'343434343434343',
|
39
|
+
'371144371144376',
|
40
|
+
'341134113411347'
|
41
|
+
],
|
42
|
+
diners_club: [
|
43
|
+
'30569309025904',
|
44
|
+
'38520000023237',
|
45
|
+
'36438936438936'
|
46
|
+
],
|
47
|
+
discover: [
|
48
|
+
'6011000990139424',
|
49
|
+
'6011111111111117',
|
50
|
+
'6011016011016011',
|
51
|
+
'6011000000000004',
|
52
|
+
'6011000995500000',
|
53
|
+
'6500000000000002'
|
54
|
+
],
|
55
|
+
master_card: [
|
56
|
+
'5555555555554444',
|
57
|
+
'5105105105105100',
|
58
|
+
'5500005555555559',
|
59
|
+
'5555555555555557',
|
60
|
+
'5454545454545454',
|
61
|
+
'5555515555555551',
|
62
|
+
'5405222222222226',
|
63
|
+
'5478050000000007',
|
64
|
+
'5111005111051128',
|
65
|
+
'5112345112345114'
|
66
|
+
],
|
67
|
+
visa: [
|
68
|
+
'4111111111111111',
|
69
|
+
'4012888888881881',
|
70
|
+
'4222222222222',
|
71
|
+
'4005519200000004',
|
72
|
+
'4009348888881881',
|
73
|
+
'4012000033330026',
|
74
|
+
'4012000077777777',
|
75
|
+
'4217651111111119',
|
76
|
+
'4500600000000061',
|
77
|
+
'4000111111111115'
|
78
|
+
],
|
79
|
+
jcb: [
|
80
|
+
'3566003566003566',
|
81
|
+
'3528000000000007'
|
82
|
+
]
|
83
|
+
}.freeze
|
84
|
+
|
85
|
+
def self.numbers
|
86
|
+
self::NUMBERS
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.testcard_numbers
|
90
|
+
self::TESTCARD_NUMBERS
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.determine_issuer(number)
|
94
|
+
numbers.each do |issuer, issuer_regexp|
|
95
|
+
return issuer if number.match(issuer_regexp)
|
96
|
+
end
|
97
|
+
nil
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.is_testcard?(number)
|
101
|
+
testcard_numbers.values.flatten.include?(number)
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.has_valid_luhn?(number)
|
105
|
+
CreditCardSupport::LuhnNumber.new(number).valid?
|
106
|
+
end
|
107
|
+
|
108
|
+
attr_accessor :number,
|
109
|
+
:expiry_year,
|
110
|
+
:expiry_month,
|
111
|
+
:holder_name,
|
112
|
+
:verification
|
113
|
+
|
114
|
+
attr_reader :issuer
|
115
|
+
|
116
|
+
def initialize(opts={}, &block)
|
117
|
+
self.number = opts[:number]
|
118
|
+
self.expiry_year = opts[:expiry_year]
|
119
|
+
self.expiry_month = opts[:expiry_month]
|
120
|
+
self.holder_name = opts[:holder_name]
|
121
|
+
self.verification = opts[:verification]
|
122
|
+
|
123
|
+
block.call(self) if block
|
124
|
+
end
|
125
|
+
|
126
|
+
def number=(number)
|
127
|
+
@number = number.to_s.gsub(/[^0-9]/, '')
|
128
|
+
@issuer = self.class.determine_issuer(self.number)
|
129
|
+
@is_testcard = self.class.is_testcard?(self.number)
|
130
|
+
@has_valid_luhn = self.class.has_valid_luhn?(self.number)
|
131
|
+
end
|
132
|
+
|
133
|
+
def expiry_year=(expiry_year)
|
134
|
+
@expiry_year = expiry_year.to_i if expiry_year
|
135
|
+
end
|
136
|
+
|
137
|
+
def expiry_month=(expiry_month)
|
138
|
+
@expiry_month = expiry_month.to_i if expiry_month
|
139
|
+
end
|
140
|
+
|
141
|
+
def expiry_year
|
142
|
+
(@expiry_year < 1900 ? 2000 + @expiry_year : @expiry_year) if @expiry_year
|
143
|
+
end
|
144
|
+
|
145
|
+
def expiration_date
|
146
|
+
Date.new(expiry_year, expiry_month, -1)
|
147
|
+
end
|
148
|
+
|
149
|
+
def expired?
|
150
|
+
expiration_date < Date.today
|
151
|
+
end
|
152
|
+
|
153
|
+
def is_testcard?
|
154
|
+
@is_testcard
|
155
|
+
end
|
156
|
+
|
157
|
+
def has_valid_luhn?
|
158
|
+
@has_valid_luhn
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
en:
|
2
|
+
credit_card_validator:
|
3
|
+
messages:
|
4
|
+
luhn_not_valid: "is not valid"
|
5
|
+
issuer_not_known: "not known issuer"
|
6
|
+
issuer_not_supported: "%{issuer} not supported"
|
7
|
+
testcard_not_supported: "testcards not supported"
|
8
|
+
cant_be_blank: "can't be blank"
|
9
|
+
is_expired: "is expired"
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module CreditCardSupport
|
2
|
+
|
3
|
+
class LuhnNumber
|
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?
|
24
|
+
return false unless full_number and full_number.length > 2
|
25
|
+
parts = full_number.split(//).map(&:to_i)
|
26
|
+
|
27
|
+
double = parts.length % 2 == 0
|
28
|
+
parts = parts.collect do |part|
|
29
|
+
number = double ? part*2 : part
|
30
|
+
double = !double
|
31
|
+
number
|
32
|
+
end
|
33
|
+
|
34
|
+
sum = parts.join("").split(//).map(&:to_i).inject(:+)
|
35
|
+
sum % 10 == 0
|
36
|
+
end
|
37
|
+
|
38
|
+
def invalid?
|
39
|
+
!valid?
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'active_model'
|
3
|
+
require 'credit_card_support/credit_card_validator'
|
4
|
+
|
5
|
+
|
6
|
+
class CreditCard
|
7
|
+
extend ActiveModel::Naming
|
8
|
+
extend ActiveModel::Translation
|
9
|
+
include ActiveModel::Validations
|
10
|
+
include ActiveModel::Conversion
|
11
|
+
|
12
|
+
attr_accessor :number, :expiry_year, :expiry_month
|
13
|
+
|
14
|
+
def initialize(opts={})
|
15
|
+
@errors = ActiveModel::Errors.new(self)
|
16
|
+
opts.each do |k,v|
|
17
|
+
send(:"#{k}=", v)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
class CreditCardTest < CreditCard
|
24
|
+
validates :number, credit_card: {
|
25
|
+
expiry_year: :expiry_year,
|
26
|
+
expiry_month: :expiry_month,
|
27
|
+
allowed_issuers: [:visa, :mastercard],
|
28
|
+
allow_testcards: true
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
class CreditCardProduction < CreditCard
|
33
|
+
validates :number, credit_card: {
|
34
|
+
expiry_year: :expiry_year,
|
35
|
+
expiry_month: :expiry_month,
|
36
|
+
allowed_issuers: [:visa, :mastercard],
|
37
|
+
allow_testcards: false
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
|
43
|
+
describe CreditCardValidator do
|
44
|
+
let(:today) { Date.today }
|
45
|
+
|
46
|
+
describe "validatable" do
|
47
|
+
|
48
|
+
subject { CreditCardTest.new(number: '4012888888881881', expiry_year: today.year, expiry_month: today.month) }
|
49
|
+
|
50
|
+
it "is valid" do
|
51
|
+
subject.should be_valid
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "#number" do
|
55
|
+
it "must exist" do
|
56
|
+
subject.number = nil
|
57
|
+
subject.should_not be_valid
|
58
|
+
end
|
59
|
+
it "must be luhn" do
|
60
|
+
subject.number = '4000111111111116'
|
61
|
+
subject.should_not be_valid
|
62
|
+
end
|
63
|
+
context "production" do
|
64
|
+
subject { CreditCardProduction.new(number: '4485071359608368', expiry_year: today.year, expiry_month: today.month) }
|
65
|
+
context "testnumber" do
|
66
|
+
it "is invalid" do
|
67
|
+
subject.number = '4012888888881881'
|
68
|
+
subject.should be_invalid
|
69
|
+
end
|
70
|
+
end
|
71
|
+
context "valid number" do
|
72
|
+
it "is valid" do
|
73
|
+
subject.should be_valid
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe "#expiry_year" do
|
80
|
+
it "must exist" do
|
81
|
+
subject.expiry_year = nil
|
82
|
+
subject.should_not be_valid
|
83
|
+
end
|
84
|
+
context "in the future" do
|
85
|
+
it "is valid" do
|
86
|
+
subject.expiry_year = today.year + 1
|
87
|
+
subject.should be_valid
|
88
|
+
end
|
89
|
+
end
|
90
|
+
context "in the past" do
|
91
|
+
it "is invalid" do
|
92
|
+
subject.expiry_year = today.year - 1
|
93
|
+
subject.should be_invalid
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
describe "#expiry_month" do
|
99
|
+
it "must exist" do
|
100
|
+
subject.expiry_month = nil
|
101
|
+
subject.should_not be_valid
|
102
|
+
end
|
103
|
+
context "in the future" do
|
104
|
+
it "is valid" do
|
105
|
+
subject.expiry_month = today.month + 1
|
106
|
+
subject.should be_valid
|
107
|
+
end
|
108
|
+
end
|
109
|
+
context "in the past" do
|
110
|
+
it "is invalid" do
|
111
|
+
subject.expiry_month = today.month - 1
|
112
|
+
subject.should be_invalid
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
|
@@ -0,0 +1,125 @@
|
|
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 "#expired?" do
|
80
|
+
context "not expired" do
|
81
|
+
it "returns false" do
|
82
|
+
subject.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.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
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe CreditCardSupport::LuhnNumber do
|
4
|
+
|
5
|
+
subject { described_class.new("4485071359608368") }
|
6
|
+
|
7
|
+
describe "#initialize" do
|
8
|
+
it "sets full_number" do
|
9
|
+
described_class.any_instance.should_receive(:full_number=).with('123')
|
10
|
+
described_class.new('123')
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "#full_number" do
|
15
|
+
it "returns only number part of set value" do
|
16
|
+
subject.full_number = '123 abc-'
|
17
|
+
subject.full_number.should == '123'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "#number_part" do
|
22
|
+
it "returns number part of full_number" do
|
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
|
40
|
+
context "valid luhn" do
|
41
|
+
it "returns true" do
|
42
|
+
described_class.new("5500005555555559").should be_valid
|
43
|
+
described_class.new("5555555555555557").should be_valid
|
44
|
+
described_class.new("5454545454545454").should be_valid
|
45
|
+
described_class.new("5555515555555551").should be_valid
|
46
|
+
described_class.new("5405222222222226").should be_valid
|
47
|
+
described_class.new("5478050000000007").should be_valid
|
48
|
+
end
|
49
|
+
end
|
50
|
+
context "invalid luhn" do
|
51
|
+
it "returns false" do
|
52
|
+
described_class.new("5500005555555558").should be_invalid
|
53
|
+
described_class.new("5555555555555556").should be_invalid
|
54
|
+
described_class.new("5454545454545453").should be_invalid
|
55
|
+
described_class.new("5555515555555550").should be_invalid
|
56
|
+
described_class.new("5405222222222225").should be_invalid
|
57
|
+
described_class.new("5478050000000006").should be_invalid
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'credit_card_support'
|
metadata
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: credit_card_support
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Margus Pärt
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-09-21 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: Detects issuer from number, has ActiveModel validations and translations
|
15
|
+
support.
|
16
|
+
email: margus@tione.eu
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- Gemfile
|
22
|
+
- README.md
|
23
|
+
- LICENSE.txt
|
24
|
+
- lib/credit_card_support/credit_card_validator.rb
|
25
|
+
- lib/credit_card_support/instrument.rb
|
26
|
+
- lib/credit_card_support/locale/en.yml
|
27
|
+
- lib/credit_card_support/luhn_number.rb
|
28
|
+
- lib/credit_card_support/version.rb
|
29
|
+
- lib/credit_card_support.rb
|
30
|
+
- spec/lib/credit_card_support/credit_card_validator_spec.rb
|
31
|
+
- spec/lib/credit_card_support/instrument_spec.rb
|
32
|
+
- spec/lib/credit_card_support/luhn_number_spec.rb
|
33
|
+
- spec/spec_helper.rb
|
34
|
+
homepage: https://github.com/tione/credit_card_support
|
35
|
+
licenses: []
|
36
|
+
post_install_message:
|
37
|
+
rdoc_options: []
|
38
|
+
require_paths:
|
39
|
+
- lib
|
40
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 1.9.3
|
46
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
47
|
+
none: false
|
48
|
+
requirements:
|
49
|
+
- - ! '>='
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '0'
|
52
|
+
requirements: []
|
53
|
+
rubyforge_project:
|
54
|
+
rubygems_version: 1.8.24
|
55
|
+
signing_key:
|
56
|
+
specification_version: 3
|
57
|
+
summary: Libary to handle CreditCard number, expiration and Luhn.
|
58
|
+
test_files: []
|
59
|
+
has_rdoc:
|