credit_officer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format Fuubar
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm gemset use credit_officer --create
data/Gemfile ADDED
@@ -0,0 +1,24 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+
6
+ # Add dependencies to develop your gem here.
7
+ # Include everything needed to run rake, tests, features, etc.
8
+ gem "activemodel", ">= 3.0.3"
9
+ gem "luhney_bin"
10
+
11
+ group :development do
12
+ gem "timecop", "0.3.5"
13
+ gem "ruby-debug"
14
+ gem "factory_girl"
15
+ gem "rspec", "~> 2.3.0"
16
+ gem "remarkable_activemodel", ">=4.0.0.alpha4"
17
+ gem "yard", "~> 0.6.0"
18
+ gem "bundler", "~> 1.0.0"
19
+ gem "jeweler", "~> 1.5.1"
20
+ gem "rcov", ">= 0"
21
+ gem "reek", "~> 1.2.8"
22
+ gem "roodi", "~> 2.1.0"
23
+ gem "fuubar"
24
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,79 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ activemodel (3.0.3)
5
+ activesupport (= 3.0.3)
6
+ builder (~> 2.1.2)
7
+ i18n (~> 0.4)
8
+ activesupport (3.0.3)
9
+ builder (2.1.2)
10
+ columnize (0.3.1)
11
+ diff-lcs (1.1.2)
12
+ factory_girl (1.3.2)
13
+ fuubar (0.0.3)
14
+ rspec (~> 2.0)
15
+ rspec-instafail (~> 0.1.4)
16
+ ruby-progressbar (~> 0.0.9)
17
+ git (1.2.5)
18
+ i18n (0.5.0)
19
+ jeweler (1.5.2)
20
+ bundler (~> 1.0.0)
21
+ git (>= 1.2.5)
22
+ rake
23
+ linecache (0.43)
24
+ luhney_bin (0.1.0)
25
+ rake (0.8.7)
26
+ rcov (0.9.9)
27
+ reek (1.2.8)
28
+ ruby2ruby (~> 1.2)
29
+ ruby_parser (~> 2.0)
30
+ sexp_processor (~> 3.0)
31
+ remarkable (4.0.0.alpha4)
32
+ rspec (>= 2.0.0.alpha11)
33
+ remarkable_activemodel (4.0.0.alpha4)
34
+ remarkable (~> 4.0.0.alpha4)
35
+ rspec (>= 2.0.0.alpha11)
36
+ roodi (2.1.0)
37
+ ruby_parser
38
+ rspec (2.3.0)
39
+ rspec-core (~> 2.3.0)
40
+ rspec-expectations (~> 2.3.0)
41
+ rspec-mocks (~> 2.3.0)
42
+ rspec-core (2.3.1)
43
+ rspec-expectations (2.3.0)
44
+ diff-lcs (~> 1.1.2)
45
+ rspec-instafail (0.1.5)
46
+ rspec-mocks (2.3.0)
47
+ ruby-debug (0.10.4)
48
+ columnize (>= 0.1)
49
+ ruby-debug-base (~> 0.10.4.0)
50
+ ruby-debug-base (0.10.4)
51
+ linecache (>= 0.3)
52
+ ruby-progressbar (0.0.9)
53
+ ruby2ruby (1.2.5)
54
+ ruby_parser (~> 2.0)
55
+ sexp_processor (~> 3.0)
56
+ ruby_parser (2.0.5)
57
+ sexp_processor (~> 3.0)
58
+ sexp_processor (3.0.5)
59
+ timecop (0.3.5)
60
+ yard (0.6.3)
61
+
62
+ PLATFORMS
63
+ ruby
64
+
65
+ DEPENDENCIES
66
+ activemodel (>= 3.0.3)
67
+ bundler (~> 1.0.0)
68
+ factory_girl
69
+ fuubar
70
+ jeweler (~> 1.5.1)
71
+ luhney_bin
72
+ rcov
73
+ reek (~> 1.2.8)
74
+ remarkable_activemodel (>= 4.0.0.alpha4)
75
+ roodi (~> 2.1.0)
76
+ rspec (~> 2.3.0)
77
+ ruby-debug
78
+ timecop (= 0.3.5)
79
+ yard (~> 0.6.0)
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Dan Pickett
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.rdoc ADDED
@@ -0,0 +1,102 @@
1
+ = credit_officer
2
+
3
+ An ActiveModel port of ActiveMerchant's credit card validations.
4
+
5
+ Use only with Rails 3/ActiveModel supported applications
6
+
7
+ == The Basics
8
+
9
+ Use this library so that you can validate credit card information before sending it to your payment processor
10
+
11
+ Checks credit card number formats, checksums, and other required details. Supports i18n for better message
12
+ customization
13
+
14
+ cc = CreditOfficer::CreditCard.new(
15
+ :number => "411111111111111",
16
+ :provider_name => "visa",
17
+ :name_on_card => "John Doe",
18
+ :expiration_year => 2010,
19
+ :expiration_month => 1,
20
+ :verification_value => "343"
21
+ }).valid? => true
22
+
23
+ cc.number = ""
24
+ cc.valid? => false
25
+ cc.errors.full_messages => ["Number can't be blank"]
26
+
27
+ == Configuring verification
28
+
29
+ If you want to turn requiring verification values off, make it so:
30
+
31
+ CreditOfficer::CreditCard.require_verification_value = false
32
+ cc = CreditOfficer::CreditCard.new(
33
+ :number => "411111111111111",
34
+ :provider_name => "visa",
35
+ :name_on_card => "John Doe",
36
+ :expiration_year => 2010,
37
+ :expiration_month => 1,
38
+ :verification_value => ""
39
+ }).valid? => true
40
+
41
+
42
+ == Configuring Providers
43
+
44
+ Want to only support certain credit cards and card number formats? Make it so:
45
+
46
+ CreditOfficer::CreditCard.supported_providers = [
47
+ 'mastercard',
48
+ 'amex'
49
+ ]
50
+
51
+ cc = CreditOfficer::CreditCard.new(
52
+ :number => "411111111111111",
53
+ :provider_name => "visa",
54
+ :name_on_card => "John Doe",
55
+ :expiration_year => 2010,
56
+ :expiration_month => 1,
57
+ :verification_value => ""
58
+ }).valid? => false
59
+
60
+ == i18n
61
+
62
+ Error messages can be customized with i18n translations
63
+
64
+ credit_officer:
65
+ errors:
66
+ messages:
67
+ expired: "is expired"
68
+ exceeds_recent_future: "is not a valid year"
69
+ invalid_format: "is not a valid card number"
70
+ unsupported_provider: "is not supported"
71
+ invalid_issue_number: "is not valid"
72
+ futuristic_start_date: "is in the future"
73
+
74
+ == Why?
75
+
76
+ ActiveMerchant has a ton of functionality and it's a great library. I just wanted the ability to validate
77
+ information before sending it to a payment processor. I don't want all the payment processor logic, etc
78
+ bloating my applications. Lean, simple, extensible, and up to date - that's how I like it!
79
+
80
+ I've also added some of the niceties that an activemodel compliant architecture provides (i18n, for example)
81
+
82
+ == A note on persistence
83
+
84
+ Do not store this data yourself unless you absolutely must. Your payment processors should have a way for you
85
+ to transmit this information and have them store it for you. Otherwise, you're beholden to all kinds of
86
+ PCI compliance issues.
87
+
88
+ == Contributing to credit_officer
89
+
90
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
91
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
92
+ * Fork the project
93
+ * Start a feature/bugfix branch
94
+ * Commit and push until you are happy with your contribution
95
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
96
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
97
+
98
+ == Copyright
99
+
100
+ Copyright (c) 2010 Dan Pickett. See LICENSE.txt for
101
+ further details.
102
+
data/Rakefile ADDED
@@ -0,0 +1,56 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'rake'
11
+
12
+ require 'jeweler'
13
+ Jeweler::Tasks.new do |gem|
14
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
15
+ gem.name = "credit_officer"
16
+ gem.homepage = "http://github.com/dpickett/credit_officer"
17
+ gem.license = "MIT"
18
+ gem.summary = %Q{An activemodel compliant credit card validator}
19
+ gem.description = %Q{An upgrade/port of ActiveMerchant's credit card class}
20
+ gem.email = "dpickett@enlightsolutions.com"
21
+ gem.authors = ["Dan Pickett"]
22
+ # Include your dependencies below. Runtime dependencies are required when using your gem,
23
+ # and development dependencies are only needed for development (ie running rake tasks, tests, etc)
24
+ # gem.add_runtime_dependency 'jabber4r', '> 0.1'
25
+ # gem.add_development_dependency 'rspec', '> 1.2.3'
26
+ end
27
+ Jeweler::RubygemsDotOrgTasks.new
28
+
29
+ require 'rspec/core'
30
+ require 'rspec/core/rake_task'
31
+ RSpec::Core::RakeTask.new(:spec) do |spec|
32
+ spec.pattern = FileList['spec/**/*_spec.rb']
33
+ end
34
+
35
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
36
+ spec.pattern = 'spec/**/*_spec.rb'
37
+ spec.rcov = true
38
+ end
39
+
40
+ require 'reek/rake/task'
41
+ Reek::Rake::Task.new do |t|
42
+ t.fail_on_error = true
43
+ t.verbose = false
44
+ t.source_files = 'lib/**/*.rb'
45
+ end
46
+
47
+ require 'roodi'
48
+ require 'roodi_task'
49
+ RoodiTask.new do |t|
50
+ t.verbose = false
51
+ end
52
+
53
+ task :default => :spec
54
+
55
+ require 'yard'
56
+ YARD::Rake::YardocTask.new
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,34 @@
1
+ require "active_model"
2
+
3
+ module CreditOfficer
4
+ class Base
5
+ extend ActiveModel::Naming
6
+ include ActiveModel::Validations
7
+ extend ActiveModel::Translation
8
+
9
+ def initialize(attributes = {})
10
+
11
+ end
12
+
13
+ def to_model
14
+ self
15
+ end
16
+
17
+ def persisted?
18
+ false
19
+ end
20
+
21
+ def to_key
22
+ nil
23
+ end
24
+
25
+ def to_param
26
+ nil
27
+ end
28
+
29
+ protected
30
+ def translate(key, options = {})
31
+ I18n.t key, options
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,235 @@
1
+ require "active_support/core_ext/class/attribute_accessors"
2
+ require "active_support/ordered_hash"
3
+ require "luhney_bin"
4
+
5
+ module CreditOfficer
6
+ #ActiveModel compliant class that represents credit card information
7
+ #Use this to populate and validate credit card details
8
+ #
9
+ #It is not recommended that you persist credit card information unless
10
+ #you absolutely must. Many payment processors proviider you with a mechanism to
11
+ #store this private information
12
+ #
13
+ #@example
14
+ # cc = CreditOfficer::CreditCard.new(
15
+ # :number => "411111111111111",
16
+ # :provider_name => "visa",
17
+ # :name_on_card => "John Doe",
18
+ # :expiration_year => 2010,
19
+ # :expiration_month => 1
20
+ # }).valid? => true
21
+ #
22
+ # cc.number = ""
23
+ # cc.valid? => false
24
+ # cc.errors.full_messages => ["Number can't be blank"]
25
+ class CreditCard < Base
26
+ PROVIDERS_AND_FORMATS = [
27
+ ['visa' , /^4\d{12}(\d{3})?$/],
28
+ ['master' , /^(5[1-5]\d{4}|677189)\d{10}$/],
29
+ ['discover' , /^(6011|65\d{2}|64[4-9]\d)\d{12}|(62\d{14})$/],
30
+ ['american_express' , /^3[47]\d{13}$/],
31
+ ['diners_club' , /^3(0[0-5]|[68]\d)\d{11}$/],
32
+ ['jcb' , /^35(28|29|[3-8]\d)\d{12}$/],
33
+ ['switch' , /^6759\d{12}(\d{2,3})?$/],
34
+ ['solo' , /^6767\d{12}(\d{2,3})?$/],
35
+ ['dankort' , /^5019\d{12}$/],
36
+ ['maestro' , /^(5[06-8]|6\d)\d{10,17}$/],
37
+ ['forbrugsforeningen' , /^600722\d{10}$/],
38
+ ['laser' , /^(6304|6706|6771|6709)\d{8}(\d{4}|\d{6,7})?$/]
39
+ ].inject(ActiveSupport::OrderedHash.new) do |ordered_hash, name_format_pair|
40
+ ordered_hash[name_format_pair[0]] = name_format_pair[1]
41
+ ordered_hash
42
+ end
43
+
44
+ SWITCH_OR_SOLO_PROVIDERS = [
45
+ 'switch',
46
+ 'solo'
47
+ ]
48
+
49
+ #[String] the number found on card
50
+ attr_accessor :number
51
+
52
+ #[String] the name found on the front of the card
53
+ attr_accessor :name_on_card
54
+
55
+ #[Integer] the integer based representation of the month when the credit card expires (1-12 e.g)
56
+ attr_accessor :expiration_month
57
+
58
+ #[Integer] the year when the credit card expires
59
+ #@note paired with the month, this must be in the future for the credit card to be valid
60
+ attr_accessor :expiration_year
61
+
62
+ #[String] the CVV/CVV2 value found on the back or front of cards depending on their brand
63
+ #validation of this string can be turned off via class setting require_verification_value
64
+ attr_accessor :verification_value
65
+
66
+ #[String] downcased name of the credit card provider
67
+ #(see {PROVIDERS_AND_FORMATS} for a valid list
68
+ attr_accessor :provider_name
69
+
70
+ #[Integer] Solo or Switch card attribute representing the start date found on the card
71
+ attr_accessor :start_month
72
+
73
+ #[Integer] Solo or Switch card attribute representing the start year found on the card
74
+ attr_accessor :start_year
75
+
76
+ #[String] Solo or Switch Card attribute representing the issue number found on the card
77
+ attr_accessor :issue_number
78
+
79
+ alias_method :brand, :provider_name
80
+
81
+ validates_presence_of :number
82
+ validates_presence_of :name_on_card
83
+
84
+ validates_presence_of :verification_value,
85
+ :if => proc{|p| p.class.verification_value_required? }
86
+
87
+ validates_inclusion_of :expiration_month,
88
+ :in => 1..12
89
+
90
+ validates_presence_of :expiration_year
91
+
92
+ validate :expiration_date_is_in_future
93
+ validate :expiration_date_is_in_recent_future
94
+ validate :number_is_valid
95
+ validate :provider_name_is_supported
96
+
97
+ #SOLO or Switch validations
98
+ validates_presence_of :start_month,
99
+ :if => proc{|cc| cc.switch_or_solo? }
100
+
101
+ validates_presence_of :start_year,
102
+ :if => proc{|cc| cc.switch_or_solo? }
103
+
104
+ validate :issue_number_is_valid,
105
+ :if => proc{|cc| cc.switch_or_solo? }
106
+
107
+ validate :start_date_is_in_the_past,
108
+ :if => proc{|cc| cc.switch_or_solo? }
109
+
110
+ #set this flag accordingly to enable/disable validating verification codes (CVV/CVV2)
111
+ cattr_accessor :require_verification_value
112
+ self.require_verification_value = true
113
+
114
+ #checks the configuration setting require_verification_value to see if
115
+ #verification is required
116
+ def self.verification_value_required?
117
+ require_verification_value
118
+ end
119
+
120
+ #@return [CreditOfficer::MonthYearPair] month year pair that represents the expiration date
121
+ def expiration_date
122
+ CreditOfficer::MonthYearPair.new(:month => expiration_month,
123
+ :year => expiration_year)
124
+ end
125
+
126
+ #@return [CreditOfficer::MonthYearPair] month year pair that represents the start date
127
+ #@note this applies to switch and solo cards only
128
+ def start_date
129
+ CreditOfficer::MonthYearPair.new(:month => start_month,
130
+ :year => start_year)
131
+ end
132
+
133
+ #sets the provider name
134
+ #@param [String] the provider name you wish to set
135
+ #sets the provider name to its downcased equivalent
136
+ #@example note the downcase
137
+ # credit_card.provider_name = "VISA" => "visa"
138
+ def provider_name=(provider)
139
+ unless provider.nil?
140
+ @provider_name = provider.downcase
141
+ end
142
+ end
143
+
144
+ #configure your list of supported providers
145
+ #@param Array<String> providers you wish to support (amex, visa, etc) (refer to {PROVIDERS_AND_FORMATS})
146
+ #@note matches specified providers against the supported whitelist {PROVIDERS_AND_FORMATS}
147
+ def self.supported_providers=(providers)
148
+ @supported_providers = providers.collect{|i| i.downcase} & PROVIDERS_AND_FORMATS.keys
149
+ end
150
+
151
+ #@return [Array<String>] list of providers
152
+ #@note defaults to {PROVIDERS_AND_FORMATS}.keys
153
+ def self.supported_providers
154
+ @supported_providers
155
+ end
156
+
157
+ self.supported_providers = PROVIDERS_AND_FORMATS.keys
158
+
159
+ #@return [Boolean] whether or not the provider name indicates the card is a switch or solo card
160
+ def switch_or_solo?
161
+ SWITCH_OR_SOLO_PROVIDERS.include?(provider_name)
162
+ end
163
+
164
+ protected
165
+ I18N_ERROR_SCOPE = [:credit_officer, :errors, :messages]
166
+
167
+ def expiration_date_is_in_future
168
+ if expiration_date.valid? && expiration_date.end_is_in_past?
169
+ errors.add(:expiration_year,
170
+ translate(:expired,
171
+ :scope => I18N_ERROR_SCOPE,
172
+ :default => "is expired"))
173
+ end
174
+ end
175
+
176
+ def expiration_date_is_in_recent_future
177
+ if expiration_date.valid? && expiration_date.exceeds_recent_future?
178
+ errors.add(:expiration_year, translate(:exceeds_recent_future,
179
+ :scope => I18N_ERROR_SCOPE,
180
+ :default => "is not a valid year"))
181
+ end
182
+ end
183
+
184
+ def number_is_valid
185
+ if provider_name.present? && number.present?
186
+ if self.class.supported_providers_and_formats[provider_name].nil? ||
187
+ !(number =~ self.class.supported_providers_and_formats[provider_name]) ||
188
+ !checksum_valid?
189
+
190
+ errors.add(:number, translate(:invalid_format,
191
+ :scope => I18N_ERROR_SCOPE,
192
+ :default => "is not a valid card number"))
193
+ end
194
+ end
195
+ end
196
+
197
+ def provider_name_is_supported
198
+ unless self.class.supported_providers.include?(provider_name.downcase)
199
+ errors.add(:provider_name, translate(:unsupported_provider,
200
+ :scope => I18N_ERROR_SCOPE,
201
+ :default => "is not supported"))
202
+ end
203
+ end
204
+
205
+ def issue_number_is_valid
206
+ unless issue_number =~ /^\d{1,2}$/
207
+ errors.add(:issue_number, translate(:invalid_issue_number,
208
+ :scope => I18N_ERROR_SCOPE,
209
+ :default => "is not valid"))
210
+ end
211
+ end
212
+
213
+ def start_date_is_in_the_past
214
+ if start_date.valid? && start_date.start_is_in_future?
215
+ errors.add(:start_year, translate(:futuristic_start_date,
216
+ :scope => I18N_ERROR_SCOPE,
217
+ :default => "is in the future"))
218
+ end
219
+ end
220
+
221
+ def checksum_valid?
222
+ LuhneyBin.validate(number)
223
+ end
224
+
225
+ def self.supported_providers_and_formats
226
+ #match supported providers against constant's whitelist
227
+ valid_supported_providers = supported_providers & PROVIDERS_AND_FORMATS.keys
228
+
229
+ supported_providers.inject(ActiveSupport::OrderedHash.new) do |ordered_hash, provider_name|
230
+ ordered_hash[provider_name] = PROVIDERS_AND_FORMATS[provider_name]
231
+ ordered_hash
232
+ end
233
+ end
234
+ end
235
+ end
@@ -0,0 +1,62 @@
1
+ require 'active_support/core_ext/time/calculations'
2
+
3
+ module CreditOfficer
4
+ #ActiveModel compliant abstraction representing the month/year pairs often found on credit cards
5
+ #
6
+ #For most, the only one found on the front is an expiration date
7
+ #
8
+ #For switch and solo cards, an additional start date might be specified
9
+ class MonthYearPair < Base
10
+ #[Integer] the year (required for validity)
11
+ attr_accessor :year
12
+
13
+ #[Integer] the numberic representation of the month (1-12) (required for validity)
14
+ attr_accessor :month
15
+
16
+ validates_inclusion_of :month,
17
+ :in => 1..12
18
+
19
+ validates_presence_of :year
20
+
21
+ #@param [Hash] hash of attributes to set
22
+ def initialize(attrs = {})
23
+ self.year = attrs[:year].to_i
24
+ self.month = attrs[:month].to_i
25
+ end
26
+
27
+ #@return [Boolean] whether the last minute of the month is in the past
28
+ def end_is_in_past? #:nodoc:
29
+ Time.now.utc > end_of_month
30
+ end
31
+
32
+ #@return [Boolean] whether the first minute of the month is in the future
33
+ def start_is_in_future?
34
+ Time.now.utc < start_of_month
35
+ end
36
+
37
+ RECENT_FUTURE_YEAR_LIMIT = 20
38
+
39
+ #@return [Boolean] whether the last minute of the month is within the bound of {RECENT_FUTURE_YEAR_LIMIT}
40
+ def exceeds_recent_future?
41
+ end_of_month <= Time.now.utc.advance(:years => RECENT_FUTURE_YEAR_LIMIT)
42
+ end
43
+
44
+ #@return [Time, nil] the first minute of the month in UTC or nil if an invalid pair was specified
45
+ def start_of_month
46
+ begin
47
+ Time.utc(year, month, 1, 0, 0, 1)
48
+ rescue ArgumentError
49
+ nil
50
+ end
51
+ end
52
+
53
+ #@return [Time, nil] the last minute of the month in UTC or nil if an invalid pair was specified
54
+ def end_of_month
55
+ begin
56
+ Time.utc(year, month, Time.days_in_month(month, year), 23, 59, 59)
57
+ rescue ArgumentError
58
+ nil
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,6 @@
1
+ module CreditOfficer
2
+ end
3
+
4
+ require "credit_officer/base"
5
+ require "credit_officer/month_year_pair"
6
+ require "credit_officer/credit_card"
File without changes
@@ -0,0 +1,127 @@
1
+ require 'spec_helper'
2
+
3
+ describe CreditOfficer::CreditCard do
4
+ subject { Factory.build(:credit_card) }
5
+ it_should_behave_like "ActiveModel"
6
+
7
+ should_validate_presence_of :number
8
+ should_validate_presence_of :name_on_card
9
+ should_validate_inclusion_of :expiration_month, :in => 1..12
10
+ should_validate_presence_of :expiration_year
11
+
12
+ it "validates that the expiration year is on or after this year" do
13
+ subject.expiration_year = Time.now.year - 1
14
+ subject.should_not be_valid
15
+ subject.errors[:expiration_year].should_not be_blank
16
+ end
17
+
18
+ it "validates that the expiration year is within 20 years from now" do
19
+ subject.expiration_year = Time.now.utc.year + 20
20
+ subject.should_not be_valid
21
+ subject.errors[:expiration_year].should_not be_blank
22
+ end
23
+
24
+ it "validates that the expiration month and year is in the future" do
25
+ Timecop.freeze(Time.utc(2010, 11, 1, 1)) do
26
+ subject.expiration_year = "2010"
27
+ subject.expiration_month = "10"
28
+ subject.should_not be_valid
29
+ subject.errors[:expiration_year].should_not be_blank
30
+ end
31
+ end
32
+
33
+ it "validates the presence of a verification value if it's enabled" do
34
+ old_setting = CreditOfficer::CreditCard.require_verification_value
35
+ CreditOfficer::CreditCard.require_verification_value = true
36
+ subject.verification_value = ""
37
+ subject.should_not be_valid
38
+ subject.errors[:verification_value].should_not be_blank
39
+
40
+ CreditOfficer::CreditCard.require_verification_value = old_setting
41
+ end
42
+
43
+ it "does not validate the presence of a verification if it's not enabled" do
44
+ old_setting = subject.class.require_verification_value
45
+ subject.class.require_verification_value = false
46
+ subject.verification_value = ""
47
+ subject.should be_valid
48
+ end
49
+
50
+ it "validates the credit card number based on its provider name's format" do
51
+ subject.provider_name = 'visa'
52
+ subject.number = '68293421'
53
+ subject.should_not be_valid
54
+ subject.errors[:number].should_not be_blank
55
+ end
56
+
57
+ it "checks the checksum of the number" do
58
+ subject.number = "4123456789012345"
59
+ subject.should_not be_valid
60
+ subject.errors[:number].should_not be_blank
61
+ end
62
+
63
+ context "supported providers" do
64
+ it "validates that my provider is in the list of supported providers" do
65
+ old_supported_providers = subject.class.supported_providers.dup
66
+ subject.class.supported_providers = ['master']
67
+ subject.should_not be_valid
68
+ subject.errors[:provider_name].should_not be_blank
69
+
70
+ #reset supported providers
71
+ subject.class.supported_providers = old_supported_providers
72
+ end
73
+
74
+ it "rejects a provider that is not in the whitelist" do
75
+ old_supported_providers = subject.class.supported_providers
76
+ subject.class.supported_providers = ["gaga", "ohlala"]
77
+ subject.class.supported_providers.should be_empty
78
+
79
+ #reset supported providers
80
+ subject.class.supported_providers = old_supported_providers
81
+ end
82
+
83
+ it "defaults to the large list of providers" do
84
+ subject.class.supported_providers.should eql(subject.class::PROVIDERS_AND_FORMATS.keys)
85
+ end
86
+ end
87
+
88
+ context "switch or solo cards" do
89
+ subject { Factory.build(:switch_credit_card) }
90
+
91
+ it { should be_valid }
92
+
93
+ it "is switch or solo if the provider name reflects that" do
94
+ [
95
+ "switch",
96
+ "solo"
97
+ ].each do |provider_name|
98
+ subject.provider_name = provider_name
99
+ subject.should be_switch_or_solo
100
+ end
101
+ end
102
+
103
+ it "requires a start month" do
104
+ subject.start_month = ""
105
+ subject.should_not be_valid
106
+ subject.errors[:start_month].should_not be_blank
107
+ end
108
+
109
+ it "requires a start year" do
110
+ subject.start_year = ""
111
+ subject.should_not be_valid
112
+ subject.errors[:start_year].should_not be_blank
113
+ end
114
+
115
+ it "requires a valid issue number" do
116
+ subject.issue_number = ""
117
+ subject.should_not be_valid
118
+ subject.errors[:issue_number].should_not be_blank
119
+ end
120
+
121
+ it "requires that the start month is not in the future" do
122
+ subject.start_year = Time.now.year + 1
123
+ subject.should_not be_valid
124
+ subject.errors[:start_year].should_not be_blank
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,52 @@
1
+ require 'spec_helper'
2
+
3
+ describe CreditOfficer::MonthYearPair do
4
+ should_validate_presence_of :year
5
+ should_validate_inclusion_of :month, :in => 1..12
6
+
7
+ subject { Factory.build(:month_year_pair) }
8
+
9
+ it "indicates when the end of the month is in the past" do
10
+ freeze_time do
11
+ subject.year = 2009
12
+ subject.end_is_in_past?.should be_true
13
+ end
14
+ end
15
+
16
+ it "indicates when the end of hte month is not in the past" do
17
+ freeze_time do
18
+ subject.year = 2011
19
+ subject.end_is_in_past?.should be_false
20
+ end
21
+ end
22
+
23
+ it "indicates when the start of the month is in the future" do
24
+ freeze_time do
25
+ subject.year = 2011
26
+ subject.start_is_in_future?.should be_true
27
+ end
28
+ end
29
+
30
+ it "indicates when the start of the month is not in the future" do
31
+ freeze_time do
32
+ subject.year = 2009
33
+ subject.start_is_in_future?.should be_false
34
+ end
35
+ end
36
+
37
+ it "has a nil start of month if the date doesn't make sense" do
38
+ subject.month = 49
39
+ subject.start_of_month.should be_nil
40
+ end
41
+
42
+ it "has a nil end of month if the date doesn't make sense" do
43
+ subject.month = 49
44
+ subject.end_of_month.should be_nil
45
+ end
46
+
47
+ def freeze_time(time = Time.utc(2010, 1, 1, 0, 0, 1), &block)
48
+ Timecop.freeze(time) do
49
+ yield
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,5 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "CreditOfficer" do
4
+
5
+ end
@@ -0,0 +1,15 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+ require 'credit_officer'
5
+ require 'remarkable/active_model'
6
+ require 'timecop'
7
+ require 'factory_girl'
8
+
9
+ # Requires supporting files with custom matchers and macros, etc,
10
+ # in ./support/ and its subdirectories.
11
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
12
+
13
+ RSpec.configure do |config|
14
+
15
+ end
@@ -0,0 +1,21 @@
1
+ # activemodel compliance test courtesy of https://gist.github.com/665629
2
+
3
+ # adapted from rspec-rails http://github.com/rspec/rspec-rails/blob/master/spec/rspec/rails/mocks/mock_model_spec.rb
4
+
5
+ shared_examples_for "ActiveModel" do
6
+ require 'test/unit/assertions'
7
+ require 'active_model/lint'
8
+ include Test::Unit::Assertions
9
+ include ActiveModel::Lint::Tests
10
+
11
+ # to_s is to support ruby-1.9
12
+ ActiveModel::Lint::Tests.public_instance_methods.map{|m| m.to_s}.grep(/^test/).each do |m|
13
+ example m.gsub('_',' ') do
14
+ send m
15
+ end
16
+ end
17
+
18
+ def model
19
+ subject
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ Factory.define :credit_card, :class => CreditOfficer::CreditCard do |c|
2
+ c.number "4111111111111111"
3
+ c.provider_name 'visa'
4
+ c.expiration_month 1
5
+ c.expiration_year { Time.now.advance(:year => 1) }
6
+ c.name_on_card "John Smith"
7
+ c.verification_value "1434"
8
+ end
9
+
10
+ Factory.define :switch_credit_card, :parent => :credit_card do |c|
11
+ c.number '675900000000000000'
12
+ c.provider_name 'switch'
13
+ c.start_month "01"
14
+ c.start_year "1990"
15
+ c.issue_number "01"
16
+ end
17
+
18
+ Factory.define :month_year_pair, :class => CreditOfficer::MonthYearPair do |c|
19
+ c.month 1
20
+ c.year 2010
21
+ end
metadata ADDED
@@ -0,0 +1,307 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: credit_officer
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Dan Pickett
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-12-22 00:00:00 -05:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ prerelease: false
23
+ type: :runtime
24
+ name: activemodel
25
+ version_requirements: &id001 !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ">="
29
+ - !ruby/object:Gem::Version
30
+ hash: 1
31
+ segments:
32
+ - 3
33
+ - 0
34
+ - 3
35
+ version: 3.0.3
36
+ requirement: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ prerelease: false
39
+ type: :runtime
40
+ name: luhney_bin
41
+ version_requirements: &id002 !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ hash: 3
47
+ segments:
48
+ - 0
49
+ version: "0"
50
+ requirement: *id002
51
+ - !ruby/object:Gem::Dependency
52
+ prerelease: false
53
+ type: :development
54
+ name: timecop
55
+ version_requirements: &id003 !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - "="
59
+ - !ruby/object:Gem::Version
60
+ hash: 25
61
+ segments:
62
+ - 0
63
+ - 3
64
+ - 5
65
+ version: 0.3.5
66
+ requirement: *id003
67
+ - !ruby/object:Gem::Dependency
68
+ prerelease: false
69
+ type: :development
70
+ name: ruby-debug
71
+ version_requirements: &id004 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ hash: 3
77
+ segments:
78
+ - 0
79
+ version: "0"
80
+ requirement: *id004
81
+ - !ruby/object:Gem::Dependency
82
+ prerelease: false
83
+ type: :development
84
+ name: factory_girl
85
+ version_requirements: &id005 !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ hash: 3
91
+ segments:
92
+ - 0
93
+ version: "0"
94
+ requirement: *id005
95
+ - !ruby/object:Gem::Dependency
96
+ prerelease: false
97
+ type: :development
98
+ name: rspec
99
+ version_requirements: &id006 !ruby/object:Gem::Requirement
100
+ none: false
101
+ requirements:
102
+ - - ~>
103
+ - !ruby/object:Gem::Version
104
+ hash: 3
105
+ segments:
106
+ - 2
107
+ - 3
108
+ - 0
109
+ version: 2.3.0
110
+ requirement: *id006
111
+ - !ruby/object:Gem::Dependency
112
+ prerelease: false
113
+ type: :development
114
+ name: remarkable_activemodel
115
+ version_requirements: &id007 !ruby/object:Gem::Requirement
116
+ none: false
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ hash: -1710980466
121
+ segments:
122
+ - 4
123
+ - 0
124
+ - 0
125
+ - alpha4
126
+ version: 4.0.0.alpha4
127
+ requirement: *id007
128
+ - !ruby/object:Gem::Dependency
129
+ prerelease: false
130
+ type: :development
131
+ name: yard
132
+ version_requirements: &id008 !ruby/object:Gem::Requirement
133
+ none: false
134
+ requirements:
135
+ - - ~>
136
+ - !ruby/object:Gem::Version
137
+ hash: 7
138
+ segments:
139
+ - 0
140
+ - 6
141
+ - 0
142
+ version: 0.6.0
143
+ requirement: *id008
144
+ - !ruby/object:Gem::Dependency
145
+ prerelease: false
146
+ type: :development
147
+ name: bundler
148
+ version_requirements: &id009 !ruby/object:Gem::Requirement
149
+ none: false
150
+ requirements:
151
+ - - ~>
152
+ - !ruby/object:Gem::Version
153
+ hash: 23
154
+ segments:
155
+ - 1
156
+ - 0
157
+ - 0
158
+ version: 1.0.0
159
+ requirement: *id009
160
+ - !ruby/object:Gem::Dependency
161
+ prerelease: false
162
+ type: :development
163
+ name: jeweler
164
+ version_requirements: &id010 !ruby/object:Gem::Requirement
165
+ none: false
166
+ requirements:
167
+ - - ~>
168
+ - !ruby/object:Gem::Version
169
+ hash: 1
170
+ segments:
171
+ - 1
172
+ - 5
173
+ - 1
174
+ version: 1.5.1
175
+ requirement: *id010
176
+ - !ruby/object:Gem::Dependency
177
+ prerelease: false
178
+ type: :development
179
+ name: rcov
180
+ version_requirements: &id011 !ruby/object:Gem::Requirement
181
+ none: false
182
+ requirements:
183
+ - - ">="
184
+ - !ruby/object:Gem::Version
185
+ hash: 3
186
+ segments:
187
+ - 0
188
+ version: "0"
189
+ requirement: *id011
190
+ - !ruby/object:Gem::Dependency
191
+ prerelease: false
192
+ type: :development
193
+ name: reek
194
+ version_requirements: &id012 !ruby/object:Gem::Requirement
195
+ none: false
196
+ requirements:
197
+ - - ~>
198
+ - !ruby/object:Gem::Version
199
+ hash: 15
200
+ segments:
201
+ - 1
202
+ - 2
203
+ - 8
204
+ version: 1.2.8
205
+ requirement: *id012
206
+ - !ruby/object:Gem::Dependency
207
+ prerelease: false
208
+ type: :development
209
+ name: roodi
210
+ version_requirements: &id013 !ruby/object:Gem::Requirement
211
+ none: false
212
+ requirements:
213
+ - - ~>
214
+ - !ruby/object:Gem::Version
215
+ hash: 11
216
+ segments:
217
+ - 2
218
+ - 1
219
+ - 0
220
+ version: 2.1.0
221
+ requirement: *id013
222
+ - !ruby/object:Gem::Dependency
223
+ prerelease: false
224
+ type: :development
225
+ name: fuubar
226
+ version_requirements: &id014 !ruby/object:Gem::Requirement
227
+ none: false
228
+ requirements:
229
+ - - ">="
230
+ - !ruby/object:Gem::Version
231
+ hash: 3
232
+ segments:
233
+ - 0
234
+ version: "0"
235
+ requirement: *id014
236
+ description: An upgrade/port of ActiveMerchant's credit card class
237
+ email: dpickett@enlightsolutions.com
238
+ executables: []
239
+
240
+ extensions: []
241
+
242
+ extra_rdoc_files:
243
+ - LICENSE.txt
244
+ - README.rdoc
245
+ files:
246
+ - .document
247
+ - .rspec
248
+ - .rvmrc
249
+ - Gemfile
250
+ - Gemfile.lock
251
+ - LICENSE.txt
252
+ - README.rdoc
253
+ - Rakefile
254
+ - VERSION
255
+ - lib/credit_officer.rb
256
+ - lib/credit_officer/base.rb
257
+ - lib/credit_officer/credit_card.rb
258
+ - lib/credit_officer/month_year_pair.rb
259
+ - spec/credit_officer/base_spec.rb
260
+ - spec/credit_officer/credit_card_spec.rb
261
+ - spec/credit_officer/month_year_pair_spec.rb
262
+ - spec/credit_officer_spec.rb
263
+ - spec/spec_helper.rb
264
+ - spec/support/active_model_shared_examples.rb
265
+ - spec/support/factories.rb
266
+ has_rdoc: true
267
+ homepage: http://github.com/dpickett/credit_officer
268
+ licenses:
269
+ - MIT
270
+ post_install_message:
271
+ rdoc_options: []
272
+
273
+ require_paths:
274
+ - lib
275
+ required_ruby_version: !ruby/object:Gem::Requirement
276
+ none: false
277
+ requirements:
278
+ - - ">="
279
+ - !ruby/object:Gem::Version
280
+ hash: 3
281
+ segments:
282
+ - 0
283
+ version: "0"
284
+ required_rubygems_version: !ruby/object:Gem::Requirement
285
+ none: false
286
+ requirements:
287
+ - - ">="
288
+ - !ruby/object:Gem::Version
289
+ hash: 3
290
+ segments:
291
+ - 0
292
+ version: "0"
293
+ requirements: []
294
+
295
+ rubyforge_project:
296
+ rubygems_version: 1.3.7
297
+ signing_key:
298
+ specification_version: 3
299
+ summary: An activemodel compliant credit card validator
300
+ test_files:
301
+ - spec/credit_officer/base_spec.rb
302
+ - spec/credit_officer/credit_card_spec.rb
303
+ - spec/credit_officer/month_year_pair_spec.rb
304
+ - spec/credit_officer_spec.rb
305
+ - spec/spec_helper.rb
306
+ - spec/support/active_model_shared_examples.rb
307
+ - spec/support/factories.rb