am_credit_card 0.0.1
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/.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
|