zuora_apiD 1.6.08

Sign up to get free protection for your applications and to get access to all the features.
data/lib/zuora_apiD.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'rails'
2
+ require "zuora_apiD/login"
3
+ require "zuora_apiD/logins/basic"
4
+ require "zuora_apiD/logins/oauth"
5
+ require 'zuora_apiD/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