zuora_api_oauth_alpha 2

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