Restructure omni services and add Chatwoot research snapshot

This commit is contained in:
Ruslan Bakiev
2026-02-21 11:11:27 +07:00
parent edea7a0034
commit b73babbbf6
7732 changed files with 978203 additions and 32 deletions

View File

@@ -0,0 +1,44 @@
class Contacts::BulkActionService
def initialize(account:, user:, params:)
@account = account
@user = user
@params = params.deep_symbolize_keys
end
def perform
return delete_contacts if delete_requested?
return assign_labels if labels_to_add.any?
Rails.logger.warn("Unknown contact bulk operation payload: #{@params.keys}")
{ success: false, error: 'unknown_operation' }
end
private
def assign_labels
Contacts::BulkAssignLabelsService.new(
account: @account,
contact_ids: ids,
labels: labels_to_add
).perform
end
def delete_contacts
Contacts::BulkDeleteService.new(
account: @account,
contact_ids: ids
).perform
end
def ids
Array(@params[:ids]).compact
end
def labels_to_add
@labels_to_add ||= Array(@params.dig(:labels, :add)).reject(&:blank?)
end
def delete_requested?
@params[:action_name] == 'delete'
end
end

View File

@@ -0,0 +1,19 @@
class Contacts::BulkAssignLabelsService
def initialize(account:, contact_ids:, labels:)
@account = account
@contact_ids = Array(contact_ids)
@labels = Array(labels).compact_blank
end
def perform
return { success: true, updated_contact_ids: [] } if @contact_ids.blank? || @labels.blank?
contacts = @account.contacts.where(id: @contact_ids)
contacts.find_each do |contact|
contact.add_labels(@labels)
end
{ success: true, updated_contact_ids: contacts.pluck(:id) }
end
end

View File

@@ -0,0 +1,18 @@
class Contacts::BulkDeleteService
def initialize(account:, contact_ids: [])
@account = account
@contact_ids = Array(contact_ids).compact
end
def perform
return if @contact_ids.blank?
contacts.find_each(&:destroy!)
end
private
def contacts
@account.contacts.where(id: @contact_ids)
end
end

View File

@@ -0,0 +1,75 @@
class Contacts::ContactableInboxesService
pattr_initialize [:contact!]
def get
account = contact.account
account.inboxes.filter_map { |inbox| get_contactable_inbox(inbox) }
end
private
def get_contactable_inbox(inbox)
case inbox.channel_type
when 'Channel::TwilioSms'
twilio_contactable_inbox(inbox)
when 'Channel::Whatsapp'
whatsapp_contactable_inbox(inbox)
when 'Channel::Sms'
sms_contactable_inbox(inbox)
when 'Channel::Email'
email_contactable_inbox(inbox)
when 'Channel::Api'
api_contactable_inbox(inbox)
when 'Channel::WebWidget'
website_contactable_inbox(inbox)
end
end
def website_contactable_inbox(inbox)
latest_contact_inbox = inbox.contact_inboxes.where(contact: @contact).last
return unless latest_contact_inbox
# FIXME : change this when multiple conversations comes in
return if latest_contact_inbox.conversations.present?
{ source_id: latest_contact_inbox.source_id, inbox: inbox }
end
def api_contactable_inbox(inbox)
latest_contact_inbox = inbox.contact_inboxes.where(contact: @contact).last
source_id = latest_contact_inbox&.source_id || SecureRandom.uuid
{ source_id: source_id, inbox: inbox }
end
def email_contactable_inbox(inbox)
return if @contact.email.blank?
{ source_id: @contact.email, inbox: inbox }
end
def whatsapp_contactable_inbox(inbox)
return if @contact.phone_number.blank?
# Remove the plus since thats the format 360 dialog uses
{ source_id: @contact.phone_number.delete('+'), inbox: inbox }
end
def sms_contactable_inbox(inbox)
return if @contact.phone_number.blank?
{ source_id: @contact.phone_number, inbox: inbox }
end
def twilio_contactable_inbox(inbox)
return if @contact.phone_number.blank?
case inbox.channel.medium
when 'sms'
{ source_id: @contact.phone_number, inbox: inbox }
when 'whatsapp'
{ source_id: "whatsapp:#{@contact.phone_number}", inbox: inbox }
end
end
end
Contacts::ContactableInboxesService.prepend_mod_with('Contacts::ContactableInboxesService')

View File

@@ -0,0 +1,50 @@
class Contacts::FilterService < FilterService
ATTRIBUTE_MODEL = 'contact_attribute'.freeze
def initialize(account, user, params)
@account = account
# TODO: Change the order of arguments in FilterService maybe?
# account, user, params makes more sense
super(params, user)
end
def perform
validate_query_operator
@contacts = query_builder(@filters['contacts'])
{
contacts: @contacts,
count: @contacts.count
}
end
def filter_values(query_hash)
current_val = query_hash['values'][0]
if query_hash['attribute_key'] == 'phone_number'
"+#{current_val&.delete('+')}"
elsif query_hash['attribute_key'] == 'country_code'
current_val.downcase
else
current_val.is_a?(String) ? current_val.downcase : current_val
end
end
def base_relation
@account.contacts.resolved_contacts(use_crm_v2: @account.feature_enabled?('crm_v2'))
end
def filter_config
{
entity: 'Contact',
table_name: 'contacts'
}
end
private
def equals_to_filter_string(filter_operator, current_index)
return "= :value_#{current_index}" if filter_operator == 'equal_to'
"!= :value_#{current_index}"
end
end

View File

@@ -0,0 +1,37 @@
class Contacts::SyncAttributes
attr_reader :contact
def initialize(contact)
@contact = contact
end
def perform
update_contact_location_and_country_code
set_contact_type
end
private
def update_contact_location_and_country_code
# Ensure that location and country_code are updated from additional_attributes.
# TODO: Remove this once all contacts are updated and both the location and country_code fields are standardized throughout the app.
@contact.location = @contact.additional_attributes['city']
@contact.country_code = @contact.additional_attributes['country']
end
def set_contact_type
# If the contact is already a lead or customer then do not change the contact type
return unless @contact.contact_type == 'visitor'
# If the contact has an email or phone number or social details( facebook_user_id, instagram_user_id, etc) then it is a lead
# If contact is from external channel like facebook, instagram, whatsapp, etc then it is a lead
return unless @contact.email.present? || @contact.phone_number.present? || social_details_present?
@contact.contact_type = 'lead'
end
def social_details_present?
@contact.additional_attributes.keys.any? do |key|
key.start_with?('social_') && @contact.additional_attributes[key].present?
end
end
end