zuora_api_D 1.6.06

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.
@@ -0,0 +1,9 @@
1
+ require 'rails'
2
+ require "zuora_api/login"
3
+ require "zuora_api/logins/basic"
4
+ require "zuora_api/logins/oauth"
5
+ require 'zuora_api/exceptions'
6
+ require "insights_api/login"
7
+ module ZuoraAPI
8
+ # Your code goes here...
9
+ end
@@ -0,0 +1,106 @@
1
+ module ZuoraAPI
2
+ module Exceptions
3
+ class Error < StandardError; end
4
+ class AuthorizationNotPerformed < Error; end
5
+ class ZuoraAPISessionError < Error
6
+ attr_reader :code, :response
7
+ attr_writer :default_message
8
+
9
+ def initialize(message = nil,response=nil, code =nil)
10
+ @code = code
11
+ @message = message
12
+ @response = response
13
+ @default_message = "Error with Zuora Session."
14
+ end
15
+
16
+ def to_s
17
+ @message || @default_message
18
+ end
19
+ end
20
+
21
+ class BadEntityError < Error
22
+ attr_reader :code, :response, :errors, :successes
23
+ attr_writer :default_message
24
+
25
+ def initialize(message = nil,response=nil, code =nil, errors = [], successes = [])
26
+ @code = code
27
+ @message = message
28
+ @response = response
29
+ @default_message = "Error with Zuora Entity"
30
+ @errors = errors
31
+ @successes = successes
32
+ end
33
+
34
+ def to_s
35
+ @message || @default_message
36
+ end
37
+ end
38
+
39
+ class ZuoraAPIError < Error
40
+ attr_reader :code, :response, :errors, :successes
41
+ attr_writer :default_message
42
+
43
+ def initialize(message = nil,response=nil, code =nil, errors = [], successes = [])
44
+ @code = code
45
+ @message = message
46
+ @response = response
47
+ @default_message = "Error communicating with Zuora."
48
+ @errors = errors
49
+ @successes = successes
50
+ end
51
+
52
+ def to_s
53
+ @message || @default_message
54
+ end
55
+ end
56
+
57
+ class ZuoraAPIRequestLimit < Error
58
+ attr_reader :code, :response
59
+ attr_writer :default_message
60
+
61
+ def initialize(message = nil,response=nil, code =nil)
62
+ @code = code
63
+ @message = message
64
+ @response = response
65
+ @default_message = "Your request limit has been exceeded for zuora."
66
+ end
67
+
68
+ def to_s
69
+ @message || @default_message
70
+ end
71
+ end
72
+
73
+ class ZuoraAPILockCompetition < Error
74
+ attr_reader :code, :response
75
+ attr_writer :default_message
76
+
77
+ def initialize(message = nil,response=nil, code =nil)
78
+ @code = code
79
+ @message = message
80
+ @response = response
81
+ @default_message = "Operation failed due to lock competition. Please retry"
82
+ end
83
+
84
+ def to_s
85
+ @message || @default_message
86
+ end
87
+ end
88
+
89
+
90
+ class ZuoraAPIAuthenticationTypeError < Error
91
+ attr_reader :code, :response
92
+ attr_writer :default_message
93
+
94
+ def initialize(message = nil,response=nil, code =nil)
95
+ @code = code
96
+ @message = message
97
+ @response = response
98
+ @default_message = "Authentication type is not supported by this Login"
99
+ end
100
+
101
+ def to_s
102
+ @message || @default_message
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,808 @@
1
+ require "httparty"
2
+ require "nokogiri"
3
+ require "uri"
4
+
5
+ module ZuoraAPI
6
+ class Login
7
+ ENVIRONMENTS = [SANDBOX = 'Sandbox', PRODUCTION = 'Production', PREFORMANCE = 'Preformance', SERVICES = 'Services', UNKNOWN = 'Unknown' ]
8
+ REGIONS = [EU = 'EU', US = 'US', NA = 'NA' ]
9
+ MIN_Endpoint = '91.0'
10
+ XML_SAVE_OPTIONS = Nokogiri::XML::Node::SaveOptions::AS_XML | Nokogiri::XML::Node::SaveOptions::NO_DECLARATION
11
+ attr_accessor :region, :url, :wsdl_number, :current_session, :environment, :status, :errors, :current_error, :user_info, :tenant_id, :tenant_name, :entity_id, :timeout_sleep
12
+
13
+ def initialize(url: nil, entity_id: nil, session: nil, status: nil, **keyword_args)
14
+ @url = url.gsub(/(\d{2}\.\d)$/, MIN_Endpoint)
15
+ @entity_id = entity_id
16
+ @errors = Hash.new
17
+ @current_session = session
18
+ @status = status.blank? ? "Active" : status
19
+ @user_info = Hash.new
20
+ self.update_environment
21
+ @timeout_sleep = 5
22
+ end
23
+
24
+ def self.environments
25
+ %w(Sandbox Production Services Performance Staging)
26
+ end
27
+
28
+ def self.regions
29
+ %w(US EU NA)
30
+ end
31
+
32
+ def self.endpoints
33
+ return {"US" => {"Sandbox" => "https://apisandbox.zuora.com/apps/services/a/",
34
+ "Production" => "https://www.zuora.com/apps/services/a/",
35
+ "Performance" => "https://pt1.zuora.com/apps/services/a/",
36
+ "Services" => "https://services347.zuora.com/apps/services/a/"},
37
+ "EU" => {"Sandbox" => "https://sandbox.eu.zuora.com/apps/services/a/",
38
+ "Production" => "https://eu.zuora.com/apps/services/a/",
39
+ "Performance" => "https://pt1.eu.zuora.com/apps/services/a/",
40
+ "Services" => "https://services347.eu.zuora.com/apps/services/a/"},
41
+ "NA" => {"Sandbox" => "https://sandbox.na.zuora.com/apps/services/a/",
42
+ "Production" => "https://na.zuora.com/apps/services/a/",
43
+ "Performance" => "https://pt1.na.zuora.com/apps/services/a/",
44
+ "Services" => "https://services347.na.zuora.com/apps/services/a/"}
45
+ }
46
+ end
47
+
48
+ def update_environment
49
+ if !self.url.blank?
50
+ env_path = self.url.split('https://').last.split('.zuora.com').first
51
+ self.region = self.url.include?("eu.") ? "EU" : self.url.include?("na.") ? "NA" : "US"
52
+ if env_path == 'apisandbox' || self.url.include?('sandbox')
53
+ self.environment = 'Sandbox'
54
+ elsif env_path == 'www' || env_path == 'api' || self.url.include?('tls10.zuora.com') || self.url.include?('origin-www.zuora.com') || self.url.include?('zforsf.zuora.com') || self.url.include?('https://zuora.com') || self.url.include?('eu.zuora.com') || self.url.include?('https://na.zuora.com')
55
+ self.environment = 'Production'
56
+ elsif env_path.include?('service') || env_path.include?('ep-edge')
57
+ self.environment = 'Services'
58
+ elsif env_path.include?('pt')
59
+ self.environment = 'Performance'
60
+ elsif env_path.include?('staging2') || env_path.include?('staging1')
61
+ self.region = 'US'
62
+ self.environment = 'Staging'
63
+ else
64
+ self.environment = 'Unknown'
65
+ end
66
+ end
67
+ end
68
+
69
+ def aqua_endpoint(url="")
70
+ return "#{self.url.split("/apps").first}/apps/api/".concat(url)
71
+ end
72
+
73
+ def rest_endpoint(url="")
74
+ if self.environment == 'Sandbox'
75
+ return self.region == "US" ? "https://rest.apisandbox.zuora.com/v1/".concat(url) : self.region == "EU" ? "https://rest.sandbox.eu.zuora.com/v1/".concat(url) : "https://rest.sandbox.na.zuora.com/v1/".concat(url)
76
+ elsif self.environment == 'Production'
77
+ return self.region == "US" ? "https://rest.zuora.com/v1/".concat(url) : self.region == "EU" ? "https://rest.eu.zuora.com/v1/".concat(url) : "https://rest.na.zuora.com/v1/".concat(url)
78
+ elsif self.environment == 'Services'
79
+ return self.url.split('/')[0..2].join('/').concat('/apps/v1/').concat(url)
80
+ elsif self.environment == 'Performance'
81
+ return self.url.split('/')[0..2].join('/').concat('/apps/v1/').concat(url)
82
+ else self.environment == 'Unknown'
83
+ return url
84
+ end
85
+ end
86
+
87
+ def fileURL(url="")
88
+ return self.url.split(".com").first.concat(".com/apps/api/file/").concat(url)
89
+ end
90
+
91
+ def dateFormat
92
+ return self.wsdl_number > 68 ? '%Y-%m-%d' : '%Y-%m-%dT%H:%M:%S'
93
+ end
94
+
95
+ def new_session(auth_type: :basic, debug: false)
96
+ end
97
+
98
+ def get_session(prefix: false, auth_type: :basic)
99
+ Rails.logger.debug("Get session for #{auth_type} - #{self.class.to_s}")
100
+ case auth_type
101
+ when :basic
102
+ if self.current_session.blank?
103
+ Rails.logger.debug("Create new session")
104
+ case self.class.to_s
105
+ when 'ZuoraAPI::Oauth'
106
+ if self.bearer_token.blank? || self.oauth_expired?
107
+ self.new_session(auth_type: :bearer)
108
+ end
109
+ self.get_z_session
110
+ when 'ZuoraAPI::Basic'
111
+ self.new_session(auth_type: :basic)
112
+ else
113
+ raise "No Zuora Login Specified"
114
+ end
115
+ end
116
+ raise ZuoraAPI::Exceptions::ZuoraAPISessionError.new(self.current_error) if self.status != 'Active'
117
+ return prefix ? "ZSession #{self.current_session}" : self.current_session.to_s
118
+ when :bearer
119
+ case self.class.to_s
120
+ when 'ZuoraAPI::Oauth'
121
+ if self.bearer_token.blank? || self.oauth_expired?
122
+ self.new_session(auth_type: :bearer)
123
+ end
124
+ when 'ZuoraAPI::Basic'
125
+ raise ZuoraAPI::Exceptions::ZuoraAPIAuthenticationTypeError.new("Basic Login, does not support Authentication of Type: #{auth_type}")
126
+ end
127
+ raise ZuoraAPI::Exceptions::ZuoraAPISessionError.new(self.current_error) if self.status != 'Active'
128
+ return prefix ? "Bearer #{self.bearer_token}" : self.bearer_token.to_s
129
+ end
130
+ end
131
+
132
+ def soap_call(ns1: 'ns1', ns2: 'ns2', batch_size: nil, single_transaction: false, debug: false, errors: [ZuoraAPI::Exceptions::ZuoraAPISessionError, ZuoraAPI::Exceptions::ZuoraAPIError, ZuoraAPI::Exceptions::ZuoraAPIRequestLimit, ZuoraAPI::Exceptions::ZuoraAPILockCompetition], z_session: true, timeout_retry: false, timeout: 120,**keyword_args)
133
+ tries ||= 2
134
+ xml = Nokogiri::XML::Builder.new do |xml|
135
+ xml['SOAP-ENV'].Envelope('xmlns:SOAP-ENV' => "http://schemas.xmlsoap.org/soap/envelope/",
136
+ "xmlns:#{ns2}" => "http://object.api.zuora.com/",
137
+ 'xmlns:xsi' => "http://www.w3.org/2001/XMLSchema-instance",
138
+ 'xmlns:api' => "http://api.zuora.com/",
139
+ "xmlns:#{ns1}" => "http://api.zuora.com/") do
140
+ xml['SOAP-ENV'].Header do
141
+ xml["#{ns1}"].SessionHeader do
142
+ xml["#{ns1}"].session self.get_session(prefix: false, auth_type: :basic)
143
+ end
144
+ if single_transaction
145
+ xml["#{ns1}"].CallOptions do
146
+ xml.useSingleTransaction single_transaction
147
+ end
148
+ end
149
+ if batch_size
150
+ xml["#{ns1}"].QueryOptions do
151
+ xml.batchSize batch_size
152
+ end
153
+ end
154
+ end
155
+ xml['SOAP-ENV'].Body do
156
+ yield xml, keyword_args
157
+ end
158
+ end
159
+ end
160
+
161
+ input_xml = Nokogiri::XML(xml.to_xml(:save_with => XML_SAVE_OPTIONS).strip)
162
+ input_xml.xpath('//ns1:session', 'ns1' =>'http://api.zuora.com/').children.remove
163
+ Rails.logger.debug("Request SOAP XML: #{input_xml.to_xml(:save_with => XML_SAVE_OPTIONS).strip}") if debug
164
+
165
+ response = HTTParty.post(self.url,:body => xml.doc.to_xml(:save_with => XML_SAVE_OPTIONS).strip, :headers => {'Content-Type' => "text/xml; charset=utf-8"}, :timeout => timeout)
166
+ output_xml = Nokogiri::XML(response.body)
167
+ Rails.logger.debug("Response SOAP XML: #{output_xml.to_xml(:save_with => XML_SAVE_OPTIONS).strip}") if debug
168
+
169
+ raise_errors(type: :SOAP, body: output_xml, response: response)
170
+ rescue ZuoraAPI::Exceptions::ZuoraAPISessionError => ex
171
+ if !(tries -= 1).zero? && z_session
172
+ Rails.logger.debug("SOAP Call - Session Invalid")
173
+ self.new_session(auth_type: :basic)
174
+ retry
175
+ else
176
+ if errors.include?(ex.class)
177
+ raise ex
178
+ else
179
+ return output_xml, input_xml, response
180
+ end
181
+ end
182
+ rescue ZuoraAPI::Exceptions::ZuoraAPIError, ZuoraAPI::Exceptions::ZuoraAPIRequestLimit, ZuoraAPI::Exceptions::ZuoraAPILockCompetition => ex
183
+ if errors.include?(ex.class)
184
+ raise ex
185
+ else
186
+ return output_xml, input_xml, response
187
+ end
188
+ rescue Net::OpenTimeout, Errno::ECONNRESET, OpenSSL::SSL::SSLError, Errno::ECONNREFUSED, SocketError => ex
189
+ if !(tries -= 1).zero? && timeout_retry
190
+ Rails.logger.info("SOAP Call - #{ex.class} Timed out will retry after 5 seconds")
191
+ sleep(self.timeout_sleep)
192
+ retry
193
+ else
194
+ raise ex
195
+ end
196
+ rescue => ex
197
+ raise ex
198
+ else
199
+ return output_xml, input_xml, response
200
+ end
201
+
202
+ def raise_errors(type: :SOAP, body: nil, response: nil)
203
+ case type
204
+ when :SOAP
205
+ error = body.xpath('//fns:FaultCode', 'fns' =>'http://fault.api.zuora.com/').text
206
+ message = body.xpath('//fns:FaultMessage', 'fns' =>'http://fault.api.zuora.com/').text
207
+
208
+ if error.blank? || message.blank?
209
+ error = body.xpath('//faultcode').text
210
+ message = body.xpath('//faultstring').text
211
+ end
212
+
213
+ if error.blank? || message.blank?
214
+ error = body.xpath('//ns1:Code', 'ns1' =>'http://api.zuora.com/').text
215
+ message = body.xpath('//ns1:Message', 'ns1' =>'http://api.zuora.com/').text
216
+ end
217
+
218
+ #Update/Create/Delete Calls with multiple requests and responses
219
+ if body.xpath('//ns1:result', 'ns1' =>'http://api.zuora.com/').size > 0 && body.xpath('//ns1:Errors', 'ns1' =>'http://api.zuora.com/').size > 0
220
+ error = []
221
+ success = []
222
+ body.xpath('//ns1:result', 'ns1' =>'http://api.zuora.com/').each_with_index do |call, object_index|
223
+
224
+ if call.xpath('./ns1:Success', 'ns1' =>'http://api.zuora.com/').text == 'false'
225
+ message = "#{call.xpath('./*/ns1:Code', 'ns1' =>'http://api.zuora.com/').text}::#{call.xpath('./*/ns1:Message', 'ns1' =>'http://api.zuora.com/').text}"
226
+ error.push(message)
227
+ else
228
+ success.push(call.xpath('./ns1:Id', 'ns1' =>'http://api.zuora.com/').text)
229
+ end
230
+ end
231
+ end
232
+
233
+ #By default response if not passed in for SOAP as all SOAP is 200
234
+ if error.present?
235
+ if error.class == String
236
+ if error == "INVALID_SESSION"
237
+ raise ZuoraAPI::Exceptions::ZuoraAPISessionError.new("#{error}::#{message}", body, response.code )
238
+ end
239
+ if error == "REQUEST_EXCEEDED_LIMIT"
240
+ raise ZuoraAPI::Exceptions::ZuoraAPIRequestLimit.new("#{error}::#{message}", body, response.code)
241
+ end
242
+ if error == "LOCK_COMPETITION"
243
+ raise ZuoraAPI::Exceptions::ZuoraAPILockCompetition.new("#{error}::#{message}", body, response.code)
244
+ end
245
+ if error.present?
246
+ raise ZuoraAPI::Exceptions::ZuoraAPIError.new("#{error}::#{message}", body, response.code)
247
+ end
248
+ elsif error.class == Array
249
+ if error[0].include?("LOCK_COMPETITION") && error.count == 1
250
+ raise ZuoraAPI::Exceptions::ZuoraAPILockCompetition.new(error.group_by {|v| v}.map {|k,v| "(#{v.size}x) - #{k == "::" ? 'UNKNOWN::No error provided' : k}"}.join(', '), body, response.code)
251
+ else
252
+ raise ZuoraAPI::Exceptions::ZuoraAPIError.new(error.group_by {|v| v}.map {|k,v| "(#{v.size}x) - #{k == "::" ? 'UNKNOWN::No error provided' : k}"}.join(', '), body, response.code, error, success)
253
+ end
254
+ end
255
+ end
256
+
257
+ if response.code == 429
258
+ raise ZuoraAPI::Exceptions::ZuoraAPIRequestLimit.new("The total number of concurrent requests has exceeded the limit allowed by the system. Please resubmit your request later.", body, response.code)
259
+ end
260
+
261
+ when :JSON
262
+ if body.class == Hash && (!body["success"] || !body["Success"] || response.code != 200)
263
+ messages_array = (body["reasons"] || []).map {|error| error['message']}.compact
264
+ codes_array = (body["reasons"] || []).map {|error| error['code']}.compact
265
+
266
+ if body['message'] == "No bearer token" && response.code == 400
267
+ raise ZuoraAPI::Exceptions::ZuoraAPIAuthenticationTypeError.new()
268
+ end
269
+
270
+ if body.dig("reasons").nil? ? false : body.dig("reasons")[0].dig("code") == 90000020
271
+ raise ZuoraAPI::Exceptions::BadEntityError.new("#{messages_array.join(', ')}", body, response.code)
272
+ end
273
+
274
+ if body['error'] == 'Unauthorized' && body['status'] = 401
275
+ raise ZuoraAPI::Exceptions::ZuoraAPISessionError.new("#{messages_array.join(', ')}", body, response.code)
276
+ end
277
+ #Authentication failed
278
+ if codes_array.map{|code| code.to_s.slice(6,7).to_i}.include?(11) || response.code == 401
279
+ raise ZuoraAPI::Exceptions::ZuoraAPISessionError.new("#{messages_array.join(', ')}", body, response.code)
280
+ end
281
+
282
+ #Zuora REST Create Amendment error #Authentication failed
283
+ if body["faultcode"].present? && body["faultcode"] == "fns:INVALID_SESSION"
284
+ raise ZuoraAPI::Exceptions::ZuoraAPISessionError.new("#{body['faultstring']}", body, response.code)
285
+ end
286
+
287
+ #Request exceeded limit
288
+ if codes_array.map{|code| code.to_s.slice(6,7).to_i}.include?(70)
289
+ raise ZuoraAPI::Exceptions::ZuoraAPIRequestLimit.new("#{messages_array.join(', ')}", body, response.code)
290
+ end
291
+
292
+ #Locking contention
293
+ if codes_array.map{|code| code.to_s.slice(6,7).to_i}.include?(50)
294
+ raise ZuoraAPI::Exceptions::ZuoraAPILockCompetition.new("#{messages_array.join(', ')}", body, response.code)
295
+ end
296
+
297
+ #All Errors catch
298
+ if codes_array.size > 0
299
+ raise ZuoraAPI::Exceptions::ZuoraAPIError.new("#{messages_array.join(', ')}", body, response.code)
300
+ end
301
+
302
+ #Zuora REST Query Errors
303
+ if body["faultcode"].present?
304
+ case body["faultcode"]
305
+ when "fns:MALFORMED_QUERY"
306
+ raise ZuoraAPI::Exceptions::ZuoraAPIError.new("#{body["faultcode"]}::#{body["faultstring"]}", body, response.code)
307
+ when "fns:REQUEST_EXCEEDED_LIMIT"
308
+ raise ZuoraAPI::Exceptions::ZuoraAPIRequestLimit.new("#{body["faultcode"]}::#{body["faultstring"]}", body, response.code)
309
+ when "fns:LOCK_COMPETITION"
310
+ raise ZuoraAPI::Exceptions::ZuoraAPILockCompetition.new("#{body["faultcode"]}::#{body["faultstring"]}", body, response.code)
311
+ when "INVALID_SESSION"
312
+ raise ZuoraAPI::Exceptions::ZuoraAPISessionError.new("#{body["faultcode"]}::#{body["faultstring"]}", body, response.code)
313
+ else
314
+ raise ZuoraAPI::Exceptions::ZuoraAPIError.new("#{body["faultcode"]}::#{body["faultstring"]}", body, response.code)
315
+ end
316
+ end
317
+
318
+ if body["Errors"].present? || body["errors"].present?
319
+ errors = []
320
+ (body["Errors"] || []).select { |obj| errors.push(obj["Message"]) }.compact
321
+ (body["errors"] || []).select { |obj| errors.push(obj["Message"]) }.compact
322
+ if errors.size > 0
323
+ raise ZuoraAPI::Exceptions::ZuoraAPIError.new("#{errors.join(", ")}", body, response.code, errors)
324
+ end
325
+ end
326
+ end
327
+
328
+ #Zuora REST Actions error (Create, Update, Delete)
329
+ if body.class == Array
330
+ all_errors = body.select {|obj| !obj['Success'] || !obj['success'] }.map {|obj| obj['Errors'] || obj['errors'] }.compact
331
+ all_success = body.select {|obj| obj['Success'] || obj['success']}.compact
332
+
333
+ if all_errors.size > 0
334
+ raise ZuoraAPI::Exceptions::ZuoraAPIError.new("#{all_errors.flatten.group_by {|error| error['Message']}.keys.uniq.join(' ')}", body, response.code, all_errors, all_success )
335
+ end
336
+ end
337
+
338
+ #All other errors
339
+ if response.code != 200
340
+ if response.code == 429
341
+ raise ZuoraAPI::Exceptions::ZuoraAPIRequestLimit.new("The total number of concurrent requests has exceeded the limit allowed by the system. Please resubmit your request later.", body, response.code)
342
+ else
343
+ raise ZuoraAPI::Exceptions::ZuoraAPIError.new("#{response.message}", body, response.code)
344
+ end
345
+ end
346
+ end
347
+ end
348
+
349
+ def aqua_query(queryName: '', query: '', version: '1.2', jobName: 'Aqua',partner: '', project: '')
350
+ params = {
351
+ "format" => 'csv',
352
+ "version" => version,
353
+ "name" => jobName,
354
+ "encrypted" => 'none',
355
+ "useQueryLabels" => 'true',
356
+ "partner" => partner,
357
+ "project" => project,
358
+ "queries" => [{
359
+ "name" => queryName,
360
+ "query" => query,
361
+ "type" => 'zoqlexport'
362
+ }]
363
+ }
364
+ response = self.rest_call(method: :post, body: params.to_json, url: self.aqua_endpoint("batch-query/"))
365
+ if(response[0]["id"].nil?)
366
+ raise ZuoraAPI::Exceptions::ZuoraAPIError.new("Error in AQuA Process. #{response}")
367
+ end
368
+ return getFileById(id: response[0]["id"])
369
+ end
370
+
371
+ def getFileById(id: "2c92c0f85e7f88ff015e86b8f8f4517f")
372
+ response = nil
373
+ result = "new"
374
+ while result != "completed" do
375
+ sleep(2)#sleep 2 seconds
376
+ response, fullResponse = self.rest_call(method: :get, body: {}, url: self.aqua_endpoint("batch-query/jobs/#{id}"))
377
+ result = response["batches"][0]["status"]
378
+ if result == "error"
379
+ raise ZuoraAPI::Exceptions::ZuoraAPIError.new("Aqua Error: #{response}")
380
+ break
381
+ end
382
+ end
383
+ fileId = response["batches"][0]["fileId"]
384
+ return self.get_file(url: self.aqua_endpoint("file/#{fileId}"))
385
+ end
386
+
387
+ def describe_call(object = nil)
388
+ tries ||= 2
389
+
390
+ base = self.url.include?(".com") ? self.url.split(".com")[0].concat(".com") : self.url.split(".eu")[0].concat(".eu")
391
+ url = object ? "#{base}/apps/api/describe/#{object}" : "#{base}/apps/api/describe/"
392
+ headers = !self.entity_id.blank? ? {"entityId" => self.entity_id, 'Content-Type' => "text/xml; charset=utf-8"} : {'Content-Type' => "text/xml; charset=utf-8"}
393
+ response = HTTParty.get(url, headers: {"Authorization" => self.get_session(prefix: true, auth_type: :basic)}.merge(headers), :timeout => 120)
394
+
395
+ raise ZuoraAPI::Exceptions::ZuoraAPISessionError.new("Session Expired") if response.code.to_s == "401"
396
+ output_xml = Nokogiri::XML(response.body)
397
+ des_hash = Hash.new
398
+ if object == nil
399
+ output_xml.xpath("//object").each do |object|
400
+ temp = {:label => object.xpath(".//label").text, :url => object.attributes["href"].value }
401
+ des_hash[object.xpath(".//name").text] = temp
402
+ end
403
+ else
404
+ output_xml.xpath("//field").each do |object|
405
+ temp = {:label => object.xpath(".//label").text,:selectable => object.xpath(".//selectable").text,
406
+ :createable => object.xpath(".//label").text == "ID" ? "false" : object.xpath(".//createable").text,
407
+ :filterable => object.xpath(".//filterable").text,
408
+ :updateable => object.xpath(".//label").text == "ID" ? "false" : object.xpath(".//updateable").text,
409
+ :custom => object.xpath(".//custom").text,:maxlength => object.xpath(".//maxlength").text,
410
+ :required => object.xpath(".//required").text,
411
+ :type => object.xpath(".//type").text,
412
+ :context => object.xpath(".//context").collect{ |x| x.text } }
413
+ temp[:options] = object.xpath(".//option").collect{ |x| x.text } if object.xpath(".//option").size > 0
414
+ des_hash[object.xpath(".//name").text.to_sym] = temp
415
+ des_hash[:fieldsToNull] = {:label => "FieldsToNull",:selectable => "false",
416
+ :createable => "false",:filterable => "false",
417
+ :updateable => "true",:custom => "false",
418
+ :required => "false",:type => "picklist",
419
+ :maxlength => "" ,:context => ["soap"],
420
+ :options => des_hash.map {|k,v| k if v[:updateable] == "true" && v[:required] == "false"}.compact.uniq }
421
+
422
+ end
423
+ des_hash[:related_objects] = output_xml.xpath(".//related-objects").xpath(".//object").map{ |x| [x.xpath(".//name").text.to_sym, [ [:url, x.attributes["href"].value], [:label, x.xpath(".//name").text ] ].to_h] }.to_h
424
+ end
425
+ rescue Net::ReadTimeout, Net::OpenTimeout, Errno::EPIPE, Errno::ECONNRESET, OpenSSL::SSL::SSLError, Errno::ECONNREFUSED, SocketError => ex
426
+ if !(tries -= 1).zero?
427
+ Rails.logger.info("Describe - #{ex.class} Timed out will retry after 5 seconds")
428
+ sleep(self.timeout_sleep)
429
+ retry
430
+ else
431
+ raise ex
432
+ end
433
+ rescue ZuoraAPI::Exceptions::ZuoraAPISessionError => ex
434
+ if !(tries -= 1).zero?
435
+ Rails.logger.info("Session expired. Starting new session.")
436
+ self.new_session
437
+ retry
438
+ else
439
+ raise ex
440
+ end
441
+ rescue => ex
442
+ raise ex
443
+ else
444
+ return des_hash
445
+ end
446
+
447
+ def rest_call(method: :get, body: nil,headers: {}, url: rest_endpoint("catalog/products?pageSize=4"), debug: false, errors: [ZuoraAPI::Exceptions::ZuoraAPISessionError, ZuoraAPI::Exceptions::ZuoraAPIError, ZuoraAPI::Exceptions::ZuoraAPIRequestLimit, ZuoraAPI::Exceptions::ZuoraAPILockCompetition], z_session: true, session_type: :basic, timeout_retry: false, timeout: 120, **keyword_args)
448
+ tries ||= 2
449
+ headers["entityId"] = self.entity_id if !self.entity_id.blank?
450
+ raise "Method not supported, supported methods include: :get, :post, :put, :delete, :patch, :head, :options" if ![:get, :post, :put, :delete, :patch, :head, :options].include?(method)
451
+
452
+ authentication_headers = z_session ? {"Authorization" => self.get_session(prefix: true, auth_type: session_type) } : {}
453
+ headers = {'Content-Type' => "application/json; charset=utf-8"}.merge(headers).merge(authentication_headers)
454
+
455
+ response = HTTParty::Request.new("Net::HTTP::#{method.to_s.capitalize}".constantize, url, body: body, headers: headers, timeout: timeout).perform
456
+ Rails.logger.debug("Response Code: #{response.code}") if debug
457
+ begin
458
+ output_json = JSON.parse(response.body)
459
+ rescue JSON::ParserError => ex
460
+ output_json = {}
461
+ end
462
+ Rails.logger.debug("Response JSON: #{output_json}") if debug && output_json.present?
463
+
464
+ raise_errors(type: :JSON, body: output_json, response: response)
465
+ rescue ZuoraAPI::Exceptions::ZuoraAPIAuthenticationTypeError => ex
466
+ if self.class.to_s == 'ZuoraAPI::Oauth'
467
+ self.rest_call(method: method.to_sym, url: url, debug: debug, errors: errors, z_session: z_session, session_type: :bearer, timeout_retry: timeout_retry, timeout: timeout)
468
+ else
469
+ Rails.logger.debug("Rest Call - Session Bad Auth type")
470
+ raise ex
471
+ end
472
+ rescue ZuoraAPI::Exceptions::ZuoraAPISessionError => ex
473
+ if !(tries -= 1).zero? && z_session
474
+ Rails.logger.debug("Rest Call - Session Invalid #{session_type}")
475
+ self.new_session(auth_type: session_type)
476
+ retry
477
+ else
478
+ if errors.include?(ex.class)
479
+ raise ex
480
+ else
481
+ return [output_json, response]
482
+ end
483
+ end
484
+ rescue ZuoraAPI::Exceptions::ZuoraAPIError, ZuoraAPI::Exceptions::ZuoraAPIRequestLimit, ZuoraAPI::Exceptions::ZuoraAPILockCompetition => ex
485
+ if errors.include?(ex.class)
486
+ raise ex
487
+ else
488
+ return [output_json, response]
489
+ end
490
+ rescue ZuoraAPI::Exceptions::BadEntityError => ex
491
+ raise ex
492
+ rescue Net::OpenTimeout, Errno::ECONNRESET, OpenSSL::SSL::SSLError, Errno::ECONNREFUSED, SocketError => ex
493
+ if !(tries -= 1).zero? && timeout_retry
494
+ Rails.logger.info("Rest Call - #{ex.class} Timed out will retry after 5 seconds")
495
+ sleep(self.timeout_sleep)
496
+ retry
497
+ else
498
+ raise ex
499
+ end
500
+ rescue => ex
501
+ raise ex
502
+ else
503
+ return [output_json, response]
504
+ end
505
+
506
+ def update_create_tenant
507
+ Rails.logger.debug("Update and/or Create Tenant")
508
+ output_xml, input_xml = soap_call() do |xml|
509
+ xml['api'].getUserInfo
510
+ end
511
+ user_info = output_xml.xpath('//ns1:getUserInfoResponse', 'ns1' =>'http://api.zuora.com/')
512
+ output_hash = Hash[user_info.children.map {|x| [x.name.to_sym, x.text] }]
513
+ self.user_info = output_hash
514
+ self.user_info['entities'] = self.rest_call(:url => self.rest_endpoint("user-access/user-profile/#{self.user_info['UserId']}/accessible-entities"))['entities']
515
+ self.tenant_name = output_hash[:TenantName]
516
+ self.tenant_id = output_hash[:TenantId]
517
+ return self
518
+ end
519
+
520
+ def get_catalog(page_size: 40)
521
+ products, catalog_map, response = [{}, {}, {'nextPage' => self.rest_endpoint("catalog/products?pageSize=#{page_size}") }]
522
+ while !response["nextPage"].blank?
523
+ url = self.rest_endpoint(response["nextPage"].split('/v1/').last)
524
+ Rails.logger.debug("Fetch Catalog URL #{url}")
525
+ output_json, response = self.rest_call(:debug => false, :url => url, :errors => [ZuoraAPI::Exceptions::ZuoraAPISessionError], :timeout_retry => true )
526
+ if !output_json['success'] =~ (/(true|t|yes|y|1)$/i) || output_json['success'].class != TrueClass
527
+ raise ZuoraAPI::Exceptions::ZuoraAPIError.new("Error Getting Catalog: #{output_json}")
528
+ end
529
+ output_json["products"].each do |product|
530
+ catalog_map[product["id"]] = {"productId" => product["id"]}
531
+ rateplans = {}
532
+
533
+ product["productRatePlans"].each do |rateplan|
534
+ catalog_map[rateplan["id"]] = {"productId" => product["id"], "productRatePlanId" => rateplan["id"]}
535
+ charges = {}
536
+
537
+ rateplan["productRatePlanCharges"].each do |charge|
538
+ catalog_map[charge["id"]] = {"productId" => product["id"], "productRatePlanId" => rateplan["id"], "productRatePlanChargeId" => charge["id"]}
539
+ charges[charge["id"]] = charge.merge({"productId" => product["id"], "productName" => product["name"], "productRatePlanId" => rateplan["id"], "productRatePlanName" => rateplan["name"] })
540
+ end
541
+
542
+ rateplan["productRatePlanCharges"] = charges
543
+ rateplans[rateplan["id"]] = rateplan.merge({"productId" => product["id"], "productName" => product["name"]})
544
+ end
545
+ product["productRatePlans"] = rateplans
546
+ products[product['id']] = product
547
+ end
548
+ end
549
+ return products, catalog_map
550
+ end
551
+
552
+ def get_file(url: nil, headers: {}, count: 3, z_session: true, tempfile: true, file_path: defined?(Rails.root.join('tmp')) ? Rails.root.join('tmp') : Pathname.new(Dir.pwd), timeout_retries: 2, timeout: 120, **execute_params)
553
+ raise "file_path must be of class Pathname" if file_path.class != Pathname
554
+
555
+ #Make sure directory exists
556
+ require 'fileutils'
557
+ FileUtils.mkdir_p(file_path) unless File.exists?(file_path)
558
+
559
+ begin
560
+ status_code = nil
561
+ uri = URI.parse(url)
562
+ http = Net::HTTP.new(uri.host, uri.port)
563
+ http.read_timeout = timeout #Seconds
564
+ http.use_ssl = true if uri.scheme.downcase == 'https'
565
+ headers = headers.merge({"Authorization" => self.get_session(prefix: true)}) if z_session
566
+
567
+ response_save = nil
568
+ http.request_get(uri.path, headers) do |response|
569
+ response_save = response
570
+ status_code = response.code if response
571
+
572
+ case response
573
+ when Net::HTTPNotFound
574
+ Rails.logger.fatal("404 - Not Found")
575
+ raise response
576
+
577
+ when Net::HTTPUnauthorized
578
+ raise ZuoraAPI::Exceptions::ZuoraAPISessionError.new(self.current_error) if count <= 0
579
+ Rails.logger.fatal("Unauthorized: Retry")
580
+ self.new_session
581
+ return get_file(:url => url, :headers => headers, :count => count - 1, :z_session => z_session, :tempfile => tempfile, :file_path => file_path, :timeout_retries => timeout_retries, :timeout => timeout)
582
+
583
+ when Net::ReadTimeout, Net::OpenTimeout, Errno::EPIPE, Errno::ECONNRESET, OpenSSL::SSL::SSLError, Errno::ECONNREFUSED, SocketError
584
+ Rails.logger.fatal("#{response.class} timeout - retry")
585
+ return get_file(:url => url, :headers => headers, :count => count, :z_session => z_session, :tempfile => tempfile, :file_path => file_path, :timeout_retries => timeout_retries - 1, :timeout => timeout)
586
+
587
+ when Net::HTTPClientError
588
+ Rails.logger.fatal("Login: #{self.username} Export")
589
+ raise response
590
+
591
+ when Net::HTTPOK
592
+ headers = {}
593
+ response.each_header do |k,v|
594
+ headers[k] = v
595
+ end
596
+ Rails.logger.debug("Headers: #{headers.to_s}")
597
+
598
+ size, export_progress = [0, 0]
599
+ encoding, type, full_filename = [nil, nil, nil]
600
+ if response.header["Content-Disposition"].present?
601
+ case response.header["Content-Disposition"]
602
+ when /.*; filename\*=.*/
603
+ full_filename = /.*; filename\*=(.*)''(.*)/.match(response.header["Content-Disposition"])[2].strip
604
+ encoding = /.*; filename\*=(.*)''(.*)/.match(response.header["Content-Disposition"])[1].strip
605
+ when /.*; filename=/
606
+ full_filename = /.*; filename=(.*)/.match(response.header["Content-Disposition"])[1].strip
607
+ end
608
+ file_ending = ".#{full_filename.partition(".").last}"
609
+ end
610
+ #If user supplied a filename use it, else default to content header filename, else default to uri pattern
611
+ filename = full_filename.present? ? full_filename.split(file_ending).first : File.basename(uri.path).rpartition('.').first
612
+
613
+ if response.header["Content-Type"].present?
614
+ case response.header["Content-Type"]
615
+ when /.*;charset=.*/
616
+ type = /(.*);charset=(.*)/.match(response.header["Content-Type"])[1]
617
+ encoding = /(.*);charset=(.*)/.match(response.header["Content-Type"])[2]
618
+ else
619
+ type = response.header["Content-Type"]
620
+ encoding ||= 'UTF-8'
621
+ end
622
+ end
623
+ Rails.logger.info("File: #{filename}#{file_ending} #{encoding} #{type}")
624
+
625
+ if response.header["Content-Length"].present?
626
+ export_size = response.header["Content-Length"].to_i
627
+ elsif response.header["ContentLength"].present?
628
+ export_size = response.header["ContentLength"].to_i
629
+ end
630
+
631
+ file_handle = nil
632
+ timestamp = Time.now.to_i
633
+ if tempfile
634
+ require 'tempfile'
635
+ file_handle = ::Tempfile.new(["#{filename}_#{timestamp}", "#{file_ending}"], file_path)
636
+ file_handle.binmode
637
+ else
638
+ file_handle = File.new(file_path.join("#{filename}_#{timestamp}#{file_ending}"), "w+")
639
+ file_handle.binmode
640
+ end
641
+
642
+ response.read_body do |chunk|
643
+ file_handle << chunk.force_encoding(encoding)
644
+
645
+ if defined?(export_size) && export_size != 0 && export_size.class == Integer
646
+ size += chunk.size
647
+ new_progress = (size * 100) / export_size
648
+ unless new_progress == export_progress
649
+ Rails.logger.debug("Login: #{self.username} Export Downloading %s (%3d%%)" % [filename, new_progress])
650
+ end
651
+ export_progress = new_progress
652
+ end
653
+ end
654
+
655
+ file_handle.close
656
+ Rails.logger.debug("Filepath: #{file_handle.path} Size: #{File.size(file_handle.path).to_f/1000000} mb")
657
+
658
+ return file_handle
659
+ end
660
+ end
661
+ rescue Exception => e
662
+ Rails.logger.fatal("Download Failed: #{response_save} - #{e.class} : #{e.message}")
663
+ Rails.logger.fatal("Download Failed: #{e.backtrace.join("\n")}")
664
+ raise
665
+ end
666
+ end
667
+
668
+ def getDataSourceExport(query, extract: true, encrypted: false, zip: true)
669
+ Rails.logger.debug("Build export")
670
+ Rails.logger.debug("#{query}")
671
+ request = Nokogiri::XML::Builder.new do |xml|
672
+ xml['SOAP-ENV'].Envelope('xmlns:SOAP-ENV' => "http://schemas.xmlsoap.org/soap/envelope/", 'xmlns:ns2' => "http://object.api.zuora.com/", 'xmlns:xsi' => "http://www.w3.org/2001/XMLSchema-instance", 'xmlns:ns1' => "http://api.zuora.com/") do
673
+ xml['SOAP-ENV'].Header do
674
+ xml['ns1'].SessionHeader do
675
+ xml['ns1'].session self.get_session(prefix: false, auth_type: :basic)
676
+ end
677
+ end
678
+ xml['SOAP-ENV'].Body do
679
+ xml['ns1'].create do
680
+ xml['ns1'].zObjects('xsi:type' => "ns2:Export") do
681
+ xml['ns2'].Format 'csv'
682
+ xml['ns2'].Zip zip
683
+ xml['ns2'].Name 'googman'
684
+ xml['ns2'].Query query
685
+ xml['ns2'].Encrypted encrypted
686
+ end
687
+ end
688
+ end
689
+ end
690
+ end
691
+
692
+ response_query = HTTParty.post(self.url, body: request.to_xml(:save_with => XML_SAVE_OPTIONS).strip, headers: {'Content-Type' => "application/json; charset=utf-8"}, :timeout => 120)
693
+
694
+ output_xml = Nokogiri::XML(response_query.body)
695
+ raise 'Export Creation Unsuccessful : ' + output_xml.xpath('//ns1:Message', 'ns1' =>'http://api.zuora.com/').text if output_xml.xpath('//ns1:Success', 'ns1' =>'http://api.zuora.com/').text != "true"
696
+ id = output_xml.xpath('//ns1:Id', 'ns1' =>'http://api.zuora.com/').text
697
+
698
+ confirmRequest = Nokogiri::XML::Builder.new do |xml|
699
+ xml['SOAP-ENV'].Envelope('xmlns:SOAP-ENV' => "http://schemas.xmlsoap.org/soap/envelope/", 'xmlns:ns2' => "http://object.api.zuora.com/", 'xmlns:xsi' => "http://www.w3.org/2001/XMLSchema-instance", 'xmlns:ns1' => "http://api.zuora.com/") do
700
+ xml['SOAP-ENV'].Header do
701
+ xml['ns1'].SessionHeader do
702
+ xml['ns1'].session self.get_session(prefix: false, auth_type: :basic)
703
+ end
704
+ end
705
+ xml['SOAP-ENV'].Body do
706
+ xml['ns1'].query do
707
+ xml['ns1'].queryString "SELECT Id, CreatedById, CreatedDate, Encrypted, FileId, Format, Name, Query, Size, Status, StatusReason, UpdatedById, UpdatedDate, Zip From Export where Id = '#{id}'"
708
+ end
709
+ end
710
+ end
711
+ end
712
+ result = 'Waiting'
713
+
714
+ while result != "Completed"
715
+ sleep 3
716
+ response_query = HTTParty.post(self.url, body: confirmRequest.to_xml(:save_with => XML_SAVE_OPTIONS).strip, headers: {'Content-Type' => "application/json; charset=utf-8"}, :timeout => 120)
717
+
718
+ output_xml = Nokogiri::XML(response_query.body)
719
+ result = output_xml.xpath('//ns2:Status', 'ns2' =>'http://object.api.zuora.com/').text
720
+ status_code = response_query.code if response_query
721
+ raise "Export Creation Unsuccessful : #{output_xml.xpath('//ns1:Message', 'ns1' =>'http://api.zuora.com/').text}" if result.blank? || result == "Failed"
722
+ end
723
+
724
+ file_id = output_xml.xpath('//ns2:FileId', 'ns2' =>'http://object.api.zuora.com/').text
725
+ Rails.logger.debug('=====> Export finished')
726
+ export_file = get_file(:url => self.fileURL(file_id))
727
+ export_file_path = export_file.path
728
+ Rails.logger.debug("=====> Export path #{export_file.path}")
729
+
730
+ if extract && zip
731
+ require "zip"
732
+ new_path = export_file_path.partition('.zip').first
733
+ zipped = Zip::File.open(export_file_path)
734
+ file_handle = zipped.entries.first
735
+ file_handle.extract(new_path)
736
+ File.delete(export_file_path)
737
+ return new_path
738
+ else
739
+ return export_file_path
740
+ end
741
+ end
742
+
743
+ def query(query, parse = false)
744
+ Rails.logger.debug("Querying Zuora for #{query}")
745
+ output_xml, input_xml = self.soap_call({:debug => false, :timeout_retry => true}) do |xml|
746
+ xml['ns1'].query do
747
+ xml['ns1'].queryString query
748
+ end
749
+ end
750
+ if parse
751
+ return [] if output_xml.xpath('//ns1:size', 'ns1' =>'http://api.zuora.com/').text == '0'
752
+ data = output_xml.xpath('//ns1:records', 'ns1' =>'http://api.zuora.com/').map {|record| record.children.map {|element| [element.name, element.text]}.to_h}
753
+ return data
754
+ else
755
+ return output_xml
756
+ end
757
+ end
758
+
759
+ def createJournalRun(call)
760
+ url = rest_endpoint("/journal-runs")
761
+ uri = URI(url)
762
+ req = Net::HTTP::Post.new(uri,initheader = {'Content-Type' =>'application/json'})
763
+ req["Authorization"] = self.get_session(prefix: true)
764
+ req.body = call
765
+
766
+ response = Net::HTTP.start(uri.host, uri.port, :use_ssl => true) do |http|
767
+ http.request req
768
+ end
769
+
770
+ Rails.logger.debug("Response #{response.code} #{response.message}: #{response.body}")
771
+
772
+ result = JSON.parse(response.body)
773
+ if result["success"]
774
+ jrNumber = result["journalRunNumber"]
775
+ return jrNumber
776
+ else
777
+ message = result["reasons"][0]["message"]
778
+ Rails.logger.error("Journal Run failed with message #{message}")
779
+ return result
780
+ end
781
+
782
+ end
783
+
784
+ def checkJRStatus(jrNumber)
785
+ Rails.logger.info("Check for completion")
786
+ url = rest_endpoint("/journal-runs/#{jrNumber}")
787
+ uri = URI(url)
788
+ req = Net::HTTP::Get.new(uri,initheader = {'Content-Type' =>'application/json'})
789
+ req["Authorization"] = self.get_session(prefix: true)
790
+
791
+ response = Net::HTTP.start(uri.host, uri.port, :use_ssl => true) do |http|
792
+ http.request req
793
+ end
794
+
795
+ result = JSON.parse(response.body)
796
+ if result["success"]
797
+ if !(result["status"].eql? "Completed")
798
+ sleep(20.seconds)
799
+ end
800
+ return result["status"]
801
+ else
802
+ message = result["reasons"][0]["message"]
803
+ Rails.logger.info("Checking status of journal run failed with message #{message}")
804
+ end
805
+ return "failure"
806
+ end
807
+ end
808
+ end