# == 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 } } ) 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 return super unless name == 'Contact' connection.execute <<~SQL.squish INSERT INTO pg_search_documents (searchable_type, searchable_id, content, account_id, conversation_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 AS conversation_id, now() AS created_at, now() AS updated_at FROM contacts LEFT OUTER JOIN conversations ON conversations.contact_id = contacts.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