credit_card_support 1.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/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:
|