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.
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: