am_credit_card 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/README.md +50 -0
- data/Rakefile +19 -0
- data/am_credit_card.gemspec +21 -0
- data/lib/active_merchant/billing/base.rb +27 -0
- data/lib/active_merchant/billing/credit_card.rb +260 -0
- data/lib/active_merchant/billing/credit_card_methods.rb +125 -0
- data/lib/active_merchant/billing/expiry_date.rb +34 -0
- data/lib/active_merchant/validateable.rb +81 -0
- data/lib/am_credit_card.rb +32 -0
- data/lib/am_credit_card_version.rb +3 -0
- data/test/test_helper.rb +174 -0
- data/test/unit/credit_card_methods_test.rb +195 -0
- data/test/unit/credit_card_test.rb +354 -0
- data/test/unit/expiry_date_test.rb +32 -0
- data/test/unit/validateable_test.rb +59 -0
- metadata +91 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
am_credit_card
|
2
|
+
==============
|
3
|
+
|
4
|
+
[ActiveMerchant::Billing::CreditCard][1], without [ActiveMerchant][2].
|
5
|
+
|
6
|
+
[1]: https://github.com/Shopify/active_merchant/blob/master/lib/active_merchant/billing/credit_card.rb
|
7
|
+
[2]: http://activemerchant.rubyforge.org/
|
8
|
+
|
9
|
+
|
10
|
+
Why?
|
11
|
+
----
|
12
|
+
|
13
|
+
ActiveMerchant has [nice credit card validations][1], but also lots of [dependencies][2] and other code. That's fine for those using the rest of ActiveMerchant, but if you're just after it's credit card model/validations, this is for you.
|
14
|
+
|
15
|
+
[1]: https://github.com/Shopify/active_merchant/blob/master/lib/active_merchant/billing/credit_card_methods.rb
|
16
|
+
[2]: https://github.com/Shopify/active_merchant/blob/master/activemerchant.gemspec#L21
|
17
|
+
|
18
|
+
|
19
|
+
Usage
|
20
|
+
-----
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
# Gemfile
|
24
|
+
gem "am_credit_card"
|
25
|
+
```
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
# Instantiation
|
29
|
+
card = ActiveMerchant::Billing::CreditCard.new(
|
30
|
+
:first_name => "Bob",
|
31
|
+
:last_name => "Bobsen",
|
32
|
+
:number => "4242424242424242",
|
33
|
+
:month => "8",
|
34
|
+
:year => "2012",
|
35
|
+
:verification_value => "123"
|
36
|
+
)
|
37
|
+
|
38
|
+
# Validation
|
39
|
+
card.valid?
|
40
|
+
card.errors
|
41
|
+
```
|
42
|
+
|
43
|
+
|
44
|
+
License
|
45
|
+
-------
|
46
|
+
|
47
|
+
ActiveMerchant is Copyright © 2005-2010 Tobias Luetke.
|
48
|
+
He has released it open-source under the [MIT license][1]
|
49
|
+
|
50
|
+
[1]: https://github.com/Shopify/active_merchant/blob/master/MIT-LICENSE
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
|
4
|
+
require 'rake'
|
5
|
+
require 'rake/testtask'
|
6
|
+
|
7
|
+
desc "Run the unit test suite"
|
8
|
+
task :default => 'test:units'
|
9
|
+
|
10
|
+
namespace :test do
|
11
|
+
|
12
|
+
Rake::TestTask.new(:units) do |t|
|
13
|
+
t.pattern = 'test/unit/**/*_test.rb'
|
14
|
+
t.ruby_opts << '-rubygems'
|
15
|
+
t.libs << 'test'
|
16
|
+
t.verbose = true
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/am_credit_card_version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Paul Annesley"]
|
6
|
+
gem.email = ["paul@annesley.cc"]
|
7
|
+
gem.description = %q{ActiveMerchant::Billing::CreditCard, without ActiveMerchant}
|
8
|
+
gem.summary = %q{ActiveMerchant has nice credit card validations, but also lots of dependencies and other code. That's fine for those using the rest of ActiveMerchant, but if you're just after it's credit card model/validations, this is for you.
|
9
|
+
}
|
10
|
+
gem.homepage = "https://github.com/pda/am_credit_card"
|
11
|
+
|
12
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
13
|
+
gem.files = `git ls-files`.split("\n")
|
14
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
15
|
+
gem.name = "am_credit_card"
|
16
|
+
gem.require_paths = ["lib"]
|
17
|
+
gem.version = AmCreditCard::VERSION
|
18
|
+
|
19
|
+
gem.add_dependency('activesupport', '>= 2.3.11')
|
20
|
+
gem.add_dependency('i18n')
|
21
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module ActiveMerchant #:nodoc:
|
2
|
+
module Billing #:nodoc:
|
3
|
+
module Base
|
4
|
+
# Set ActiveMerchant gateways in test mode.
|
5
|
+
#
|
6
|
+
# ActiveMerchant::Billing::Base.gateway_mode = :test
|
7
|
+
mattr_accessor :gateway_mode
|
8
|
+
|
9
|
+
# Set both the mode of both the gateways and integrations
|
10
|
+
# at once
|
11
|
+
mattr_reader :mode
|
12
|
+
|
13
|
+
def self.mode=(mode)
|
14
|
+
@@mode = mode
|
15
|
+
self.gateway_mode = mode
|
16
|
+
end
|
17
|
+
|
18
|
+
self.mode = :production
|
19
|
+
|
20
|
+
# A check to see if we're in test mode
|
21
|
+
def self.test?
|
22
|
+
self.gateway_mode == :test
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
@@ -0,0 +1,260 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'date'
|
3
|
+
require 'active_merchant/billing/expiry_date'
|
4
|
+
|
5
|
+
module ActiveMerchant #:nodoc:
|
6
|
+
module Billing #:nodoc:
|
7
|
+
# A +CreditCard+ object represents a physical credit card, and is capable of validating the various
|
8
|
+
# data associated with these.
|
9
|
+
#
|
10
|
+
# At the moment, the following credit card types are supported:
|
11
|
+
#
|
12
|
+
# * Visa
|
13
|
+
# * MasterCard
|
14
|
+
# * Discover
|
15
|
+
# * American Express
|
16
|
+
# * Diner's Club
|
17
|
+
# * JCB
|
18
|
+
# * Switch
|
19
|
+
# * Solo
|
20
|
+
# * Dankort
|
21
|
+
# * Maestro
|
22
|
+
# * Forbrugsforeningen
|
23
|
+
# * Laser
|
24
|
+
#
|
25
|
+
# For testing purposes, use the 'bogus' credit card type. This skips the vast majority of
|
26
|
+
# validations, allowing you to focus on your core concerns until you're ready to be more concerned
|
27
|
+
# with the details of particular credit cards or your gateway.
|
28
|
+
#
|
29
|
+
# == Testing With CreditCard
|
30
|
+
# Often when testing we don't care about the particulars of a given card type. When using the 'test'
|
31
|
+
# mode in your {Gateway}, there are six different valid card numbers: 1, 2, 3, 'success', 'fail',
|
32
|
+
# and 'error'.
|
33
|
+
#
|
34
|
+
# For details, see {CreditCardMethods::ClassMethods#valid_number?}
|
35
|
+
#
|
36
|
+
# == Example Usage
|
37
|
+
# cc = CreditCard.new(
|
38
|
+
# :first_name => 'Steve',
|
39
|
+
# :last_name => 'Smith',
|
40
|
+
# :month => '9',
|
41
|
+
# :year => '2010',
|
42
|
+
# :type => 'visa',
|
43
|
+
# :number => '4242424242424242'
|
44
|
+
# )
|
45
|
+
#
|
46
|
+
# cc.valid? # => true
|
47
|
+
# cc.display_number # => XXXX-XXXX-XXXX-4242
|
48
|
+
#
|
49
|
+
class CreditCard
|
50
|
+
include CreditCardMethods
|
51
|
+
include Validateable
|
52
|
+
|
53
|
+
cattr_accessor :require_verification_value
|
54
|
+
self.require_verification_value = true
|
55
|
+
|
56
|
+
# Returns or sets the credit card number.
|
57
|
+
#
|
58
|
+
# @return [String]
|
59
|
+
attr_accessor :number
|
60
|
+
|
61
|
+
# Returns or sets the expiry month for the card.
|
62
|
+
#
|
63
|
+
# @return [Integer]
|
64
|
+
attr_accessor :month
|
65
|
+
|
66
|
+
# Returns or sets the expiry year for the card.
|
67
|
+
#
|
68
|
+
# @return [Integer]
|
69
|
+
attr_accessor :year
|
70
|
+
|
71
|
+
# Returns or sets the credit card type.
|
72
|
+
#
|
73
|
+
# Valid card types are
|
74
|
+
#
|
75
|
+
# * +'visa'+
|
76
|
+
# * +'master'+
|
77
|
+
# * +'discover'+
|
78
|
+
# * +'american_express'+
|
79
|
+
# * +'diners_club'+
|
80
|
+
# * +'jcb'+
|
81
|
+
# * +'switch'+
|
82
|
+
# * +'solo'+
|
83
|
+
# * +'dankort'+
|
84
|
+
# * +'maestro'+
|
85
|
+
# * +'forbrugsforeningen'+
|
86
|
+
# * +'laser'+
|
87
|
+
#
|
88
|
+
# Or, if you wish to test your implementation, +'bogus'+.
|
89
|
+
#
|
90
|
+
# @return (String) the credit card type
|
91
|
+
attr_accessor :type
|
92
|
+
|
93
|
+
# Returns or sets the first name of the card holder.
|
94
|
+
#
|
95
|
+
# @return [String]
|
96
|
+
attr_accessor :first_name
|
97
|
+
|
98
|
+
# Returns or sets the last name of the card holder.
|
99
|
+
#
|
100
|
+
# @return [String]
|
101
|
+
attr_accessor :last_name
|
102
|
+
|
103
|
+
# Required for Switch / Solo cards
|
104
|
+
attr_accessor :start_month, :start_year, :issue_number
|
105
|
+
|
106
|
+
# Returns or sets the card verification value.
|
107
|
+
#
|
108
|
+
# This attribute is optional but recommended. The verification value is
|
109
|
+
# a {card security code}[http://en.wikipedia.org/wiki/Card_security_code]. If provided,
|
110
|
+
# the gateway will attempt to validate the value.
|
111
|
+
#
|
112
|
+
# @return [String] the verification value
|
113
|
+
attr_accessor :verification_value
|
114
|
+
|
115
|
+
alias_method :brand, :type
|
116
|
+
|
117
|
+
# Provides proxy access to an expiry date object
|
118
|
+
#
|
119
|
+
# @return [ExpiryDate]
|
120
|
+
def expiry_date
|
121
|
+
ExpiryDate.new(@month, @year)
|
122
|
+
end
|
123
|
+
|
124
|
+
# Returns whether the credit card has expired.
|
125
|
+
#
|
126
|
+
# @return +true+ if the card has expired, +false+ otherwise
|
127
|
+
def expired?
|
128
|
+
expiry_date.expired?
|
129
|
+
end
|
130
|
+
|
131
|
+
# Returns whether either the +first_name+ or the +last_name+ attributes has been set.
|
132
|
+
def name?
|
133
|
+
first_name? || last_name?
|
134
|
+
end
|
135
|
+
|
136
|
+
# Returns whether the +first_name+ attribute has been set.
|
137
|
+
def first_name?
|
138
|
+
@first_name.present?
|
139
|
+
end
|
140
|
+
|
141
|
+
# Returns whether the +last_name+ attribute has been set.
|
142
|
+
def last_name?
|
143
|
+
@last_name.present?
|
144
|
+
end
|
145
|
+
|
146
|
+
# Returns the full name of the card holder.
|
147
|
+
#
|
148
|
+
# @return [String] the full name of the card holder
|
149
|
+
def name
|
150
|
+
[@first_name, @last_name].compact.join(' ')
|
151
|
+
end
|
152
|
+
|
153
|
+
def name=(full_name)
|
154
|
+
names = full_name.split
|
155
|
+
self.last_name = names.pop
|
156
|
+
self.first_name = names.join(" ")
|
157
|
+
end
|
158
|
+
|
159
|
+
def verification_value?
|
160
|
+
!@verification_value.blank?
|
161
|
+
end
|
162
|
+
|
163
|
+
# Returns a display-friendly version of the card number.
|
164
|
+
#
|
165
|
+
# All but the last 4 numbers are replaced with an "X", and hyphens are
|
166
|
+
# inserted in order to improve legibility.
|
167
|
+
#
|
168
|
+
# @example
|
169
|
+
# credit_card = CreditCard.new(:number => "2132542376824338")
|
170
|
+
# credit_card.display_number # "XXXX-XXXX-XXXX-4338"
|
171
|
+
#
|
172
|
+
# @return [String] a display-friendly version of the card number
|
173
|
+
def display_number
|
174
|
+
self.class.mask(number)
|
175
|
+
end
|
176
|
+
|
177
|
+
def last_digits
|
178
|
+
self.class.last_digits(number)
|
179
|
+
end
|
180
|
+
|
181
|
+
# Validates the credit card details.
|
182
|
+
#
|
183
|
+
# Any validation errors are added to the {#errors} attribute.
|
184
|
+
def validate
|
185
|
+
validate_essential_attributes
|
186
|
+
|
187
|
+
# Bogus card is pretty much for testing purposes. Lets just skip these extra tests if its used
|
188
|
+
return if type == 'bogus'
|
189
|
+
|
190
|
+
validate_card_type
|
191
|
+
validate_card_number
|
192
|
+
validate_verification_value
|
193
|
+
validate_switch_or_solo_attributes
|
194
|
+
end
|
195
|
+
|
196
|
+
def self.requires_verification_value?
|
197
|
+
require_verification_value
|
198
|
+
end
|
199
|
+
|
200
|
+
private
|
201
|
+
|
202
|
+
def before_validate #:nodoc:
|
203
|
+
self.month = month.to_i
|
204
|
+
self.year = year.to_i
|
205
|
+
self.start_month = start_month.to_i unless start_month.nil?
|
206
|
+
self.start_year = start_year.to_i unless start_year.nil?
|
207
|
+
self.number = number.to_s.gsub(/[^\d]/, "")
|
208
|
+
self.type.downcase! if type.respond_to?(:downcase)
|
209
|
+
self.type = self.class.type?(number) if type.blank?
|
210
|
+
end
|
211
|
+
|
212
|
+
def validate_card_number #:nodoc:
|
213
|
+
if number.blank?
|
214
|
+
errors.add :number, "is required"
|
215
|
+
elsif !CreditCard.valid_number?(number)
|
216
|
+
errors.add :number, "is not a valid credit card number"
|
217
|
+
end
|
218
|
+
|
219
|
+
unless errors.on(:number) || errors.on(:type)
|
220
|
+
errors.add :type, "is not the correct card type" unless CreditCard.matching_type?(number, type)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def validate_card_type #:nodoc:
|
225
|
+
errors.add :type, "is required" if type.blank? && number.present?
|
226
|
+
errors.add :type, "is invalid" unless type.blank? || CreditCard.card_companies.keys.include?(type)
|
227
|
+
end
|
228
|
+
|
229
|
+
def validate_essential_attributes #:nodoc:
|
230
|
+
errors.add :first_name, "cannot be empty" if @first_name.blank?
|
231
|
+
errors.add :last_name, "cannot be empty" if @last_name.blank?
|
232
|
+
|
233
|
+
if @month.to_i.zero? || @year.to_i.zero?
|
234
|
+
errors.add :month, "is required" if @month.to_i.zero?
|
235
|
+
errors.add :year, "is required" if @year.to_i.zero?
|
236
|
+
else
|
237
|
+
errors.add :month, "is not a valid month" unless valid_month?(@month)
|
238
|
+
errors.add :year, "expired" if expired?
|
239
|
+
errors.add :year, "is not a valid year" unless expired? || valid_expiry_year?(@year)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
def validate_switch_or_solo_attributes #:nodoc:
|
244
|
+
if %w[switch solo].include?(type)
|
245
|
+
unless valid_month?(@start_month) && valid_start_year?(@start_year) || valid_issue_number?(@issue_number)
|
246
|
+
errors.add :start_month, "is invalid" unless valid_month?(@start_month)
|
247
|
+
errors.add :start_year, "is invalid" unless valid_start_year?(@start_year)
|
248
|
+
errors.add :issue_number, "cannot be empty" unless valid_issue_number?(@issue_number)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
def validate_verification_value #:nodoc:
|
254
|
+
if CreditCard.requires_verification_value?
|
255
|
+
errors.add :verification_value, "is required" unless verification_value?
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
module ActiveMerchant #:nodoc:
|
2
|
+
module Billing #:nodoc:
|
3
|
+
# Convenience methods that can be included into a custom Credit Card object, such as an ActiveRecord based Credit Card object.
|
4
|
+
module CreditCardMethods
|
5
|
+
CARD_COMPANIES = {
|
6
|
+
'visa' => /^4\d{12}(\d{3})?$/,
|
7
|
+
'master' => /^(5[1-5]\d{4}|677189)\d{10}$/,
|
8
|
+
'discover' => /^(6011|65\d{2}|64[4-9]\d)\d{12}|(62\d{14})$/,
|
9
|
+
'american_express' => /^3[47]\d{13}$/,
|
10
|
+
'diners_club' => /^3(0[0-5]|[68]\d)\d{11}$/,
|
11
|
+
'jcb' => /^35(28|29|[3-8]\d)\d{12}$/,
|
12
|
+
'switch' => /^6759\d{12}(\d{2,3})?$/,
|
13
|
+
'solo' => /^6767\d{12}(\d{2,3})?$/,
|
14
|
+
'dankort' => /^5019\d{12}$/,
|
15
|
+
'maestro' => /^(5[06-8]|6\d)\d{10,17}$/,
|
16
|
+
'forbrugsforeningen' => /^600722\d{10}$/,
|
17
|
+
'laser' => /^(6304|6706|6771|6709)\d{8}(\d{4}|\d{6,7})?$/
|
18
|
+
}
|
19
|
+
|
20
|
+
def self.included(base)
|
21
|
+
base.extend(ClassMethods)
|
22
|
+
end
|
23
|
+
|
24
|
+
def valid_month?(month)
|
25
|
+
(1..12).include?(month.to_i)
|
26
|
+
end
|
27
|
+
|
28
|
+
def valid_expiry_year?(year)
|
29
|
+
(Time.now.year..Time.now.year + 20).include?(year.to_i)
|
30
|
+
end
|
31
|
+
|
32
|
+
def valid_start_year?(year)
|
33
|
+
year.to_s =~ /^\d{4}$/ && year.to_i > 1987
|
34
|
+
end
|
35
|
+
|
36
|
+
def valid_issue_number?(number)
|
37
|
+
number.to_s =~ /^\d{1,2}$/
|
38
|
+
end
|
39
|
+
|
40
|
+
module ClassMethods
|
41
|
+
# Returns true if it validates. Optionally, you can pass a card type as an argument and
|
42
|
+
# make sure it is of the correct type.
|
43
|
+
#
|
44
|
+
# References:
|
45
|
+
# - http://perl.about.com/compute/perl/library/nosearch/P073000.htm
|
46
|
+
# - http://www.beachnet.com/~hstiles/cardtype.html
|
47
|
+
def valid_number?(number)
|
48
|
+
valid_test_mode_card_number?(number) ||
|
49
|
+
valid_card_number_length?(number) &&
|
50
|
+
valid_checksum?(number)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Regular expressions for the known card companies.
|
54
|
+
#
|
55
|
+
# References:
|
56
|
+
# - http://en.wikipedia.org/wiki/Credit_card_number
|
57
|
+
# - http://www.barclaycardbusiness.co.uk/information_zone/processing/bin_rules.html
|
58
|
+
def card_companies
|
59
|
+
CARD_COMPANIES
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns a string containing the type of card from the list of known information below.
|
63
|
+
# Need to check the cards in a particular order, as there is some overlap of the allowable ranges
|
64
|
+
#--
|
65
|
+
# TODO Refactor this method. We basically need to tighten up the Maestro Regexp.
|
66
|
+
#
|
67
|
+
# Right now the Maestro regexp overlaps with the MasterCard regexp (IIRC). If we can tighten
|
68
|
+
# things up, we can boil this whole thing down to something like...
|
69
|
+
#
|
70
|
+
# def type?(number)
|
71
|
+
# return 'visa' if valid_test_mode_card_number?(number)
|
72
|
+
# card_companies.find([nil]) { |type, regexp| number =~ regexp }.first.dup
|
73
|
+
# end
|
74
|
+
#
|
75
|
+
def type?(number)
|
76
|
+
return 'bogus' if valid_test_mode_card_number?(number)
|
77
|
+
|
78
|
+
card_companies.reject { |c,p| c == 'maestro' }.each do |company, pattern|
|
79
|
+
return company.dup if number =~ pattern
|
80
|
+
end
|
81
|
+
|
82
|
+
return 'maestro' if number =~ card_companies['maestro']
|
83
|
+
|
84
|
+
return nil
|
85
|
+
end
|
86
|
+
|
87
|
+
def last_digits(number)
|
88
|
+
number.to_s.length <= 4 ? number : number.to_s.slice(-4..-1)
|
89
|
+
end
|
90
|
+
|
91
|
+
def mask(number)
|
92
|
+
"XXXX-XXXX-XXXX-#{last_digits(number)}"
|
93
|
+
end
|
94
|
+
|
95
|
+
# Checks to see if the calculated type matches the specified type
|
96
|
+
def matching_type?(number, type)
|
97
|
+
type?(number) == type
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def valid_card_number_length?(number) #:nodoc:
|
103
|
+
number.to_s.length >= 12
|
104
|
+
end
|
105
|
+
|
106
|
+
def valid_test_mode_card_number?(number) #:nodoc:
|
107
|
+
ActiveMerchant::Billing::Base.test? &&
|
108
|
+
%w[1 2 3 success failure error].include?(number.to_s)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Checks the validity of a card number by use of the the Luhn Algorithm.
|
112
|
+
# Please see http://en.wikipedia.org/wiki/Luhn_algorithm for details.
|
113
|
+
def valid_checksum?(number) #:nodoc:
|
114
|
+
sum = 0
|
115
|
+
for i in 0..number.length
|
116
|
+
weight = number[-1 * (i + 2), 1].to_i * (2 - (i % 2))
|
117
|
+
sum += (weight < 10) ? weight : weight - 9
|
118
|
+
end
|
119
|
+
|
120
|
+
(number[-1,1].to_i == (10 - sum % 10) % 10)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|