Completed
Pull Request — master (#126)
by
unknown
01:39
created

XmlTransaction   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 276
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
dl 0
loc 276
rs 10
c 1
b 0
f 1
wmc 30

9 Methods

Rating   Name   Duplication   Size   Complexity  
A setOAuthOptions() 0 17 2
A initialize() 0 21 1
B make_request() 0 36 4
B has_content() 0 25 1
A has_response? 0 3 1
B build_nodes() 0 35 1
F convert_field() 0 41 18
A run() 0 3 1
A test? 0 3 1
1
module AuthorizeNet
2
  # The ARB transaction class.
3
  class XmlTransaction < AuthorizeNet::Transaction
4
    # The XML namespace used by the ARB API.
5
    XML_NAMESPACE = 'AnetApi/xml/v1/schema/AnetApiSchema.xsd'.freeze
6
7
    # Constants for both the various Authorize.Net subscription gateways are defined here.
8
    module Gateway
9
      LIVE = 'https://api2.authorize.net/xml/v1/request.api'.freeze
10
      TEST = 'https://apitest.authorize.net/xml/v1/request.api'.freeze
11
    end
12
13
    # Constants for both the various Authorize.Net transaction types are defined here.
14
    module Type
15
      ARB_CREATE = "ARBCreateSubscriptionRequest".freeze
16
      ARB_UPDATE = "ARBUpdateSubscriptionRequest".freeze
17
      ARB_GET_STATUS = "ARBGetSubscriptionStatusRequest".freeze
18
      ARB_CANCEL = "ARBCancelSubscriptionRequest".freeze
19
      ARB_GET_SUBSCRIPTION_LIST = "ARBGetSubscriptionListRequest".freeze
20
      CIM_CREATE_PROFILE = "createCustomerProfileRequest".freeze
21
      CIM_CREATE_PAYMENT = "createCustomerPaymentProfileRequest".freeze
22
      CIM_CREATE_ADDRESS = "createCustomerShippingAddressRequest".freeze
23
      CIM_CREATE_TRANSACTION = "createCustomerProfileTransactionRequest".freeze
24
      CIM_DELETE_PROFILE = "deleteCustomerProfileRequest".freeze
25
      CIM_DELETE_PAYMENT = "deleteCustomerPaymentProfileRequest".freeze
26
      CIM_DELETE_ADDRESS = "deleteCustomerShippingAddressRequest".freeze
27
      CIM_GET_PROFILE_IDS = "getCustomerProfileIdsRequest".freeze
28
      CIM_GET_PROFILE = "getCustomerProfileRequest".freeze
29
      CIM_GET_PAYMENT = "getCustomerPaymentProfileRequest".freeze
30
      CIM_GET_ADDRESS = "getCustomerShippingAddressRequest".freeze
31
      CIM_GET_HOSTED_PROFILE = "getHostedProfilePageRequest".freeze
32
      CIM_UPDATE_PROFILE = "updateCustomerProfileRequest".freeze
33
      CIM_UPDATE_PAYMENT = "updateCustomerPaymentProfileRequest".freeze
34
      CIM_UPDATE_ADDRESS = "updateCustomerShippingAddressRequest".freeze
35
      CIM_UPDATE_SPLIT = "updateSplitTenderGroupRequest".freeze
36
      CIM_VALIDATE_PAYMENT = "validateCustomerPaymentProfileRequest".freeze
37
      REPORT_GET_BATCH_LIST = "getSettledBatchListRequest".freeze
38
      REPORT_GET_TRANSACTION_LIST = "getTransactionListRequest".freeze
39
      REPORT_GET_UNSETTLED_TRANSACTION_LIST = "getUnsettledTransactionListRequest".freeze
40
      REPORT_GET_TRANSACTION_DETAILS = "getTransactionDetailsRequest".freeze
41
    end
42
43
    # Fields to convert to/from booleans.
44
    @@boolean_fields = []
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using a class variable like @@boolean_fields is generally not recommended; did you consider
using an class instance variable instead?
Loading history...
45
46
    # Fields to convert to/from BigDecimal.
47
    @@decimal_fields = []
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using a class variable like @@decimal_fields is generally not recommended; did you consider
using an class instance variable instead?
Loading history...
48
49
    # Fields to convert to/from Date.
50
    @@date_fields = []
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using a class variable like @@date_fields is generally not recommended; did you consider
using an class instance variable instead?
Loading history...
51
52
    # Fields to convert to/from DateTime.
53
    @@datetime_fields = []
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using a class variable like @@datetime_fields is generally not recommended; did you consider
using an class instance variable instead?
Loading history...
54
55
    # The class to wrap our response in.
56
    @response_class = AuthorizeNet::XmlResponse
57
58
    # The default options for the constructor.
59
    @@option_defaults = {
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using a class variable like @@option_defaults is generally not recommended; did you consider
using an class instance variable instead?
Loading history...
60
      gateway: :production,
61
      verify_ssl: true,
62
      reference_id: nil
63
    }
64
65
    # DO NOT USE. Instantiate AuthorizeNet::ARB::Transaction or AuthorizeNet::CIM::Transaction instead.
66
    def initialize(api_login_id, api_transaction_key, options = {})
67
      super()
68
      @api_login_id = api_login_id
69
      @api_transaction_key = api_transaction_key
70
71
      @response ||= nil
72
      @type ||= nil
73
74
      options = @@option_defaults.merge(options)
75
      @verify_ssl = options[:verify_ssl]
76
      @reference_id = options[:reference_id]
77
      @gateway = case options[:gateway].to_s
78
                 when 'sandbox', 'test'
79
                   Gateway::TEST
80
                 when 'production', 'live'
81
                   Gateway::LIVE
82
                 else
83
                   @gateway = options[:gateway]
84
                   options[:gateway]
85
      end
86
    end
87
88
    def setOAuthOptions
89
      unless @options_OAuth.blank?
90
        @options_OAuth = @@option_defaults.merge(@options_OAuth)
91
        @verify_ssl = options_OAuth[:verify_ssl]
92
        @reference_id = options_OAuth[:reference_id]
93
94
        @gateway = case options_OAuth[:gateway].to_s
95
                   when 'sandbox', 'test'
96
                     Gateway::TEST
97
                   when 'production', 'live'
98
                     Gateway::LIVE
99
                   else
100
                     @gateway = options_OAuth[:gateway]
101
                     options_OAuth[:gateway]
102
        end
103
      end
104
    end
105
106
    # Checks if the transaction has been configured for the sandbox or not. Return FALSE if the
107
    # transaction is running against the production, TRUE otherwise.
108
    def test?
109
      @gateway != Gateway::LIVE
110
    end
111
112
    # Checks to see if the transaction has a response (meaning it has been submitted to the gateway).
113
    # Returns TRUE if a response is present, FALSE otherwise.
114
    def has_response?
115
      [email protected]?
116
    end
117
118
    # Retrieve the response object (or Nil if transaction hasn't been sent to the gateway).
119
    attr_reader :response
120
121
    # Submits the transaction to the gateway for processing. Returns a response object. If the transaction
122
    # has already been run, it will return nil.
123
    def run
124
      make_request
125
    end
126
127
    # Returns a deep-copy of the XML object sent to the payment gateway. Or nil if there was no XML payload.
128
    attr_reader :xml
129
130
    #:enddoc:
131
    protected
132
133
    # Takes a list of nodes (a Hash is a node, and Array is a list) and returns True if any nodes
134
    # would be built by build_nodes. False if no new nodes would be generated.
135
    def has_content(nodeList, data)
136
      nodeList.each do |node|
137
        nodeName = (node.keys.reject { |_k| nodeName.to_s[0..0] == '_' }).first
138
        multivalue = node[:_multivalue]
139
        conditional = node[:_conditional]
140
        value = node[nodeName]
141
        value = send(conditional, nodeName) unless conditional.nil?
142
        case value
143
        when Array
144
          if multivalue.nil?
145
            return true if has_content(value, data)
146
          else
147
            data[multivalue].each do |v|
148
              return true if has_content(value, v)
149
            end
150
          end
151
        when Symbol
152
          converted = convert_field(value, data[value])
153
          return true unless converted.nil?
154
        else
155
          return true
156
        end
157
      end
158
      false
159
    end
160
161
    # Takes a list of nodes (a Hash is a node, and Array is a list) and recursively builds the XML by pulling
162
    # values as needed from data.
163
    def build_nodes(builder, nodeList, data)
164
      nodeList.each do |node|
165
        # TODO: - ADD COMMENTS HERE
166
        nodeName = (node.keys.reject { |k| k.to_s[0..0] == '_' }).first
167
        multivalue = node[:_multivalue]
168
        conditional = node[:_conditional]
169
        value = node[nodeName]
170
171
        value = send(conditional, nodeName) unless conditional.nil?
172
        case value
173
        when Array # node containing other nodes
174
          if multivalue.nil?
175
            proc = proc { build_nodes(builder, value, data) }
176
            builder.send(nodeName, &proc) if has_content(value, data)
177
          else
178
            data[multivalue].to_a.each do |v|
179
              proc = proc { build_nodes(builder, value, v) }
180
              builder.send(nodeName, &proc) if has_content(value, v)
181
            end
182
          end
183
        when Symbol # node containing actual data
184
          if data[value].is_a?(Array)
185
            data[value].each do |v|
186
              converted = convert_field(value, v)
187
              builder.send(nodeName, converted) unless converted.nil?
188
            end
189
          else
190
            converted = convert_field(value, data[value])
191
            builder.send(nodeName, converted) unless converted.nil?
192
          end
193
        else
194
          builder.send(nodeName, value)
195
        end
196
      end
197
    end
198
199
    def convert_field(field, value)
200
      if @@boolean_fields.include?(field) && !value.nil?
201
        return boolean_to_value(value)
202
      elsif @@decimal_fields.include?(field) && !value.nil?
203
        return decimal_to_value(value)
204
      elsif @@date_fields.include?(field) && !value.nil?
205
        return date_to_value(value)
206
      elsif @@datetime_fields.include?(field) && !value.nil?
207
        return datetime_to_value(value)
208
      elsif field == :extra_options
209
        # handle converting extra options
210
        options = []
211
        value.each_pair { |k, v| options <<= to_param(k, v) } unless value.nil?
212
        unless @custom_fields.nil?
213
          # special sort to maintain compatibility with AIM custom field ordering
214
          # FIXME - This should be DRY'd up.
215
          custom_field_keys = @custom_fields.keys.collect(&:to_s).sort.collect(&:to_sym)
216
          for key in custom_field_keys
217
            options <<= to_param(key, @custom_fields[key.to_sym], '')
218
          end
219
        end
220
221
        if !options.empty?
222
          return options.join('&')
223
        else
224
          return nil
225
        end
226
      elsif field == :exp_date
227
        # convert MMYY expiration dates into the XML equivalent
228
        unless value.nil?
229
          begin
230
            return value.to_s.casecmp('xxxx').zero? ? 'XXXX' : Date.strptime(value.to_s, '%m%y').strftime('%Y-%m')
231
          rescue
232
            # If we didn't get the exp_date in MMYY format, try our best to convert it
233
            return Date.parse(value.to_s).strftime('%Y-%m')
234
          end
235
        end
236
      end
237
238
      value
239
    end
240
241
    # An internal method that builds the POST body, submits it to the gateway, and constructs a Response object with the response.
242
    def make_request
243
      return nil if has_response?
244
245
      fields = @fields
246
247
      builder = Nokogiri::XML::Builder.new(encoding: 'utf-8') do |x|
248
        x.send(@type.to_sym, xmlns: XML_NAMESPACE) do
249
          x.merchantAuthentication do
250
            x.name @api_login_id
251
            x.transactionKey @api_transaction_key
252
          end
253
          build_nodes(x, self.class.const_get(:FIELDS)[@type], fields)
254
        end
255
      end
256
      @xml = builder.to_xml
257
258
      url = URI.parse(@gateway)
259
260
      request = Net::HTTP::Post.new(url.path)
261
      request.content_type = 'text/xml'
262
      request.body = @xml
263
      connection = Net::HTTP.new(url.host, url.port)
264
      connection.use_ssl = true
265
      if @verify_ssl
266
        connection.verify_mode = OpenSSL::SSL::VERIFY_PEER
267
      else
268
        connection.verify_mode = OpenSSL::SSL::VERIFY_NONE
269
      end
270
271
      # Use our Class's @response_class variable to find the Response class we are supposed to use.
272
      begin
273
        @response = self.class.instance_variable_get(:@response_class).new((connection.start { |http| http.request(request) }), self)
274
      rescue
275
        @response = self.class.instance_variable_get(:@response_class).new($ERROR_INFO, self)
276
      end
277
    end
278
  end
279
end
280