feat(crm): merge company slogan variants in dedupe grouping

Treat names like "Affirm" and "Affirm — Fraud Engineering" as the same
company for dedupe, deal matching, members, and applications sync lookup.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-24 12:40:27 +01:00
co-authored by Cursor
parent b79c097874
commit a59b4fb296
6 changed files with 108 additions and 8 deletions
+2
View File
@@ -15,6 +15,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
`MergeContacts`, `UpdateOpportunityTitle`, `ListAllContacts`, `MergeContacts`, `UpdateOpportunityTitle`, `ListAllContacts`,
`ListAllOpportunities`, `DeleteContactInfo`, `RemoveOpportunityMember`, `ListAllOpportunities`, `DeleteContactInfo`, `RemoveOpportunityMember`,
`IsOpportunityMember`, `HasContactInfo`, `CleanupCRM`. `IsOpportunityMember`, `HasContactInfo`, `CleanupCRM`.
- Slogan-aware grouping: `StripSloganSuffix`, `CompanyGroupingKey` merge names
like `Affirm` and `Affirm — Fraud Engineering` (companies, deals, members).
- [`crm_integration_test.go`](crm_integration_test.go) — live merge, rename, - [`crm_integration_test.go`](crm_integration_test.go) — live merge, rename,
dedupe smoke tests. dedupe smoke tests.
+5 -2
View File
@@ -47,7 +47,7 @@ func (c *Client) GetContact(ctx context.Context, contactID string) (map[string]a
// FindCompany searches for a company contact with an exact (case-insensitive) // FindCompany searches for a company contact with an exact (case-insensitive)
// displayName match. Returns nil when not found. // displayName match. Returns nil when not found.
func (c *Client) FindCompany(ctx context.Context, name string) (map[string]any, error) { func (c *Client) FindCompany(ctx context.Context, name string) (map[string]any, error) {
needle := strings.ToLower(strings.TrimSpace(name)) needle := CompanyGroupingKey(name)
const page = 50 const page = 50
for start := 0; ; start += page { for start := 0; ; start += page {
items, total, err := c.ListContacts(ctx, page, start, name) items, total, err := c.ListContacts(ctx, page, start, name)
@@ -55,7 +55,10 @@ func (c *Client) FindCompany(ctx context.Context, name string) (map[string]any,
return nil, err return nil, err
} }
for _, co := range items { for _, co := range items {
if isCompany(co) && strings.ToLower(fmt.Sprint(co["displayName"])) == needle { if !isCompany(co) {
continue
}
if CompanyGroupingKey(fmt.Sprint(co["displayName"])) == needle {
return co, nil return co, nil
} }
} }
+13 -2
View File
@@ -38,7 +38,7 @@ func GroupCompaniesByName(items []map[string]any) map[string][]map[string]any {
if !isCompany(row) { if !isCompany(row) {
continue continue
} }
key := NormalizeCompanyName(rowDisplayName(row)) key := CompanyGroupingKey(rowDisplayName(row))
if key == "" { if key == "" {
continue continue
} }
@@ -65,10 +65,21 @@ func GroupPersonsByKey(items []map[string]any) map[string][]map[string]any {
// DealTitleKey returns the grouping key for an opportunity title. // DealTitleKey returns the grouping key for an opportunity title.
func DealTitleKey(title string, ignoreCompanySuffix bool) string { func DealTitleKey(title string, ignoreCompanySuffix bool) string {
title = strings.TrimSpace(title)
if ignoreCompanySuffix { if ignoreCompanySuffix {
return NormalizeOpportunityTitle(StripCompanySuffix(title)) return NormalizeOpportunityTitle(StripCompanySuffix(title))
} }
return NormalizeOpportunityTitle(title) if i := strings.LastIndex(title, " @ "); i >= 0 {
pos := strings.TrimSpace(title[:i])
co := StripSloganSuffix(strings.TrimSpace(title[i+len(" @ "):]))
if pos == "" && co != "" {
return NormalizeCompanyName(co)
}
if pos != "" && co != "" {
return collapseKey(pos + " @ " + co)
}
}
return NormalizeCompanyName(StripSloganSuffix(title))
} }
// GroupOpportunitiesByTitle buckets deals by title key. // GroupOpportunitiesByTitle buckets deals by title key.
+31 -2
View File
@@ -24,7 +24,7 @@ func TestGroupCompaniesByName(t *testing.T) {
if len(groups) != 2 { if len(groups) != 2 {
t.Fatalf("groups: %d", len(groups)) t.Fatalf("groups: %d", len(groups))
} }
key := NormalizeCompanyName("contoso") key := CompanyGroupingKey("contoso")
if len(groups[key]) != 2 { if len(groups[key]) != 2 {
t.Fatalf("contoso group: %d", len(groups[key])) t.Fatalf("contoso group: %d", len(groups[key]))
} }
@@ -61,11 +61,40 @@ func TestGroupContactInfoRows(t *testing.T) {
} }
} }
func TestGroupCompaniesBySlogan(t *testing.T) {
items := []map[string]any{
{"id": float64(1), "displayName": "Affirm", "isCompany": true},
{"id": float64(2), "displayName": "Affirm — Fraud Engineering", "isCompany": true},
}
groups := GroupCompaniesByName(items)
if len(groups) != 1 {
t.Fatalf("groups: %d", len(groups))
}
key := CompanyGroupingKey("Affirm")
if len(groups[key]) != 2 {
t.Fatalf("affirm group: %d", len(groups[key]))
}
}
func TestDedupeMembersBySloganDisplayName(t *testing.T) {
members := []map[string]any{
{"id": float64(1), "displayName": "Affirm"},
{"id": float64(2), "displayName": "Affirm — Fraud Engineering"},
}
remove := DedupeMembersByDisplayName(members)
if !reflect.DeepEqual(remove, []int64{2}) {
t.Fatalf("remove %v", remove)
}
}
func TestDealTitleKey(t *testing.T) { func TestDealTitleKey(t *testing.T) {
if got := DealTitleKey("Dev @ Acme", false); got != NormalizeOpportunityTitle("Dev @ Acme") { if got := DealTitleKey("Dev @ Acme", false); got != collapseKey("Dev @ Acme") {
t.Fatalf("got %q", got) t.Fatalf("got %q", got)
} }
if got := DealTitleKey("Dev @ Acme", true); got != NormalizeOpportunityTitle("Dev") { if got := DealTitleKey("Dev @ Acme", true); got != NormalizeOpportunityTitle("Dev") {
t.Fatalf("got %q", got) t.Fatalf("got %q", got)
} }
if got := DealTitleKey("Dev @ Affirm — Fraud Engineering", false); got != DealTitleKey("Dev @ Affirm", false) {
t.Fatalf("slogan keys differ: %q vs %q", got, DealTitleKey("Dev @ Affirm", false))
}
} }
+26 -2
View File
@@ -7,6 +7,26 @@ import (
var multiSpace = regexp.MustCompile(`\s+`) var multiSpace = regexp.MustCompile(`\s+`)
// sloganSeparators split a company name from a trailing tagline/slogan.
var sloganSeparators = []string{" — ", " – ", " - ", "—", "–"}
// StripSloganSuffix returns the part before an em/en dash tagline, e.g.
// "Affirm — Fraud Engineering" → "Affirm".
func StripSloganSuffix(s string) string {
s = strings.TrimSpace(s)
for _, sep := range sloganSeparators {
if i := strings.Index(s, sep); i > 0 {
return strings.TrimSpace(s[:i])
}
}
return s
}
// CompanyGroupingKey normalizes a company name for dedupe (ignores slogans).
func CompanyGroupingKey(s string) string {
return NormalizeCompanyName(StripSloganSuffix(s))
}
// NormalizeCompanyName lowercases and collapses whitespace for grouping. // NormalizeCompanyName lowercases and collapses whitespace for grouping.
func NormalizeCompanyName(s string) string { func NormalizeCompanyName(s string) string {
return collapseKey(s) return collapseKey(s)
@@ -61,7 +81,7 @@ func ContactInfoKey(infoType, value string) string {
// MemberDisplayKey normalizes a member displayName for duplicate detection. // MemberDisplayKey normalizes a member displayName for duplicate detection.
func MemberDisplayKey(displayName string) string { func MemberDisplayKey(displayName string) string {
return NormalizeCompanyName(displayName) return CompanyGroupingKey(displayName)
} }
func collapseKey(s string) string { func collapseKey(s string) string {
@@ -70,7 +90,11 @@ func collapseKey(s string) string {
return strings.ToLower(s) return strings.ToLower(s)
} }
// DealTitleForApplication builds the sync deal title from position and company. // OpportunityTitlesMatch reports whether two deal titles refer to the same role+company.
func OpportunityTitlesMatch(a, b string) bool {
return DealTitleKey(a, false) == DealTitleKey(b, false)
}
func DealTitleForApplication(position, company string) string { func DealTitleForApplication(position, company string) string {
position = strings.TrimSpace(position) position = strings.TrimSpace(position)
company = strings.TrimSpace(company) company = strings.TrimSpace(company)
+31
View File
@@ -42,6 +42,37 @@ func TestFixDealTitle(t *testing.T) {
} }
} }
func TestStripSloganSuffix(t *testing.T) {
tests := []struct {
in, want string
}{
{"Affirm — Fraud Engineering", "Affirm"},
{"Affirm - Fraud Engineering", "Affirm"},
{"Affirm – Fraud Engineering", "Affirm"},
{"Affirm", "Affirm"},
{"— leading", "— leading"},
}
for _, tc := range tests {
if got := StripSloganSuffix(tc.in); got != tc.want {
t.Errorf("StripSloganSuffix(%q) = %q, want %q", tc.in, got, tc.want)
}
}
}
func TestCompanyGroupingKey(t *testing.T) {
a := CompanyGroupingKey("Affirm")
b := CompanyGroupingKey("Affirm — Fraud Engineering")
if a != b {
t.Fatalf("%q != %q", a, b)
}
}
func TestOpportunityTitlesMatchSlogan(t *testing.T) {
if !OpportunityTitlesMatch("Dev @ Affirm — Fraud Engineering", "Dev @ Affirm") {
t.Fatal("expected match")
}
}
func TestStripCompanySuffix(t *testing.T) { func TestStripCompanySuffix(t *testing.T) {
if got := StripCompanySuffix("Dev @ Acme"); got != "Dev" { if got := StripCompanySuffix("Dev @ Acme"); got != "Dev" {
t.Fatalf("got %q", got) t.Fatalf("got %q", got)