require "httparty" require "nokogiri" require "uri" require 'zuora_api/exceptions' module ZuoraAPI class Login ENVIRONMENTS = [SANDBOX = 'Sandbox', PRODUCTION = 'Production', PREFORMANCE = 'Preformance', SERVICES = 'Services', UNKNOWN = 'Unknown', STAGING = 'Staging' ] REGIONS = [EU = 'EU', US = 'US', NA = 'NA' ] MIN_Endpoint = '96.0' XML_SAVE_OPTIONS = Nokogiri::XML::Node::SaveOptions::AS_XML | Nokogiri::XML::Node::SaveOptions::NO_DECLARATION CONNECTION_EXCEPTIONS = [ Net::OpenTimeout, OpenSSL::SSL::SSLError, Errno::ECONNREFUSED, SocketError, Errno::EHOSTUNREACH, Errno::EADDRNOTAVAIL, Errno::ETIMEDOUT, ].freeze CONNECTION_READ_EXCEPTIONS = [ Net::ReadTimeout, Errno::ECONNRESET, Errno::EPIPE ].freeze ZUORA_API_ERRORS = [ ZuoraAPI::Exceptions::ZuoraAPIError, ZuoraAPI::Exceptions::ZuoraAPIRequestLimit, ZuoraAPI::Exceptions::ZuoraAPILockCompetition, ZuoraAPI::Exceptions::ZuoraAPITemporaryError, ZuoraAPI::Exceptions::ZuoraDataIntegrity, ZuoraAPI::Exceptions::ZuoraAPIInternalServerError, ZuoraAPI::Exceptions::ZuoraUnexpectedError, ZuoraAPI::Exceptions::ZuoraAPIUnkownError ].freeze ZUORA_SERVER_ERRORS = [ ZuoraAPI::Exceptions::ZuoraAPIInternalServerError, ZuoraAPI::Exceptions::ZuoraAPIConnectionTimeout, ZuoraAPI::Exceptions::ZuoraAPIReadTimeout, ].freeze attr_accessor :region, :url, :wsdl_number, :current_session, :bearer_token, :oauth_session_expires_at, :environment, :status, :errors, :current_error, :user_info, :tenant_id, :tenant_name, :entity_id, :timeout_sleep, :hostname, :zconnect_provider def initialize(url: nil, entity_id: nil, session: nil, status: nil, bearer_token: nil, oauth_session_expires_at: nil, **keyword_args) raise "URL is nil or empty, but URL is required" if url.nil? || url.empty? # raise "URL is improper. URL must contain zuora.com, zuora.eu, or zuora.na" if /zuora.com|zuora.eu|zuora.na/ === url self.hostname = /(?<=https:\/\/|http:\/\/)(.*?)(?=\/|$)/.match(url)[0] if !/(?<=https:\/\/|http:\/\/)(.*?)(?=\/|$)/.match(url).nil? if !/apps\/services\/a\/\d+\.\d$/.match(url.strip) self.url = "https://#{hostname}/apps/services/a/#{MIN_Endpoint}" elsif MIN_Endpoint.to_f > url.scan(/(\d+\.\d)$/).dig(0,0).to_f self.url = url.gsub(/(\d+\.\d)$/, MIN_Endpoint) else self.url = url end self.entity_id = get_entity_id(entity_id: entity_id) self.errors = Hash.new self.current_session = session self.bearer_token = bearer_token self.oauth_session_expires_at = oauth_session_expires_at self.status = status.blank? ? "Active" : status self.user_info = Hash.new self.update_region self.update_environment self.update_zconnect_provider @timeout_sleep = 5 end def get_identity(cookies) zsession = cookies["ZSession"] zconnect_accesstoken = get_zconnect_accesstoken(cookies) begin if !zsession.blank? response = HTTParty.get("https://#{self.hostname}/apps/v1/identity", :headers => {'Cookie' => "ZSession=#{zsession}", 'Content-Type' => 'application/json'}) output_json = JSON.parse(response.body) elsif zconnect_accesstoken.present? begin code = zconnect_accesstoken.split("#!").last encrypted_token, tenant_id = Base64.decode64(code).split(":") body = {'token' => encrypted_token}.to_json rescue => ex raise ZuoraAPI::Exceptions::ZuoraAPIError.new("Invalid ZConnect Cookie") end Rails.logger.info("Using ZConnect cookie in get_identity method") response = HTTParty.post("https://#{self.hostname}/apps/zconnectsession/identity", :body => body, :headers => { 'Content-Type' => 'application/json' }) output_json = JSON.parse(response.body) else if zconnect_accesstoken.blank? && cookies.keys.any? { |x| x.include? "ZConnect"} raise ZuoraAPI::Exceptions::ZuoraAPIError.new("No ZConnect cookie present matching #{self.hostname}") else raise ZuoraAPI::Exceptions::ZuoraAPIError.new("No ZSession cookie present") end end rescue JSON::ParserError => ex output_json = {} end raise_errors(type: :JSON, body: output_json, response: response) return output_json end def get_full_nav(cookies) zsession = cookies["ZSession"] zconnect_accesstoken = get_zconnect_accesstoken(cookies) begin if zsession.present? response = HTTParty.get("https://#{self.hostname}/apps/v1/navigation", :headers => {'Cookie' => "ZSession=#{zsession}", 'Content-Type' => 'application/json'}) output_json = JSON.parse(response.body) elsif zconnect_accesstoken.present? Rails.logger.info("Using ZConnect cookie in get_full_nav method") response = HTTParty.get("https://#{self.hostname}/apps/zconnectsession/navigation", :headers => {'Cookie' => "#{self.zconnect_provider}=#{zconnect_accesstoken}",'Content-Type' => 'application/json'}) output_json = JSON.parse(response.body) else if zconnect_accesstoken.blank? && cookies.keys.any? { |x| x.include? "ZConnect"} raise ZuoraAPI::Exceptions::ZuoraAPIError.new("No ZConnect cookie present matching #{self.hostname}") else raise ZuoraAPI::Exceptions::ZuoraAPIError.new("No ZSession cookie present") end end rescue JSON::ParserError => ex output_json = {} end raise_errors(type: :JSON, body: output_json, response: response) return output_json end def set_nav(state, cookies) zsession = cookies["ZSession"] zconnect_accesstoken = get_zconnect_accesstoken(cookies) begin if !zsession.blank? response = HTTParty.put("https://#{self.hostname}/apps/v1/preference/navigation", :body => state.to_json, :headers => {'Cookie' => "ZSession=#{zsession}", 'Content-Type' => 'application/json'}) output_json = JSON.parse(response.body) elsif !zconnect_accesstoken.blank? Rails.logger.info("Using ZConnect cookie in set_nav method") response = HTTParty.post("https://#{self.hostname}/apps/zconnectsession/navigationstate", :body => state.to_json, :headers => {'Cookie' => "#{self.zconnect_provider}=#{zconnect_accesstoken}", 'Content-Type' => 'application/json'}) output_json = JSON.parse(response.body) else if zconnect_accesstoken.blank? && cookies.keys.any? { |x| x.include? "ZConnect"} raise ZuoraAPI::Exceptions::ZuoraAPIError.new("No ZConnect cookie present matching #{self.hostname}") else raise ZuoraAPI::Exceptions::ZuoraAPIError.new("No ZSession cookie present") end end rescue JSON::ParserError => ex output_json = {} end raise_errors(type: :JSON, body: output_json, response: response) return output_json end def refresh_nav(cookies) zsession = cookies["ZSession"] zconnect_accesstoken = get_zconnect_accesstoken(cookies) begin if !zsession.blank? response = HTTParty.post("https://#{self.hostname}/apps/v1/navigation/fetch", :headers => {'Cookie' => "ZSession=#{zsession}", 'Content-Type' => 'application/json'}) output_json = JSON.parse(response.body) elsif !zconnect_accesstoken.blank? Rails.logger.info("Using ZConnect cookie in refresh_nav method") response = HTTParty.post("https://#{self.hostname}/apps/zconnectsession/refresh-navbarcache", :headers => {'Cookie' => "#{self.zconnect_provider}=#{zconnect_accesstoken}", 'Content-Type' => 'application/json'}) output_json = JSON.parse(response.body) else if zconnect_accesstoken.blank? && cookies.keys.any? { |x| x.include? "ZConnect"} raise ZuoraAPI::Exceptions::ZuoraAPIError.new("No ZConnect cookie present matching #{self.hostname}") else raise ZuoraAPI::Exceptions::ZuoraAPIError.new("No ZSession cookie present") end end rescue JSON::ParserError => ex output_json = {} end raise_errors(type: :JSON, body: output_json, response: response) return output_json end def get_zconnect_accesstoken(cookies) accesstoken = nil self.update_zconnect_provider if !cookies[self.zconnect_provider].nil? && !cookies[self.zconnect_provider].empty? accesstoken = cookies[self.zconnect_provider] end return accesstoken end def reporting_url(path) map = {"US" => {"Sandbox" => "https://zconnectsandbox.zuora.com/api/rest/v1/", "Production" => "https://zconnect.zuora.com/api/rest/v1/", "Services"=> ""}, "EU" => {"Sandbox" => "https://zconnect.sandbox.eu.zuora.com/api/rest/v1/", "Production" => "https://zconnect.eu.zuora.com/api/rest/v1/", "Services"=> ""}, "NA" => {"Sandbox" => "https://zconnect.sandbox.na.zuora.com/api/rest/v1/", "Production" => "https://zconnect.na.zuora.com/api/rest/v1/", "Services"=> ""} } return map[zuora_client.region][zuora_client.environment].insert(-1, path) end # There are two ways to call this method. The first way is best. # 1. Pass in cookies and optionally custom_authorities, name, and description # 2. Pass in user_id, entity_ids, client_id, client_secret, and optionally custom_authorities, name, and description # https://intranet.zuora.com/confluence/display/Sunburst/Create+an+OAuth+Client+through+API+Gateway#CreateanOAuthClientthroughAPIGateway-ZSession def get_oauth_client (custom_authorities = [], info_name: "No Name", info_desc: "This client was created without a description.", user_id: nil, entity_ids: nil, client_id: nil, client_secret: nil, new_client_id: nil, new_client_secret: nil, cookies: nil) authorization = "" new_client_id = SecureRandom.uuid if new_client_id.blank? new_client_secret = SecureRandom.hex(10) if new_client_secret.blank? if !cookies.nil? authorization = cookies["ZSession"] authorization = "ZSession-a3N2w #{authorization}" if entity_ids.blank? && cookies["ZuoraCurrentEntity"].present? entity_ids = Array(cookies["ZuoraCurrentEntity"].unpack("a8a4a4a4a12").join('-')) else raise ZuoraAPI::Exceptions::ZuoraAPIError.new("Zuora Entity ID not provided") end if user_id.blank? && cookies["Zuora-User-Id"].present? user_id = cookies["Zuora-User-Id"] else raise ZuoraAPI::Exceptions::ZuoraAPIError.new("Zuora User ID not provided") end elsif !client_id.nil? && !client_secret.nil? bearer_response = HTTParty.post("https://#{self.hostname}/oauth/token", :headers => {'Content-Type' => 'application/x-www-form-urlencoded', 'Accept' => 'application/json'}, :body => {'client_id' => client_id, 'client_secret' => URI::encode(client_secret), 'grant_type' => 'client_credentials'}) bearer_hash = JSON.parse(bearer_response.body) bearer_token = bearer_hash["access_token"] authorization = "Bearer #{bearer_token}" end if !authorization.blank? && !user_id.blank? && !entity_ids.blank? endpoint = self.rest_endpoint("genesis/clients") oauth_response = HTTParty.post(endpoint, :headers => {'authorization' => authorization, 'Content-Type' => 'application/json'}, :body => {'clientId' => new_client_id, 'clientSecret' => new_client_secret, 'userId' => user_id, 'entityIds' => entity_ids, 'customAuthorities' => custom_authorities, 'additionalInformation' => {'description' => info_desc, 'name' => info_name}}.to_json) output_json = JSON.parse(oauth_response.body) if oauth_response.code == 201 output_json["clientSecret"] = new_client_secret return output_json elsif oauth_response.code == 401 && !oauth_response.message.blank? raise ZuoraAPI::Exceptions::ZuoraAPIError.new(output_json["message"], oauth_response) else raise ZuoraAPI::Exceptions::ZuoraAPIError.new(output_json["error"], oauth_response) end else raise ZuoraAPI::Exceptions::ZuoraAPIError.new("Insufficient credentials provided") end end def self.environments %w(Sandbox Production Services Performance Staging) end def self.regions %w(US EU NA) 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/", "Staging" => "https://staging2.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/"}, "NA" => {"Sandbox" => "https://sandbox.na.zuora.com/apps/services/a/", "Production" => "https://na.zuora.com/apps/services/a/", "Performance" => "https://pt1.na.zuora.com/apps/services/a/", "Services" => "https://services347.na.zuora.com/apps/services/a/"} } end def get_entity_id(entity_id: nil) if entity_id.present? entity_match = /[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}$/.match(entity_id) if entity_match.blank? raise "Entity length is wrong." if entity_id.length != 32 part_one, part_two, part_three, part_four, part_five = [entity_id[0..7], entity_id[8..11], entity_id[12..15], entity_id[16..19], entity_id[20..31]] entity_id = "#{part_one}-#{part_two}-#{part_three}-#{part_four}-#{part_five}" end end return entity_id end def update_region if !self.hostname.blank? if /(?<=\.|\/|^)(eu)(?=\.|\/|$)/ === self.hostname self.region = "EU" elsif /(?<=\.|\/|^)(na)(?=\.|\/|$)/ === self.hostname self.region = "NA" else self.region = "US" end else # This will never happen # raise "Can't update region because URL is blank" self.region = "Unknown" end end def update_environment if !self.url.blank? if /(?<=\.|\/|-|^)(apisandbox|sandbox)(?=\.|\/|-|$)/ === self.hostname self.environment = 'Sandbox' elsif /(?<=\.|\/|^)(service[\d]*|services[\d]*|ep-edge)(?=\.|\/|$)/ === self.hostname self.environment = 'Services' elsif /(?<=\.|\/|-|^)(pt[\d]*)(?=\.|\/|-|$)/ === self.hostname self.environment = 'Performance' elsif /(?<=\.|\/|^)(staging1|staging2|stg)(?=\.|\/|$)/ === self.hostname self.environment = 'Staging' elsif is_prod_env self.environment = 'Production' else self.environment = 'Unknown' end else # this will never happen raise "Can't determine environment from blank URL" end end def is_prod_env is_prod = false www_or_api = /(?<=\.|\/|^)(www|api)(?=\.|\/|$)/ === self.hostname host_prefix_match = /(^|tls10\.|origin-www\.|zforsf\.|eu\.|na\.)(zuora\.com)/ === self.hostname if www_or_api || host_prefix_match is_prod = true end return is_prod end def update_zconnect_provider region = update_region environment = update_environment mappings = {"US" => {"Sandbox" => "ZConnectSbx", "KubeSTG" => "ZConnectDev", "KubeDEV" => "ZConnectDev", "KubePROD" => "ZConnectDev", "Services" => "ZConnectSvcUS", "Production" => "ZConnectProd", "Performance" => "ZConnectPT1", "Staging" => "ZConnectQA"}, "NA" => {"Sandbox" => "ZConnectSbxNA", "Services" => "ZConnectSvcNA", "Production" => "ZConnectProdNA", "Performance" => "ZConnectPT1NA"}, "EU" => {"Sandbox" => "ZConnectSbxEU", "Services" => "ZConnectSvcEU", "Production" => "ZConnectProdEU", "Performance" => "ZConnectPT1EU"}, "Unknown" => {"Unknown" => "Unknown"}} self.zconnect_provider = mappings[region][environment] end def aqua_endpoint(url="") match = /.*(\/apps\/)/.match(self.url) if !match.nil? url_slash_apps_slash = match[0] else raise "self.url has no /apps in it" end return "#{url_slash_apps_slash}api/#{url}" end def rest_endpoint(url="", domain=true, prefix='/v1/') update_environment endpoint = url case self.environment when 'Sandbox' case self.region when 'US' endpoint = "https://rest.apisandbox.zuora.com" when 'EU' endpoint = "https://rest.sandbox.eu.zuora.com" when 'NA' endpoint = "https://rest.sandbox.na.zuora.com" end when 'Production' case self.region when 'US' endpoint = "https://rest.zuora.com" when 'EU' endpoint = "https://rest.eu.zuora.com" when 'NA' endpoint = "https://rest.na.zuora.com" end when 'Performance' endpoint = "https://rest.pt1.zuora.com" when 'Services' https = /https:\/\/|http:\/\//.match(self.url)[0] host = self.hostname endpoint = "#{https}rest#{host}" when 'Staging' endpoint = "https://rest-staging2.zuora.com" when 'Unknown' raise "Environment unknown, returning passed in parameter unaltered" end return domain ? endpoint.concat(prefix).concat(url) : prefix.concat(url) end def rest_domain return URI(self.rest_endpoint).host end def fileURL(url="") return self.rest_endpoint("file/").concat(url) end def dateFormat return self.wsdl_number > 68 ? '%Y-%m-%d' : '%Y-%m-%dT%H:%M:%S' end def new_session(auth_type: :basic, debug: false, zuora_track_id: nil) end def get_session(prefix: false, auth_type: :basic, zuora_track_id: nil) Rails.logger.debug("Get session for #{auth_type} - #{self.class.to_s}") if Rails.env.to_s == 'development' case auth_type when :basic if self.current_session.blank? case self.class.to_s when 'ZuoraAPI::Oauth' if self.bearer_token.blank? || self.oauth_expired? self.new_session(auth_type: :bearer, zuora_track_id: zuora_track_id) end self.get_z_session(zuora_track_id: zuora_track_id) if self.status == 'Active' when 'ZuoraAPI::Basic' self.new_session(auth_type: :basic, zuora_track_id: zuora_track_id) else self.new_session(auth_type: :basic, zuora_track_id: zuora_track_id) end end raise ZuoraAPI::Exceptions::ZuoraAPISessionError.new(self.current_error) if self.status != 'Active' return prefix ? "ZSession #{self.current_session}" : self.current_session.to_s when :bearer case self.class.to_s when 'ZuoraAPI::Oauth' if self.bearer_token.blank? || self.oauth_expired? self.new_session(auth_type: :bearer, zuora_track_id: zuora_track_id) end when 'ZuoraAPI::Basic' raise ZuoraAPI::Exceptions::ZuoraAPIAuthenticationTypeError.new("Basic Login, does not support Authentication of Type: #{auth_type}") else raise ZuoraAPI::Exceptions::ZuoraAPIAuthenticationTypeError.new("Unknown Login, does not support Authentication of Type: #{auth_type}") end raise ZuoraAPI::Exceptions::ZuoraAPISessionError.new(self.current_error) if self.status != 'Active' return prefix ? "Bearer #{self.bearer_token}" : self.bearer_token.to_s end end def soap_call( ns1: 'ns1', ns2: 'ns2', batch_size: nil, single_transaction: false, debug: false, zuora_track_id: nil, errors: [ZuoraAPI::Exceptions::ZuoraAPISessionError].concat(ZUORA_API_ERRORS), z_session: true, timeout_retry: false, timeout: 120, output_exception_messages: true, **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(prefix: false, auth_type: :basic, zuora_track_id: zuora_track_id) 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 => XML_SAVE_OPTIONS).strip) input_xml.xpath('//ns1:session', 'ns1' =>'http://api.zuora.com/').children.remove Rails.logger.debug("Request SOAP XML: #{input_xml.to_xml(:save_with => XML_SAVE_OPTIONS).strip}") if debug headers = { 'Content-Type' => "text/xml; charset=utf-8" } headers['Zuora-Track-Id'] = zuora_track_id if zuora_track_id.present? response = HTTParty.post( self.url, :body => xml.doc.to_xml(:save_with => XML_SAVE_OPTIONS).strip, :headers => headers, :timeout => timeout ) output_xml = Nokogiri::XML(response.body) Rails.logger.debug("Response SOAP XML: #{output_xml.to_xml(:save_with => XML_SAVE_OPTIONS).strip}") if debug raise_errors(type: :SOAP, body: output_xml, response: response) rescue ZuoraAPI::Exceptions::ZuoraAPISessionError => ex if !tries.zero? && z_session tries -= 1 Rails.logger.debug("SOAP Call - Session Invalid") begin self.new_session(auth_type: :basic, zuora_track_id: zuora_track_id) rescue *ZUORA_API_ERRORS => ex return output_xml, input_xml, ex.response end retry else if errors.include?(ex.class) raise ex else return output_xml, input_xml, response end end rescue *ZUORA_API_ERRORS => ex if errors.include?(ex.class) raise ex else response = ex.response unless response return output_xml, input_xml, response end rescue *CONNECTION_EXCEPTIONS => ex if tries.zero? if output_exception_messages if Rails.logger.class.to_s == "Ougai::Logger" Rails.logger.error("SOAP Call - Timed out will retry after #{self.timeout_sleep} seconds", ex) else Rails.logger.error("SOAP Call - #{ex.class} Timed out will retry after #{self.timeout_sleep} seconds") end end raise ex end tries -= 1 sleep(self.timeout_sleep) retry rescue *CONNECTION_READ_EXCEPTIONS => ex if tries.zero? if output_exception_messages if Rails.logger.class.to_s == "Ougai::Logger" Rails.logger.error("SOAP Call - Timed out will retry after #{self.timeout_sleep} seconds", ex) else Rails.logger.error("SOAP Call - #{ex.class} Timed out will retry after #{self.timeout_sleep} seconds") end end raise ex end tries -= 1 if ex.is_a?(Errno::ECONNRESET) && ex.message.include?('SSL_connect') retry elsif timeout_retry sleep(self.timeout_sleep) retry else if output_exception_messages if Rails.logger.class.to_s == "Ougai::Logger" Rails.logger.error("SOAP Call - Timed out will retry after #{self.timeout_sleep} seconds", ex) else Rails.logger.error("SOAP Call - #{ex.class} Timed out will retry after #{self.timeout_sleep} seconds") end end raise ex end rescue => ex raise ex else return output_xml, input_xml, response end def raise_errors(type: :SOAP, body: nil, response: nil) if [502,503].include?(response.code) raise ZuoraAPI::Exceptions::ZuoraAPIConnectionTimeout.new("Received #{response.code} from #{response.request.uri}", response) end # Check failure response code case response.code when 504 raise ZuoraAPI::Exceptions::ZuoraAPIReadTimeout.new("Received 504 from #{response.request.uri}", response) when 429 raise ZuoraAPI::Exceptions::ZuoraAPIRequestLimit.new("The total number of concurrent requests has exceeded the limit allowed by the system. Please resubmit your request later.", response) end case type when :SOAP error, success, message = get_soap_error_and_message(body) if body.xpath('//ns1:queryResponse', 'ns1' => 'http://api.zuora.com/').present? && body.xpath( '//ns1:records[@xsi:type="ns2:Export"]', 'ns1' => 'http://api.zuora.com/', 'xsi' => 'http://www.w3.org/2001/XMLSchema-instance' ).present? result = body.xpath('//ns2:Status', 'ns2' => 'http://object.api.zuora.com/').text if result == 'Failed' reason = body.xpath('//ns2:StatusReason', 'ns2' => 'http://object.api.zuora.com/').text if reason.present? message = body.xpath('//ns2:StatusReason', 'ns2' => 'http://object.api.zuora.com/').text error = message.include?('Unexpected error') ? 'UNEXPECTED_ERROR' : 'FATAL_ERROR' else error = 'FATAL_ERROR' message = 'Export failed due to unknown reason. Consult api logs.' end end end #By default response if not passed in for SOAP as all SOAP is 200 if error.present? if error.class == String raise_errors_helper(error: error, message: message, response: response) elsif error.class == Array if error.uniq.size == 1 err, msg = error[0].split('::') raise_errors_helper(error: err, message: msg, response: response, errors: error, success: success) else raise ZuoraAPI::Exceptions::ZuoraAPIError.new(error.group_by {|v| v}.map {|k,v| "(#{v.size}x) - #{k == "::" ? 'UNKNOWN::No error provided' : k}"}.join(', '), response, error, success) end end end if response.code == 500 || (response.code == 400 && response.headers.fetch('content-type', []).include?('text/html')) raise ZuoraAPI::Exceptions::ZuoraAPIInternalServerError.new(response.body, response) end when :JSON if body.class == Hash request = response.request match_string = "#{request.http_method.to_s.split("Net::HTTP::").last.upcase}::#{response.code}::#{request.path.path}" case response.request.path.path when /^\/query\/jobs.*/ #DataQuery Paths case match_string when /^GET::200::\/query\/jobs\/([a-zA-Z0-9\-_]*)$/ #Get DQ job if (body.dig('data', "errorMessage").present? || body.dig('data', "queryStatus") == "failed") #&& raise ZuoraAPI::Exceptions::ZuoraAPIError.new(body.dig('data', "errorMessage"), response) end when /^GET::404::\/query\/jobs\/([a-zA-Z0-9\-_]*)$/ #Get DQ job not found raise ZuoraAPI::Exceptions::ZuoraAPIError.new(body.dig('message'), response) if body.dig('message').present? when /^POST::400::\/query\/jobs$/ #Create DQ job raise ZuoraAPI::Exceptions::ZuoraAPIError.new(body.dig('message'), response) if body.dig('message').present? end end end body = body.dig("results").present? ? body["results"] : body if body.class == Hash if body.class == Hash && (!body["success"] || !body["Success"] || response.code != 200) messages_array = body.fetch("reasons", []).map {|error| error['message']}.compact messages_array = messages_array.push(body.dig("error", 'message')).compact if body.dig('error').class == Hash codes_array = body.fetch("reasons", []).map {|error| error['code']}.compact codes_array = codes_array.push(body.dig("error", 'code')).compact if body.dig('error').class == Hash if body['message'] == 'request exceeded limit' raise ZuoraAPI::Exceptions::ZuoraAPIRequestLimit.new("The total number of concurrent requests has exceeded the limit allowed by the system. Please resubmit your request later.", response) end if (body.dig('message') || '').downcase.include?('unexpected error') && response.code != 500 raise ZuoraAPI::Exceptions::ZuoraUnexpectedError.new(body['message'], response) end if body['message'] == "No bearer token" && response.code == 400 raise ZuoraAPI::Exceptions::ZuoraAPIAuthenticationTypeError.new("Authentication type is not supported by this Login", response) end if body['errorMessage'] raise ZuoraAPI::Exceptions::ZuoraAPIError.new(body['errorMessage'], response) end if body.dig("reasons").nil? ? false : body.dig("reasons")[0].dig("code") == 90000020 raise ZuoraAPI::Exceptions::BadEntityError.new("#{messages_array.join(', ')}", response) end #Oauth Tokens - User deactivated if body['path'] == '/oauth/token' if body['status'] == 403 && response.code == 403 raise ZuoraAPI::Exceptions::ZuoraAPISessionError.new("Forbidden", response) elsif body['status'] == 400 && response.code == 400 && body['message'].include?("Invalid client id") raise ZuoraAPI::Exceptions::ZuoraAPISessionError.new("Invalid Oauth Client Id", response) end end if body['error'] == 'Unauthorized' && body['status'] == 401 raise ZuoraAPI::Exceptions::ZuoraAPISessionError.new("#{messages_array.join(', ')}", response) end #Authentication failed if (codes_array.map{|code| code.to_s.slice(6,7).to_i}.include?(11) || response.code == 401) && !codes_array.include?(422) new_message = messages_array.join(', ') if new_message.present? raise ZuoraAPI::Exceptions::ZuoraAPISessionError.new(new_message, response) else raise ZuoraAPI::Exceptions::ZuoraAPISessionError.new(body['message'], response) end end #Zuora REST Create Amendment error #Authentication failed if body["faultcode"].present? && body["faultcode"] == "fns:INVALID_SESSION" raise ZuoraAPI::Exceptions::ZuoraAPISessionError.new("#{body['faultstring']}", response) 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(', ')}", response) end #Internal Server Error if codes_array.map{|code| code.to_s.slice(6,7).to_i}.include?(60) raise ZuoraAPI::Exceptions::ZuoraAPIInternalServerError.new("#{messages_array.join(', ')}", response) 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(', ')}", response) end #All Errors catch if codes_array.size > 0 raise ZuoraAPI::Exceptions::ZuoraAPIError.new("#{messages_array.join(', ')}", response) end #Zuora REST Query Errors if body["faultcode"].present? raise_errors_helper(error: body["faultcode"], message: body["faultstring"], response: response) end if body["Errors"].present? || body["errors"].present? codes, messages = [[],[]] body.fetch("Errors", []).select { |obj| messages.push(obj["Message"]); codes.push(obj["Code"]) } body.fetch("errors", []).select { |obj| messages.push(obj["Message"]); codes.push(obj["Code"]) } if codes.size > 0 if codes.uniq.size == 1 raise_errors_helper(error: codes.first, message: messages.first, response: response, errors: messages) else raise ZuoraAPI::Exceptions::ZuoraAPIError.new("#{messages.join(", ")}", response, messages) end end end end #Zuora REST Actions error (Create, Update, Delete, Amend) 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_success.blank? && all_errors.present? error_codes = all_errors.flatten.group_by {|error| error['Code']}.keys.uniq error_messages = all_errors.flatten.group_by {|error| error['Message']}.keys.uniq if error_codes.size == 1 || error_messages.size == 1 if error_codes.first == "LOCK_COMPETITION" raise ZuoraAPI::Exceptions::ZuoraAPILockCompetition.new("Retry Lock Competition", response) elsif error_messages.first.include?("data integrity violation") raise ZuoraAPI::Exceptions::ZuoraDataIntegrity.new("Data Integrity Violation", response) end end end if all_errors.size > 0 raise ZuoraAPI::Exceptions::ZuoraAPIError.new("#{all_errors.flatten.group_by {|error| error['Message']}.keys.uniq.join(' ')}", response, all_errors, all_success) end end #All other errors if response.code == 500 if body.class == Hash && body['message'].present? raise ZuoraAPI::Exceptions::ZuoraAPIInternalServerError.new(body['message'], response) else raise ZuoraAPI::Exceptions::ZuoraAPIInternalServerError.new(response.body, response) end elsif ![200,201].include?(response.code) if body['message'].present? raise ZuoraAPI::Exceptions::ZuoraAPIError.new(body['message'], response) else if 403 == response.code && response.body.include?("Forbidden") raise ZuoraAPI::Exceptions::ZuoraAPIInternalServerError.new(response.body, response) else raise ZuoraAPI::Exceptions::ZuoraAPIError.new(response.body, response) end end end end end def get_soap_error_and_message(body) 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('//faultcode').text message = body.xpath('//faultstring').text end 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 #Update/Create/Delete Calls with multiple requests and responses if body.xpath('//ns1:result', 'ns1' =>'http://api.zuora.com/').size > 0 && body.xpath('//ns1:Errors', 'ns1' =>'http://api.zuora.com/').size > 0 error = [] success = [] body.xpath('//ns1:result', 'ns1' =>'http://api.zuora.com/').each_with_index do |call, object_index| if call.xpath('./ns1:Success', 'ns1' =>'http://api.zuora.com/').text == 'false' && call.xpath('./ns1:Errors', 'ns1' =>'http://api.zuora.com/').size > 0 message = "#{call.xpath('./*/ns1:Code', 'ns1' =>'http://api.zuora.com/').text}::#{call.xpath('./*/ns1:Message', 'ns1' =>'http://api.zuora.com/').text}" error.push(message) else success.push(call.xpath('./ns1:Id', 'ns1' =>'http://api.zuora.com/').text) end end end return error, success, message end def raise_errors_helper(error: nil, message: nil, response: nil, errors: [], success: []) case error when /.*INVALID_SESSION/ raise ZuoraAPI::Exceptions::ZuoraAPISessionError.new(message, response, errors, success) when /.*REQUEST_EXCEEDED_LIMIT/ raise ZuoraAPI::Exceptions::ZuoraAPIRequestLimit.new(message, response, errors, success) when /.*LOCK_COMPETITION/ raise ZuoraAPI::Exceptions::ZuoraAPILockCompetition.new(message, response, errors, success) when /.*BATCH_FAIL_ERROR/ if message.include?("optimistic locking failed") raise ZuoraAPI::Exceptions::ZuoraAPILockCompetition.new(message, response, errors, success) elsif message.include?("org.hibernate.exception.ConstraintViolationException") raise ZuoraAPI::Exceptions::ZuoraAPIInternalServerError.new(message, response, errors, success) end raise ZuoraAPI::Exceptions::ZuoraAPIError.new(message, response, errors, success) when /.*TEMPORARY_ERROR/ raise ZuoraAPI::Exceptions::ZuoraAPITemporaryError.new(message, response, errors, success) when /.*INVALID_VALUE/ if message.include?("data integrity violation") raise ZuoraAPI::Exceptions::ZuoraDataIntegrity.new("Data Integrity Violation", response, errors), success end raise ZuoraAPI::Exceptions::ZuoraAPIError.new(message, response, errors, success) when /.*UNKNOWN_ERROR/ if /payment\/refund|Credit Balance Adjustment|Payment Gateway|ARSettlement permission/.match(message).nil? raise ZuoraAPI::Exceptions::ZuoraAPIUnkownError.new(message, response, errors, success) end raise ZuoraAPI::Exceptions::ZuoraAPIError.new(message, response, errors, success) when /INVALID_ID/, /MAX_RECORDS_EXCEEDED/, /INVALID_FIELD/, /MALFORMED_QUERY/, /NO_PERMISSION/, /PDF_QUERY_ERROR/, /MISSING_REQUIRED_VALUE/, /INVALID_TYPE/, /TRANSACTION_FAILED/, /API_DISABLED/, /CANNOT_DELETE/, /ACCOUNTING_PERIOD_CLOSED/ raise ZuoraAPI::Exceptions::ZuoraAPIError.new(message, response, errors, success) when /.*UNEXPECTED_ERROR/ raise ZuoraAPI::Exceptions::ZuoraUnexpectedError.new(message, response, errors, success) when /.*soapenv:Server.*/ if /Invalid value.*for type|Id is invalid/.match(message).present? raise ZuoraAPI::Exceptions::ZuoraAPIError.new(message, response, errors, success) end raise ZuoraAPI::Exceptions::ZuoraAPIInternalServerError.new(message, response, errors, success) else raise ZuoraAPI::Exceptions::ZuoraAPIInternalServerError.new("Z:#{error}::#{message}", response, errors, success) 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, log_errors = true) tries ||= 2 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.present? ? {"Zuora-Entity-Ids" => self.entity_id, 'Content-Type' => "text/xml; charset=utf-8"} : {'Content-Type' => "text/xml; charset=utf-8"} response = HTTParty.get(url, headers: {"Authorization" => self.get_session(prefix: true, auth_type: :basic)}.merge(headers), :timeout => 120) raise ZuoraAPI::Exceptions::ZuoraAPISessionError.new(self.current_error.present? ? self.current_error : 'Describe call 401', response) if response.code == 401 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 *(CONNECTION_EXCEPTIONS + CONNECTION_READ_EXCEPTIONS) => ex if tries.zero? if log_errors if Rails.logger.class.to_s == "Ougai::Logger" Rails.logger.error("Describe - Timed out will retry after #{self.timeout_sleep} seconds", ex) else Rails.logger.error("Describe - #{ex.class} Timed out will retry after #{self.timeout_sleep} seconds") end end raise ex end tries -= 1 sleep(self.timeout_sleep) retry rescue ZuoraAPI::Exceptions::ZuoraAPISessionError => ex if !tries.zero? && self.status == 'Active' tries -= 1 Rails.logger.debug("Describe session expired. Starting new session.") self.new_session retry else Rails.logger.error("Describe session expired. Starting new session.") if log_errors raise ex end rescue => ex raise ex else return des_hash end def rest_call( method: :get, body: nil, headers: {}, url: rest_endpoint("catalog/products?pageSize=4"), debug: false, errors: [ZuoraAPI::Exceptions::ZuoraAPISessionError].concat(ZUORA_API_ERRORS), z_session: true, session_type: :basic, timeout_retry: false, timeout: 120, multipart: false, stream_body: false, output_exception_messages: true, **keyword_args, &block ) tries ||= 2 if self.entity_id.present? headers["Zuora-Entity-Ids"] = self.entity_id if headers.dig("Zuora-Entity-Ids").nil? headers.delete_if { |key, value| ["entityId", "entityName"].include?(key.to_s) } end raise "Method not supported, supported methods include: :get, :post, :put, :delete, :patch, :head, :options" if ![:get, :post, :put, :delete, :patch, :head, :options].include?(method) authentication_headers = z_session ? {"Authorization" => self.get_session(prefix: true, auth_type: session_type) } : {} modified_headers = {'Content-Type' => "application/json; charset=utf-8"}.merge(authentication_headers).merge(headers) begin response = HTTParty::Request.new( "Net::HTTP::#{method.to_s.capitalize}".constantize, url, body: body, headers: modified_headers, timeout: timeout, multipart: multipart, stream_body: stream_body ).perform(&block) rescue rewind_files(body) if multipart raise end Rails.logger.debug("Response Code: #{response.code}") if debug begin output_json = JSON.parse(response.body) rescue JSON::ParserError => ex output_json = {} end Rails.logger.debug("Response JSON: #{output_json}") if debug && output_json.present? raise_errors(type: :JSON, body: output_json, response: response) rescue ZuoraAPI::Exceptions::ZuoraAPIAuthenticationTypeError => ex if self.class.to_s == 'ZuoraAPI::Oauth' && ex.message.include?("Authentication type is not supported by this Login") session_type = :bearer retry else Rails.logger.debug("Rest Call - Session Bad Auth type") raise ex end rescue ZuoraAPI::Exceptions::ZuoraAPISessionError => ex if !tries.zero? && z_session tries -= 1 Rails.logger.debug("Rest Call - Session Invalid #{session_type}") begin self.new_session(auth_type: session_type) rescue *ZUORA_API_ERRORS => ex return [output_json, ex.response] end retry else if errors.include?(ex.class) raise ex else return [output_json, response] end end rescue *ZUORA_API_ERRORS => ex if errors.include?(ex.class) raise ex else response = ex.response unless response return [output_json, response] end rescue ZuoraAPI::Exceptions::BadEntityError => ex raise ex rescue *CONNECTION_EXCEPTIONS => ex if tries.zero? if output_exception_messages if Rails.logger.class.to_s == "Ougai::Logger" Rails.logger.error("Rest Call - Timed out will retry after #{self.timeout_sleep} seconds", ex) else Rails.logger.error("Rest Call - #{ex.class} Timed out will retry after #{self.timeout_sleep} seconds") end end raise ex end tries -= 1 sleep(self.timeout_sleep) retry rescue *CONNECTION_READ_EXCEPTIONS => ex if tries.zero? if output_exception_messages if Rails.logger.class.to_s == "Ougai::Logger" Rails.logger.error("Rest Call - Timed out will retry after #{self.timeout_sleep} seconds", ex) else Rails.logger.error("Rest Call - #{ex.class} Timed out will retry after #{self.timeout_sleep} seconds") end end raise ex end tries -= 1 if ex.is_a?(Errno::ECONNRESET) && ex.message.include?('SSL_connect') retry elsif timeout_retry sleep(self.timeout_sleep) retry else if output_exception_messages if Rails.logger.class.to_s == "Ougai::Logger" Rails.logger.error("Rest Call - Timed out will retry after #{self.timeout_sleep} seconds", ex) else Rails.logger.error("Rest Call - #{ex.class} Timed out will retry after #{self.timeout_sleep} seconds") end end 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}", response) 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: {}, z_session: true, tempfile: true, output_file_name: nil, zuora_track_id: nil, add_timestamp: true, file_path: defined?(Rails.root.join('tmp')) ? Rails.root.join('tmp') : Pathname.new(Dir.pwd), timeout_retries: 3, timeout: 120, session_type: :basic, **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) status_code = nil uri = URI.parse(url) http = Net::HTTP.new(uri.host, uri.port) http.read_timeout = timeout #Seconds http.use_ssl = true if !uri.scheme.nil? && uri.scheme.downcase == 'https' if z_session headers = headers.merge({"Authorization" => self.get_session(prefix: true)}) headers = headers.merge({"Zuora-Entity-Ids" => self.entity_id}) if !self.entity_id.blank? end headers['Zuora-Track-Id'] = zuora_track_id if zuora_track_id.present? response_save = nil retry_count ||= timeout_retries http.request_get(uri.request_uri, headers) do |response| response_save = response status_code = response.code if response case response when Net::HTTPOK headers = {} response.each_header do |k,v| headers[k] = v end Rails.logger.debug("Headers: #{headers.to_s}") if output_file_name.present? file_ending ||= output_file_name.end_with?(".csv.zip") ? ".csv.zip" : File.extname(output_file_name) filename ||= File.basename(output_file_name, file_ending) end 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 else raise "Can't parse Content-Disposition header: #{response.header["Content-Disposition"]}" end file_ending ||= full_filename.end_with?(".csv.zip") ? ".csv.zip" : File.extname(full_filename) filename ||= File.basename(full_filename, file_ending) end #If user supplied a filename use it, else default to content header filename, else default to uri pattern file_ending ||= uri.path.end_with?(".csv.zip") ? ".csv.zip" : File.extname(uri.path) filename ||= File.basename(uri.path, file_ending) 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 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 Rails.logger.info("File: #{filename}#{file_ending} #{encoding} #{type} #{export_size}") file_prefix = add_timestamp ? "#{filename}_#{Time.now.to_i}" : filename if tempfile require 'tempfile' file_handle = ::Tempfile.new([file_prefix, "#{file_ending}"], file_path) else file_handle = File.new(file_path.join("#{file_prefix}#{file_ending}"), "w+") end file_handle.binmode response.read_body do |chunk| file_handle << chunk 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: Export Downloading %s (%3d%%)" % [filename, new_progress]) end export_progress = new_progress end end file_handle.close Rails.logger.debug("Filepath: #{file_handle.path} Size: #{File.size(file_handle.path).to_f/1000000} mb") raise ZuoraAPI::Exceptions::ZuoraAPIError.new("Downloaded file is not a file: #{file_handle.class}") if !["Tempfile", "File"].include?(file_handle.class.to_s) return file_handle when Net::HTTPUnauthorized if z_session if !(retry_count -= 1).zero? self.new_session raise response.class else raise ZuoraAPI::Exceptions::ZuoraAPISessionError.new(self.current_error) end else raise end else raise ZuoraAPI::Exceptions::FileDownloadError.new("File Download Failed #{response.class}") end end rescue => ex sleep(5) if (retry_count -= 1) >= 0 retry else Rails.logger.error("File Download Failed") raise end end def getDataSourceExport(query, extract: true, encrypted: false, zip: true) 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.get_session(prefix: false, auth_type: :basic) 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(:save_with => XML_SAVE_OPTIONS).strip, 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.get_session(prefix: false, auth_type: :basic) 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(:save_with => XML_SAVE_OPTIONS).strip, 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 status_code = response_query.code if response_query 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 export_file = get_file(:url => self.fileURL(file_id)) export_file_path = export_file.path Rails.logger.debug("=====> 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) 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["Authorization"] = self.get_session(prefix: true) req.body = call response = Net::HTTP.start(uri.host, uri.port, :use_ssl => true) do |http| http.request req end Rails.logger.debug("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.error("Journal Run failed with message #{message}") return result end end def checkJRStatus(jrNumber) Rails.logger.info("Check for completion") url = rest_endpoint("/journal-runs/#{jrNumber}") uri = URI(url) req = Net::HTTP::Get.new(uri,initheader = {'Content-Type' =>'application/json'}) req["Authorization"] = self.get_session(prefix: true) 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("Checking status of journal run failed with message #{message}") end return "failure" end def rewind_files(body) return unless body.is_a? Hash body.values.each do |value| value.rewind if value.is_a? File end end end end