Chatwoot/app/models/contact.rb

218 lines
6.7 KiB
Ruby

# == Schema Information
#
# Table name: contacts
#
# id :integer not null, primary key
# additional_attributes :jsonb
# custom_attributes :jsonb
# email :string
# identifier :string
# last_activity_at :datetime
# name :string
# phone_number :string
# created_at :datetime not null
# updated_at :datetime not null
# account_id :integer not null
#
# Indexes
#
# index_contacts_on_account_id (account_id)
# index_contacts_on_phone_number_and_account_id (phone_number,account_id)
# uniq_email_per_account_contact (email,account_id) UNIQUE
# uniq_identifier_per_account_contact (identifier,account_id) UNIQUE
#
class Contact < ApplicationRecord
include Avatarable
include AvailabilityStatusable
include Labelable
include PgSearch::Model
include MultiSearchableHelpers
multisearchable(
against: [:id, :email, :name, :phone_number],
additional_attributes: ->(contact) { { conversation_id: nil, account_id: contact.account_id, inbox_id: nil } }
)
validates :account_id, presence: true
validates :email, allow_blank: true, uniqueness: { scope: [:account_id], case_sensitive: false },
format: { with: Devise.email_regexp, message: I18n.t('errors.contacts.email.invalid') }
validates :identifier, allow_blank: true, uniqueness: { scope: [:account_id] }
validates :phone_number,
allow_blank: true, uniqueness: { scope: [:account_id] },
format: { with: /\+[1-9]\d{1,14}\z/, message: I18n.t('errors.contacts.phone_number.invalid') }
validates :name, length: { maximum: 255 }
belongs_to :account
has_many :conversations, dependent: :destroy_async
has_many :contact_inboxes, dependent: :destroy_async
has_many :csat_survey_responses, dependent: :destroy_async
has_many :inboxes, through: :contact_inboxes
has_many :messages, as: :sender, dependent: :destroy_async
has_many :notes, dependent: :destroy_async
before_validation :prepare_contact_attributes
after_create_commit :dispatch_create_event, :ip_lookup
after_update_commit :dispatch_update_event
after_destroy_commit :dispatch_destroy_event
scope :order_on_last_activity_at, lambda { |direction|
order(
Arel::Nodes::SqlLiteral.new(
sanitize_sql_for_order("\"contacts\".\"last_activity_at\" #{direction}
NULLS LAST")
)
)
}
scope :order_on_company_name, lambda { |direction|
order(
Arel::Nodes::SqlLiteral.new(
sanitize_sql_for_order(
"\"contacts\".\"additional_attributes\"->>'company_name' #{direction}
NULLS LAST"
)
)
)
}
scope :order_on_city, lambda { |direction|
order(
Arel::Nodes::SqlLiteral.new(
sanitize_sql_for_order(
"\"contacts\".\"additional_attributes\"->>'city' #{direction}
NULLS LAST"
)
)
)
}
scope :order_on_country_name, lambda { |direction|
order(
Arel::Nodes::SqlLiteral.new(
sanitize_sql_for_order(
"\"contacts\".\"additional_attributes\"->>'country' #{direction}
NULLS LAST"
)
)
)
}
scope :order_on_name, lambda { |direction|
order(
Arel::Nodes::SqlLiteral.new(
sanitize_sql_for_order(
"CASE
WHEN \"contacts\".\"name\" ~~* '^+\d*' THEN 'z'
WHEN \"contacts\".\"name\" ~~* '^\b*' THEN 'z'
ELSE LOWER(\"contacts\".\"name\")
END #{direction}"
)
)
)
}
def get_source_id(inbox_id)
contact_inboxes.find_by!(inbox_id: inbox_id).source_id
end
def push_event_data
{
additional_attributes: additional_attributes,
custom_attributes: custom_attributes,
email: email,
id: id,
identifier: identifier,
name: name,
phone_number: phone_number,
thumbnail: avatar_url,
type: 'contact'
}
end
def webhook_data
{
id: id,
name: name,
avatar: avatar_url,
type: 'contact',
account: account.webhook_data
}
end
def self.resolved_contacts
where.not(email: [nil, '']).or(
Current.account.contacts.where.not(phone_number: [nil, ''])
).or(Current.account.contacts.where.not(identifier: [nil, '']))
end
def discard_invalid_attrs
phone_number_format
email_format
end
# NOTE: To add multi search records with conversation_id associated to contacts for previously added records.
# We can not find conversation_id from contacts directly so we added this joins here.
def self.rebuild_pg_search_documents(account_id)
return super unless name == 'Contact'
connection.execute <<~SQL.squish
INSERT INTO pg_search_documents (searchable_type, searchable_id, content, account_id, conversation_id, inbox_id, created_at, updated_at)
SELECT 'Contact' AS searchable_type,
contacts.id AS searchable_id,
CONCAT_WS(' ', contacts.id, contacts.email, contacts.name, contacts.phone_number, contacts.account_id) AS content,
contacts.account_id::int AS account_id,
conversations.id::int AS conversation_id,
conversations.inbox_id::int AS inbox_id,
now() AS created_at,
now() AS updated_at
FROM contacts
INNER JOIN conversations
ON conversations.contact_id = contacts.id
WHERE contacts.account_id = #{account_id}
SQL
end
private
def ip_lookup
return unless account.feature_enabled?('ip_lookup')
ContactIpLookupJob.perform_later(self)
end
def phone_number_format
return if phone_number.blank?
self.phone_number = phone_number_was unless phone_number.match?(/\+[1-9]\d{1,14}\z/)
end
def email_format
return if email.blank?
self.email = email_was unless email.match(Devise.email_regexp)
end
def prepare_contact_attributes
prepare_email_attribute
prepare_jsonb_attributes
end
def prepare_email_attribute
# So that the db unique constraint won't throw error when email is ''
self.email = email.present? ? email.downcase : nil
end
def prepare_jsonb_attributes
self.additional_attributes = {} if additional_attributes.blank?
self.custom_attributes = {} if custom_attributes.blank?
end
def dispatch_create_event
Rails.configuration.dispatcher.dispatch(CONTACT_CREATED, Time.zone.now, contact: self)
end
def dispatch_update_event
Rails.configuration.dispatcher.dispatch(CONTACT_UPDATED, Time.zone.now, contact: self)
end
def dispatch_destroy_event
Rails.configuration.dispatcher.dispatch(CONTACT_DELETED, Time.zone.now, contact: self)
end
end