app/models/zuora_connect/app_instance_base.rb



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: {})
      return session
    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

    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