feat(search): align contact resolved scope
This commit is contained in:
@@ -16,25 +16,27 @@ import (
|
||||
// This is a repository-level mirror of the search.SearchFilter struct to avoid
|
||||
// an import cycle between internal/search and internal/repository.
|
||||
type RepoSearchFilter struct {
|
||||
SearchMode search.SearchMode // ilike or trigram
|
||||
Status []string
|
||||
Priority []string
|
||||
AssigneeID *uint
|
||||
TeamID *uint
|
||||
InboxID *uint
|
||||
Labels []string
|
||||
ContactSource string
|
||||
MessageType string
|
||||
SenderType string
|
||||
SenderID *uint
|
||||
ContentType string
|
||||
Private *bool
|
||||
DateFrom *time.Time
|
||||
DateTo *time.Time
|
||||
SortBy string
|
||||
SortOrder string
|
||||
Page int
|
||||
PerPage int
|
||||
SearchMode search.SearchMode // ilike or trigram
|
||||
Status []string
|
||||
Priority []string
|
||||
AssigneeID *uint
|
||||
TeamID *uint
|
||||
InboxID *uint
|
||||
Labels []string
|
||||
ContactSource string
|
||||
ContactResolvedScope bool
|
||||
ContactCRMV2 bool
|
||||
MessageType string
|
||||
SenderType string
|
||||
SenderID *uint
|
||||
ContentType string
|
||||
Private *bool
|
||||
DateFrom *time.Time
|
||||
DateTo *time.Time
|
||||
SortBy string
|
||||
SortOrder string
|
||||
Page int
|
||||
PerPage int
|
||||
|
||||
// Article-specific filters (Knowledge Base)
|
||||
PortalID *uint
|
||||
@@ -86,28 +88,30 @@ func searchFilterToRepo(f *search.SearchFilter) *RepoSearchFilter {
|
||||
mode = search.SearchModeILike
|
||||
}
|
||||
return &RepoSearchFilter{
|
||||
SearchMode: mode,
|
||||
Status: f.Status,
|
||||
Priority: f.Priority,
|
||||
AssigneeID: f.AssigneeID,
|
||||
TeamID: f.TeamID,
|
||||
InboxID: f.InboxID,
|
||||
Labels: f.Labels,
|
||||
ContactSource: f.ContactSource,
|
||||
MessageType: f.MessageType,
|
||||
SenderType: f.SenderType,
|
||||
SenderID: f.SenderID,
|
||||
ContentType: f.ContentType,
|
||||
Private: f.Private,
|
||||
DateFrom: f.DateFrom,
|
||||
DateTo: f.DateTo,
|
||||
SortBy: f.SortBy,
|
||||
SortOrder: f.SortOrder,
|
||||
Page: f.Page,
|
||||
PerPage: f.PerPage,
|
||||
PortalID: f.PortalID,
|
||||
ArticleStatus: f.ArticleStatus,
|
||||
ArticleLocale: f.ArticleLocale,
|
||||
SearchMode: mode,
|
||||
Status: f.Status,
|
||||
Priority: f.Priority,
|
||||
AssigneeID: f.AssigneeID,
|
||||
TeamID: f.TeamID,
|
||||
InboxID: f.InboxID,
|
||||
Labels: f.Labels,
|
||||
ContactSource: f.ContactSource,
|
||||
ContactResolvedScope: f.ContactResolvedScope,
|
||||
ContactCRMV2: f.ContactCRMV2,
|
||||
MessageType: f.MessageType,
|
||||
SenderType: f.SenderType,
|
||||
SenderID: f.SenderID,
|
||||
ContentType: f.ContentType,
|
||||
Private: f.Private,
|
||||
DateFrom: f.DateFrom,
|
||||
DateTo: f.DateTo,
|
||||
SortBy: f.SortBy,
|
||||
SortOrder: f.SortOrder,
|
||||
Page: f.Page,
|
||||
PerPage: f.PerPage,
|
||||
PortalID: f.PortalID,
|
||||
ArticleStatus: f.ArticleStatus,
|
||||
ArticleLocale: f.ArticleLocale,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,12 +242,27 @@ func (r *SearchRepo) SearchContacts(ctx context.Context, accountID uint, query s
|
||||
return r.searchContactsInternal(ctx, accountID, query, repoFilter)
|
||||
}
|
||||
|
||||
func (r *SearchRepo) AccountFeatureEnabled(ctx context.Context, accountID uint, flag string) bool {
|
||||
var account model.Account
|
||||
if err := r.db.WithContext(ctx).Select("feature_flags").Where("id = ?", accountID).First(&account).Error; err != nil {
|
||||
return false
|
||||
}
|
||||
return contactFeatureFlagEnabled(account.FeatureFlags, flag)
|
||||
}
|
||||
|
||||
// searchContactsInternal is the internal implementation using *RepoSearchFilter.
|
||||
func (r *SearchRepo) searchContactsInternal(ctx context.Context, accountID uint, query string, filter *RepoSearchFilter) ([]model.Contact, int64, error) {
|
||||
var contacts []model.Contact
|
||||
var total int64
|
||||
|
||||
q := r.db.WithContext(ctx).Model(&model.Contact{}).Where("account_id = ?", accountID)
|
||||
if filter.ContactResolvedScope {
|
||||
if filter.ContactCRMV2 {
|
||||
q = q.Where("contacts.contact_type = ?", "lead")
|
||||
} else {
|
||||
q = q.Where("(contacts.email <> '' OR contacts.phone_number <> '' OR contacts.identifier <> '')")
|
||||
}
|
||||
}
|
||||
|
||||
// Text search on contact fields — ILIKE or pg_trgm fuzzy match
|
||||
if query != "" {
|
||||
|
||||
@@ -271,6 +271,44 @@ func TestSearchRepo_SearchContacts_EmailMatch(t *testing.T) {
|
||||
assert.NotEmpty(t, results)
|
||||
}
|
||||
|
||||
func TestSearchRepo_SearchContacts_ResolvedScope(t *testing.T) {
|
||||
skipIfSQLite(t)
|
||||
db := setupTestDB(t)
|
||||
repo := NewSearchRepo(db)
|
||||
|
||||
account := createTestAccountForSearch(t, db)
|
||||
resolved := createTestContactForSearch(t, db, account.ID, "Scope Resolved", "scope@example.com", "")
|
||||
unresolved := createTestContactForSearch(t, db, account.ID, "Scope Anonymous", "", "")
|
||||
|
||||
filter := &search.SearchFilter{Page: 1, PerPage: 25, ContactResolvedScope: true}
|
||||
results, total, err := repo.SearchContacts(context.Background(), account.ID, "Scope", filter)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), total)
|
||||
assert.Len(t, results, 1)
|
||||
assert.Equal(t, resolved.ID, results[0].ID)
|
||||
assert.NotEqual(t, unresolved.ID, results[0].ID)
|
||||
}
|
||||
|
||||
func TestSearchRepo_SearchContacts_ResolvedScopeCRMV2(t *testing.T) {
|
||||
skipIfSQLite(t)
|
||||
db := setupTestDB(t)
|
||||
repo := NewSearchRepo(db)
|
||||
|
||||
account := createTestAccountForSearch(t, db)
|
||||
require.NoError(t, db.Model(account).Update("feature_flags", `{"crm_v2":true}`).Error)
|
||||
lead := createTestContactForSearch(t, db, account.ID, "Scope Lead", "", "")
|
||||
require.NoError(t, db.Model(lead).Update("contact_type", "lead").Error)
|
||||
customer := createTestContactForSearch(t, db, account.ID, "Scope Customer", "customer@example.com", "")
|
||||
require.NoError(t, db.Model(customer).Update("contact_type", "customer").Error)
|
||||
|
||||
filter := &search.SearchFilter{Page: 1, PerPage: 25, ContactResolvedScope: true, ContactCRMV2: repo.AccountFeatureEnabled(context.Background(), account.ID, "crm_v2")}
|
||||
results, total, err := repo.SearchContacts(context.Background(), account.ID, "Scope", filter)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), total)
|
||||
require.Len(t, results, 1)
|
||||
assert.Equal(t, lead.ID, results[0].ID)
|
||||
}
|
||||
|
||||
// ========== SearchArticles DB tests (PG only for ILIKE) ==========
|
||||
|
||||
func TestSearchRepo_SearchArticles_TitleMatch(t *testing.T) {
|
||||
@@ -377,4 +415,4 @@ func TestSearchFilter_IsTrigram_Unit(t *testing.T) {
|
||||
sf := &search.SearchFilter{SearchMode: search.SearchModeTrigram}
|
||||
assert.True(t, sf.IsTrigram())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user