feat(crm): align contact company payloads
This commit is contained in:
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
"github.com/gochat/gochat/internal/search"
|
||||
@@ -21,6 +20,10 @@ func NewCompanyRepo(db *gorm.DB) *CompanyRepo {
|
||||
return &CompanyRepo{db: db}
|
||||
}
|
||||
|
||||
func (r *CompanyRepo) DB() *gorm.DB {
|
||||
return r.db
|
||||
}
|
||||
|
||||
// FindByID retrieves a company by primary key.
|
||||
func (r *CompanyRepo) FindByID(ctx context.Context, id uint) (*model.Company, error) {
|
||||
var company model.Company
|
||||
@@ -111,22 +114,34 @@ func (r *CompanyRepo) ListContacts(ctx context.Context, companyID, accountID uin
|
||||
var contacts []model.Contact
|
||||
var total int64
|
||||
|
||||
// Count contacts in the join table that belong to the account
|
||||
countDB := r.db.WithContext(ctx).Model(&model.Contact{}).
|
||||
Joins("JOIN company_contacts ON company_contacts.contact_id = contacts.id").
|
||||
Where("company_contacts.company_id = ? AND contacts.account_id = ?", companyID, accountID)
|
||||
Where("company_id = ? AND account_id = ?", companyID, accountID)
|
||||
if err := countDB.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
err := r.db.WithContext(ctx).
|
||||
Joins("JOIN company_contacts ON company_contacts.contact_id = contacts.id").
|
||||
Where("company_contacts.company_id = ? AND contacts.account_id = ?", companyID, accountID).
|
||||
Offset(offset).Limit(limit).Order("contacts.id DESC").
|
||||
Where("company_id = ? AND account_id = ?", companyID, accountID).
|
||||
Offset(offset).Limit(limit).Order("name ASC, id ASC").
|
||||
Find(&contacts).Error
|
||||
return contacts, total, err
|
||||
}
|
||||
|
||||
func (r *CompanyRepo) SearchAssignableContacts(ctx context.Context, companyID, accountID uint, query string, offset, limit int) ([]model.Contact, int64, error) {
|
||||
var contacts []model.Contact
|
||||
var total int64
|
||||
likeQuery := "%" + query + "%"
|
||||
condition := r.db.WithContext(ctx).Model(&model.Contact{}).
|
||||
Where("account_id = ?", accountID).
|
||||
Where("company_id IS NULL OR company_id != ?", companyID).
|
||||
Where("LOWER(name) LIKE LOWER(?) OR LOWER(email) LIKE LOWER(?) OR LOWER(phone_number) LIKE LOWER(?) OR LOWER(identifier) LIKE LOWER(?)", likeQuery, likeQuery, likeQuery, likeQuery)
|
||||
if err := condition.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
err := condition.Offset(offset).Limit(limit).Order("name ASC, id ASC").Find(&contacts).Error
|
||||
return contacts, total, err
|
||||
}
|
||||
|
||||
// ListNotes retrieves notes for a company.
|
||||
func (r *CompanyRepo) ListNotes(ctx context.Context, companyID uint, offset, limit int) ([]model.CompanyNote, int64, error) {
|
||||
var notes []model.CompanyNote
|
||||
@@ -154,26 +169,12 @@ func (r *CompanyRepo) DeleteNote(ctx context.Context, id uint) error {
|
||||
return r.db.WithContext(ctx).Delete(&model.CompanyNote{}, id).Error
|
||||
}
|
||||
|
||||
// AddContact adds a contact to a company via the company_contacts join table.
|
||||
// Uses GORM's clause.OnConflict for dialect-aware upsert (works on both PG and SQLite).
|
||||
func (r *CompanyRepo) AddContact(ctx context.Context, companyID, contactID uint) error {
|
||||
association := struct {
|
||||
CompanyID uint `gorm:"primaryKey"`
|
||||
ContactID uint `gorm:"primaryKey"`
|
||||
}{
|
||||
CompanyID: companyID,
|
||||
ContactID: contactID,
|
||||
}
|
||||
return r.db.WithContext(ctx).Table("company_contacts").
|
||||
Clauses(clause.OnConflict{DoNothing: true}).
|
||||
Create(&association).Error
|
||||
return r.db.WithContext(ctx).Model(&model.Contact{}).Where("id = ?", contactID).Update("company_id", companyID).Error
|
||||
}
|
||||
|
||||
// RemoveContact removes a contact from a company via the company_contacts join table.
|
||||
func (r *CompanyRepo) RemoveContact(ctx context.Context, companyID, contactID uint) error {
|
||||
return r.db.WithContext(ctx).Table("company_contacts").
|
||||
Where("company_id = ? AND contact_id = ?", companyID, contactID).
|
||||
Delete(nil).Error
|
||||
return r.db.WithContext(ctx).Model(&model.Contact{}).Where("id = ? AND company_id = ?", contactID, companyID).Update("company_id", nil).Error
|
||||
}
|
||||
|
||||
// resolveCompanySort maps a sort parameter to a SQL ORDER BY clause.
|
||||
|
||||
@@ -265,6 +265,7 @@ func TestCompanyRepo_CreateNote(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.NotZero(t, note.ID)
|
||||
}
|
||||
|
||||
// ========== DeleteNote Tests ==========
|
||||
|
||||
func TestCompanyRepo_DeleteNote(t *testing.T) {
|
||||
@@ -314,10 +315,10 @@ func TestCompanyRepo_AddContact(t *testing.T) {
|
||||
err := repo.AddContact(context.Background(), company.ID, contact.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify the association exists in the join table
|
||||
var count int64
|
||||
db.Table("company_contacts").Where("company_id = ? AND contact_id = ?", company.ID, contact.ID).Count(&count)
|
||||
assert.Equal(t, int64(1), count)
|
||||
var updated model.Contact
|
||||
require.NoError(t, db.First(&updated, contact.ID).Error)
|
||||
require.NotNil(t, updated.CompanyID)
|
||||
assert.Equal(t, company.ID, *updated.CompanyID)
|
||||
}
|
||||
|
||||
func TestCompanyRepo_AddContact_Duplicate(t *testing.T) {
|
||||
@@ -335,14 +336,14 @@ func TestCompanyRepo_AddContact_Duplicate(t *testing.T) {
|
||||
err := repo.AddContact(context.Background(), company.ID, contact.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Adding duplicate should succeed (ON CONFLICT DO NOTHING)
|
||||
// Assigning the same company twice should stay idempotent.
|
||||
err = repo.AddContact(context.Background(), company.ID, contact.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify only one association exists
|
||||
var count int64
|
||||
db.Table("company_contacts").Where("company_id = ? AND contact_id = ?", company.ID, contact.ID).Count(&count)
|
||||
assert.Equal(t, int64(1), count)
|
||||
var updated model.Contact
|
||||
require.NoError(t, db.First(&updated, contact.ID).Error)
|
||||
require.NotNil(t, updated.CompanyID)
|
||||
assert.Equal(t, company.ID, *updated.CompanyID)
|
||||
}
|
||||
|
||||
// ========== RemoveContact Tests ==========
|
||||
@@ -355,19 +356,15 @@ func TestCompanyRepo_RemoveContact(t *testing.T) {
|
||||
require.NoError(t, db.Create(account).Error)
|
||||
|
||||
company := createTestCompany(t, db, account.ID, "RemoveContactCorp", "removecontact.example.com")
|
||||
contact := &model.Contact{AccountID: account.ID, Name: "Remove Contact", Email: "remove@example.com"}
|
||||
contact := &model.Contact{AccountID: account.ID, Name: "Remove Contact", Email: "remove@example.com", CompanyID: &company.ID}
|
||||
require.NoError(t, db.Create(contact).Error)
|
||||
|
||||
// Create association via GORM many2many
|
||||
require.NoError(t, db.Model(&company).Association("Contacts").Append(contact))
|
||||
|
||||
err := repo.RemoveContact(context.Background(), company.ID, contact.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify the association is gone
|
||||
var count int64
|
||||
db.Table("company_contacts").Where("company_id = ? AND contact_id = ?", company.ID, contact.ID).Count(&count)
|
||||
assert.Equal(t, int64(0), count)
|
||||
var updated model.Contact
|
||||
require.NoError(t, db.First(&updated, contact.ID).Error)
|
||||
assert.Nil(t, updated.CompanyID)
|
||||
}
|
||||
|
||||
func TestCompanyRepo_RemoveContact_NotAssociated(t *testing.T) {
|
||||
|
||||
@@ -33,6 +33,10 @@ func (r *ContactRepo) FindByID(ctx context.Context, id uint) (*model.Contact, er
|
||||
return &contact, nil
|
||||
}
|
||||
|
||||
func (r *ContactRepo) DB() *gorm.DB {
|
||||
return r.db
|
||||
}
|
||||
|
||||
// FindByAccount retrieves all contacts for an account with optional sort.
|
||||
// sort: default "last_activity_at DESC, id DESC", alternatives: "name ASC", "email ASC", "created_at DESC"
|
||||
func (r *ContactRepo) FindByAccount(ctx context.Context, accountID uint, offset, limit int, sort string) ([]model.Contact, int64, error) {
|
||||
@@ -62,12 +66,12 @@ func (r *ContactRepo) Search(ctx context.Context, accountID uint, query string,
|
||||
if query != "" {
|
||||
if searchMode == search.SearchModeTrigram {
|
||||
// pg_trgm fuzzy match on contact fields
|
||||
condition = condition.Where("name % ? OR email % ? OR phone ILIKE ? OR identifier % ?",
|
||||
condition = condition.Where("name % ? OR email % ? OR phone_number ILIKE ? OR identifier % ?",
|
||||
query, query, query, query)
|
||||
} else {
|
||||
// ILIKE substring match (default)
|
||||
// Case-insensitive substring match that works on PostgreSQL and SQLite tests.
|
||||
likeQuery := "%" + query + "%"
|
||||
condition = condition.Where("name ILIKE ? OR email ILIKE ? OR phone ILIKE ? OR identifier ILIKE ?",
|
||||
condition = condition.Where("LOWER(name) LIKE LOWER(?) OR LOWER(email) LIKE LOWER(?) OR LOWER(phone_number) LIKE LOWER(?) OR LOWER(identifier) LIKE LOWER(?)",
|
||||
likeQuery, likeQuery, likeQuery, likeQuery)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user