- Refactored clearbit enrichment backend to use dedicated objects for syncing and extracted mapping logic to ExternalSync.
- Replaced String keys with Symbols to reduce memory load.
- Added tests 🚀
- Added basic Group factory. Groups simplicity is perfect for usage in tests which need a simple model.
This commit is contained in:
parent
aa6ba57cde
commit
bd690f6039
7 changed files with 586 additions and 314 deletions
|
@ -2,4 +2,83 @@
|
||||||
|
|
||||||
class ExternalSync < ApplicationModel
|
class ExternalSync < ApplicationModel
|
||||||
store :last_payload
|
store :last_payload
|
||||||
|
|
||||||
|
class << self
|
||||||
|
|
||||||
|
def changed?(object:, previous_changes: {}, current_changes:)
|
||||||
|
changed = false
|
||||||
|
previous_changes ||= {}
|
||||||
|
current_changes.each { |attribute, value|
|
||||||
|
next if !object.attributes.key?(attribute.to_s)
|
||||||
|
next if object[attribute] == value
|
||||||
|
next if object[attribute].present? && object[attribute] != previous_changes[attribute]
|
||||||
|
|
||||||
|
begin
|
||||||
|
object[attribute] = value
|
||||||
|
changed ||= true
|
||||||
|
rescue => e
|
||||||
|
Rails.logger.error "ERROR: Unable to assign attribute #{attribute} to object #{object.class.name}: #{e.inspect}"
|
||||||
|
end
|
||||||
|
}
|
||||||
|
changed
|
||||||
|
end
|
||||||
|
|
||||||
|
def map(mapping: {}, source:)
|
||||||
|
|
||||||
|
information_source = if source.is_a?(Hash)
|
||||||
|
source.deep_symbolize_keys
|
||||||
|
else
|
||||||
|
source.clone
|
||||||
|
end
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
mapping.each { |remote_key, local_key|
|
||||||
|
|
||||||
|
local_key_sym = local_key.to_sym
|
||||||
|
|
||||||
|
next if result[local_key_sym].present?
|
||||||
|
value = extract(remote_key, information_source)
|
||||||
|
next if value.blank?
|
||||||
|
result[local_key_sym] = value
|
||||||
|
}
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def extract(remote_key, structure)
|
||||||
|
return if !structure
|
||||||
|
|
||||||
|
information_source = structure.clone
|
||||||
|
result = nil
|
||||||
|
information_path = remote_key.split('.')
|
||||||
|
information_path.each do |segment|
|
||||||
|
|
||||||
|
segment_sym = segment.to_sym
|
||||||
|
|
||||||
|
if information_source.is_a?(Hash)
|
||||||
|
value = information_source[segment_sym]
|
||||||
|
elsif information_source.respond_to?(segment_sym)
|
||||||
|
# prevent accessing non-attributes (e.g. destroy)
|
||||||
|
if information_source.respond_to?(:attributes)
|
||||||
|
break if !information_source.attributes.key?(segment)
|
||||||
|
end
|
||||||
|
value = information_source.send(segment_sym)
|
||||||
|
end
|
||||||
|
break if !value
|
||||||
|
|
||||||
|
storable = value.class.ancestors.any? do |ancestor|
|
||||||
|
%w(String Integer Float Bool).include?(ancestor.to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
if storable
|
||||||
|
result = value
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
information_source = value
|
||||||
|
end
|
||||||
|
result
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -25,10 +25,8 @@ class Transaction::ClearbitEnrichment
|
||||||
|
|
||||||
# return if we run import mode
|
# return if we run import mode
|
||||||
return if Setting.get('import_mode')
|
return if Setting.get('import_mode')
|
||||||
|
|
||||||
return if @item[:object] != 'User'
|
return if @item[:object] != 'User'
|
||||||
return if @item[:type] != 'create'
|
return if @item[:type] != 'create'
|
||||||
|
|
||||||
return if !Setting.get('clearbit_integration')
|
return if !Setting.get('clearbit_integration')
|
||||||
|
|
||||||
config = Setting.get('clearbit_config')
|
config = Setting.get('clearbit_config')
|
||||||
|
@ -38,317 +36,10 @@ class Transaction::ClearbitEnrichment
|
||||||
user = User.lookup(id: @item[:object_id])
|
user = User.lookup(id: @item[:object_id])
|
||||||
return if !user
|
return if !user
|
||||||
|
|
||||||
Transaction::ClearbitEnrichment.sync_user(user)
|
user_enrichment = Enrichment::Clearbit::User.new(user)
|
||||||
end
|
return if !user_enrichment.synced?
|
||||||
|
|
||||||
=begin
|
|
||||||
|
|
||||||
sync all users against clearbit
|
|
||||||
|
|
||||||
Transaction::ClearbitEnrichment.sync
|
|
||||||
|
|
||||||
=end
|
|
||||||
|
|
||||||
def self.sync
|
|
||||||
users = User.of_role(Role.signup_roles)
|
|
||||||
users.each { |user|
|
|
||||||
sync_user(user)
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
=begin
|
|
||||||
|
|
||||||
sync one users against clearbit
|
|
||||||
|
|
||||||
Transaction::ClearbitEnrichment.sync_user(user)
|
|
||||||
|
|
||||||
users = [...]
|
|
||||||
|
|
||||||
users.each {|user|
|
|
||||||
Transaction::ClearbitEnrichment.sync_user(user)
|
|
||||||
}
|
|
||||||
|
|
||||||
=end
|
|
||||||
|
|
||||||
def self.sync_user(user)
|
|
||||||
UserInfo.current_user_id = 1
|
|
||||||
|
|
||||||
return if user.email.empty?
|
|
||||||
data = fetch(user.email)
|
|
||||||
#p 'OO: ' + data.inspect
|
|
||||||
return if !data
|
|
||||||
|
|
||||||
config = Setting.get('clearbit_config')
|
|
||||||
return if !config
|
|
||||||
|
|
||||||
# get new user sync attributes
|
|
||||||
user_sync = config['user_sync']
|
|
||||||
user_sync_values = {}
|
|
||||||
if user_sync
|
|
||||||
user_sync.each { |callback, destination|
|
|
||||||
next if !user_sync_values[destination].empty?
|
|
||||||
value = _replace(callback, data)
|
|
||||||
next if !value
|
|
||||||
user_sync_values[destination] = value
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
# get new organization sync attributes
|
|
||||||
organization_sync = config['organization_sync']
|
|
||||||
organization_sync_values = {}
|
|
||||||
if organization_sync
|
|
||||||
organization_sync.each { |callback, destination|
|
|
||||||
next if !organization_sync_values[destination].empty?
|
|
||||||
value = _replace(callback, data)
|
|
||||||
next if !value
|
|
||||||
organization_sync_values[destination] = value
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
# get latest user synced attributes
|
|
||||||
external_syn_user = nil
|
|
||||||
user_sync_values_last_time = {}
|
|
||||||
if data && data['person'] && data['person']['id']
|
|
||||||
external_syn_user = ExternalSync.find_by(
|
|
||||||
source: 'clearbit',
|
|
||||||
source_id: data['person']['id'],
|
|
||||||
object: 'User',
|
|
||||||
o_id: user.id,
|
|
||||||
)
|
|
||||||
if external_syn_user && external_syn_user.last_payload
|
|
||||||
user_sync.each { |callback, destination|
|
|
||||||
next if !user_sync_values_last_time[destination].empty?
|
|
||||||
value = _replace(callback, external_syn_user.last_payload)
|
|
||||||
next if !value
|
|
||||||
user_sync_values_last_time[destination] = value
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# if person record exists
|
|
||||||
user_has_changed = false
|
|
||||||
user_sync_values.each { |destination, value|
|
|
||||||
attribute = destination.sub(/^user\./, '')
|
|
||||||
next if user[attribute] == value
|
|
||||||
next if !user[attribute].empty? && user_sync_values_last_time[destination] != user[attribute]
|
|
||||||
begin
|
|
||||||
user[attribute] = value
|
|
||||||
rescue => e
|
|
||||||
Rails.logger.error "ERROR: Unable to assign user.#{attribute}: #{e.inspect}"
|
|
||||||
end
|
|
||||||
user_has_changed = true
|
|
||||||
}
|
|
||||||
if user_has_changed
|
|
||||||
user.updated_by_id = 1
|
|
||||||
if data['person'] && data['person']['id']
|
|
||||||
if external_syn_user
|
|
||||||
external_syn_user.last_payload = data
|
|
||||||
external_syn_user.save
|
|
||||||
else
|
|
||||||
external_syn_user = ExternalSync.create(
|
|
||||||
source: 'clearbit',
|
|
||||||
source_id: data['person']['id'],
|
|
||||||
object: 'User',
|
|
||||||
o_id: user.id,
|
|
||||||
last_payload: data,
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# if no company record exists or no organization should be created
|
|
||||||
if !data['company'] || config['organization_autocreate'] != true
|
|
||||||
if user_has_changed
|
|
||||||
user.save
|
|
||||||
end
|
|
||||||
Observer::Transaction.commit
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
# if company record exists
|
|
||||||
external_syn_organization = ExternalSync.find_by(
|
|
||||||
source: 'clearbit',
|
|
||||||
source_id: data['company']['id'],
|
|
||||||
)
|
|
||||||
|
|
||||||
# create new organization
|
|
||||||
if !external_syn_organization
|
|
||||||
|
|
||||||
# if organization is already assigned, do not create a new one
|
|
||||||
if user.organization_id
|
|
||||||
if user_has_changed
|
|
||||||
user.save
|
|
||||||
Observer::Transaction.commit
|
|
||||||
end
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
# can't create organization without name
|
|
||||||
if organization_sync_values['organization.name'].empty?
|
|
||||||
Observer::Transaction.commit
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
# find by name
|
|
||||||
organization = Organization.find_by(name: organization_sync_values['organization.name'])
|
|
||||||
|
|
||||||
# create new organization
|
|
||||||
if !organization
|
|
||||||
organization = Organization.new(
|
|
||||||
shared: config['organization_shared'],
|
|
||||||
)
|
|
||||||
organization_sync_values.each { |destination, value|
|
|
||||||
attribute = destination.sub(/^organization\./, '')
|
|
||||||
next if !organization[attribute].empty?
|
|
||||||
begin
|
|
||||||
organization[attribute] = value
|
|
||||||
rescue => e
|
|
||||||
Rails.logger.error "ERROR: Unable to assign organization.#{attribute}: #{e.inspect}"
|
|
||||||
end
|
|
||||||
}
|
|
||||||
organization.save
|
|
||||||
end
|
|
||||||
ExternalSync.create(
|
|
||||||
source: 'clearbit',
|
|
||||||
source_id: data['company']['id'],
|
|
||||||
object: 'Organization',
|
|
||||||
o_id: organization.id,
|
|
||||||
last_payload: data,
|
|
||||||
)
|
|
||||||
|
|
||||||
# assign new organization to user
|
|
||||||
if !user.organization_id
|
|
||||||
user.organization_id = organization.id
|
|
||||||
user.save
|
|
||||||
end
|
|
||||||
Observer::Transaction.commit
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
# get latest organization synced attributes
|
|
||||||
organization_sync_values_last_time = {}
|
|
||||||
if external_syn_organization && external_syn_organization.last_payload
|
|
||||||
organization_sync.each { |callback, destination|
|
|
||||||
next if !organization_sync_values_last_time[destination].empty?
|
|
||||||
value = _replace(callback, external_syn_organization.last_payload)
|
|
||||||
next if !value
|
|
||||||
organization_sync_values_last_time[destination] = value
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
# update existing organization
|
|
||||||
organization = Organization.find(external_syn_organization[:o_id])
|
|
||||||
organization_has_changed = false
|
|
||||||
organization_sync_values.each { |destination, value|
|
|
||||||
attribute = destination.sub(/^organization\./, '')
|
|
||||||
next if organization[attribute] == value
|
|
||||||
next if !organization[attribute].empty? && organization_sync_values_last_time[destination] != organization[attribute]
|
|
||||||
begin
|
|
||||||
organization[attribute] = value
|
|
||||||
rescue => e
|
|
||||||
Rails.logger.error "ERROR: Unable to assign organization.#{attribute}: #{e.inspect}"
|
|
||||||
end
|
|
||||||
organization_has_changed = true
|
|
||||||
}
|
|
||||||
if organization_has_changed
|
|
||||||
organization.updated_by_id = 1
|
|
||||||
organization.save
|
|
||||||
external_syn_organization.last_payload = data
|
|
||||||
external_syn_organization.save
|
|
||||||
end
|
|
||||||
|
|
||||||
# assign new organization to user
|
|
||||||
if !user.organization_id
|
|
||||||
user_has_changed = true
|
|
||||||
user.organization_id = organization.id
|
|
||||||
end
|
|
||||||
|
|
||||||
if user_has_changed
|
|
||||||
user.save
|
|
||||||
end
|
|
||||||
|
|
||||||
Observer::Transaction.commit
|
Observer::Transaction.commit
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
def self._replace(callback, data)
|
|
||||||
object_name = nil
|
|
||||||
object_method = nil
|
|
||||||
placeholder = nil
|
|
||||||
|
|
||||||
if callback =~ /\A ( [\w]+ )\.( [\w\.]+ ) \z/x
|
|
||||||
object_name = $1
|
|
||||||
object_method = $2
|
|
||||||
end
|
|
||||||
|
|
||||||
return if !data
|
|
||||||
return if !data[object_name]
|
|
||||||
|
|
||||||
# do validaton, ignore some methodes
|
|
||||||
if callback =~ /(`|\.(|\s*)(save|destroy|delete|remove|drop|update\(|update_att|create\(|new|all|where|find))/i
|
|
||||||
placeholder = "#{callback} (not allowed)"
|
|
||||||
|
|
||||||
# get value based on object_name and object_method
|
|
||||||
elsif object_name && object_method
|
|
||||||
object_refs = data[object_name]
|
|
||||||
object_methods = object_method.split('.')
|
|
||||||
object_methods_s = ''
|
|
||||||
object_methods.each { |method|
|
|
||||||
if object_methods_s != ''
|
|
||||||
object_methods_s += '.'
|
|
||||||
end
|
|
||||||
object_methods_s += method
|
|
||||||
|
|
||||||
# if method exists
|
|
||||||
break if !object_refs.respond_to?(method.to_sym) && !object_refs[method]
|
|
||||||
|
|
||||||
object_refs = if object_refs.respond_to?(method.to_sym)
|
|
||||||
object_refs.send(method.to_sym)
|
|
||||||
else
|
|
||||||
object_refs[method]
|
|
||||||
end
|
|
||||||
}
|
|
||||||
if object_refs.class == String
|
|
||||||
placeholder = object_refs
|
|
||||||
end
|
|
||||||
end
|
|
||||||
placeholder
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.fetch(email)
|
|
||||||
if !Rails.env.production?
|
|
||||||
filename = "#{Rails.root}/test/fixtures/clearbit/#{email}.json"
|
|
||||||
if File.exist?(filename)
|
|
||||||
data = IO.binread(filename)
|
|
||||||
return JSON.parse(data) if data
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
config = Setting.get('clearbit_config')
|
|
||||||
return if !config
|
|
||||||
return if config['api_key'].empty?
|
|
||||||
|
|
||||||
record = {
|
|
||||||
direction: 'out',
|
|
||||||
facility: 'clearbit',
|
|
||||||
url: "clearbit -> #{email}",
|
|
||||||
status: 200,
|
|
||||||
ip: nil,
|
|
||||||
request: { content: email },
|
|
||||||
response: {},
|
|
||||||
method: 'GET',
|
|
||||||
}
|
|
||||||
|
|
||||||
begin
|
|
||||||
Clearbit.key = config['api_key']
|
|
||||||
result = Clearbit::Enrichment.find(email: email, stream: true)
|
|
||||||
record[:response] = { code: 200, content: result.to_s }
|
|
||||||
rescue => e
|
|
||||||
record[:status] = 500
|
|
||||||
record[:response] = { code: 500, content: e.inspect }
|
|
||||||
end
|
|
||||||
HttpLog.create(record)
|
|
||||||
result
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
144
lib/enrichment/clearbit/organization.rb
Normal file
144
lib/enrichment/clearbit/organization.rb
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
module Enrichment
|
||||||
|
module Clearbit
|
||||||
|
class Organization
|
||||||
|
|
||||||
|
def initialize(user:, payload:)
|
||||||
|
@user = user
|
||||||
|
@payload = payload
|
||||||
|
@source = 'clearbit'
|
||||||
|
@config = Setting.get('clearbit_config')
|
||||||
|
@object = 'Organization'
|
||||||
|
end
|
||||||
|
|
||||||
|
def synced?
|
||||||
|
return false if !@config
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
UserInfo.current_user_id = 1
|
||||||
|
|
||||||
|
return false if !mapping?
|
||||||
|
return false if !changes?
|
||||||
|
|
||||||
|
# create new organization
|
||||||
|
return organization_created? if !remote_id? || !external_found?
|
||||||
|
|
||||||
|
# update existing organization
|
||||||
|
organization = existing_organization
|
||||||
|
|
||||||
|
return true if @user.organization_id
|
||||||
|
|
||||||
|
# assign new organization to user
|
||||||
|
update_user(organization)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def mapping?
|
||||||
|
@mapping = @config['organization_sync'].dup
|
||||||
|
return false if @mapping.blank?
|
||||||
|
# TODO: Refactoring:
|
||||||
|
# Currently all target keys are prefixed with
|
||||||
|
# organization.
|
||||||
|
# which is not necessary since the target object
|
||||||
|
# is allways an organization
|
||||||
|
@mapping.transform_values! { |value| value.sub('organization.', '') }
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def changes?
|
||||||
|
@current_changes = ExternalSync.map(
|
||||||
|
mapping: @mapping,
|
||||||
|
source: @payload
|
||||||
|
)
|
||||||
|
@current_changes.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def remote_id?
|
||||||
|
return if !@payload['company']
|
||||||
|
@remote_id = @payload['company']['id']
|
||||||
|
end
|
||||||
|
|
||||||
|
def external_found?
|
||||||
|
return true if @external_organization
|
||||||
|
@external_organization = ExternalSync.find_by(
|
||||||
|
source: @source,
|
||||||
|
source_id: @remote_id,
|
||||||
|
object: @object,
|
||||||
|
)
|
||||||
|
@external_organization.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def organization_created?
|
||||||
|
|
||||||
|
# if organization is already assigned, do not create a new one
|
||||||
|
return false if @user.organization_id
|
||||||
|
|
||||||
|
# can't create organization without name
|
||||||
|
return false if @current_changes[:name].empty?
|
||||||
|
|
||||||
|
organization = create_current
|
||||||
|
|
||||||
|
# assign new organization to user
|
||||||
|
update_user(organization)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_current
|
||||||
|
organization = ::Organization.find_by(name: @current_changes[:name])
|
||||||
|
|
||||||
|
return organization if organization
|
||||||
|
|
||||||
|
organization = ::Organization.new(
|
||||||
|
shared: @config['organization_shared'],
|
||||||
|
)
|
||||||
|
|
||||||
|
return organization if !ExternalSync.changed?(
|
||||||
|
object: organization,
|
||||||
|
current_changes: @current_changes,
|
||||||
|
)
|
||||||
|
organization.save
|
||||||
|
|
||||||
|
ExternalSync.create(
|
||||||
|
source: @source,
|
||||||
|
source_id: @remote_id,
|
||||||
|
object: @object,
|
||||||
|
o_id: organization.id,
|
||||||
|
last_payload: @payload,
|
||||||
|
)
|
||||||
|
organization
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_previous_changes
|
||||||
|
last_payload = @external_organization.last_payload
|
||||||
|
return if !last_payload
|
||||||
|
@previous_changes = ExternalSync.map(
|
||||||
|
mapping: @mapping,
|
||||||
|
source: last_payload
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def existing_organization
|
||||||
|
load_previous_changes
|
||||||
|
|
||||||
|
organization = ::Organization.find(@external_organization[:o_id])
|
||||||
|
return organization if !ExternalSync.changed?(
|
||||||
|
object: organization,
|
||||||
|
previous_changes: @previous_changes,
|
||||||
|
current_changes: @current_changes,
|
||||||
|
)
|
||||||
|
|
||||||
|
organization.updated_by_id = 1
|
||||||
|
organization.save
|
||||||
|
|
||||||
|
@external_organization.last_payload = @payload
|
||||||
|
@external_organization.save
|
||||||
|
|
||||||
|
organization
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_user(organization)
|
||||||
|
@user.organization_id = organization.id
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
166
lib/enrichment/clearbit/user.rb
Normal file
166
lib/enrichment/clearbit/user.rb
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
module Enrichment
|
||||||
|
module Clearbit
|
||||||
|
class User
|
||||||
|
|
||||||
|
def initialize(user)
|
||||||
|
@local_user = user
|
||||||
|
@source = 'clearbit'
|
||||||
|
@config = Setting.get('clearbit_config')
|
||||||
|
end
|
||||||
|
|
||||||
|
def synced?
|
||||||
|
return false if !@config
|
||||||
|
return false if @local_user.email.blank?
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
UserInfo.current_user_id = 1
|
||||||
|
|
||||||
|
return false if !mapping?
|
||||||
|
|
||||||
|
payload = fetch
|
||||||
|
return false if !payload
|
||||||
|
|
||||||
|
attributes_changed = attributes_changed?(payload)
|
||||||
|
|
||||||
|
organization_synced = false
|
||||||
|
if payload['company'] && @config['organization_autocreate']
|
||||||
|
organization = Enrichment::Clearbit::Organization.new(
|
||||||
|
user: @local_user,
|
||||||
|
payload: payload
|
||||||
|
)
|
||||||
|
organization_synced = organization.synced?
|
||||||
|
end
|
||||||
|
|
||||||
|
return false if !attributes_changed && !organization_synced
|
||||||
|
@local_user.save if attributes_changed || organization_synced
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def mapping?
|
||||||
|
@mapping = @config['user_sync'].dup
|
||||||
|
return false if @mapping.blank?
|
||||||
|
# TODO: Refactoring:
|
||||||
|
# Currently all target keys are prefixed with
|
||||||
|
# user.
|
||||||
|
# which is not necessary since the target object
|
||||||
|
# is allways an user
|
||||||
|
@mapping.transform_values! { |value| value.sub('user.', '') }
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_remote(data)
|
||||||
|
return if !remote_id?(data)
|
||||||
|
return if !external_found?
|
||||||
|
load_previous_changes
|
||||||
|
end
|
||||||
|
|
||||||
|
def remote_id?(data)
|
||||||
|
return if !data
|
||||||
|
return if !data['person']
|
||||||
|
@remote_id = data['person']['id']
|
||||||
|
end
|
||||||
|
|
||||||
|
def external_found?
|
||||||
|
return true if @external_user
|
||||||
|
@external_user = ExternalSync.find_by(
|
||||||
|
source: @source,
|
||||||
|
source_id: @remote_id,
|
||||||
|
object: @local_user.class.name,
|
||||||
|
o_id: @local_user.id,
|
||||||
|
)
|
||||||
|
@external_user.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_previous_changes
|
||||||
|
last_payload = @external_user.last_payload
|
||||||
|
return if !last_payload
|
||||||
|
@previous_changes = ExternalSync.map(
|
||||||
|
mapping: @mapping,
|
||||||
|
source: last_payload
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def attributes_changed?(payload)
|
||||||
|
current_changes = ExternalSync.map(
|
||||||
|
mapping: @mapping,
|
||||||
|
source: payload
|
||||||
|
)
|
||||||
|
|
||||||
|
return false if !current_changes
|
||||||
|
|
||||||
|
previous_changes = load_remote(payload)
|
||||||
|
|
||||||
|
return false if !ExternalSync.changed?(
|
||||||
|
object: @local_user,
|
||||||
|
previous_changes: previous_changes,
|
||||||
|
current_changes: current_changes,
|
||||||
|
)
|
||||||
|
|
||||||
|
@local_user.updated_by_id = 1
|
||||||
|
return true if !@remote_id
|
||||||
|
|
||||||
|
store_current(payload)
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def store_current(payload)
|
||||||
|
if !external_found?
|
||||||
|
@external_user = ExternalSync.new(
|
||||||
|
source: @source,
|
||||||
|
source_id: @remote_id,
|
||||||
|
object: @local_user.class.name,
|
||||||
|
o_id: @local_user.id,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
@external_user.last_payload = payload
|
||||||
|
@external_user.save
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch
|
||||||
|
if !Rails.env.production?
|
||||||
|
filename = "#{Rails.root}/test/fixtures/clearbit/#{@local_user.email}.json"
|
||||||
|
if File.exist?(filename)
|
||||||
|
data = IO.binread(filename)
|
||||||
|
return JSON.parse(data) if data
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return if @config['api_key'].empty?
|
||||||
|
|
||||||
|
record = {
|
||||||
|
direction: 'out',
|
||||||
|
facility: 'clearbit',
|
||||||
|
url: "clearbit -> #{@local_user.email}",
|
||||||
|
status: 200,
|
||||||
|
ip: nil,
|
||||||
|
request: { content: @local_user.email },
|
||||||
|
response: {},
|
||||||
|
method: 'GET',
|
||||||
|
}
|
||||||
|
|
||||||
|
begin
|
||||||
|
::Clearbit.key = @config['api_key']
|
||||||
|
result = ::Clearbit::Enrichment.find(email: @local_user.email, stream: true)
|
||||||
|
record[:response] = { code: 200, content: result.to_s }
|
||||||
|
rescue => e
|
||||||
|
record[:status] = 500
|
||||||
|
record[:response] = { code: 500, content: e.inspect }
|
||||||
|
end
|
||||||
|
HttpLog.create(record)
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
class << self
|
||||||
|
|
||||||
|
def all
|
||||||
|
users = User.of_role(Role.signup_roles)
|
||||||
|
users.each { |user|
|
||||||
|
new(user).synced?
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
175
spec/external_sync_spec.rb
Normal file
175
spec/external_sync_spec.rb
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe ExternalSync do
|
||||||
|
|
||||||
|
context '#changed?' do
|
||||||
|
|
||||||
|
it 'keeps ActiveRecord instance unchanged on local but no remote changes' do
|
||||||
|
object = create(:group)
|
||||||
|
previous_changes = { name: 'Changed' }
|
||||||
|
current_changes = previous_changes.dup
|
||||||
|
|
||||||
|
result = described_class.changed?(
|
||||||
|
object: object,
|
||||||
|
previous_changes: previous_changes,
|
||||||
|
current_changes: current_changes,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).to be false
|
||||||
|
expect(object.changed?).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'keeps ActiveRecord instance unchanged on local and remote changes' do
|
||||||
|
object = create(:group)
|
||||||
|
previous_changes = { name: 'Initial' }
|
||||||
|
current_changes = { name: 'Changed' }
|
||||||
|
|
||||||
|
result = described_class.changed?(
|
||||||
|
object: object,
|
||||||
|
previous_changes: previous_changes,
|
||||||
|
current_changes: current_changes,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).to be false
|
||||||
|
expect(object.changed?).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'changes ActiveRecord instance attribute(s) for remote changes' do
|
||||||
|
object = create(:group)
|
||||||
|
previous_changes = { name: object.name }
|
||||||
|
current_changes = { name: 'Changed' }
|
||||||
|
|
||||||
|
result = described_class.changed?(
|
||||||
|
object: object,
|
||||||
|
previous_changes: previous_changes,
|
||||||
|
current_changes: current_changes,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).to be true
|
||||||
|
expect(object.changed?).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'prevents ActiveRecord method calls' do
|
||||||
|
|
||||||
|
object = create(:group)
|
||||||
|
previous_changes = { name: object.name }
|
||||||
|
current_changes = { destroy: 'Changed' }
|
||||||
|
|
||||||
|
result = described_class.changed?(
|
||||||
|
object: object,
|
||||||
|
previous_changes: previous_changes,
|
||||||
|
current_changes: current_changes,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).to be false
|
||||||
|
expect(object.changed?).to be false
|
||||||
|
expect(object.destroyed?).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
context '#map' do
|
||||||
|
it 'maps to symbol keys' do
|
||||||
|
mapping = {
|
||||||
|
'key' => 'key'
|
||||||
|
}
|
||||||
|
|
||||||
|
source = {
|
||||||
|
'key' => 'value'
|
||||||
|
}
|
||||||
|
|
||||||
|
result = {
|
||||||
|
key: 'value'
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(described_class.map(mapping: mapping, source: source)).to eq(result)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'resolves deep structures' do
|
||||||
|
mapping = {
|
||||||
|
'sub.structure.key' => 'key',
|
||||||
|
}
|
||||||
|
|
||||||
|
source = {
|
||||||
|
'sub' => {
|
||||||
|
'structure' => {
|
||||||
|
'key' => 'value'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = {
|
||||||
|
key: 'value'
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(described_class.map(mapping: mapping, source: source)).to eq(result)
|
||||||
|
|
||||||
|
# check if sub structure is untouched
|
||||||
|
expect(source['sub'].key?('structure')).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'skips irrelevant keys' do
|
||||||
|
mapping = {
|
||||||
|
'key' => 'key'
|
||||||
|
}
|
||||||
|
|
||||||
|
source = {
|
||||||
|
'key' => 'value',
|
||||||
|
'skipped' => 'skipped'
|
||||||
|
}
|
||||||
|
|
||||||
|
result = {
|
||||||
|
key: 'value'
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(described_class.map(mapping: mapping, source: source)).to eq(result)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'can handle object instances' do
|
||||||
|
|
||||||
|
mapping = {
|
||||||
|
'name' => 'key'
|
||||||
|
}
|
||||||
|
|
||||||
|
source = double(name: 'value')
|
||||||
|
|
||||||
|
result = {
|
||||||
|
key: 'value'
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(described_class.map(mapping: mapping, source: source)).to eq(result)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'can handle ActiveRecord instances' do
|
||||||
|
|
||||||
|
mapping = {
|
||||||
|
'name' => 'key'
|
||||||
|
}
|
||||||
|
|
||||||
|
source = create(:group, name: 'value')
|
||||||
|
|
||||||
|
result = {
|
||||||
|
key: 'value'
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(described_class.map(mapping: mapping, source: source)).to eq(result)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'prevents ActiveRecord method calls' do
|
||||||
|
|
||||||
|
mapping = {
|
||||||
|
'name' => 'key',
|
||||||
|
'destroy' => 'evil'
|
||||||
|
}
|
||||||
|
|
||||||
|
source = create(:group, name: 'value')
|
||||||
|
|
||||||
|
result = {
|
||||||
|
key: 'value'
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(described_class.map(mapping: mapping, source: source)).to eq(result)
|
||||||
|
expect(source.destroyed?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
14
spec/factories/group.rb
Normal file
14
spec/factories/group.rb
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
FactoryGirl.define do
|
||||||
|
sequence :test_group_name do |n|
|
||||||
|
"TestGroup#{n}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
FactoryGirl.define do
|
||||||
|
|
||||||
|
factory :group do
|
||||||
|
name { generate(:test_group_name) }
|
||||||
|
created_by_id 1
|
||||||
|
updated_by_id 1
|
||||||
|
end
|
||||||
|
end
|
|
@ -110,7 +110,8 @@ class ClearbitTest < ActiveSupport::TestCase
|
||||||
assert_equal('changed by my self', customer2_lookup.note)
|
assert_equal('changed by my self', customer2_lookup.note)
|
||||||
assert_equal('Norsk-Data-Straße 1, 61352 Bad Homburg vor der Höhe, Germany', customer2_lookup.address)
|
assert_equal('Norsk-Data-Straße 1, 61352 Bad Homburg vor der Höhe, Germany', customer2_lookup.address)
|
||||||
|
|
||||||
Transaction::ClearbitEnrichment.sync_user(customer2)
|
customer2_enrichment = Enrichment::Clearbit::User.new(customer2)
|
||||||
|
customer2_enrichment.synced?
|
||||||
Scheduler.worker(true)
|
Scheduler.worker(true)
|
||||||
|
|
||||||
customer2_lookup = User.lookup(id: customer2.id)
|
customer2_lookup = User.lookup(id: customer2.id)
|
||||||
|
@ -126,7 +127,8 @@ class ClearbitTest < ActiveSupport::TestCase
|
||||||
note: 'changed by my self',
|
note: 'changed by my self',
|
||||||
)
|
)
|
||||||
|
|
||||||
Transaction::ClearbitEnrichment.sync_user(customer2)
|
customer2_enrichment = Enrichment::Clearbit::User.new(customer2)
|
||||||
|
customer2_enrichment.synced?
|
||||||
Scheduler.worker(true)
|
Scheduler.worker(true)
|
||||||
|
|
||||||
customer2_lookup = User.lookup(id: customer2.id)
|
customer2_lookup = User.lookup(id: customer2.id)
|
||||||
|
@ -141,7 +143,8 @@ class ClearbitTest < ActiveSupport::TestCase
|
||||||
email: 'me2@example.com',
|
email: 'me2@example.com',
|
||||||
)
|
)
|
||||||
|
|
||||||
Transaction::ClearbitEnrichment.sync_user(customer2)
|
customer2_enrichment = Enrichment::Clearbit::User.new(customer2)
|
||||||
|
customer2_enrichment.synced?
|
||||||
Scheduler.worker(true)
|
Scheduler.worker(true)
|
||||||
|
|
||||||
customer2_lookup = User.lookup(id: customer2.id)
|
customer2_lookup = User.lookup(id: customer2.id)
|
||||||
|
|
Loading…
Reference in a new issue