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