feat: give models.AddContact an optional Subscribed pointer and honour it in the pg_contact upsert (passed twice with explicit boolean casts because one placeholder cannot serve both the INSERT value and the DO UPDATE set without tripping Postgres parameter inference), where nil now means leave the flag alone, and stop the same upsert erasing a populated first_name, last_name, company or phone when the incoming row's cell is blank so re-importing a partial export enriches contacts instead of wiping them

This commit is contained in:
Matthew Meszaros
2026-08-27 03:43:54 -07:00
parent 66bd9e54e2
commit ae17c8dead
2 changed files with 20 additions and 6 deletions
+6
View File
@@ -321,6 +321,12 @@ type AddContact struct {
Categories []string `json:"categories"`
CustomFields map[string]string `json:"custom_fields"`
// Subscribed is the marketing-consent flag to store. nil means "don't
// decide": a new contact defaults to subscribed, an existing one keeps
// whatever it already had. Set explicitly by the importer when the file
// carries a subscribed column.
Subscribed *bool `json:"subscribed,omitempty"`
}
type SearchContactsFilterType string
+14 -6
View File
@@ -232,21 +232,29 @@ func (r *contactRepository) Add(ctx context.Context, userID string, orgID uuid.U
insertBatch := pgx.Batch{}
for _, lead := range normalized {
insertBatch.Queue(
// $9 and $10 are the same value: a parameter used both as an
// INSERT value and inside the DO UPDATE set gives Postgres two
// inference sites for one placeholder, so it is passed twice with
// explicit casts. NULL means "leave the flag alone".
`INSERT INTO contacts (
id, user_id, organization_id, first_name, last_name, email, company, phone, custom_fields
id, user_id, organization_id, first_name, last_name, email, company, phone, custom_fields, subscribed
) VALUES (
gen_random_uuid(), $1, $2, $3, $4, LOWER($5), $6, $7, $8
gen_random_uuid(), $1, $2, $3, $4, LOWER($5), $6, $7, $8, COALESCE($9::boolean, TRUE)
)
ON CONFLICT (user_id, (LOWER(email))) DO UPDATE SET
organization_id = COALESCE(contacts.organization_id, EXCLUDED.organization_id),
first_name = EXCLUDED.first_name,
last_name = EXCLUDED.last_name,
company = EXCLUDED.company,
phone = EXCLUDED.phone,
-- Enrich, never erase: a blank cell in a re-imported file must
-- not wipe a name we already have.
first_name = COALESCE(NULLIF(EXCLUDED.first_name, ''), contacts.first_name),
last_name = COALESCE(NULLIF(EXCLUDED.last_name, ''), contacts.last_name),
company = COALESCE(NULLIF(EXCLUDED.company, ''), contacts.company),
phone = COALESCE(NULLIF(EXCLUDED.phone, ''), contacts.phone),
custom_fields = contacts.custom_fields || EXCLUDED.custom_fields,
subscribed = COALESCE($10::boolean, contacts.subscribed),
updated_at = NOW()
RETURNING id, first_name, last_name, email, company, phone, custom_fields, subscribed, updated_at, created_at`,
userID, orgID, lead.FirstName, lead.LastName, lead.Email, lead.Company, lead.Phone, lead.CustomFields,
lead.Subscribed, lead.Subscribed,
)
}