module ZuoraConnect class AppInstanceBase < ActiveRecord::Base default_scope {select(ZuoraConnect::AppInstance.column_names.delete_if {|x| ["catalog_mapping", "catalog"].include?(x) }) } after_initialize :init self.table_name = "zuora_connect_app_instances" attr_accessor :options, :mode, :logins, :valid, :task_data, :last_refresh, :username, :password, :s3_client, :api_version def init @options = Hash.new @logins = Hash.new @valid = false self.attr_builder("timezone", ZuoraConnect.configuration.default_time_zone) self.attr_builder("locale", ZuoraConnect.configuration.default_locale) PaperTrail.whodunnit = "Backend" if defined?(PaperTrail) Apartment::Tenant.switch!(self.id) if(ActiveRecord::Migrator.needs_migration?) Apartment::Migrator.migrate(self.id) end Thread.current[:appinstance] = self end def data_lookup(session: {}) if defined?(PaperTrail) PaperTrail.whodunnit = session["#{self.id}::user::email"].present? ? session["#{self.id}::user::email"] : nil if session.present? end if defined?(Redis.current) return Redis.current.get("AppInstance:#{self.id}").blank? ? {} : decrypt_data(Redis.current.get("AppInstance:#{self.id}")) else return session end end def cache_app_instance if defined?(Redis.current) Redis.current.set("AppInstance:#{self.id}", encrypt_data(self.save_data)) Redis.current.expire("AppInstance:#{self.id}", 60.minutes.to_i) Redis.current.del("Deleted:#{self.id}") end end def decrypt_data(data) return data if data.blank? return Rails.env == 'development' ? JSON.parse(data) : JSON.parse(encryptor.decrypt_and_verify(CGI::unescape(data))) end def encrypt_data(data) return data if data.blank? return Rails.env == 'development' ? data.to_json : encryptor.encrypt_and_sign(data.to_json) end def encryptor # Default values for Rails 4 apps key_iter_num, key_size, salt, signed_salt = [1000, 64, "encrypted cookie", "signed encrypted cookie"] key_generator = ActiveSupport::KeyGenerator.new(Rails.application.secrets.secret_key_base, iterations: key_iter_num) secret, sign_secret = [key_generator.generate_key(salt), key_generator.generate_key(signed_salt)] return ActiveSupport::MessageEncryptor.new(secret, sign_secret) end def catalog_outdated?(time: Time.now - 12.hours) return self.catalog_updated_at.blank? || (self.catalog_updated_at < time) end # Catalog lookup provides method to lookup zuora catalog efficiently. # entity_id: If the using catalog json be field to store multiple entity product catalogs. # object: The Object class desired to be returned. Available [:product, :rateplan, :charge] # object_id: The id or id's of the object/objects to be returned. # child_objects: Whether to include child objects of the object in question. # cache: Store individual "1" object lookup in redis for caching. def catalog_lookup(entity_id: nil, object: :product, object_id: nil, child_objects: false, cache: false) entity_reference = entity_id.blank? ? 'Default' : entity_id if object_id.present? && ![Array, String].include?(object_id.class) raise "Object Id can only be a string or an array of strings" end if defined?(Redis.current) && object_id.present? && object_id.class == String stub_catalog = decrypt_data(Redis.current.get("Catalog:#{self.id}:#{object_id}:Children:#{child_objects}")) object_hierarchy = decrypt_data(Redis.current.get("Catalog:#{self.id}:#{object_id}:Hierarchy")) end if defined?(object_hierarchy) object_hierarchy ||= (JSON.parse(ActiveRecord::Base.connection.execute('SELECT catalog_mapping #> \'{%s}\' AS item FROM "public"."zuora_connect_app_instances" WHERE "id" = %s LIMIT 1' % [entity_reference, self.id]).first["item"] || "{}") [object_id] || {"productId" => "SAFTEY", "productRatePlanId" => "SAFTEY", "productRatePlanChargeId" => "SAFTEY"}) end case object when :product if object_id.blank? string = "SELECT "\ "json_object_agg(product_id, product #{child_objects ? '' : '- \'productRatePlans\''}) AS item "\ "FROM "\ "\"public\".\"zuora_connect_app_instances\", "\ "jsonb_each((\"public\".\"zuora_connect_app_instances\".\"catalog\" #> '{%s}' )) AS e(product_id, product) "\ "WHERE "\ "\"id\" = %s" % [entity_reference, self.id] else if object_id.class == String string = "SELECT "\ "(catalog #> '{%s, %s}') #{child_objects ? '' : '- \'productRatePlans\''} AS item "\ "FROM "\ "\"public\".\"zuora_connect_app_instances\" "\ "WHERE "\ "\"id\" = %s" % [entity_reference, object_id, self.id] elsif object_id.class == Array string = "SELECT "\ "json_object_agg(product_id, product #{child_objects ? '' : '- \'productRatePlans\''}) AS item "\ "FROM "\ "\"public\".\"zuora_connect_app_instances\", "\ "jsonb_each((\"public\".\"zuora_connect_app_instances\".\"catalog\" #> '{%s}' )) AS e(product_id, product) "\ "WHERE "\ "\"product_id\" IN (\'%s\') AND "\ "\"id\" = %s" % [entity_reference, object_id.join("\',\'"), self.id] end end when :rateplan if object_id.blank? string = "SELECT "\ "json_object_agg(rateplan_id, rateplan #{child_objects ? '' : '- \'productRatePlanCharges\''}) AS item "\ "FROM "\ "\"public\".\"zuora_connect_app_instances\", "\ "jsonb_each((\"public\".\"zuora_connect_app_instances\".\"catalog\" #> '{%s}' )) AS e(product_id, product), "\ "jsonb_each(product #> '{productRatePlans}') AS ee(rateplan_id, rateplan) "\ "WHERE "\ "\"id\" = %s" % [entity_reference, self.id] else if object_id.class == String string = "SELECT "\ "(catalog #> '{%s, %s, productRatePlans, %s}') #{child_objects ? '' : '- \'productRatePlanCharges\''} AS item "\ "FROM "\ "\"public\".\"zuora_connect_app_instances\" "\ "WHERE "\ "\"id\" = %s" % [entity_reference, object_hierarchy['productId'], object_id, self.id] elsif object_id.class == Array string = "SELECT "\ "json_object_agg(rateplan_id, rateplan #{child_objects ? '' : '- \'productRatePlanCharges\''}) AS item "\ "FROM "\ "\"public\".\"zuora_connect_app_instances\", "\ "jsonb_each((\"public\".\"zuora_connect_app_instances\".\"catalog\" #> '{%s}' )) AS e(product_id, product), "\ "jsonb_each(product #> '{productRatePlans}') AS ee(rateplan_id, rateplan) "\ "WHERE "\ "\"rateplan_id\" IN (\'%s\') AND "\ "\"id\" = %s" % [entity_reference, object_id.join("\',\'"), self.id] end end when :charge if object_id.blank? string = "SELECT "\ "json_object_agg(charge_id, charge) as item "\ "FROM "\ "\"public\".\"zuora_connect_app_instances\", "\ "jsonb_each((\"public\".\"zuora_connect_app_instances\".\"catalog\" #> '{%s}' )) AS e(product_id, product), "\ "jsonb_each(product #> '{productRatePlans}') AS ee(rateplan_id, rateplan), "\ "jsonb_each(rateplan #> '{productRatePlanCharges}') AS eee(charge_id, charge) "\ "WHERE "\ "\"id\" = %s" % [entity_reference, self.id] else if object_id.class == String string = "SELECT "\ "catalog #> '{%s, %s, productRatePlans, %s, productRatePlanCharges, %s}' AS item "\ "FROM "\ "\"public\".\"zuora_connect_app_instances\" "\ "WHERE "\ "\"id\" = %s" % [entity_reference, object_hierarchy['productId'], object_hierarchy['productRatePlanId'], object_id, self.id] elsif object_id.class == Array string = "SELECT "\ "json_object_agg(charge_id, charge) AS item "\ "FROM "\ "\"public\".\"zuora_connect_app_instances\", "\ "jsonb_each((\"public\".\"zuora_connect_app_instances\".\"catalog\" #> '{%s}' )) AS e(product_id, product), "\ "jsonb_each(product #> '{productRatePlans}') AS ee(rateplan_id, rateplan), "\ "jsonb_each(rateplan #> '{productRatePlanCharges}') AS eee(charge_id, charge) "\ "WHERE "\ "\"charge_id\" IN (\'%s\') AND "\ "\"id\" = %s" % [entity_reference, object_id.join("\',\'"), self.id] end end else raise "Available objects include [:product, :rateplan, :charge]" end stub_catalog ||= JSON.parse(ActiveRecord::Base.connection.execute(string).first["item"] || "{}") if defined?(Redis.current) && object_id.present? && object_id.class == String Redis.current.set("Catalog:#{self.id}:#{object_id}:Hierarchy", encrypt_data(object_hierarchy)) Redis.current.set("Catalog:#{self.id}:#{object_id}:Children:#{child_objects}", encrypt_data(stub_catalog)) if cache end return stub_catalog end def instance_failure(failure) raise failure end def login_lookup(type: "Zuora") results = [] self.logins.each do |name, login| results << login if login.tenant_type == type end return results end def get_catalog(page_size: 5, zuora_login: self.login_lookup(type: "Zuora").first, entity_id: nil) entity_reference = entity_id.blank? ? 'Default' : entity_id Rails.logger.debug("Fetching catalog for default") if entity_id.blank? Rails.logger.debug("Fetching catalog for entity #{entity_id}") if !entity_id.blank? Rails.logger.debug("Fetch Catalog") login = zuora_login.client(entity_reference) old_logger = ActiveRecord::Base.logger ActiveRecord::Base.logger = nil ActiveRecord::Base.connection.execute('UPDATE "public"."zuora_connect_app_instances" SET "catalog" = jsonb_set("catalog", \'{tmp}\', \'{}\'), "catalog_mapping" = jsonb_set("catalog_mapping", \'{tmp}\', \'{}\') where "id" = %{id}' % {:id => self.id}) response = {'nextPage' => login.rest_endpoint("catalog/products?pageSize=#{page_size}")} while !response["nextPage"].blank? url = login.rest_endpoint(response["nextPage"].split('/v1/').last) Rails.logger.debug("Fetch Catalog URL #{url}") output_json, response = login.rest_call(:debug => false, :url => url) 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| ActiveRecord::Base.connection.execute('UPDATE "public"."zuora_connect_app_instances" SET "catalog_mapping" = jsonb_set("catalog_mapping", \'{tmp, %s}\', \'%s\') where "id" = %s' % [product["id"], {"productId" => product["id"]}.to_json.gsub("'", "''"), self.id]) rateplans = {} product["productRatePlans"].each do |rateplan| ActiveRecord::Base.connection.execute('UPDATE "public"."zuora_connect_app_instances" SET "catalog_mapping" = jsonb_set("catalog_mapping", \'{tmp, %s}\', \'%s\') where "id" = %s' % [rateplan["id"], {"productId" => product["id"], "productRatePlanId" => rateplan["id"]}.to_json.gsub("'", "''"), self.id]) charges = {} rateplan["productRatePlanCharges"].each do |charge| ActiveRecord::Base.connection.execute('UPDATE "public"."zuora_connect_app_instances" SET "catalog_mapping" = jsonb_set("catalog_mapping", \'{tmp, %s}\', \'%s\') where "id" = %s' % [charge["id"], {"productId" => product["id"], "productRatePlanId" => rateplan["id"], "productRatePlanChargeId" => charge["id"]}.to_json.gsub("'", "''"), self.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 ActiveRecord::Base.connection.execute('UPDATE "public"."zuora_connect_app_instances" SET "catalog" = jsonb_set("catalog", \'{tmp, %s}\', \'%s\') where "id" = %s' % [product["id"], product.to_json.gsub("'", "''"), self.id]) end end # Move from tmp to actual ActiveRecord::Base.connection.execute('UPDATE "public"."zuora_connect_app_instances" SET "catalog" = jsonb_set("catalog", \'{%{entity}}\', "catalog" #> \'{tmp}\'), "catalog_mapping" = jsonb_set("catalog_mapping", \'{%{entity}}\', "catalog_mapping" #> \'{tmp}\') where "id" = %{id}' % {:entity => entity_reference, :id => self.id}) if defined?(Redis.current) Redis.current.keys("Catalog:#{self.id}:*").each do |key| Redis.current.del(key.to_s) end end # Clear tmp holder ActiveRecord::Base.connection.execute('UPDATE "public"."zuora_connect_app_instances" SET "catalog" = jsonb_set("catalog", \'{tmp}\', \'{}\'), "catalog_mapping" = jsonb_set("catalog_mapping", \'{tmp}\', \'{}\') where "id" = %{id}' % {:id => self.id}) ActiveRecord::Base.logger = old_logger self.update_column(:catalog_updated_at, Time.now.utc) self.touch # DO NOT RETURN CATALOG. THIS IS NOT SCALABLE WITH LARGE CATALOGS. USE THE CATALOG_LOOKUP method provided return true end def new_session(session: self.data_lookup, username: self.access_token, password: self.refresh_token) @api_version = (username.include?("@") ? "v1" : "v2") @username = username @password = password @last_refresh = session["#{self.id}::last_refresh"] ## DEV MODE TASK DATA MOCKUP if ZuoraConnect.configuration.mode != "Production" self.options = ZuoraConnect.configuration.dev_mode_options ZuoraConnect.configuration.dev_mode_logins.each do |k,v| tmp = ZuoraConnect::Login.new(v) self.logins[k] = tmp self.attr_builder(k, tmp) end self.mode = ZuoraConnect.configuration.dev_mode_mode else if session.nil? || (!session.nil? && self.id != session["appInstance"].to_i) || session["#{self.id}::task_data"].blank? || ( session["#{self.id}::last_refresh"].blank? || session["#{self.id}::last_refresh"].to_i < ZuoraConnect.configuration.timeout.ago.to_i ) Rails.logger.debug("[#{self.id}] REFRESHING - Session Nil") if session.nil? Rails.logger.debug("[#{self.id}] REFRESHING - AppInstance ID(#{self.id}) does not match session id(#{session["appInstance"].to_i})") if (!session.nil? && self.id != session["appInstance"].to_i) Rails.logger.debug("[#{self.id}] REFRESHING - Task Data Blank") if session["#{self.id}::task_data"].blank? Rails.logger.debug("[#{self.id}] REFRESHING - No Time on Cookie") if session["#{self.id}::last_refresh"].blank? Rails.logger.debug("[#{self.id}] REFRESHING - Session Old") if (session["#{self.id}::last_refresh"].blank? || session["#{self.id}::last_refresh"].to_i < ZuoraConnect.configuration.timeout.ago.to_i ) self.refresh(session) else Rails.logger.debug("[#{self.id}] REBUILDING") build_task(session["#{self.id}::task_data"], session) end end I18n.locale = self.locale Time.zone = self.timezone @valid = true return self end def save_data(session = Hash.new) self.logins.each do |key, login| if login.tenant_type == "Zuora" if login.available_entities.size > 1 && Rails.application.config.session_store != ActionDispatch::Session::CookieStore login.available_entities.each do |entity_key| session["#{self.id}::#{key}::#{entity_key}:session"] = login.client(entity_key).current_session end else session["#{self.id}::#{key}:session"] = login.client.current_session end end end session["#{self.id}::task_data"] = self.task_data session["#{self.id}::last_refresh"] = self.last_refresh session["appInstance"] = self.id return session end def updateOption(optionId, value) if self.access_token && self.refresh_token if self.api_version == "v1" return HTTParty.get(ZuoraConnect.configuration.url + "/api/#{self.api_version}/tools/application_options/#{optionId}/edit?value=#{value}",:basic_auth => auth = {:username => self.username, :password => self.password}) else return HTTParty.get(ZuoraConnect.configuration.url + "/api/#{self.api_version}/tools/application_options/#{optionId}/edit?value=#{value}",:body => {:access_token => self.username}) end else return false end end #Example #{"name": "ftp_login_14","username": "ftplogin7","tenant_type": "Custom","password": "test2","url": "www.ftp.com","custom_data": { "path": "/var/usr/test"}} #This can update an existing login #This can add a new login #This can change to another existing login def update_logins(options) count ||= 0 response = HTTParty.post(ZuoraConnect.configuration.url + "/api/v2/tools/tasks/#{self.id}/logins",:body => {:access_token => self.username}.merge(options)) if response.code == 200 return JSON.parse(response.body) else raise ZuoraConnect::Exceptions::ConnectCommunicationError.new("Error Communicating with Connect", response.body, response.code) end rescue if count < 2 && response.code == 401 self.refresh_oauth count += 1 retry else raise end end def refresh(session = nil) count ||= 0 if self.api_version == "v1" response = HTTParty.get(ZuoraConnect.configuration.url + "/api/#{self.api_version}/tools/tasks/#{self.id}.json",:basic_auth => auth = {:username => self.username, :password => self.password}) else response = HTTParty.get(ZuoraConnect.configuration.url + "/api/#{self.api_version}/tools/tasks/#{self.id}.json",:body => {:access_token => self.access_token}) end if response.code == 200 @last_refresh = Time.now.to_i build_task(JSON.parse(response.body), session) else raise ZuoraConnect::Exceptions::ConnectCommunicationError.new("Error Communicating with Connect", response.body, response.code) end rescue if self.api_version == "v2" && count < 2 && response.code == 401 self.refresh_oauth count += 1 retry else raise end end def build_task(task_data, session) @task_data = task_data @mode = @task_data["mode"] @task_data.each do |k,v| if k.match(/^(.*)_login$/) tmp = ZuoraConnect::Login.new(v) if !session.nil? && v["tenant_type"] == "Zuora" if tmp.entities.size > 0 tmp.entities.each do |value| entity_id = value["id"] tmp.client(entity_id).current_session = session["#{self.id}::#{k}::#{entity_id}:session"] if !session.nil? && v["tenant_type"] == "Zuora" && session["#{self.id}::#{k}::#{entity_id}:session"] end else tmp.client.current_session = session["#{self.id}::#{k}:session"] if !session.nil? && v["tenant_type"] == "Zuora" && session["#{self.id}::#{k}:session"] end end @logins[k] = tmp self.attr_builder(k, @logins[k]) elsif k == "options" v.each do |opt| @options[opt["config_name"]] = opt end elsif k == "user_settings" self.timezone = v["timezone"] self.locale = v["local"] end end end def send_email end def upload_to_s3(local_file,s3_path = nil) s3_path = local_file.split("/").last if s3_path.nil? obj = self.s3_client.bucket(ZuoraConnect.configuration.s3_bucket_name).object("#{ZuoraConnect.configuration.s3_folder_name}/#{self.id.to_s}/#{s3_path}}") obj.upload_file(local_file) end def get_s3_file_url(key) signer = Aws::S3::Presigner.new(client: self.s3_client) url = signer.presigned_url(:get_object, bucket: ZuoraConnect.configuration.s3_bucket_name, key: "#{ZuoraConnect.configuration.s3_folder_name}/#{self.id.to_s}/#{key}") end def s3_client if ZuoraConnect.configuration.mode == "Development" @s3_client ||= Aws::S3::Resource.new(region: ZuoraConnect.configuration.aws_region,access_key_id: ZuoraConnect.configuration.dev_mode_access_key_id,secret_access_key: ZuoraConnect.configuration.dev_mode_secret_access_key) else @s3_client ||= Aws::S3::Resource.new(region: ZuoraConnect.configuration.aws_region) end end def self.decrypt_response(resp) OpenSSL::PKey::RSA.new(ZuoraConnect.configuration.private_key).private_decrypt(resp) end def refresh_oauth count ||= 0 Rails.logger.debug("[#{self.id}] REFRESHING - OAuth") params = { :grant_type => "refresh_token", :redirect_uri => ZuoraConnect.configuration.oauth_client_redirect_uri, :refresh_token => self.refresh_token } response = HTTParty.post("#{ZuoraConnect.configuration.url}/oauth/token",:body => params) response_body = JSON.parse(response.body) if response.code == 200 self.refresh_token = response_body["refresh_token"] self.access_token = response_body["access_token"] self.oauth_expires_at = Time.at(response_body["created_at"].to_i) + response_body["expires_in"].seconds self.save else Rails.logger.debug("REFRESHING - OAuth Failed - Code #{response.code}") raise ZuoraConnect::Exceptions::ConnectCommunicationError.new("Error Refreshing Access Token", response.body, response.code) end rescue if count < 3 Rails.logger.debug("REFRESHING - OAuth Failed - Retrying(#{count})") self.reload sleep(5) count += 1 retry else Rails.logger.fatal("REFRESHING - OAuth Failed") raise end end def oauth_expired? (expires_at < Time.now) end def attr_builder(field,val) singleton_class.class_eval { attr_accessor "#{field}" } send("#{field}=", val) end end end