package discovery import ( "context" "strings" "unicode" ) func (s *Service) loadNCContacts(ctx context.Context, ncUserID, bookID string) []ncContact { if s.nc == nil || ncUserID == "" { return nil } if bookID == "" { bookID = "contacts" } path := "/addressbooks/" + ncUserID + "/" + bookID + "/" contacts, err := s.nc.ListContacts(ctx, ncUserID, path) if err != nil { s.logger.Warn("load nc contacts for enrichment", "error", err) return nil } return contacts } func (s *Service) resolveDiscoveryNCContext(ctx context.Context, externalUserID string) (ncUserID, bookID string) { bookID = "contacts" _ = s.db.QueryRow(ctx, ` SELECT COALESCE(NULLIF(nc_user_id, ''), ''), COALESCE(NULLIF(book_id, ''), 'contacts') FROM contact_discovery_scans WHERE user_id = (SELECT id FROM users WHERE external_id = $1) ORDER BY started_at DESC LIMIT 1 `, externalUserID).Scan(&ncUserID, &bookID) return } func findExistingContact(contacts []ncContact, email string) *ncContact { email = strings.ToLower(strings.TrimSpace(email)) for i := range contacts { if strings.EqualFold(strings.TrimSpace(contacts[i].Email), email) { return &contacts[i] } } return nil } func normalizePhone(s string) string { var b strings.Builder for _, r := range s { if unicode.IsDigit(r) { b.WriteRune(r) } } return b.String() } func phonesEqual(a, b string) bool { na, nb := normalizePhone(a), normalizePhone(b) if na == "" || nb == "" { return false } if len(na) >= 9 && len(nb) >= 9 { return na[len(na)-9:] == nb[len(nb)-9:] } return na == nb } func stringsEqualFoldTrim(a, b string) bool { return strings.EqualFold(strings.TrimSpace(a), strings.TrimSpace(b)) } func suggestionAlreadyOnContact(existing *ncContact, sug Suggestion) bool { if existing == nil { return false } val := strings.TrimSpace(sug.SuggestedValue) if val == "" { return true } switch sug.FieldPath { case "full_name": return stringsEqualFoldTrim(val, existing.FullName) case "company": return stringsEqualFoldTrim(val, existing.Org) case "phones": return phonesEqual(val, existing.Phone) case "emails": return strings.EqualFold(val, strings.TrimSpace(existing.Email)) default: return false } } func filterRedundantSuggestions(suggestions []Suggestion, contacts []ncContact) []Suggestion { if len(suggestions) == 0 || len(contacts) == 0 { return suggestions } byUID := make(map[string]*ncContact, len(contacts)) for i := range contacts { byUID[contacts[i].UID] = &contacts[i] } filtered := suggestions[:0] for _, sug := range suggestions { if sug.SuggestionType != "enrich_contact" || sug.TargetContactUID == "" { filtered = append(filtered, sug) continue } if suggestionAlreadyOnContact(byUID[sug.TargetContactUID], sug) { continue } filtered = append(filtered, sug) } return filtered } func enrichExistingContactSuggestions(profileID, contactUID string, data *EnrichedContactData, existing *ncContact) []Suggestion { if data == nil || existing == nil { return nil } var out []Suggestion addIfMissing := func(fieldPath, value, label string) { value = strings.TrimSpace(value) if value == "" { return } sug := Suggestion{ ProfileID: profileID, TargetContactUID: contactUID, SuggestionType: "enrich_contact", FieldPath: fieldPath, SuggestedValue: value, SuggestedLabel: label, Confidence: 0.7, Status: SuggestionPending, } if suggestionAlreadyOnContact(existing, sug) { return } out = append(out, sug) } fullName := strings.TrimSpace(data.FirstName + " " + data.LastName) addIfMissing("full_name", fullName, "") addIfMissing("company", data.Company, "") addIfMissing("job_title", data.JobTitle, "") for _, p := range data.Phones { addIfMissing("phones", p.Value, p.Label) } for _, e := range data.Emails { if !strings.EqualFold(e.Value, existing.Email) { addIfMissing("emails", e.Value, e.Label) } } for _, sp := range data.SocialProfiles { addIfMissing("social_profiles", sp.Value, sp.Label) } return out }