require "httparty" require "zip" require "nokogiri" module ZuoraAPI class Login ENVIRONMENTS = [SANDBOX = 'Sandbox', PRODUCTION = 'Production', PREFORMANCE = 'Preformance', SERVICES = 'Services', UNKNOWN = 'Unknown' ] attr_accessor :username, :password, :url, :wsdl_number, :status, :current_session, :environment, :status, :errors, :current_error, :user_info, :tenant_id, :tenant_name, :entity_id, :export_progress, :export_size def initialize(username: nil, password: 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 = "Active" @user_info = Hash.new self.update_environment end def self.environments %w(Sandbox Production Services Performance) end def self.endpoints return {"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/"} end def update_environment if !self.url.blank? env_path = self.url.split('https://').last.split('.zuora.com').first if env_path == 'apisandbox' || self.url.include?('tls10.apisandbox.zuora.com') self.environment = 'Sandbox' elsif env_path == 'www' || env_path == 'api' || self.url.include?('tls10.zuora.com') self.environment = 'Production' elsif env_path.include?('service') self.environment = 'Services' elsif env_path.include?('pt') self.environment = 'Performance' else self.environment = 'Unknown' end end end def aqua_endpoint(url="") if self.environment == 'Sandbox' return "https://apisandbox.zuora.com/apps/api/".concat(url) elsif self.environment == 'Production' return "https://zuora.com/apps/api/".concat(url) else self.environment == 'Unknown' return url end end def rest_endpoint(url="") if self.environment == 'Sandbox' return "https://rest.apisandbox.zuora.com/v1/".concat(url) elsif self.environment == 'Production' return "https://rest.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 return self.url.split(".com").first.concat(".com/apps/api/file/") end def dateFormat return self.wsdl_number > 68 ? '%Y-%m-%d' : '%Y-%m-%dT%H:%M:%S' end def new_session 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 end end end end end @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) 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 if @response_query.timed_out? self.current_error = "Request timed out. Try again" self.status = 'Timeout' else self.current_error = " Code = #{@response_query.code} Message = #{@response_query.return_code}" self.status = 'No Service' end end else self.current_session = (self.password.blank? && !self.current_session.blank?) ? self.current_session : @output_xml.xpath('//ns1:Session', 'ns1' =>'http://api.zuora.com/').text self.username = @output_xml.xpath('//ns1:Username', 'ns1' =>'http://api.zuora.com/').text if self.username.blank? self.current_error = nil self.status = 'Active' end return self.status 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, **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 => 10) 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 ZuoraAPI::Exceptions::ZuoraAPISessionError.new("#{output_xml.xpath('//fns:FaultCode', 'fns' =>'http://fault.api.zuora.com/').text}::#{output_xml.xpath('//fns:FaultMessage', 'fns' =>'http://fault.api.zuora.com/').text}") if (!output_xml.xpath('//fns:FaultCode', 'fns' =>'http://fault.api.zuora.com/').text.blank? && output_xml.xpath('//fns:FaultCode', 'fns' =>'http://fault.api.zuora.com/').text == "INVALID_SESSION") raise ZuoraAPI::Exceptions::ZuoraAPIRequestLimit.new("#{output_xml.xpath('//fns:FaultCode', 'fns' =>'http://fault.api.zuora.com/').text}::#{output_xml.xpath('//fns:FaultMessage', 'fns' =>'http://fault.api.zuora.com/').text}") if (!output_xml.xpath('//fns:FaultCode', 'fns' =>'http://fault.api.zuora.com/').text.blank? && output_xml.xpath('//fns:FaultCode', 'fns' =>'http://fault.api.zuora.com/').text == "REQUEST_EXCEEDED_LIMIT") raise ZuoraAPI::Exceptions::ZuoraAPILockCompetition.new("#{output_xml.xpath('//fns:FaultCode', 'fns' =>'http://fault.api.zuora.com/').text}::#{output_xml.xpath('//fns:FaultMessage', 'fns' =>'http://fault.api.zuora.com/').text}") if (!output_xml.xpath('//fns:FaultCode', 'fns' =>'http://fault.api.zuora.com/').text.blank? && output_xml.xpath('//fns:FaultCode', 'fns' =>'http://fault.api.zuora.com/').text == "LOCK_COMPETITION") raise ZuoraAPI::Exceptions::ZuoraAPIError.new("#{output_xml.xpath('//fns:FaultCode', 'fns' =>'http://fault.api.zuora.com/').text}::#{output_xml.xpath('//fns:FaultMessage', 'fns' =>'http://fault.api.zuora.com/').text}") if !output_xml.xpath('//fns:FaultCode', 'fns' =>'http://fault.api.zuora.com/').text.blank? raise ZuoraAPI::Exceptions::ZuoraAPIError.new("#{output_xml.xpath('//faultcode').text}::#{output_xml.xpath('//faultstring').text}") if !output_xml.xpath('//faultcode').text.blank? rescue ZuoraAPI::Exceptions::ZuoraAPISessionError => ex if !(tries -= 1).zero? Rails.logger.debug {"Session Invalid"} self.new_session retry else raise ex end rescue ZuoraAPI::Exceptions::ZuoraAPIError, ZuoraAPI::Exceptions::ZuoraAPIRequestLimit, ZuoraAPI::Exceptions::ZuoraAPILockCompetition => ex if debug raise ex else return [output_xml, input_xml] end rescue => ex raise ex else return [output_xml, input_xml] end def describe_call(object = nil) self.get_session base = self.url.include?(".com") ? self.url.split(".com")[0] : self.url.split(".eu")[0] url = object ? "#{base}.com/apps/api/describe/#{object}" : "#{base}.com/apps/api/describe/" response = HTTParty.get(url, :headers => {'Content-Type' => "text/xml; charset=utf-8"}, basic_auth: {:username => self.username, :password => self.password}) 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 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 return des_hash end def rest_call(method: :get, body: {},headers: {}, url: rest_endpoint("catalog/products?pageSize=4") , debug: true, **keyword_args) tries ||= 2 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", "Authorization" => "ZSession #{self.get_session}"}.merge(headers)).perform Rails.logger.debug('Connect') { response.code} if debug output_json = JSON.parse(response.body) Rails.logger.debug('Connect') {"Response JSON: #{output_json}"} if debug #Zuora Regular REST API Unauthorized raise ZuoraAPI::Exceptions::ZuoraAPISessionError.new("#{output_json["reasons"][0]["message"]}") if output_json.class != Array && (!output_json["success"] && !output_json["reasons"].blank? && output_json["reasons"] == Array && output_json["reasons"][0]["code"] == 90000011 && response.code == 401) #Zuora AQuA Unauthorized raise ZuoraAPI::Exceptions::ZuoraAPISessionError.new("Unauthorized") if response.code == 401 #Zuora REST API Limit Errors raise ZuoraAPI::Exceptions::ZuoraAPIRequestLimit.new("#{output_json["reasons"][0]["message"]}") if output_json.class != Array && (!output_json["success"] && !output_json["reasons"].blank? && output_json["reasons"] == Array && output_json["reasons"][0]["code"] == 50000070 && response.code == 429) #Zuora REST Query Errors raise ZuoraAPI::Exceptions::ZuoraAPIError.new("#{output_json["faultcode"]}::#{output_json["faultstring"]}") if output_json.class != Array && !output_json["faultcode"].blank? #Zuora REST actions error raise ZuoraAPI::Exceptions::ZuoraAPIError.new("#{output_json["Errors"][0]["Code"]}::#{output_json["Errors"][0]["Message"]}") if output_json.class != Array && !output_json["Success"] && output_json["Errors"] #Zuora All Other API Errors raise ZuoraAPI::Exceptions::ZuoraAPIError.new("#{response.message}") if response.code != 200 rescue ZuoraAPI::Exceptions::ZuoraAPISessionError => ex if !(tries -= 1).zero? Rails.logger.debug {"Session Invalid"} self.new_session retry else raise ex end rescue ZuoraAPI::Exceptions::ZuoraAPIError, ZuoraAPI::Exceptions::ZuoraAPIRequestLimit => ex if debug raise ex else return [output_json, response] 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_file(file_name: 'example.csv', url: nil, headers: {}, count: 3) begin uri = URI.parse(url) filename = File.basename(uri.path) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true if uri.scheme.downcase == 'https' headers = headers.merge({"Authorization" => "ZSession #{self.get_session}"}) http.request_get(uri.path, headers) do |response| case response when Net::HTTPNotFound Rails.logger.debug("404 - Not Found") return false when Net::HTTPUnauthorized raise ZuoraAPI::Exceptions::ZuoraAPISessionError.new(zuora_client.current_error) if count <= 0 Rails.logger.debug("Unauthorized: Retry") zuora_client.new_session return get_file(:url => url, :count => count - 1, :headers => headers) when Net::HTTPClientError Rails.logger.debug("Login: #{self.username} Export") raise ex when Net::HTTPOK temp_file = Tempfile.new([filename.rpartition('.').first, ".zip"], "#{Rails.root}/tmp") temp_file.binmode size, self.export_progress = [0, 0] self.export_size = response.header["Content-Length"].to_i response.read_body do |chunk| temp_file << chunk.force_encoding("UTF-8") size += chunk.size new_progress = (size * 100) / self.export_size unless new_progress == self.export_progress Rails.logger.debug("Login: #{self.username} Export Downloading %s (%3d%%)" % [filename, new_progress]) end self.export_progress = new_progress end temp_file.close return temp_file end end rescue Exception => e raise e end end def getDataSourceExport(query, extract: true, encrypted: false, zip: true) Rails.logger.info('Export') {"Build export"} Rails.logger.info('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"}) output_xml = Nokogiri::XML(response_query.body) return '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"}) output_xml = Nokogiri::XML(response_query.body) result = output_xml.xpath('//ns2:Status', 'ns2' =>'http://object.api.zuora.com/').text return 'Export Creation Unsuccessful : ' + output_xml.xpath('//ns1:Message', 'ns1' =>'http://api.zuora.com/').text if result == "Failed" end file_id = output_xml.xpath('//ns2:FileId', 'ns2' =>'http://object.api.zuora.com/').text Rails.logger.info('Export') {'=====> Export finished'} zip_file = get_file(:file_name => "#{file_id}.zip" ,:url => "#{self.fileURL}#{file_id}?file-id=#{file_id}") if extract && zip location = extract_zip(zip_file.path, "#{file_id}") File.delete(zip_file.path) return location else return zip_file.path end end def extract_zip(file,filename) files = Array.new FileUtils.mkdir_p("#{Rails.root}/tmp/#{filename}") ::Zip::File.open(file) do |zip_file| zip_file.each do |f| files << "#{Rails.root}/tmp/#{filename}/#{f.name}" fpath = File.join("#{Rails.root}/tmp/#{filename}", f.name) zip_file.extract(f, fpath) unless File.exist?(fpath) end end return files end def query(query) Rails.logger.info('query') {"Querying Zuora for #{query}"} 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 query end end end end response_query = HTTParty.post(self.url, body: confirmRequest.to_xml, :headers => {'Content-Type' => "text/xml; charset=utf-8"}) output_xml = Nokogiri::XML(response_query.body) Rails.logger.info('query') {"#{output_xml}"} return output_xml 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.info('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.info('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