an 0.0.1.rc1 → 0.0.1.rc2
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/an.gemspec +1 -1
- data/lib/an.rb +123 -154
- data/test/an.rb +85 -50
- data/test/helper.rb +3 -0
- metadata +7 -6
data/an.gemspec
CHANGED
data/lib/an.rb
CHANGED
@@ -1,93 +1,128 @@
|
|
1
|
+
require "mote"
|
1
2
|
require "net/http"
|
2
3
|
require "net/https"
|
3
4
|
require "uri"
|
4
|
-
require "
|
5
|
+
require "xmlsimple"
|
5
6
|
|
6
7
|
class AN
|
7
|
-
|
8
|
-
|
9
|
-
|
8
|
+
# In production systems, you can simply set
|
9
|
+
#
|
10
|
+
# AUTHORIZE_NET_URL=https://login:key@api.authorize.net/xml/v1/request.api
|
11
|
+
#
|
12
|
+
# in the appropriate location (e.g. /etc/profile.d, ~/.bashrc, or
|
13
|
+
# whatever you're most comfortable with.
|
14
|
+
#
|
15
|
+
# The TEST URL is https://apikey.authorize.net/xml/v1/request.api
|
16
|
+
def self.connect(url = ENV["AUTHORIZE_NET_URL"])
|
17
|
+
new(URI(url))
|
18
|
+
end
|
19
|
+
|
20
|
+
TEMPLATES = File.expand_path("../templates", File.dirname(__FILE__))
|
21
|
+
|
22
|
+
include Mote::Helpers
|
23
|
+
|
24
|
+
attr :url
|
25
|
+
attr :auth
|
26
|
+
attr :client
|
27
|
+
|
28
|
+
def initialize(uri)
|
29
|
+
@auth = { login: uri.user, transaction_key: uri.password }
|
30
|
+
@client = Client.new(uri)
|
10
31
|
end
|
11
32
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
ret["x_#{att}"] = val
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
33
|
+
def transact(params)
|
34
|
+
call("createTransactionRequest", params)
|
35
|
+
end
|
36
|
+
|
37
|
+
def create_profile(params)
|
38
|
+
call("createCustomerProfileRequest", params)
|
22
39
|
end
|
23
40
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
41
|
+
def create_payment_profile(params)
|
42
|
+
call("createCustomerPaymentProfileRequest", params)
|
43
|
+
end
|
44
|
+
|
45
|
+
def create_profile_transaction(params)
|
46
|
+
call("createCustomerProfileTransactionRequest", params)
|
47
|
+
end
|
29
48
|
|
30
|
-
|
31
|
-
|
32
|
-
|
49
|
+
private
|
50
|
+
def call(api_call, params)
|
51
|
+
Response.new(post(payload(api_call, params)))
|
52
|
+
end
|
53
|
+
|
54
|
+
def post(xml)
|
55
|
+
client.post(xml, "Content-Type" => "text/xml")
|
56
|
+
end
|
33
57
|
|
34
|
-
|
58
|
+
def payload(api_call, params)
|
59
|
+
mote(File.join(TEMPLATES, "%s.mote" % api_call), params.merge(auth))
|
60
|
+
end
|
61
|
+
|
62
|
+
class Response
|
63
|
+
attr :data
|
64
|
+
|
65
|
+
OK = "Ok"
|
35
66
|
|
36
|
-
|
37
|
-
|
38
|
-
assert(exp_in_future, [:exp_date, :not_valid])
|
67
|
+
def initialize(xml)
|
68
|
+
@data = XmlSimple.xml_in(xml, forcearray: false)
|
39
69
|
end
|
40
70
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
m = "%.2i" % exp_month
|
71
|
+
def success?
|
72
|
+
data["messages"]["resultCode"] == OK
|
73
|
+
end
|
45
74
|
|
46
|
-
|
75
|
+
def transaction_id
|
76
|
+
data["transactionResponse"]["transId"]
|
47
77
|
end
|
48
78
|
|
49
|
-
def
|
50
|
-
|
51
|
-
"x_exp_date" => exp_date }
|
79
|
+
def reference_id
|
80
|
+
data["refId"]
|
52
81
|
end
|
53
82
|
|
54
|
-
|
55
|
-
|
56
|
-
Time.new(exp_year, exp_month) > Time.now
|
83
|
+
def profile_id
|
84
|
+
data["customerProfileId"]
|
57
85
|
end
|
58
|
-
end
|
59
86
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
attr_accessor :address
|
64
|
-
attr_accessor :state
|
65
|
-
attr_accessor :zip
|
66
|
-
attr_accessor :email
|
87
|
+
def payment_profile_id
|
88
|
+
data["customerPaymentProfileId"]
|
89
|
+
end
|
67
90
|
|
68
|
-
def
|
69
|
-
|
70
|
-
|
91
|
+
def validation_response
|
92
|
+
if resp = data["validationDirectResponse"] || data["directResponse"]
|
93
|
+
ValidationResponse.new(resp)
|
94
|
+
end
|
71
95
|
end
|
72
96
|
end
|
73
97
|
|
74
|
-
class
|
75
|
-
|
76
|
-
|
77
|
-
|
98
|
+
class ValidationResponse
|
99
|
+
RESPONSE_FIELDS = %w[code subcode reason_code reason_text
|
100
|
+
authorization_code avs_response trans_id
|
101
|
+
invoice_number description amount method
|
102
|
+
transaction_type customer_id first_name
|
103
|
+
last_name company address city state zip
|
104
|
+
country phone fax email
|
105
|
+
shipping_first_name shipping_last_name
|
106
|
+
shipping_company shipping_address shipping_city
|
107
|
+
shipping_state shipping_zip shipping_country
|
108
|
+
tax duty freight tax_exempt purchase_order_number
|
109
|
+
md5_hash card_code_response cavv_response
|
110
|
+
_41 _42 _43 _44 _45 _46 _47 _48 _49 _50
|
111
|
+
account_number card_type split_tender_id
|
112
|
+
requested_amount balance_on_card].freeze
|
113
|
+
|
114
|
+
attr :fields
|
78
115
|
|
79
|
-
def
|
80
|
-
|
81
|
-
@amount = nil
|
82
|
-
else
|
83
|
-
@amount = "%.2f" % amount
|
84
|
-
end
|
116
|
+
def initialize(data, delimiter = ",")
|
117
|
+
@fields = Hash[RESPONSE_FIELDS.zip(data.split(delimiter))]
|
85
118
|
end
|
86
119
|
|
87
|
-
def
|
88
|
-
|
89
|
-
|
90
|
-
|
120
|
+
def success?
|
121
|
+
fields["code"] == "1" && fields["reason_code"] == "1"
|
122
|
+
end
|
123
|
+
|
124
|
+
def transaction_id
|
125
|
+
fields["trans_id"]
|
91
126
|
end
|
92
127
|
end
|
93
128
|
|
@@ -102,8 +137,8 @@ class AN
|
|
102
137
|
@http.use_ssl = true if uri.scheme == "https"
|
103
138
|
end
|
104
139
|
|
105
|
-
def post(params)
|
106
|
-
reply(http.post(path, params))
|
140
|
+
def post(params, *args)
|
141
|
+
reply(http.post(path, params, *args))
|
107
142
|
end
|
108
143
|
|
109
144
|
def reply(res)
|
@@ -117,20 +152,22 @@ class AN
|
|
117
152
|
end
|
118
153
|
end
|
119
154
|
|
120
|
-
|
121
|
-
|
155
|
+
end
|
156
|
+
|
157
|
+
__END__
|
122
158
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
"x_method" => "CC"
|
129
|
-
}.freeze
|
159
|
+
module AN
|
160
|
+
require_relative "an/client"
|
161
|
+
require_relative "an/model"
|
162
|
+
require_relative "an/aim"
|
163
|
+
require_relative "an/cim"
|
130
164
|
|
131
|
-
|
132
|
-
|
165
|
+
class << self
|
166
|
+
attr_accessor :login_id
|
167
|
+
attr_accessor :transaction_key
|
168
|
+
end
|
133
169
|
|
170
|
+
class PaymentResponse
|
134
171
|
RESPONSE_FIELDS = %w[code subcode reason_code reason_text
|
135
172
|
authorization_code avs_response trans_id
|
136
173
|
invoice_number description amount method
|
@@ -145,94 +182,26 @@ class AN
|
|
145
182
|
account_number card_type split_tender_id
|
146
183
|
requested_amount balance_on_card]
|
147
184
|
|
148
|
-
|
149
|
-
new(TEST)
|
150
|
-
end
|
151
|
-
|
152
|
-
def self.live
|
153
|
-
new(LIVE)
|
154
|
-
end
|
155
|
-
|
156
|
-
def initialize(url)
|
157
|
-
@client = Client.connect(url)
|
158
|
-
@params = DEFAULT_PARAMS.merge(
|
159
|
-
"x_login" => AN.login_id,
|
160
|
-
"x_tran_key" => AN.transaction_key
|
161
|
-
)
|
162
|
-
end
|
163
|
-
|
164
|
-
def sale(customer, invoice, card)
|
165
|
-
transact("AUTH_CAPTURE", customer, invoice, card)
|
166
|
-
end
|
185
|
+
attr :fields
|
167
186
|
|
168
|
-
def
|
169
|
-
|
187
|
+
def initialize(data, delimiter = ",")
|
188
|
+
@fields = Hash[RESPONSE_FIELDS.zip(data.split(delimiter))]
|
170
189
|
end
|
171
190
|
|
172
|
-
def
|
173
|
-
|
174
|
-
"x_trans_id" => trans_id
|
175
|
-
})
|
191
|
+
def success?
|
192
|
+
fields["code"] == "1" && fields["reason_code"] == "1"
|
176
193
|
end
|
177
194
|
|
178
|
-
def
|
179
|
-
|
180
|
-
"x_trans_id" => trans_id,
|
181
|
-
"x_card_num" => card_num,
|
182
|
-
"x_amount" => amount
|
183
|
-
})
|
195
|
+
def code
|
196
|
+
fields["code"]
|
184
197
|
end
|
185
198
|
|
186
|
-
def
|
187
|
-
|
188
|
-
"x_trans_id" => trans_id,
|
189
|
-
"x_split_tender_id" => split_tender_id
|
190
|
-
})
|
199
|
+
def message
|
200
|
+
fields["reason_text"]
|
191
201
|
end
|
192
202
|
|
193
|
-
|
194
|
-
|
195
|
-
params = @params.merge("x_type" => type, "x_email_customer" => "TRUE")
|
196
|
-
models.each { |model| params.merge!(model.to_hash) }
|
197
|
-
|
198
|
-
response = Hash[RESPONSE_FIELDS.zip(post(params))]
|
199
|
-
|
200
|
-
if response["code"] == "1"
|
201
|
-
response
|
202
|
-
else
|
203
|
-
raise TransactionFailed.new(response), response["reason_text"]
|
204
|
-
end
|
205
|
-
end
|
206
|
-
|
207
|
-
def post(params)
|
208
|
-
@client.post(URI.encode_www_form(params)).split(DELIMITER)
|
209
|
-
end
|
210
|
-
end
|
211
|
-
|
212
|
-
class TransactionFailed < StandardError
|
213
|
-
attr :response
|
214
|
-
|
215
|
-
def initialize(response)
|
216
|
-
@response = response
|
217
|
-
end
|
218
|
-
end
|
219
|
-
|
220
|
-
# @see http://en.wikipedia.org/wiki/Luhn_algorithm
|
221
|
-
# @credit https://gist.github.com/1182499
|
222
|
-
module Luhn
|
223
|
-
RELATIVE_NUM = { '0' => 0, '1' => 2, '2' => 4, '3' => 6, '4' => 8,
|
224
|
-
'5' => 1, '6' => 3, '7' => 5, '8' => 7, '9' => 9 }
|
225
|
-
|
226
|
-
def self.check(number)
|
227
|
-
number = number.to_s.gsub(/\D/, "").reverse
|
228
|
-
|
229
|
-
sum = 0
|
230
|
-
|
231
|
-
number.split("").each_with_index do |n, i|
|
232
|
-
sum += (i % 2 == 0) ? n.to_i : RELATIVE_NUM[n]
|
233
|
-
end
|
234
|
-
|
235
|
-
sum % 10 == 0
|
203
|
+
def trans_id
|
204
|
+
fields["trans_id"]
|
236
205
|
end
|
237
206
|
end
|
238
207
|
end
|
data/test/an.rb
CHANGED
@@ -1,63 +1,98 @@
|
|
1
|
-
require_relative "
|
2
|
-
require "securerandom"
|
3
|
-
require "benchmark"
|
4
|
-
|
5
|
-
AN.login_id = ENV["LOGIN_ID"]
|
6
|
-
AN.transaction_key = ENV["TRANS_KEY"]
|
1
|
+
require_relative "helper"
|
7
2
|
|
8
3
|
setup do
|
9
|
-
|
10
|
-
last_name: "Doe",
|
11
|
-
zip: "98004",
|
12
|
-
email: "me@cyrildavid.com")
|
13
|
-
|
14
|
-
invoice = AN::Invoice.new(invoice_num: SecureRandom.hex(20),
|
15
|
-
amount: "19.99",
|
16
|
-
description: "Sample Transaction")
|
17
|
-
|
18
|
-
card = AN::CreditCard.new(card_num: "4111111111111111",
|
19
|
-
card_code: "123",
|
20
|
-
exp_month: "1", exp_year: "2015")
|
21
|
-
|
22
|
-
[customer, invoice, card]
|
4
|
+
AN.connect
|
23
5
|
end
|
24
6
|
|
25
|
-
test "
|
26
|
-
|
27
|
-
|
7
|
+
test "AIM basic transaction" do |gateway|
|
8
|
+
resp = gateway.transact({
|
9
|
+
:card_number => "4111111111111111",
|
10
|
+
:card_code => "123",
|
11
|
+
:expiration_date => "2015-01",
|
12
|
+
:amount => "10.00",
|
13
|
+
:invoice_number => SecureRandom.hex(10),
|
14
|
+
:description => "Aeutsahoesuhtaeu",
|
15
|
+
:first_name => "John",
|
16
|
+
:last_name => "Doe",
|
17
|
+
:address => "12345 foobar street",
|
18
|
+
:zip => "90210"
|
19
|
+
})
|
28
20
|
|
29
|
-
|
21
|
+
assert resp.success?
|
22
|
+
assert resp.transaction_id
|
30
23
|
end
|
31
24
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
25
|
+
# CIM (Customer Information Manager)
|
26
|
+
scope do
|
27
|
+
test do |gateway|
|
28
|
+
reference_id = SecureRandom.hex(10)
|
29
|
+
customer_id = SecureRandom.hex(10)
|
37
30
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
31
|
+
# So this step ideally should be done in a background process
|
32
|
+
# after the user on your site signs up.
|
33
|
+
resp = gateway.create_profile(reference_id: reference_id,
|
34
|
+
customer_id: customer_id,
|
35
|
+
email: "foo@bar.com")
|
36
|
+
|
37
|
+
assert resp.success?
|
38
|
+
assert_equal reference_id, resp.reference_id
|
39
|
+
assert resp.profile_id
|
43
40
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
end
|
41
|
+
# After a successful response in the background process, you
|
42
|
+
# should store the profile id in your User hash / table / relation.
|
43
|
+
profile_id = resp.profile_id
|
48
44
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
45
|
+
# Now this happens when the customer provides his credit card details
|
46
|
+
# the first time he tries to go into a page or resource that requires
|
47
|
+
# a form of payment. For example in heroku, you need to add a credit
|
48
|
+
# card as soon as you try to use any kind of add-on.
|
49
|
+
resp = gateway.create_payment_profile(profile_id: profile_id,
|
50
|
+
first_name: "Quentin",
|
51
|
+
last_name: "Tarantino",
|
52
|
+
card_number: "4111111111111111",
|
53
|
+
card_code: "123",
|
54
|
+
address: "#12345 Foobar street",
|
55
|
+
zip: "90210",
|
56
|
+
expiration_date: "2015-01")
|
53
57
|
|
54
|
-
|
55
|
-
|
58
|
+
# If you're to allow the entry of this payment profile, then you
|
59
|
+
# should verify 2 things:
|
60
|
+
#
|
61
|
+
# 1. the actual response is successful
|
62
|
+
# 2. the payment response is successful.
|
63
|
+
#
|
64
|
+
# By default the validation method used is liveMode which returns an
|
65
|
+
# AIM-like payment response string related to the credit card details
|
66
|
+
# passed as part of creating the payment profile.
|
67
|
+
assert resp.success?
|
68
|
+
assert resp.payment_profile_id
|
69
|
+
assert resp.validation_response.success?
|
70
|
+
|
71
|
+
assert_equal "XXXX1111", resp.validation_response.fields["account_number"]
|
72
|
+
assert_equal "Visa", resp.validation_response.fields["card_type"]
|
73
|
+
|
74
|
+
# The payment profile id should then be saved together with the user.
|
75
|
+
# You may also do a one-to-many setup similar to amazon where they can
|
76
|
+
# add multiple credit cards. If that's the case, simply use the
|
77
|
+
# account_number / card type in order to let the customer identify
|
78
|
+
# which credit card is which.
|
79
|
+
payment_profile_id = resp.payment_profile_id
|
56
80
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
81
|
+
# Now this should be executed when the customer does a one-click
|
82
|
+
# payment option similar to amazon, or when the end of month utility
|
83
|
+
# bill is due (i.e. AWS / Heroku).
|
84
|
+
resp = gateway.create_profile_transaction({
|
85
|
+
profile_id: profile_id,
|
86
|
+
payment_profile_id: payment_profile_id,
|
87
|
+
amount: "11.95",
|
88
|
+
invoice_number: SecureRandom.hex(10),
|
89
|
+
description: "Jan - Feb",
|
90
|
+
purchase_order_number: "001"
|
91
|
+
})
|
92
|
+
|
93
|
+
assert resp.success?
|
94
|
+
assert resp.validation_response.success?
|
95
|
+
assert_equal "XXXX1111", resp.validation_response.fields["account_number"]
|
96
|
+
assert_equal "Visa", resp.validation_response.fields["card_type"]
|
97
|
+
end
|
63
98
|
end
|
data/test/helper.rb
ADDED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: an
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.1.
|
4
|
+
version: 0.0.1.rc2
|
5
5
|
prerelease: 6
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-02-
|
12
|
+
date: 2012-02-15 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: scrivener
|
16
|
-
requirement: &
|
16
|
+
requirement: &2156032980 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *2156032980
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: cutest
|
27
|
-
requirement: &
|
27
|
+
requirement: &2155894040 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,7 +32,7 @@ dependencies:
|
|
32
32
|
version: '0'
|
33
33
|
type: :development
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *2155894040
|
36
36
|
description: AN is a simplified client for integration with Authorize.NET.
|
37
37
|
email:
|
38
38
|
- me@cyrildavid.com
|
@@ -45,6 +45,7 @@ files:
|
|
45
45
|
- lib/an.rb
|
46
46
|
- an.gemspec
|
47
47
|
- test/an.rb
|
48
|
+
- test/helper.rb
|
48
49
|
homepage: http://github.com/cyx/an
|
49
50
|
licenses: []
|
50
51
|
post_install_message:
|