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
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module ActiveMerchant
|
4
|
+
module Billing
|
5
|
+
class CreditCard
|
6
|
+
class ExpiryDate #:nodoc:
|
7
|
+
attr_reader :month, :year
|
8
|
+
def initialize(month, year)
|
9
|
+
@month = month.to_i
|
10
|
+
@year = year.to_i
|
11
|
+
end
|
12
|
+
|
13
|
+
def expired? #:nodoc:
|
14
|
+
Time.now.utc > expiration
|
15
|
+
end
|
16
|
+
|
17
|
+
def expiration #:nodoc:
|
18
|
+
begin
|
19
|
+
Time.utc(year, month, month_days, 23, 59, 59)
|
20
|
+
rescue ArgumentError
|
21
|
+
Time.at(0).utc
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
def month_days
|
27
|
+
mdays = [nil,31,28,31,30,31,30,31,31,30,31,30,31]
|
28
|
+
mdays[2] = 29 if Date.leap?(year)
|
29
|
+
mdays[month]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module ActiveMerchant #:nodoc:
|
2
|
+
module Validateable #:nodoc:
|
3
|
+
def valid?
|
4
|
+
errors.clear
|
5
|
+
|
6
|
+
before_validate if respond_to?(:before_validate, true)
|
7
|
+
validate if respond_to?(:validate, true)
|
8
|
+
|
9
|
+
errors.empty?
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(attributes = {})
|
13
|
+
self.attributes = attributes
|
14
|
+
end
|
15
|
+
|
16
|
+
def errors
|
17
|
+
@errors ||= Errors.new(self)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def attributes=(attributes)
|
23
|
+
unless attributes.nil?
|
24
|
+
for key, value in attributes
|
25
|
+
send("#{key}=", value )
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# This hash keeps the errors of the object
|
31
|
+
class Errors < HashWithIndifferentAccess
|
32
|
+
|
33
|
+
def initialize(base)
|
34
|
+
super() { |h, k| h[k] = [] ; h[k] }
|
35
|
+
@base = base
|
36
|
+
end
|
37
|
+
|
38
|
+
def count
|
39
|
+
size
|
40
|
+
end
|
41
|
+
|
42
|
+
def empty?
|
43
|
+
all? { |k, v| v && v.empty? }
|
44
|
+
end
|
45
|
+
|
46
|
+
# returns a specific fields error message.
|
47
|
+
# if more than one error is available we will only return the first. If no error is available
|
48
|
+
# we return an empty string
|
49
|
+
def on(field)
|
50
|
+
self[field].to_a.first
|
51
|
+
end
|
52
|
+
|
53
|
+
def add(field, error)
|
54
|
+
self[field] << error
|
55
|
+
end
|
56
|
+
|
57
|
+
def add_to_base(error)
|
58
|
+
add(:base, error)
|
59
|
+
end
|
60
|
+
|
61
|
+
def each_full
|
62
|
+
full_messages.each { |msg| yield msg }
|
63
|
+
end
|
64
|
+
|
65
|
+
def full_messages
|
66
|
+
result = []
|
67
|
+
|
68
|
+
self.each do |key, messages|
|
69
|
+
next if messages.blank?
|
70
|
+
if key == 'base'
|
71
|
+
result << "#{messages.first}"
|
72
|
+
else
|
73
|
+
result << "#{key.to_s.humanize} #{messages.first}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
result
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2005-2010 Tobias Luetke
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# "Software"), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
require 'active_support/core_ext/class/attribute_accessors'
|
25
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
26
|
+
require 'active_support/core_ext/object/conversions'
|
27
|
+
require 'active_support/core_ext/object/blank'
|
28
|
+
|
29
|
+
require 'active_merchant/validateable'
|
30
|
+
require 'active_merchant/billing/base'
|
31
|
+
require 'active_merchant/billing/credit_card_methods'
|
32
|
+
require 'active_merchant/billing/credit_card'
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$:.unshift File.expand_path('../../lib', __FILE__)
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'rubygems'
|
6
|
+
require 'bundler'
|
7
|
+
Bundler.setup
|
8
|
+
rescue LoadError => e
|
9
|
+
puts "Error loading bundler (#{e.message}): \"gem install bundler\" for bundler support."
|
10
|
+
end
|
11
|
+
|
12
|
+
require 'test/unit'
|
13
|
+
require "am_credit_card"
|
14
|
+
|
15
|
+
module ActiveMerchant
|
16
|
+
module Assertions
|
17
|
+
AssertionClass = RUBY_VERSION > '1.9' ? MiniTest::Assertion : Test::Unit::AssertionFailedError
|
18
|
+
|
19
|
+
def assert_field(field, value)
|
20
|
+
clean_backtrace do
|
21
|
+
assert_equal value, @helper.fields[field]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Allows the testing of you to check for negative assertions:
|
26
|
+
#
|
27
|
+
# # Instead of
|
28
|
+
# assert !something_that_is_false
|
29
|
+
#
|
30
|
+
# # Do this
|
31
|
+
# assert_false something_that_should_be_false
|
32
|
+
#
|
33
|
+
# An optional +msg+ parameter is available to help you debug.
|
34
|
+
def assert_false(boolean, message = nil)
|
35
|
+
message = build_message message, '<?> is not false or nil.', boolean
|
36
|
+
|
37
|
+
clean_backtrace do
|
38
|
+
assert_block message do
|
39
|
+
not boolean
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# A handy little assertion to check for a successful response:
|
45
|
+
#
|
46
|
+
# # Instead of
|
47
|
+
# assert_success response
|
48
|
+
#
|
49
|
+
# # DRY that up with
|
50
|
+
# assert_success response
|
51
|
+
#
|
52
|
+
# A message will automatically show the inspection of the response
|
53
|
+
# object if things go afoul.
|
54
|
+
def assert_success(response)
|
55
|
+
clean_backtrace do
|
56
|
+
assert response.success?, "Response failed: #{response.inspect}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# The negative of +assert_success+
|
61
|
+
def assert_failure(response)
|
62
|
+
clean_backtrace do
|
63
|
+
assert_false response.success?, "Response expected to fail: #{response.inspect}"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def assert_valid(validateable)
|
68
|
+
clean_backtrace do
|
69
|
+
assert validateable.valid?, "Expected to be valid"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def assert_not_valid(validateable)
|
74
|
+
clean_backtrace do
|
75
|
+
assert_false validateable.valid?, "Expected to not be valid"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def assert_deprecation_warning(message, target)
|
80
|
+
target.expects(:deprecated).with(message)
|
81
|
+
yield
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
def clean_backtrace(&block)
|
86
|
+
yield
|
87
|
+
rescue AssertionClass => e
|
88
|
+
path = File.expand_path(__FILE__)
|
89
|
+
raise AssertionClass, e.message, e.backtrace.reject { |line| File.expand_path(line) =~ /#{path}/ }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
module Fixtures
|
94
|
+
HOME_DIR = RUBY_PLATFORM =~ /mswin32/ ? ENV['HOMEPATH'] : ENV['HOME'] unless defined?(HOME_DIR)
|
95
|
+
LOCAL_CREDENTIALS = File.join(HOME_DIR.to_s, '.active_merchant/fixtures.yml') unless defined?(LOCAL_CREDENTIALS)
|
96
|
+
DEFAULT_CREDENTIALS = File.join(File.dirname(__FILE__), 'fixtures.yml') unless defined?(DEFAULT_CREDENTIALS)
|
97
|
+
|
98
|
+
private
|
99
|
+
def credit_card(number = '4242424242424242', options = {})
|
100
|
+
defaults = {
|
101
|
+
:number => number,
|
102
|
+
:month => 9,
|
103
|
+
:year => Time.now.year + 1,
|
104
|
+
:first_name => 'Longbob',
|
105
|
+
:last_name => 'Longsen',
|
106
|
+
:verification_value => '123',
|
107
|
+
:type => 'visa'
|
108
|
+
}.update(options)
|
109
|
+
|
110
|
+
Billing::CreditCard.new(defaults)
|
111
|
+
end
|
112
|
+
|
113
|
+
def check(options = {})
|
114
|
+
defaults = {
|
115
|
+
:name => 'Jim Smith',
|
116
|
+
:routing_number => '244183602',
|
117
|
+
:account_number => '15378535',
|
118
|
+
:account_holder_type => 'personal',
|
119
|
+
:account_type => 'checking',
|
120
|
+
:number => '1'
|
121
|
+
}.update(options)
|
122
|
+
|
123
|
+
Billing::Check.new(defaults)
|
124
|
+
end
|
125
|
+
|
126
|
+
def address(options = {})
|
127
|
+
{
|
128
|
+
:name => 'Jim Smith',
|
129
|
+
:address1 => '1234 My Street',
|
130
|
+
:address2 => 'Apt 1',
|
131
|
+
:company => 'Widgets Inc',
|
132
|
+
:city => 'Ottawa',
|
133
|
+
:state => 'ON',
|
134
|
+
:zip => 'K1C2N6',
|
135
|
+
:country => 'CA',
|
136
|
+
:phone => '(555)555-5555',
|
137
|
+
:fax => '(555)555-6666'
|
138
|
+
}.update(options)
|
139
|
+
end
|
140
|
+
|
141
|
+
def all_fixtures
|
142
|
+
@@fixtures ||= load_fixtures
|
143
|
+
end
|
144
|
+
|
145
|
+
def fixtures(key)
|
146
|
+
data = all_fixtures[key] || raise(StandardError, "No fixture data was found for '#{key}'")
|
147
|
+
|
148
|
+
data.dup
|
149
|
+
end
|
150
|
+
|
151
|
+
def load_fixtures
|
152
|
+
file = File.exists?(LOCAL_CREDENTIALS) ? LOCAL_CREDENTIALS : DEFAULT_CREDENTIALS
|
153
|
+
yaml_data = YAML.load(File.read(file))
|
154
|
+
symbolize_keys(yaml_data)
|
155
|
+
|
156
|
+
yaml_data
|
157
|
+
end
|
158
|
+
|
159
|
+
def symbolize_keys(hash)
|
160
|
+
return unless hash.is_a?(Hash)
|
161
|
+
|
162
|
+
hash.symbolize_keys!
|
163
|
+
hash.each{|k,v| symbolize_keys(v)}
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
ActiveMerchant::Billing::Base.mode = :test
|
169
|
+
|
170
|
+
Test::Unit::TestCase.class_eval do
|
171
|
+
include ActiveMerchant::Billing
|
172
|
+
include ActiveMerchant::Assertions
|
173
|
+
include ActiveMerchant::Fixtures
|
174
|
+
end
|
@@ -0,0 +1,195 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class CreditCardMethodsTest < Test::Unit::TestCase
|
4
|
+
include ActiveMerchant::Billing::CreditCardMethods
|
5
|
+
|
6
|
+
class CreditCard
|
7
|
+
include ActiveMerchant::Billing::CreditCardMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
def maestro_card_numbers
|
11
|
+
%w[
|
12
|
+
5000000000000000 5099999999999999 5600000000000000
|
13
|
+
5899999999999999 6000000000000000 6999999999999999
|
14
|
+
6761999999999999 6763000000000000 5038999999999999
|
15
|
+
]
|
16
|
+
end
|
17
|
+
|
18
|
+
def non_maestro_card_numbers
|
19
|
+
%w[
|
20
|
+
4999999999999999 5100000000000000 5599999999999999
|
21
|
+
5900000000000000 5999999999999999 7000000000000000
|
22
|
+
]
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_should_be_able_to_identify_valid_expiry_months
|
26
|
+
assert_false valid_month?(-1)
|
27
|
+
assert_false valid_month?(13)
|
28
|
+
assert_false valid_month?(nil)
|
29
|
+
assert_false valid_month?('')
|
30
|
+
|
31
|
+
1.upto(12) { |m| assert valid_month?(m) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_should_be_able_to_identify_valid_expiry_years
|
35
|
+
assert_false valid_expiry_year?(-1)
|
36
|
+
assert_false valid_expiry_year?(Time.now.year + 21)
|
37
|
+
|
38
|
+
0.upto(20) { |n| assert valid_expiry_year?(Time.now.year + n) }
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_should_be_able_to_identify_valid_start_years
|
42
|
+
assert valid_start_year?(1988)
|
43
|
+
assert valid_start_year?(2007)
|
44
|
+
assert valid_start_year?(3000)
|
45
|
+
|
46
|
+
assert_false valid_start_year?(1987)
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_valid_start_year_can_handle_strings
|
50
|
+
assert valid_start_year?("2009")
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_valid_month_can_handle_strings
|
54
|
+
assert valid_month?("1")
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_valid_expiry_year_can_handle_strings
|
58
|
+
year = Time.now.year + 1
|
59
|
+
assert valid_expiry_year?(year.to_s)
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_should_be_able_to_identify_valid_issue_numbers
|
63
|
+
assert valid_issue_number?(1)
|
64
|
+
assert valid_issue_number?(10)
|
65
|
+
assert valid_issue_number?('12')
|
66
|
+
assert valid_issue_number?(0)
|
67
|
+
|
68
|
+
assert_false valid_issue_number?(-1)
|
69
|
+
assert_false valid_issue_number?(123)
|
70
|
+
assert_false valid_issue_number?('CAT')
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_should_ensure_type_from_credit_card_class_is_not_frozen
|
74
|
+
assert_false CreditCard.type?('4242424242424242').frozen?
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_should_be_dankort_card_type
|
78
|
+
assert_equal 'dankort', CreditCard.type?('5019717010103742')
|
79
|
+
end
|
80
|
+
|
81
|
+
def test_should_detect_visa_dankort_as_visa
|
82
|
+
assert_equal 'visa', CreditCard.type?('4571100000000000')
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_should_detect_electron_dk_as_visa
|
86
|
+
assert_equal 'visa', CreditCard.type?('4175001000000000')
|
87
|
+
end
|
88
|
+
|
89
|
+
def test_should_detect_diners_club
|
90
|
+
assert_equal 'diners_club', CreditCard.type?('36148010000000')
|
91
|
+
end
|
92
|
+
|
93
|
+
def test_should_detect_diners_club_dk
|
94
|
+
assert_equal 'diners_club', CreditCard.type?('30401000000000')
|
95
|
+
end
|
96
|
+
|
97
|
+
def test_should_detect_maestro_dk_as_maestro
|
98
|
+
assert_equal 'maestro', CreditCard.type?('6769271000000000')
|
99
|
+
end
|
100
|
+
|
101
|
+
def test_should_detect_maestro_cards
|
102
|
+
assert_equal 'maestro', CreditCard.type?('5020100000000000')
|
103
|
+
|
104
|
+
maestro_card_numbers.each { |number| assert_equal 'maestro', CreditCard.type?(number) }
|
105
|
+
non_maestro_card_numbers.each { |number| assert_not_equal 'maestro', CreditCard.type?(number) }
|
106
|
+
end
|
107
|
+
|
108
|
+
def test_should_detect_mastercard
|
109
|
+
assert_equal 'master', CreditCard.type?('6771890000000000')
|
110
|
+
assert_equal 'master', CreditCard.type?('5413031000000000')
|
111
|
+
end
|
112
|
+
|
113
|
+
def test_should_detect_forbrugsforeningen
|
114
|
+
assert_equal 'forbrugsforeningen', CreditCard.type?('6007221000000000')
|
115
|
+
end
|
116
|
+
|
117
|
+
def test_should_detect_laser_card
|
118
|
+
# 16 digits
|
119
|
+
assert_equal 'laser', CreditCard.type?('6304985028090561')
|
120
|
+
|
121
|
+
# 18 digits
|
122
|
+
assert_equal 'laser', CreditCard.type?('630498502809056151')
|
123
|
+
|
124
|
+
# 19 digits
|
125
|
+
assert_equal 'laser', CreditCard.type?('6304985028090561515')
|
126
|
+
|
127
|
+
# 17 digits
|
128
|
+
assert_not_equal 'laser', CreditCard.type?('63049850280905615')
|
129
|
+
|
130
|
+
# 15 digits
|
131
|
+
assert_not_equal 'laser', CreditCard.type?('630498502809056')
|
132
|
+
|
133
|
+
# Alternate format
|
134
|
+
assert_equal 'laser', CreditCard.type?('6706950000000000000')
|
135
|
+
|
136
|
+
# Alternate format (16 digits)
|
137
|
+
assert_equal 'laser', CreditCard.type?('6706123456789012')
|
138
|
+
|
139
|
+
# New format (16 digits)
|
140
|
+
assert_equal 'laser', CreditCard.type?('6709123456789012')
|
141
|
+
|
142
|
+
# Ulster bank (Ireland) with 12 digits
|
143
|
+
assert_equal 'laser', CreditCard.type?('677117111234')
|
144
|
+
end
|
145
|
+
|
146
|
+
def test_should_detect_when_an_argument_type_does_not_match_calculated_type
|
147
|
+
assert CreditCard.matching_type?('4175001000000000', 'visa')
|
148
|
+
assert_false CreditCard.matching_type?('4175001000000000', 'master')
|
149
|
+
end
|
150
|
+
|
151
|
+
def test_detecting_full_range_of_maestro_card_numbers
|
152
|
+
maestro = '50000000000'
|
153
|
+
|
154
|
+
assert_equal 11, maestro.length
|
155
|
+
assert_not_equal 'maestro', CreditCard.type?(maestro)
|
156
|
+
|
157
|
+
while maestro.length < 19
|
158
|
+
maestro << '0'
|
159
|
+
assert_equal 'maestro', CreditCard.type?(maestro)
|
160
|
+
end
|
161
|
+
|
162
|
+
assert_equal 19, maestro.length
|
163
|
+
|
164
|
+
maestro << '0'
|
165
|
+
assert_not_equal 'maestro', CreditCard.type?(maestro)
|
166
|
+
end
|
167
|
+
|
168
|
+
def test_matching_discover_card
|
169
|
+
assert_equal 'discover', CreditCard.type?('6011000000000000')
|
170
|
+
assert_equal 'discover', CreditCard.type?('6500000000000000')
|
171
|
+
assert_equal 'discover', CreditCard.type?('6221260000000000')
|
172
|
+
assert_equal 'discover', CreditCard.type?('6450000000000000')
|
173
|
+
|
174
|
+
assert_not_equal 'discover', CreditCard.type?('6010000000000000')
|
175
|
+
assert_not_equal 'discover', CreditCard.type?('6600000000000000')
|
176
|
+
end
|
177
|
+
|
178
|
+
def test_16_digit_maestro_uk
|
179
|
+
number = '6759000000000000'
|
180
|
+
assert_equal 16, number.length
|
181
|
+
assert_equal 'switch', CreditCard.type?(number)
|
182
|
+
end
|
183
|
+
|
184
|
+
def test_18_digit_maestro_uk
|
185
|
+
number = '675900000000000000'
|
186
|
+
assert_equal 18, number.length
|
187
|
+
assert_equal 'switch', CreditCard.type?(number)
|
188
|
+
end
|
189
|
+
|
190
|
+
def test_19_digit_maestro_uk
|
191
|
+
number = '6759000000000000000'
|
192
|
+
assert_equal 19, number.length
|
193
|
+
assert_equal 'switch', CreditCard.type?(number)
|
194
|
+
end
|
195
|
+
end
|