Skip to content

bug(leads): creating another contact at the same company overwrites the lead and clears omitted fields #15

Description

@tartakovsky

What happened?

POST /api/lead silently replaces an existing contact when another contact has the same company name and city. It also clears previously populated company/enrichment fields that were omitted from the new request.

Reproduced through the public API with synthetic records and no enrichment-provider calls. Both create requests returned the same lead ID. After adding Person Two, Person One's name/email were replaced, and website, phone, and specialization became empty strings.

This affects a company-first workflow: enrich a company, then discover multiple people at that company. Adding a person can erase the company information already collected, and adding a second person replaces the first.

Expected: omitted fields preserve existing values. A distinct person's identity should not silently replace another contact. If Leads intentionally represent one company with one contact slot, the API should reject/report the collision or require an explicit update, and document the supported path for multiple people per company. This report does not prescribe a broader schema redesign.

Steps to reproduce

Use a disposable workspace and an authenticated editor/admin session. Set TOKEN to an existing access token and WORKSPACE_ID to that workspace's ID. Requires curl and jq. The script uses a unique synthetic company and leaves the resulting test record for inspection.

set -eu
BASE_URL=http://localhost:3010
: "${TOKEN:?Set an existing API access token}"
: "${WORKSPACE_ID:?Set a disposable workspace ID}"
COMPANY="Overwrite repro $(date +%s)-$$"

api() {
  curl --fail-with-body --silent --show-error \
    -H "Authorization: Bearer $TOKEN" \
    -H "X-Workspace-Id: $WORKSPACE_ID" \
    -H 'Content-Type: application/json' "$@"
}

FIRST=$(api -X POST "$BASE_URL/api/lead" --data \
  "$(jq -n --arg company "$COMPANY" '{company:$company, city:"Repro City", website:"https://example.invalid", contact_person:"Person One", email:"one@example.invalid"}')")
LEAD_ID=$(printf '%s' "$FIRST" | jq -r '.id')
printf 'First create: %s\n' "$FIRST"

api -X PUT "$BASE_URL/api/lead/$LEAD_ID" \
  --data '{"specialization":"B2B SaaS","phone":"+15555550100"}'
api "$BASE_URL/api/lead/$LEAD_ID" | jq \
  '{id,company,city,website,contact_person,email,phone,specialization}'

SECOND=$(api -X POST "$BASE_URL/api/lead" --data \
  "$(jq -n --arg company "$COMPANY" '{company:$company, city:"Repro City", contact_person:"Person Two", email:"two@example.invalid"}')")
printf 'Second create: %s\n' "$SECOND"
api "$BASE_URL/api/lead/$LEAD_ID" | jq \
  '{id,company,city,website,contact_person,email,phone,specialization}'

The second request deliberately omits website, phone, and specialization; it does not request that they be cleared. The PUT simulates values already saved by an enrichment step without calling a provider.

How are you running OpenGTM?

Docker Compose (bundled Postgres), macOS ARM64 with Colima. The API reports PG_LEAD_STORE=True, and use_pg_store() returns True.

Version / commit

  • Running image: ghcr.io/debpalash/opengtm:3.0.0
  • Image digest: sha256:43cd3ae23422135cac37cc52f7cc942c191d8ce586ce40e573095dcbb415d362
  • Source inspection: 9fe0b57689230335aceceac691de42f620365b0f; behavior reproduced on the published 3.0.0 image.

Relevant logs

Observed API responses and selected readback fields:

First create:  {"ok":true,"id":1}
Second create: {"ok":true,"id":1}

field            before                      after
contact_person   Person One                  Person Two
email            one@example.invalid         two@example.invalid
website          https://example.invalid     ""
phone            +15555550100                ""
specialization   B2B SaaS                    ""

No API error was returned. All data above is synthetic. The local reproduction record was deleted after capturing readback.

Likely cause and regression coverage

AddLeadRequest / add_lead defaults omitted fields to empty strings and passes body.model_dump() to the store. PgLeadStore.upsert_lead matches workspace + company + city, then assigns every payload field except created_at to the existing row. Contact identity is not part of that match.

A regression test through the API should establish that omitted values survive a repeated import and that submitting a different person does not silently replace the existing contact. Explicit clearing should remain distinguishable from omission. Runtime reproduction here covers the Postgres path; SQLite behavior was not tested.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions