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 CHANGED
@@ -1,3 +1,8 @@
1
+ 2.0.0
2
+
3
+ Rewrite.
4
+
5
+
1
6
  1.0.2
2
7
 
3
8
  Readme improvement.
data/README.md CHANGED
@@ -12,18 +12,19 @@ Basic usage
12
12
  -----------
13
13
 
14
14
  ```ruby
15
- credit_card = CreditCardSupport::Instrument.new(
16
- number: '4222222222222',
17
- expiry_year: 13,
18
- expiry_month: 11,
19
- holder_name: 'A B', # optional
20
- verification: '1234' # optional
21
- )
22
-
23
- credit_card.is_expired? # returns false
24
- credit_card.expiration_date # Date (last day of the month for expire month)
25
- credit_card.issuer # :visa
26
- credit_card.is_testcard? # true
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
- ##### ActiveRecord or Mongoid
43
+ #### ActiveRecord or Mongoid
38
44
 
39
45
  ```ruby
40
46
  class CreditCard < ActiveRecord::Base
41
47
 
42
- attr_accessible :number, :expiry_year, :expiry_month
48
+ attr_accessible :number
43
49
 
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: true # default: False
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
- ##### Only ActiveModel
63
+ #### Only ActiveModel
55
64
 
56
- ###### Rails 3
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, credit_card: {
72
- expiry_year: :expiry_year, # default: :expiry_year
73
- expiry_month: :expiry_month, # default: :expiry_month
74
- allowed_issuers: [:visa, :mastercard], # default: all known issuers
75
- allow_testcards: true # default: False
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
- ###### Rails 4
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, credit_card: {
90
- expiry_year: :expiry_year, # default: :expiry_year
91
- expiry_month: :expiry_month, # default: :expiry_month
92
- allowed_issuers: [:visa, :mastercard], # default: all known issuers
93
- allow_testcards: true # default: False
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
- # RAILS_ENV == 'test'
109
- creditcard = CreditCard.new(number: '4000111111111115', expiry_year: today.year, expiry_month: today.month)
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
 
@@ -1,3 +1,3 @@
1
1
  require 'credit_card_support/luhn_number'
2
- require 'credit_card_support/instrument'
3
- require 'credit_card_support/credit_card_validator' if Object.const_defined?(:ActiveModel)
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
- 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"
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
- 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?
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
@@ -1,8 +1,8 @@
1
1
  module CreditCardSupport
2
2
  module Version
3
- MAJOR = 1
3
+ MAJOR = 2
4
4
  MINOR = 0
5
- PATCH = 2
5
+ PATCH = 0
6
6
 
7
7
  STRING = [MAJOR, MINOR, PATCH].compact.join('.')
8
8
  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
- 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
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
- 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
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
- 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
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: 1.0.2
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/credit_card_validator.rb
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/credit_card_validator_spec.rb
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