diff --git a/app/controllers/api/v1/accounts/contacts_controller.rb b/app/controllers/api/v1/accounts/contacts_controller.rb index 4fbe50902..039786905 100644 --- a/app/controllers/api/v1/accounts/contacts_controller.rb +++ b/app/controllers/api/v1/accounts/contacts_controller.rb @@ -122,7 +122,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController def resolved_contacts return @resolved_contacts if @resolved_contacts - @resolved_contacts = Current.account.contacts.resolved_contacts + @resolved_contacts = Current.account.contacts.resolved_contacts(use_crm_v2: Current.account.feature_enabled?('crm_v2')) @resolved_contacts = @resolved_contacts.tagged_with(params[:labels], any: true) if params[:labels].present? @resolved_contacts diff --git a/app/jobs/account/contacts_export_job.rb b/app/jobs/account/contacts_export_job.rb index 33edcaa34..952795604 100644 --- a/app/jobs/account/contacts_export_job.rb +++ b/app/jobs/account/contacts_export_job.rb @@ -29,9 +29,9 @@ class Account::ContactsExportJob < ApplicationJob result = ::Contacts::FilterService.new(@account, @account_user, @params).perform result[:contacts] elsif @params[:label].present? - @account.contacts.resolved_contacts.tagged_with(@params[:label], any: true) + @account.contacts.resolved_contacts(use_crm_v2: @account.feature_enabled?('crm_v2')).tagged_with(@params[:label], any: true) else - @account.contacts.resolved_contacts + @account.contacts.resolved_contacts(use_crm_v2: @account.feature_enabled?('crm_v2')) end end diff --git a/app/models/contact.rb b/app/models/contact.rb index 83920d9fc..a3570b2af 100644 --- a/app/models/contact.rb +++ b/app/models/contact.rb @@ -25,6 +25,7 @@ # Indexes # # index_contacts_on_account_id (account_id) +# index_contacts_on_account_id_and_contact_type (account_id,contact_type) # index_contacts_on_account_id_and_last_activity_at (account_id,last_activity_at DESC NULLS LAST) # index_contacts_on_blocked (blocked) # index_contacts_on_lower_email_account_id (lower((email)::text), account_id) @@ -175,8 +176,12 @@ class Contact < ApplicationRecord } end - def self.resolved_contacts - where("contacts.email <> '' OR contacts.phone_number <> '' OR contacts.identifier <> ''") + def self.resolved_contacts(use_crm_v2: false) + if use_crm_v2 + where(contact_type: 'lead') + else + where("contacts.email <> '' OR contacts.phone_number <> '' OR contacts.identifier <> ''") + end end def discard_invalid_attrs diff --git a/app/services/search_service.rb b/app/services/search_service.rb index 9766943cc..30d9a186c 100644 --- a/app/services/search_service.rb +++ b/app/services/search_service.rb @@ -95,7 +95,9 @@ class SearchService @contacts = current_account.contacts.where( "name ILIKE :search OR email ILIKE :search OR phone_number ILIKE :search OR identifier ILIKE :search", search: "%#{search_query}%" - ).resolved_contacts.order_on_last_activity_at('desc').page(params[:page]).per(15) + ).resolved_contacts( + use_crm_v2: current_account.feature_enabled?('crm_v2') + ).order_on_last_activity_at('desc').page(params[:page]).per(15) end def filter_articles diff --git a/config/features.yml b/config/features.yml index 4706c46f0..db2d46700 100644 --- a/config/features.yml +++ b/config/features.yml @@ -187,3 +187,7 @@ - name: whatsapp_campaign display_name: WhatsApp Campaign enabled: false +- name: crm_v2 + display_name: CRM V2 + enabled: false + chatwoot_internal: true diff --git a/db/migrate/20250722152516_add_index_on_contact_type_and_account_id_to_contacts.rb b/db/migrate/20250722152516_add_index_on_contact_type_and_account_id_to_contacts.rb new file mode 100644 index 000000000..1233e417c --- /dev/null +++ b/db/migrate/20250722152516_add_index_on_contact_type_and_account_id_to_contacts.rb @@ -0,0 +1,7 @@ +class AddIndexOnContactTypeAndAccountIdToContacts < ActiveRecord::Migration[7.1] + disable_ddl_transaction! + + def change + add_index :contacts, [:account_id, :contact_type], name: 'index_contacts_on_account_id_and_contact_type', algorithm: :concurrently + end +end diff --git a/db/schema.rb b/db/schema.rb index 34637315b..2ec37fec9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2025_07_14_104358) do +ActiveRecord::Schema[7.1].define(version: 2025_07_22_152516) do # These extensions should be enabled to support this database enable_extension "pg_stat_statements" enable_extension "pg_trgm" @@ -539,6 +539,7 @@ ActiveRecord::Schema[7.1].define(version: 2025_07_14_104358) do t.string "country_code", default: "" t.boolean "blocked", default: false, null: false t.index "lower((email)::text), account_id", name: "index_contacts_on_lower_email_account_id" + t.index ["account_id", "contact_type"], name: "index_contacts_on_account_id_and_contact_type" t.index ["account_id", "email", "phone_number", "identifier"], name: "index_contacts_on_nonempty_fields", where: "(((email)::text <> ''::text) OR ((phone_number)::text <> ''::text) OR ((identifier)::text <> ''::text))" t.index ["account_id", "last_activity_at"], name: "index_contacts_on_account_id_and_last_activity_at", order: { last_activity_at: "DESC NULLS LAST" } t.index ["account_id"], name: "index_contacts_on_account_id" diff --git a/spec/models/contact_spec.rb b/spec/models/contact_spec.rb index 2ca65fa4e..f21a81978 100644 --- a/spec/models/contact_spec.rb +++ b/spec/models/contact_spec.rb @@ -102,4 +102,98 @@ RSpec.describe Contact do expect(contact.contact_type).to eq 'lead' end end + + describe '.resolved_contacts' do + let(:account) { create(:account) } + + context 'when crm_v2 feature flag is disabled' do + it 'returns contacts with email, phone_number, or identifier using feature flag value' do + # Create contacts with different attributes + contact_with_email = create(:contact, account: account, email: 'test@example.com', name: 'John Doe') + contact_with_phone = create(:contact, account: account, phone_number: '+1234567890', name: 'Jane Smith') + contact_with_identifier = create(:contact, account: account, identifier: 'user123', name: 'Bob Wilson') + contact_without_details = create(:contact, account: account, name: 'Alice Johnson', email: nil, phone_number: nil, identifier: nil) + + resolved = account.contacts.resolved_contacts(use_crm_v2: false) + + expect(resolved).to include(contact_with_email, contact_with_phone, contact_with_identifier) + expect(resolved).not_to include(contact_without_details) + end + end + + context 'when crm_v2 feature flag is enabled' do + it 'returns only contacts with contact_type lead' do + # Contact with email and phone - should be marked as lead + contact_with_details = create(:contact, account: account, email: 'customer@example.com', phone_number: '+1234567890', name: 'Customer One') + expect(contact_with_details.contact_type).to eq('lead') + + # Contact without email/phone - should be marked as visitor + contact_without_details = create(:contact, account: account, name: 'Lead', email: nil, phone_number: nil) + expect(contact_without_details.contact_type).to eq('visitor') + + # Force set contact_type to lead for testing + contact_without_details.update!(contact_type: 'lead') + + resolved = account.contacts.resolved_contacts(use_crm_v2: true) + + expect(resolved).to include(contact_with_details) + expect(resolved).to include(contact_without_details) + end + + it 'includes all lead contacts regardless of email/phone presence' do + # Create a lead contact with only name + lead_contact = create(:contact, account: account, name: 'Test Lead') + lead_contact.update!(contact_type: 'lead') + + # Create a customer contact + customer_contact = create(:contact, account: account, email: 'customer@test.com') + customer_contact.update!(contact_type: 'customer') + + # Create a visitor contact + visitor_contact = create(:contact, account: account, name: 'Visitor') + expect(visitor_contact.contact_type).to eq('visitor') + + resolved = account.contacts.resolved_contacts(use_crm_v2: true) + + expect(resolved).to include(lead_contact) + expect(resolved).not_to include(customer_contact) + expect(resolved).not_to include(visitor_contact) + end + + it 'returns contacts with email, phone_number, or identifier when explicitly passing use_crm_v2: false' do + # Even though feature flag is enabled, we're explicitly passing false + contact_with_email = create(:contact, account: account, email: 'test@example.com', name: 'John Doe') + contact_with_phone = create(:contact, account: account, phone_number: '+1234567890', name: 'Jane Smith') + contact_without_details = create(:contact, account: account, name: 'Alice Johnson', email: nil, phone_number: nil, identifier: nil) + + resolved = account.contacts.resolved_contacts(use_crm_v2: false) + + # Should use the old logic despite feature flag being enabled + expect(resolved).to include(contact_with_email, contact_with_phone) + expect(resolved).not_to include(contact_without_details) + end + end + + context 'with mixed contact types' do + it 'correctly filters based on use_crm_v2 parameter regardless of feature flag' do + # Create different types of contacts + visitor_contact = create(:contact, account: account, name: 'Visitor') + lead_with_email = create(:contact, account: account, email: 'lead@example.com', name: 'Lead') + lead_without_email = create(:contact, account: account, name: 'Lead Only') + lead_without_email.update!(contact_type: 'lead') + customer_contact = create(:contact, account: account, email: 'customer@example.com', name: 'Customer') + customer_contact.update!(contact_type: 'customer') + + # Test with use_crm_v2: false + resolved_old = account.contacts.resolved_contacts(use_crm_v2: false) + expect(resolved_old).to include(lead_with_email, customer_contact) + expect(resolved_old).not_to include(visitor_contact, lead_without_email) + + # Test with use_crm_v2: true + resolved_new = account.contacts.resolved_contacts(use_crm_v2: true) + expect(resolved_new).to include(lead_with_email, lead_without_email) + expect(resolved_new).not_to include(visitor_contact, customer_contact) + end + end + end end