Completed
Pull Request — master (#156)
by
unknown
02:20
created

XmlTransaction.response()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1 1
module AuthorizeNet
2
  # The ARB transaction class.
3 1
  class XmlTransaction < AuthorizeNet::Transaction
4
    # The XML namespace used by the ARB API.
5 1
    XML_NAMESPACE = 'AnetApi/xml/v1/schema/AnetApiSchema.xsd'.freeze
6
7
    # Constants for both the various Authorize.Net subscription gateways are defined here.
8 1
    module Gateway
9 1
      LIVE = 'https://api2.authorize.net/xml/v1/request.api'.freeze
10 1
      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 1
    module Type
15 1
      ARB_CREATE = "ARBCreateSubscriptionRequest".freeze
16 1
      ARB_UPDATE = "ARBUpdateSubscriptionRequest".freeze
17 1
      ARB_GET_STATUS = "ARBGetSubscriptionStatusRequest".freeze
18 1
      ARB_CANCEL = "ARBCancelSubscriptionRequest".freeze
19 1
      ARB_GET_SUBSCRIPTION_LIST = "ARBGetSubscriptionListRequest".freeze
20 1
      CIM_CREATE_PROFILE = "createCustomerProfileRequest".freeze
21 1
      CIM_CREATE_PAYMENT = "createCustomerPaymentProfileRequest".freeze
22 1
      CIM_CREATE_ADDRESS = "createCustomerShippingAddressRequest".freeze
23 1
      CIM_CREATE_TRANSACTION = "createCustomerProfileTransactionRequest".freeze
24 1
      CIM_DELETE_PROFILE = "deleteCustomerProfileRequest".freeze
25 1
      CIM_DELETE_PAYMENT = "deleteCustomerPaymentProfileRequest".freeze
26 1
      CIM_DELETE_ADDRESS = "deleteCustomerShippingAddressRequest".freeze
27 1
      CIM_GET_PROFILE_IDS = "getCustomerProfileIdsRequest".freeze
28 1
      CIM_GET_PROFILE = "getCustomerProfileRequest".freeze
29 1
      CIM_GET_PAYMENT = "getCustomerPaymentProfileRequest".freeze
30 1
      CIM_GET_ADDRESS = "getCustomerShippingAddressRequest".freeze
31 1
      CIM_GET_HOSTED_PROFILE = "getHostedProfilePageRequest".freeze
32 1
      CIM_UPDATE_PROFILE = "updateCustomerProfileRequest".freeze
33 1
      CIM_UPDATE_PAYMENT = "updateCustomerPaymentProfileRequest".freeze
34 1
      CIM_UPDATE_ADDRESS = "updateCustomerShippingAddressRequest".freeze
35 1
      CIM_UPDATE_SPLIT = "updateSplitTenderGroupRequest".freeze
36 1
      CIM_VALIDATE_PAYMENT = "validateCustomerPaymentProfileRequest".freeze
37 1
      REPORT_GET_BATCH_LIST = "getSettledBatchListRequest".freeze
38 1
      REPORT_GET_TRANSACTION_LIST = "getTransactionListRequest".freeze
39 1
      REPORT_GET_UNSETTLED_TRANSACTION_LIST = "getUnsettledTransactionListRequest".freeze
40 1
      REPORT_GET_TRANSACTION_DETAILS = "getTransactionDetailsRequest".freeze
41
    end
42
43
    # Fields to convert to/from booleans.
44 1
    @@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 1
    @@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 1
    @@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 1
    @@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 1
    @response_class = AuthorizeNet::XmlResponse
57
58
    # The default options for the constructor.
59 1
    @@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 1
    def initialize(api_login_id, api_transaction_key, options = {})
67 12
      super()
68 12
      @api_login_id = api_login_id
69 12
      @api_transaction_key = api_transaction_key
70
71 12
      @response ||= nil
72 12
      @type ||= nil
73
74 12
      options = @@option_defaults.merge(options)
75 12
      @verify_ssl = options[:verify_ssl]
76 12
      @reference_id = options[:reference_id]
77 12
      @gateway = case options[:gateway].to_s
78
                 when 'sandbox', 'test'
79 12
                   Gateway::TEST
80
                 when 'production', 'live'
81
                   Gateway::LIVE
82
                 else
83
                   @gateway = options[:gateway]
84
                   options[:gateway]
85
      end
86
    end
87
88 1
    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 1
    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 1
    def has_response?
115 11
      [email protected]?
116
    end
117
118
    # Retrieve the response object (or Nil if transaction hasn't been sent to the gateway).
119 1
    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 1
    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 1
    attr_reader :xml
129
130
    #:enddoc:
131 1
    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 1
    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 1
    def build_nodes(builder, nodeList, data)
164 11
      nodeList.each do |node|
165
        # TODO: - ADD COMMENTS HERE
166 40
        nodeName = (node.keys.reject { |k| k.to_s[0..0] == '_' }).first
167 20
        multivalue = node[:_multivalue]
168 20
        conditional = node[:_conditional]
169 20
        value = node[nodeName]
170
171 20
        value = send(conditional, nodeName) unless conditional.nil?
172 20
        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 20
          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 20
            converted = convert_field(value, data[value])
191 20
            builder.send(nodeName, converted) unless converted.nil?
192
          end
193
        else
194
          builder.send(nodeName, value)
195
        end
196
      end
197
    end
198
199 1
    def convert_field(field, value)
200 20
      if @@boolean_fields.include?(field) && !value.nil?
201
        return boolean_to_value(value)
202 20
      elsif @@decimal_fields.include?(field) && !value.nil?
203
        return decimal_to_value(value)
204 20
      elsif @@date_fields.include?(field) && !value.nil?
205
        return date_to_value(value)
206 20
      elsif @@datetime_fields.include?(field) && !value.nil?
207 3
        return datetime_to_value(value)
208 17
      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 17
      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 StandardError
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 17
      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 1
    def make_request
243 11
      return nil if has_response?
244
245 11
      fields = @fields
246
247 11
      builder = Nokogiri::XML::Builder.new(encoding: 'utf-8') do |x|
248 11
        x.send(@type.to_sym, xmlns: XML_NAMESPACE) do
249 11
          x.merchantAuthentication do
250 11
            x.name @api_login_id
251 11
            x.transactionKey @api_transaction_key
252
          end
253 11
          build_nodes(x, self.class.const_get(:FIELDS)[@type], fields)
254
        end
255
      end
256 11
      @xml = builder.to_xml
257
258 11
      url = URI.parse(@gateway)
259
260 11
      request = Net::HTTP::Post.new(url.path)
261 11
      request.content_type = 'text/xml'
262 11
      request.body = @xml
263 11
      connection = Net::HTTP.new(url.host, url.port)
264 11
      connection.use_ssl = true
265 11
      if @verify_ssl
266 11
        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 11
      begin
273 17
        @response = self.class.instance_variable_get(:@response_class).new((connection.start { |http| http.request(request) }), self)
274
      rescue StandardError
275
        @response = self.class.instance_variable_get(:@response_class).new($ERROR_INFO, self)
276
      end
277
    end
278
  end
279
end
280