require "httparty" require "nokogiri" module ZuoraAPI class Login ENVIRONMENTS = [SANDBOX = 'Sandbox', PRODUCTION = 'Production', PREFORMANCE = 'Preformance', SERVICES = 'Services', UNKNOWN = 'Unknown' ] REGIONS = [EU = 'EU', US = 'US' ] 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 def initialize(username: nil, password: nil, status: nil, url: nil, entity_id: nil, session: nil, **keyword_args) @username = username @password = password @url = url @entity_id = entity_id @current_session = session @errors = Hash.new @status = status.blank? ? "Active" : status @user_info = Hash.new self.update_environment @timeout_sleep = 5 end def self.environments %w(Sandbox Production Services Performance) end def self.regions %w(US EU) end def self.endpoints return {"US" => {"Sandbox" => "https://apisandbox.zuora.com/apps/services/a/", "Production" => "https://www.zuora.com/apps/services/a/", "Performance" => "https://pt1.zuora.com/apps/services/a/", "Services" => "https://services347.zuora.com/apps/services/a/"}, "EU" => {"Sandbox" => "https://sandbox.eu.zuora.com/apps/services/a/", "Production" => "https://eu.zuora.com/apps/services/a/", "Performance" => "https://pt1.eu.zuora.com/apps/services/a/", "Services" => "https://services347.eu.zuora.com/apps/services/a/"} } end def update_environment if !self.url.blank? env_path = self.url.split('https://').last.split('.zuora.com').first self.region = self.url.include?("eu.") ? "EU" : "US" if env_path == 'apisandbox' || self.url.include?('sandbox') self.environment = 'Sandbox' 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.environment = 'Production' elsif env_path.include?('service') || env_path.include?('ep-edge') self.environment = 'Services' elsif env_path.include?('pt') self.environment = 'Performance' elsif env_path.include?('app-0') self.environment = 'Staging' else self.environment = 'Unknown' end end end def aqua_endpoint(url="") return "#{self.url.split("/apps").first}/apps/api/".concat(url) end def rest_endpoint(url="") if self.environment == 'Sandbox' return self.region == "US" ? "https://rest.apisandbox.zuora.com/v1/".concat(url) : "https://rest.sandbox.eu.zuora.com/v1/".concat(url) elsif self.environment == 'Production' return self.region == "US" ? "https://rest.zuora.com/v1/".concat(url) : "https://rest.eu.zuora.com/v1/".concat(url) elsif self.environment == 'Services' return self.url.split('/')[0..2].join('/').concat('/apps/v1/').concat(url) elsif self.environment == 'Performance' return self.url.split('/')[0..2].join('/').concat('/apps/v1/').concat(url) else self.environment == 'Unknown' return url end end def fileURL(url="") return self.url.split(".com").first.concat(".com/apps/api/file/").concat(url) end def dateFormat return self.wsdl_number > 68 ? '%Y-%m-%d' : '%Y-%m-%dT%H:%M:%S' end def new_session(debug: false) tries ||= 2 request = Nokogiri::XML::Builder.new do |xml| xml['SOAP-ENV'].Envelope('xmlns:SOAP-ENV' =>"http://schemas.xmlsoap.org/soap/envelope/", 'xmlns:api' => "http://api.zuora.com/" ) do if (self.password.blank? && !self.current_session.blank?) Rails.logger.debug("Method [Session]") xml['SOAP-ENV'].Header do xml['api'].SessionHeader do xml['api'].session self.current_session end end xml['SOAP-ENV'].Body do xml['api'].getUserInfo end else xml['SOAP-ENV'].Header xml['SOAP-ENV'].Body do xml['api'].login do xml['api'].username self.username xml['api'].password self.password xml['api'].entityId self.entity_id if !self.entity_id.blank? end end end end end input_xml = Nokogiri::XML(request.to_xml(:save_with => Nokogiri::XML::Node::SaveOptions::AS_XML | Nokogiri::XML::Node::SaveOptions::NO_DECLARATION).strip) input_xml.xpath('//ns1:session', 'ns1' =>'http://api.zuora.com/').children.remove 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 @response_query = HTTParty.post(self.url,:body => request.to_xml, :headers => {'Content-Type' => "text/xml; charset=utf-8"}, :timeout => 10) @output_xml = Nokogiri::XML(@response_query.body) 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 if !@response_query.success? self.current_session = nil if @output_xml.namespaces.size > 0 && @output_xml.xpath('//soapenv:Fault').size > 0 self.current_error = @output_xml.xpath('//fns:FaultMessage', 'fns' =>'http://fault.api.zuora.com/').text if self.current_error.include?('deactivated') self.status = 'Deactivated' self.current_error = 'Deactivated user login, please check with Zuora tenant administrator' self.errors[:username] = self.current_error elsif self.current_error.include?('inactive') self.status = 'Inactive' self.current_error = 'Inactive user login, please check with Zuora tenant administrator' self.errors[:username] = self.current_error elsif self.current_error.include?("invalid username or password") || self.current_error.include?("Invalid login. User name and password do not match.") self.status = 'Invalid Login' self.current_error = 'Invalid login, please check username and password or URL endpoint' self.errors[:username] = self.current_error self.errors[:password] = self.current_error elsif self.current_error.include?('unsupported version') self.status = 'Unsupported API Version' self.current_error = 'Unsupported API version, please verify URL endpoint' self.errors[:url] = self.current_error elsif self.current_error.include?('invalid api version') self.status = 'Invalid API Version' self.current_error = 'Invalid API version, please verify URL endpoint' self.errors[:url] = self.current_error elsif self.current_error.include?('invalid session') self.status = 'Invalid Session' self.current_error = 'Session invalid, please update session and verify URL endpoint' self.errors[:session] = self.current_error elsif self.current_error.include?('Your IP address') self.status = 'Restricted IP' self.current_error = 'IP restricted, contact Zuora tenant administrator and remove IP restriction' self.errors[:base] = self.current_error elsif self.current_error.include?('This account has been locked') self.status = 'Locked' self.current_error = 'Locked user login, please wait or navigate to Zuora to unlock user' self.errors[:username] = self.current_error else self.status = 'Unknown' self.current_error = @output_xml.xpath('//faultstring').text if self.current_error.blank? self.errors[:base] = self.current_error end else self.current_error = "Code = #{@response_query.code} Message = #{@response_query.body.to_s}" self.status = 'No Service' end else #If Session only is used for Gem TODO Depercate if (self.password.blank? && self.current_session.present?) self.current_session = self.current_session self.username = @output_xml.xpath('//ns1:Username', 'ns1' =>'http://api.zuora.com/').text if self.username.blank? #Username & password combo elsif (self.password.present? && self.username.present?) retrieved_session = @output_xml.xpath('//ns1:Session', 'ns1' =>'http://api.zuora.com/').text raise ZuoraAPI::Exceptions::ZuoraAPISessionError.new("No session found for api call.") if retrieved_session.blank? self.current_session = retrieved_session end self.current_error = nil self.status = 'Active' end return self.status rescue Net::ReadTimeout, Net::OpenTimeout, Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNREFUSED, SocketError => ex if !(tries -= 1).zero? Rails.logger.info {"#{ex.class} Timed out will retry after 5 seconds"} sleep(self.timeout_sleep) retry else self.current_error = "Request timed out. Try again" self.status = 'Timeout' return self.status end end def get_session Rails.logger.debug("Create new session") if self.current_session.blank? self.new_session if self.current_session.blank? raise ZuoraAPI::Exceptions::ZuoraAPISessionError.new(self.current_error) if self.status != 'Active' return self.current_session end 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) tries ||= 2 xml = Nokogiri::XML::Builder.new do |xml| 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:api' => "http://api.zuora.com/", "xmlns:#{ns1}" => "http://api.zuora.com/") do xml['SOAP-ENV'].Header do xml["#{ns1}"].SessionHeader do xml["#{ns1}"].session self.get_session end if single_transaction xml["#{ns1}"].CallOptions do xml.useSingleTransaction single_transaction end end if batch_size xml["#{ns1}"].QueryOptions do xml.batchSize batch_size end end end xml['SOAP-ENV'].Body do yield xml, keyword_args end end end input_xml = Nokogiri::XML(xml.to_xml(:save_with => Nokogiri::XML::Node::SaveOptions::AS_XML | Nokogiri::XML::Node::SaveOptions::NO_DECLARATION).strip) input_xml.xpath('//ns1:session', 'ns1' =>'http://api.zuora.com/').children.remove 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 response = HTTParty.post(self.url,:body => xml.doc.to_xml, :headers => {'Content-Type' => "text/xml; charset=utf-8"}, :timeout => timeout) output_xml = Nokogiri::XML(response.body) 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 raise_errors(type: :SOAP, body: output_xml, response: response) rescue ZuoraAPI::Exceptions::ZuoraAPISessionError => ex if !(tries -= 1).zero? && z_session Rails.logger.debug {"Session Invalid"} self.new_session retry else if errors.include?(ex.class) raise ex else return [output_xml, input_xml] end end rescue ZuoraAPI::Exceptions::ZuoraAPIError, ZuoraAPI::Exceptions::ZuoraAPIRequestLimit, ZuoraAPI::Exceptions::ZuoraAPILockCompetition => ex if errors.include?(ex.class) raise ex else return [output_xml, input_xml] end rescue Net::OpenTimeout, Errno::ECONNRESET, Errno::ECONNREFUSED, SocketError => ex if !(tries -= 1).zero? && timeout_retry Rails.logger.info {"#{ex.class} Timed out will retry after 5 seconds"} sleep(self.timeout_sleep) retry else raise ex end rescue => ex raise ex else return [output_xml, input_xml] end def raise_errors(type: :SOAP, body: nil, response: nil) case type when :SOAP error = body.xpath('//fns:FaultCode', 'fns' =>'http://fault.api.zuora.com/').text message = body.xpath('//fns:FaultMessage', 'fns' =>'http://fault.api.zuora.com/').text if error.blank? || message.blank? error = body.xpath('//ns1:Code', 'ns1' =>'http://api.zuora.com/').text message = body.xpath('//ns1:Message', 'ns1' =>'http://api.zuora.com/').text end if error.blank? || message.blank? error = body.xpath('//faultcode').text message = body.xpath('//faultstring').text end if error.present? if error == "INVALID_SESSION" raise ZuoraAPI::Exceptions::ZuoraAPISessionError.new("#{error}::#{message}") end if error == "REQUEST_EXCEEDED_LIMIT" raise ZuoraAPI::Exceptions::ZuoraAPIRequestLimit.new("#{error}::#{message}") end if error == "LOCK_COMPETITION" raise ZuoraAPI::Exceptions::ZuoraAPILockCompetition.new("#{error}::#{message}") end if error.present? raise ZuoraAPI::Exceptions::ZuoraAPIError.new("#{error}::#{message}") end end when :JSON if body.class == Hash && (!body["success"] || !body["Success"] || response.code != 200) messages_array = (body["reasons"] || []).map {|error| error['message']}.compact codes_array = (body["reasons"] || []).map {|error| error['code']}.compact #Authentication failed if codes_array.map{|code| code.to_s.slice(6,7).to_i}.include?(11) || response.code == 401 raise ZuoraAPI::Exceptions::ZuoraAPISessionError.new("#{messages_array.join(', ')}") end #Zuora REST Create Amendment error #Authentication failed if body["faultcode"].present? && body["faultcode"] == "fns:INVALID_SESSION" raise ZuoraAPI::Exceptions::ZuoraAPISessionError.new("#{body['faultstring']}") end #Request exceeded limit if codes_array.map{|code| code.to_s.slice(6,7).to_i}.include?(70) raise ZuoraAPI::Exceptions::ZuoraAPIRequestLimit.new("#{messages_array.join(', ')}") end #Locking contention if codes_array.map{|code| code.to_s.slice(6,7).to_i}.include?(50) raise ZuoraAPI::Exceptions::ZuoraAPILockCompetition.new("#{messages_array.join(', ')}") end #All Errors catch if codes_array.size > 0 raise ZuoraAPI::Exceptions::ZuoraAPIError.new("#{messages_array.join(', ')}") end #Zuora REST Query Errors if body["faultcode"].present? case body["faultcode"] when "fns:MALFORMED_QUERY" raise ZuoraAPI::Exceptions::ZuoraAPIError.new("#{body["faultcode"]}::#{body["faultstring"]}") when "fns:REQUEST_EXCEEDED_LIMIT" raise ZuoraAPI::Exceptions::ZuoraAPIRequestLimit.new("#{body["faultcode"]}::#{body["faultstring"]}") when "fns:LOCK_COMPETITION" raise ZuoraAPI::Exceptions::ZuoraAPILockCompetition.new("#{body["faultcode"]}::#{body["faultstring"]}") when "INVALID_SESSION" raise ZuoraAPI::Exceptions::ZuoraAPISessionError.new("#{body["faultcode"]}::#{body["faultstring"]}") else raise ZuoraAPI::Exceptions::ZuoraAPIError.new("#{body["faultcode"]}::#{body["faultstring"]}") end end if body["Errors"].present? || body["errors"].present? errors = [] (body["Errors"] || []).select { |obj| errors.push(obj["Message"]) }.compact (body["errors"] || []).select { |obj| errors.push(obj["Message"]) }.compact if errors.size > 0 raise ZuoraAPI::Exceptions::ZuoraAPIError.new("#{errors.join(", ")}", nil, nil, errors) end end end #Zuora REST Actions error (Create, Update, Delete) if body.class == Array all_errors = body.select {|obj| !obj['Success'] || !obj['success'] }.map {|obj| obj['Errors'] || obj['errors'] }.compact all_success = body.select {|obj| obj['Success'] || obj['success']}.compact if all_errors.size > 0 raise ZuoraAPI::Exceptions::ZuoraAPIError.new("#{all_errors.flatten.group_by {|error| error['Message']}.keys.uniq.join(' ')}", nil, nil, all_errors, all_success ) end end #All other errors if response.code != 200 raise ZuoraAPI::Exceptions::ZuoraAPIError.new("#{response.message}") end end end def aqua_query(queryName: '', query: '', version: '1.2', jobName: 'Aqua',partner: '', project: '') params = { "format" => 'csv', "version" => version, "name" => jobName, "encrypted" => 'none', "useQueryLabels" => 'true', "partner" => partner, "project" => project, "queries" => [{ "name" => queryName, "query" => query, "type" => 'zoqlexport' }] } response = self.rest_call(method: :post, body: params.to_json, url: self.aqua_endpoint("batch-query/")) if(response[0]["id"].nil?) raise ZuoraAPI::Exceptions::ZuoraAPIError.new("Error in AQuA Process. #{response}") end return getFileById(id: response[0]["id"]) end def getFileById(id: "2c92c0f85e7f88ff015e86b8f8f4517f") response = nil result = "new" while result != "completed" do sleep(2)#sleep 2 seconds response, fullResponse = self.rest_call(method: :get, body: {}, url: self.aqua_endpoint("batch-query/jobs/#{id}")) result = response["batches"][0]["status"] if result == "error" raise ZuoraAPI::Exceptions::ZuoraAPIError.new("Aqua Error: #{response}") break end end fileId = response["batches"][0]["fileId"] return self.get_file(url: self.aqua_endpoint("file/#{fileId}")) end def describe_call(object = nil) tries ||= 2 self.get_session base = self.url.include?(".com") ? self.url.split(".com")[0].concat(".com") : self.url.split(".eu")[0].concat(".eu") url = object ? "#{base}/apps/api/describe/#{object}" : "#{base}/apps/api/describe/" headers = !self.entity_id.blank? ? {"entityId" => self.entity_id, 'Content-Type' => "text/xml; charset=utf-8"} : {'Content-Type' => "text/xml; charset=utf-8"} response = HTTParty.get(url, :headers => headers , basic_auth: {:username => self.username, :password => self.password}, :timeout => 120) output_xml = Nokogiri::XML(response.body) des_hash = Hash.new if object == nil output_xml.xpath("//object").each do |object| temp = {:label => object.xpath(".//label").text, :url => object.attributes["href"].value } des_hash[object.xpath(".//name").text] = temp end else output_xml.xpath("//field").each do |object| temp = {:label => object.xpath(".//label").text,:selectable => object.xpath(".//selectable").text, :createable => object.xpath(".//label").text == "ID" ? "false" : object.xpath(".//createable").text, :filterable => object.xpath(".//filterable").text, :updateable => object.xpath(".//label").text == "ID" ? "false" : object.xpath(".//updateable").text, :custom => object.xpath(".//custom").text,:maxlength => object.xpath(".//maxlength").text, :required => object.xpath(".//required").text, :type => object.xpath(".//type").text, :context => object.xpath(".//context").collect{ |x| x.text } } temp[:options] = object.xpath(".//option").collect{ |x| x.text } if object.xpath(".//option").size > 0 des_hash[object.xpath(".//name").text.to_sym] = temp des_hash[:fieldsToNull] = {:label => "FieldsToNull",:selectable => "false", :createable => "false",:filterable => "false", :updateable => "true",:custom => "false", :required => "false",:type => "picklist", :maxlength => "" ,:context => ["soap"], :options => des_hash.map {|k,v| k if v[:updateable] == "true" && v[:required] == "false"}.compact.uniq } end 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 end rescue Net::ReadTimeout, Net::OpenTimeout, Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNREFUSED, SocketError => ex if !(tries -= 1).zero? Rails.logger.info {"#{ex.class} Timed out will retry after 5 seconds"} sleep(self.timeout_sleep) retry else raise ex end rescue => ex raise ex else return des_hash end 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, timeout_retry: false, timeout: 120, **keyword_args) tries ||= 2 headers["entityId"] = self.entity_id if !self.entity_id.blank? raise "Method not supported, supported methods include: :get, :post, :put, :delete, :patch, :head, :options" if ![:get, :post, :put, :delete, :patch, :head, :options].include?(method) 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" => "ZSession #{self.get_session}"} : {}).merge(headers), timeout: timeout).perform Rails.logger.debug('Connect') {"Response Code: #{response.code}" } if debug begin output_json = JSON.parse(response.body) rescue JSON::ParserError => ex output_json = {} end Rails.logger.debug('Connect') {"Response JSON: #{output_json}"} if debug && output_json.present? raise_errors(type: :JSON, body: output_json, response: response) rescue ZuoraAPI::Exceptions::ZuoraAPISessionError => ex if !(tries -= 1).zero? && z_session Rails.logger.debug {"Session Invalid"} self.new_session retry else if errors.include?(ex.class) raise ex else return [output_json, response] end end rescue ZuoraAPI::Exceptions::ZuoraAPIError, ZuoraAPI::Exceptions::ZuoraAPIRequestLimit, ZuoraAPI::Exceptions::ZuoraAPILockCompetition => ex if errors.include?(ex.class) raise ex else return [output_json, response] end rescue Net::OpenTimeout, Errno::ECONNRESET, Errno::ECONNREFUSED, SocketError => ex if !(tries -= 1).zero? && timeout_retry Rails.logger.info {"#{ex.class} Timed out will retry after 5 seconds"} sleep(self.timeout_sleep) retry else raise ex end rescue => ex raise ex else return [output_json, response] end def update_create_tenant Rails.logger.debug("Update and/or Create Tenant") output_xml, input_xml = soap_call() do |xml| xml['api'].getUserInfo end user_info = output_xml.xpath('//ns1:getUserInfoResponse', 'ns1' =>'http://api.zuora.com/') output_hash = Hash[user_info.children.map {|x| [x.name.to_sym, x.text] }] self.user_info = output_hash self.user_info['entities'] = self.rest_call(:url => self.rest_endpoint("user-access/user-profile/#{self.user_info['UserId']}/accessible-entities"))['entities'] self.tenant_name = output_hash[:TenantName] self.tenant_id = output_hash[:TenantId] return self end def get_catalog(page_size: 40) products, catalog_map, response = [{}, {}, {'nextPage' => self.rest_endpoint("catalog/products?pageSize=#{page_size}") }] while !response["nextPage"].blank? url = self.rest_endpoint(response["nextPage"].split('/v1/').last) Rails.logger.debug("Fetch Catalog URL #{url}") output_json, response = self.rest_call(:debug => false, :url => url, :errors => [ZuoraAPI::Exceptions::ZuoraAPISessionError], :timeout_retry => true ) if !output_json['success'] =~ (/(true|t|yes|y|1)$/i) || output_json['success'].class != TrueClass raise ZuoraAPI::Exceptions::ZuoraAPIError.new("Error Getting Catalog: #{output_json}") end output_json["products"].each do |product| catalog_map[product["id"]] = {"productId" => product["id"]} rateplans = {} product["productRatePlans"].each do |rateplan| catalog_map[rateplan["id"]] = {"productId" => product["id"], "productRatePlanId" => rateplan["id"]} charges = {} rateplan["productRatePlanCharges"].each do |charge| catalog_map[charge["id"]] = {"productId" => product["id"], "productRatePlanId" => rateplan["id"], "productRatePlanChargeId" => charge["id"]} charges[charge["id"]] = charge.merge({"productId" => product["id"], "productName" => product["name"], "productRatePlanId" => rateplan["id"], "productRatePlanName" => rateplan["name"] }) end rateplan["productRatePlanCharges"] = charges rateplans[rateplan["id"]] = rateplan.merge({"productId" => product["id"], "productName" => product["name"]}) end product["productRatePlans"] = rateplans products[product['id']] = product end end return products, catalog_map end 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) raise "file_path must be of class Pathname" if file_path.class != Pathname #Make sure directory exists require 'fileutils' FileUtils.mkdir_p(file_path) unless File.exists?(file_path) begin uri = URI.parse(url) http = Net::HTTP.new(uri.host, uri.port) http.read_timeout = timeout #Seconds http.use_ssl = true if uri.scheme.downcase == 'https' headers = headers.merge({"Authorization" => "ZSession #{self.get_session}"}) if z_session response_save = nil http.request_get(uri.path, headers) do |response| response_save = response case response when Net::HTTPNotFound Rails.logger.fatal("404 - Not Found") raise response when Net::HTTPUnauthorized raise ZuoraAPI::Exceptions::ZuoraAPISessionError.new(self.current_error) if count <= 0 Rails.logger.fatal("Unauthorized: Retry") self.new_session 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) when Net::ReadTimeout, Net::OpenTimeout, Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNREFUSED, SocketError Rails.logger.fatal("#{response.class} timeout - retry") 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) when Net::HTTPClientError Rails.logger.fatal("Login: #{self.username} Export") raise response when Net::HTTPOK headers = {} response.each_header do |k,v| headers[k] = v end Rails.logger.debug("Headers: #{headers.to_s}") size, export_progress = [0, 0] encoding, type, full_filename = [nil, nil, nil] if response.header["Content-Disposition"].present? case response.header["Content-Disposition"] when /.*; filename\*=.*/ full_filename = /.*; filename\*=(.*)''(.*)/.match(response.header["Content-Disposition"])[2].strip encoding = /.*; filename\*=(.*)''(.*)/.match(response.header["Content-Disposition"])[1].strip when /.*; filename=/ full_filename = /.*; filename=(.*)/.match(response.header["Content-Disposition"])[1].strip end file_ending = ".#{full_filename.partition(".").last}" end #If user supplied a filename use it, else default to content header filename, else default to uri pattern filename = full_filename.present? ? full_filename.split(file_ending).first : File.basename(uri.path).rpartition('.').first if response.header["Content-Type"].present? case response.header["Content-Type"] when /.*;charset=.*/ type = /(.*);charset=(.*)/.match(response.header["Content-Type"])[1] encoding = /(.*);charset=(.*)/.match(response.header["Content-Type"])[2] else type = response.header["Content-Type"] encoding ||= 'UTF-8' end end Rails.logger.info("File: #{filename}#{file_ending} #{encoding} #{type}") if response.header["Content-Length"].present? export_size = response.header["Content-Length"].to_i elsif response.header["ContentLength"].present? export_size = response.header["ContentLength"].to_i end file_handle = nil timestamp = Time.now.to_i if tempfile require 'tempfile' file_handle = ::Tempfile.new(["#{filename}_#{timestamp}", "#{file_ending}"], file_path) file_handle.binmode else file_handle = File.new(file_path.join("#{filename}_#{timestamp}#{file_ending}"), "w+") file_handle.binmode end response.read_body do |chunk| file_handle << chunk.force_encoding(encoding) if defined?(export_size) && export_size != 0 && export_size.class == Integer size += chunk.size new_progress = (size * 100) / export_size unless new_progress == export_progress Rails.logger.debug("Login: #{self.username} Export Downloading %s (%3d%%)" % [filename, new_progress]) end export_progress = new_progress end end file_handle.close Rails.logger.info("Filepath: #{file_handle.path} Size: #{File.size(file_handle.path).to_f/1000000} mb") return file_handle end end rescue Exception => e Rails.logger.fatal('GetFile') {"Download Failed: #{response_save} - #{e.message}"} Rails.logger.fatal('GetFile') {"Download Failed: #{e.backtrace.join("\n")}"} raise end end def getDataSourceExport(query, extract: true, encrypted: false, zip: true) Rails.logger.info('Export') {"Build export"} Rails.logger.debug('Export query') {"#{query}"} request = Nokogiri::XML::Builder.new do |xml| 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 xml['SOAP-ENV'].Header do xml['ns1'].SessionHeader do xml['ns1'].session self.current_session end end xml['SOAP-ENV'].Body do xml['ns1'].create do xml['ns1'].zObjects('xsi:type' => "ns2:Export") do xml['ns2'].Format 'csv' xml['ns2'].Zip zip xml['ns2'].Name 'googman' xml['ns2'].Query query xml['ns2'].Encrypted encrypted end end end end end response_query = HTTParty.post(self.url, body: request.to_xml, headers: {'Content-Type' => "application/json; charset=utf-8"}, :timeout => 120) output_xml = Nokogiri::XML(response_query.body) 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" id = output_xml.xpath('//ns1:Id', 'ns1' =>'http://api.zuora.com/').text confirmRequest = Nokogiri::XML::Builder.new do |xml| 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 xml['SOAP-ENV'].Header do xml['ns1'].SessionHeader do xml['ns1'].session self.current_session end end xml['SOAP-ENV'].Body do xml['ns1'].query do xml['ns1'].queryString "SELECT Id, CreatedById, CreatedDate, Encrypted, FileId, Format, Name, Query, Size, Status, StatusReason, UpdatedById, UpdatedDate, Zip From Export where Id = '#{id}'" end end end end result = 'Waiting' while result != "Completed" sleep 3 response_query = HTTParty.post(self.url, body: confirmRequest.to_xml, headers: {'Content-Type' => "application/json; charset=utf-8"}, :timeout => 120) output_xml = Nokogiri::XML(response_query.body) result = output_xml.xpath('//ns2:Status', 'ns2' =>'http://object.api.zuora.com/').text raise "Export Creation Unsuccessful : #{output_xml.xpath('//ns1:Message', 'ns1' =>'http://api.zuora.com/').text}" if result.blank? || result == "Failed" end file_id = output_xml.xpath('//ns2:FileId', 'ns2' =>'http://object.api.zuora.com/').text Rails.logger.info('Export') {'=====> Export finished'} export_file = get_file(:url => self.fileURL(file_id)) export_file_path = export_file.path Rails.logger.info('Export') {"=====> Export path #{export_file.path}"} if extract && zip require "zip" new_path = export_file_path.partition('.zip').first zipped = Zip::File.open(export_file_path) file_handle = zipped.entries.first file_handle.extract(new_path) File.delete(export_file_path) return new_path else return export_file_path end end def query(query, parse = false) Rails.logger.debug('query') {"Querying Zuora for #{query}"} output_xml, input_xml = self.soap_call({:debug => false, :timeout_retry => true}) do |xml| xml['ns1'].query do xml['ns1'].queryString query end end if parse return [] if output_xml.xpath('//ns1:size', 'ns1' =>'http://api.zuora.com/').text == '0' data = output_xml.xpath('//ns1:records', 'ns1' =>'http://api.zuora.com/').map {|record| record.children.map {|element| [element.name, element.text]}.to_h} return data else return output_xml end end def createJournalRun(call) url = rest_endpoint("/journal-runs") uri = URI(url) req = Net::HTTP::Post.new(uri,initheader = {'Content-Type' =>'application/json'}) req.basic_auth self.username, self.password req.body = call response = Net::HTTP.start(uri.host, uri.port, :use_ssl => true) do |http| http.request req end Rails.logger.debug('Journal Run') {"Response #{response.code} #{response.message}: #{response.body}"} result = JSON.parse(response.body) if result["success"] jrNumber = result["journalRunNumber"] return jrNumber else message = result["reasons"][0]["message"] Rails.logger.debug('Journal Run') {"Journal Run failed with message #{message}"} return result end end def checkJRStatus(jrNumber) Rails.logger.info('Journal Run') {"Check for completion"} url = rest_endpoint("/journal-runs/#{jrNumber}") uri = URI(url) req = Net::HTTP::Get.new(uri,initheader = {'Content-Type' =>'application/json'}) req.basic_auth self.username, self.password response = Net::HTTP.start(uri.host, uri.port, :use_ssl => true) do |http| http.request req end result = JSON.parse(response.body) if result["success"] if !(result["status"].eql? "Completed") sleep(20.seconds) end return result["status"] else message = result["reasons"][0]["message"] Rails.logger.info('Journal Run') {"Checking status of journal run failed with message #{message}"} end return "failure" end end end