diff --git a/internal/handler/api/v1/contact_handler_crud_test.go b/internal/handler/api/v1/contact_handler_crud_test.go index 088987a7..f25e17d8 100644 --- a/internal/handler/api/v1/contact_handler_crud_test.go +++ b/internal/handler/api/v1/contact_handler_crud_test.go @@ -614,6 +614,8 @@ func (s *ContactHandlerCRUDTestSuite) TestMerge_ChatwootActionsPathReturnsRawCon } func (s *ContactHandlerCRUDTestSuite) TestImport_CreatesDataImportAndReturnsOK() { + s.Require().NoError(s.db.Create(&model.Tag{AccountID: s.account.ID, Name: "vip"}).Error) + var body bytes.Buffer writer := multipart.NewWriter(&body) part, err := writer.CreateFormFile("import_file", "contacts.csv") diff --git a/internal/service/contact_service.go b/internal/service/contact_service.go index 277e7438..fbfba87c 100644 --- a/internal/service/contact_service.go +++ b/internal/service/contact_service.go @@ -801,6 +801,15 @@ func (s *ContactService) ImportCSV(ctx context.Context, accountID uint, r io.Rea if idx, ok := colIndex["labels"]; ok && idx < len(row) { labels = splitImportLabels(row[idx]) } + labels, invalidLabels, err := s.resolveApprovedImportLabels(ctx, accountID, labels) + if err != nil { + return nil, err + } + if len(invalidLabels) > 0 { + applogger.L().Warnf("Skipping imported contact row with unknown labels: %s", strings.Join(invalidLabels, ", ")) + result.Failed++ + continue + } known := map[string]struct{}{"name": {}, "email": {}, "phone_number": {}, "identifier": {}, "country_code": {}, "location": {}, "city": {}, "company_name": {}, "contact_type": {}, "labels": {}} for i, col := range header { col = strings.TrimSpace(col) @@ -849,6 +858,45 @@ func (s *ContactService) ImportCSV(ctx context.Context, accountID uint, r io.Rea return result, nil } +func (s *ContactService) resolveApprovedImportLabels(ctx context.Context, accountID uint, labels []string) ([]string, []string, error) { + if len(labels) == 0 { + return nil, nil, nil + } + approved := map[string]string{} + var tags []model.Tag + if err := s.repo.DB().WithContext(ctx).Where("account_id = ?", accountID).Find(&tags).Error; err != nil { + return nil, nil, err + } + for _, tag := range tags { + approved[strings.ToLower(strings.TrimSpace(tag.Name))] = tag.Name + } + + seen := map[string]struct{}{} + resolved := make([]string, 0, len(labels)) + invalid := make([]string, 0) + for _, label := range labels { + label = strings.TrimSpace(label) + if label == "" { + continue + } + key := strings.ToLower(label) + canonical, ok := approved[key] + if !ok { + if _, exists := seen["invalid:"+key]; !exists { + invalid = append(invalid, key) + seen["invalid:"+key] = struct{}{} + } + continue + } + if _, exists := seen[key]; exists { + continue + } + seen[key] = struct{}{} + resolved = append(resolved, canonical) + } + return resolved, invalid, nil +} + func (s *ContactService) findImportContact(ctx context.Context, accountID uint, contact *model.Contact) *model.Contact { if contact.Identifier != "" { if existing, err := s.repo.FindByIdentifier(ctx, accountID, contact.Identifier); err == nil { diff --git a/internal/service/contact_service_g3_test.go b/internal/service/contact_service_g3_test.go index 16e51929..a114243a 100644 --- a/internal/service/contact_service_g3_test.go +++ b/internal/service/contact_service_g3_test.go @@ -331,6 +331,7 @@ func TestContactService_ImportContacts_CreatesCompletedDataImport(t *testing.T) db, _, svc := setupContactService(t) account := createTestAccount(t, db) userID := uint(42) + require.NoError(t, db.Create(&model.Tag{AccountID: account.ID, Name: "vip"}).Error) csvData := "name,email,labels\nAlice,alice@test.com,vip\n" dataImport, err := svc.ImportContacts(context.Background(), account.ID, userID, strings.NewReader(csvData)) @@ -347,6 +348,30 @@ func TestContactService_ImportContacts_CreatesCompletedDataImport(t *testing.T) assert.Equal(t, int64(1), labelCount) } +func TestContactService_ImportContacts_RejectsUnknownLabels(t *testing.T) { + db, _, svc := setupContactService(t) + account := createTestAccount(t, db) + require.NoError(t, db.Create(&model.Tag{AccountID: account.ID, Name: "vip"}).Error) + + csvData := "name,email,labels\nAlice,alice@test.com,vip\nBob,bob@test.com,unknown\n" + dataImport, err := svc.ImportContacts(context.Background(), account.ID, 0, strings.NewReader(csvData)) + require.NoError(t, err) + assert.Equal(t, string(model.DataImportStatusCompleted), dataImport.Status) + assert.Equal(t, 2, dataImport.TotalRecords) + assert.Equal(t, 1, dataImport.ProcessedRecords) + assert.Equal(t, 1, dataImport.FailedRecords) + + var alice model.Contact + require.NoError(t, db.Where("account_id = ? AND email = ?", account.ID, "alice@test.com").First(&alice).Error) + var bobCount int64 + db.Model(&model.Contact{}).Where("account_id = ? AND email = ?", account.ID, "bob@test.com").Count(&bobCount) + assert.Equal(t, int64(0), bobCount) + + var tagCount int64 + db.Model(&model.Tag{}).Where("account_id = ? AND name = ?", account.ID, "unknown").Count(&tagCount) + assert.Equal(t, int64(0), tagCount) +} + func TestContactService_ImportCSV_FailsOnBadCSV(t *testing.T) { db, _, svc := setupContactService(t) account := createTestAccount(t, db)