feat: fallback on phone number to update lead (#13910)

When syncing contacts to LeadSquared, the `Lead.CreateOrUpdate` API
defaults to searching by email. If a contact has no email (or a
different email) but a phone number matching an existing lead, the API
fails with `MXDuplicateEntryException` instead of finding and updating
the existing lead. This accounted for ~69% of all LeadSquared
integration errors, and cascaded into "Lead not found" failures when
posting transcript and conversation activities (~14% of errors).

## What changed

- `LeadClient#create_or_update_lead` now catches
`MXDuplicateEntryException` and retries the request once with
`SearchBy=Phone` appended to the body, telling the API to match on phone
number instead
- Once the retry succeeds, the returned lead ID is stored on the contact
(existing behavior), so all future events use the direct `update_lead`
path and never hit the duplicate error again

## How to reproduce

1. Create a lead in LeadSquared with phone number `+91-75076767676` and
email `a@example.com`
2. In Chatwoot, create a contact with the same phone number but a
different email (or no email)
3. Trigger a contact sync (via conversation creation or contact update)
4. Before fix: `MXDuplicateEntryException` error in logs, contact fails
to sync
5. After fix: retry with `SearchBy=Phone` finds and updates the existing
lead, stores the lead ID on the contact
This commit is contained in:
Shivam Mishra
2026-03-26 12:32:27 +05:30
committed by GitHub
parent 742c5cc1f4
commit e4c3f0ac2f
2 changed files with 143 additions and 4 deletions

View File

@@ -12,16 +12,21 @@ class Crm::Leadsquared::Api::LeadClient < Crm::Leadsquared::Api::BaseClient
# https://apidocs.leadsquared.com/create-or-update/#api
# The email address and phone fields are used as the default search criteria.
# If none of these match with an existing lead, a new lead will be created.
# We can pass the "SearchBy" attribute in the JSON body to search by a particular parameter, however
# we don't need this capability at the moment
# We pass the "SearchBy" attribute with value "Phone" when a MXDuplicateEntryException
# occurs, indicating a duplicate mobile number match that the default search missed.
def create_or_update_lead(lead_data)
raise ArgumentError, 'Lead data is required' if lead_data.blank?
path = 'LeadManagement.svc/Lead.CreateOrUpdate'
formatted_data = format_lead_data(lead_data)
response = post(path, {}, formatted_data)
response = post(path, {}, formatted_data)
response['Message']['Id']
rescue ApiError => e
raise unless duplicate_phone_error?(e) && lead_data.key?('Mobile')
Rails.logger.warn 'LeadSquared duplicate phone detected, retrying with SearchBy=Phone'
response = post(path, {}, formatted_data + [{ 'Attribute' => 'SearchBy', 'Value' => 'Phone' }])
response['Message']['Id']
end
@@ -47,4 +52,13 @@ class Crm::Leadsquared::Api::LeadClient < Crm::Leadsquared::Api::BaseClient
}
end
end
def duplicate_phone_error?(error)
return false if error.response.blank?
parsed = error.response.parsed_response
parsed.is_a?(Hash) && parsed['ExceptionType'] == 'MXDuplicateEntryException'
rescue StandardError
false
end
end