an 0.0.1.rc1 → 0.0.1.rc2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. data/an.gemspec +1 -1
  2. data/lib/an.rb +123 -154
  3. data/test/an.rb +85 -50
  4. data/test/helper.rb +3 -0
  5. metadata +7 -6
data/an.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "an"
3
- s.version = "0.0.1.rc1"
3
+ s.version = "0.0.1.rc2"
4
4
  s.summary = "A thin Authorize.NET client."
5
5
  s.description = "AN is a simplified client for integration with Authorize.NET."
6
6
  s.authors = ["Cyril David"]
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 "scrivener"
5
+ require "xmlsimple"
5
6
 
6
7
  class AN
7
- class << self
8
- attr_accessor :login_id
9
- attr_accessor :transaction_key
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
- class ValueObject < Scrivener
13
- # This is an Authorize.net specific implementation detail,
14
- # basically all their field names are prefixed with an `x_`.
15
- def to_hash
16
- {}.tap do |ret|
17
- attributes.each do |att, val|
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
- class CreditCard < ValueObject
25
- attr_accessor :card_num
26
- attr_accessor :card_code
27
- attr_accessor :exp_month
28
- attr_accessor :exp_year
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
- def validate
31
- assert_present(:card_num) &&
32
- assert(Luhn.check(card_num), [:card_num, :not_valid])
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
- assert_present(:card_code)
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
- assert_format(:exp_month, /\A\d{1,2}\z/) &&
37
- assert_format(:exp_year, /\A\d{4}\z/) &&
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
- # Convert to the expiry date expected by the API which is in MMYY.
42
- def exp_date
43
- y = "%.4i" % exp_year
44
- m = "%.2i" % exp_month
71
+ def success?
72
+ data["messages"]["resultCode"] == OK
73
+ end
45
74
 
46
- "#{m}#{y[-2..-1]}"
75
+ def transaction_id
76
+ data["transactionResponse"]["transId"]
47
77
  end
48
78
 
49
- def to_hash
50
- { "x_card_num" => card_num, "x_card_code" => card_code,
51
- "x_exp_date" => exp_date }
79
+ def reference_id
80
+ data["refId"]
52
81
  end
53
82
 
54
- private
55
- def exp_in_future
56
- Time.new(exp_year, exp_month) > Time.now
83
+ def profile_id
84
+ data["customerProfileId"]
57
85
  end
58
- end
59
86
 
60
- class Customer < ValueObject
61
- attr_accessor :first_name
62
- attr_accessor :last_name
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 validate
69
- assert_present :first_name
70
- assert_present :last_name
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 Invoice < ValueObject
75
- attr_accessor :invoice_num
76
- attr_accessor :amount
77
- attr_accessor :description
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 amount=(amount)
80
- if amount.nil? || amount.empty?
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 validate
88
- assert_present(:invoice_num) && assert_length(:invoice_num, 1..20)
89
- assert_present(:amount)
90
- assert_present(:description)
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
- class AIM
121
- DELIMITER = "<|>"
155
+ end
156
+
157
+ __END__
122
158
 
123
- DEFAULT_PARAMS = {
124
- "x_version" => "3.1",
125
- "x_delim_data" => "TRUE",
126
- "x_delim_char" => DELIMITER,
127
- "x_relay_response" => "FALSE",
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
- TEST = "https://test.authorize.net/gateway/transact.dll"
132
- LIVE = "https://secure.authorize.net/gateway/transact.dll"
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
- def self.test
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 authorize(customer, invoice, card)
169
- transact("AUTH_ONLY", customer, invoice, card)
187
+ def initialize(data, delimiter = ",")
188
+ @fields = Hash[RESPONSE_FIELDS.zip(data.split(delimiter))]
170
189
  end
171
190
 
172
- def capture(trans_id)
173
- transact("PRIOR_AUTH_CAPTURE", {
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 refund(trans_id, card_num, amount)
179
- transact("CREDIT", {
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 void(trans_id, split_tender_id = nil)
187
- transact("VOID", {
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
- private
194
- def transact(type, *models)
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 "../lib/an"
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
- customer = AN::Customer.new(first_name: "John",
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 "straight up sale" do |customer, invoice, card|
26
- gateway = AN::AIM.test
27
- response = gateway.sale(customer, invoice, card)
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
- assert_equal "1", response["code"]
21
+ assert resp.success?
22
+ assert resp.transaction_id
30
23
  end
31
24
 
32
- test "wrong credit card number" do |customer, invoice, card|
33
- gateway = AN::AIM.test
34
- card.card_num = "4111222233334444"
35
-
36
- ex = nil
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
- begin
39
- gateway.sale(customer, invoice, card)
40
- rescue Exception => e
41
- ex = e
42
- end
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
- assert ex.kind_of?(AN::TransactionFailed)
45
- assert_equal "6", ex.response["reason_code"]
46
- assert_equal "The credit card number is invalid.", ex.response["reason_text"]
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
- test "authorize and capture" do |customer, invoice, card|
50
- gateway = AN::AIM.test
51
- auth = gateway.authorize(customer, invoice, card)
52
- capt = gateway.capture(auth["trans_id"])
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
- assert_equal "1", capt["code"]
55
- end
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
- test "authorize and void" do |customer, invoice, card|
58
- gateway = AN::AIM.test
59
- auth = gateway.authorize(customer, invoice, card)
60
- void = gateway.void(auth["trans_id"])
61
-
62
- assert_equal "1", void["code"]
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
@@ -0,0 +1,3 @@
1
+ require "cutest"
2
+ require "securerandom"
3
+ require_relative "../lib/an"
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.rc1
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-10 00:00:00.000000000 Z
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: &2151820040 !ruby/object:Gem::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: *2151820040
24
+ version_requirements: *2156032980
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: cutest
27
- requirement: &2151819460 !ruby/object:Gem::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: *2151819460
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: