diff --git a/app/services/crm/leadsquared/api/lead_client.rb b/app/services/crm/leadsquared/api/lead_client.rb index 21e7d6937..f8e238a18 100644 --- a/app/services/crm/leadsquared/api/lead_client.rb +++ b/app/services/crm/leadsquared/api/lead_client.rb @@ -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 diff --git a/spec/services/crm/leadsquared/api/lead_client_spec.rb b/spec/services/crm/leadsquared/api/lead_client_spec.rb index 0be37adb7..01223daf8 100644 --- a/spec/services/crm/leadsquared/api/lead_client_spec.rb +++ b/spec/services/crm/leadsquared/api/lead_client_spec.rb @@ -153,6 +153,131 @@ RSpec.describe Crm::Leadsquared::Api::LeadClient do end end + context 'when request fails with MXDuplicateEntryException and lead has Mobile' do + let(:lead_data) do + { + 'FirstName' => 'John', + 'EmailAddress' => 'john.doe@example.com', + 'Mobile' => '+91-7507684392' + } + end + let(:formatted_lead_data) do + lead_data.map { |key, value| { 'Attribute' => key, 'Value' => value } } + end + let(:formatted_lead_data_with_search_by) do + formatted_lead_data + [{ 'Attribute' => 'SearchBy', 'Value' => 'Phone' }] + end + let(:lead_id) { SecureRandom.uuid } + let(:duplicate_error_response) do + { + 'Status' => 'Error', + 'ExceptionType' => 'MXDuplicateEntryException', + 'ExceptionMessage' => 'A Lead with same Primary Mobile Number already exists.', + 'IsMXException' => true + } + end + let(:success_response) do + { 'Status' => 'Success', 'Message' => { 'Id' => lead_id } } + end + + before do + stub_request(:post, full_url) + .with(body: formatted_lead_data.to_json, headers: headers) + .to_return(status: 500, body: duplicate_error_response.to_json, headers: { 'Content-Type' => 'application/json' }) + stub_request(:post, full_url) + .with(body: formatted_lead_data_with_search_by.to_json, headers: headers) + .to_return(status: 200, body: success_response.to_json, headers: { 'Content-Type' => 'application/json' }) + end + + it 'retries with SearchBy=Phone and returns lead ID' do + expect(client.create_or_update_lead(lead_data)).to eq(lead_id) + end + end + + context 'when request fails with MXDuplicateEntryException but lead has no Mobile' do + let(:duplicate_error_response) do + { + 'Status' => 'Error', + 'ExceptionType' => 'MXDuplicateEntryException', + 'ExceptionMessage' => 'A Lead with same Primary Mobile Number already exists.', + 'IsMXException' => true + } + end + + before do + stub_request(:post, full_url) + .with(body: formatted_lead_data.to_json, headers: headers) + .to_return(status: 500, body: duplicate_error_response.to_json, headers: { 'Content-Type' => 'application/json' }) + end + + it 'raises ApiError without retrying' do + expect { client.create_or_update_lead(lead_data) } + .to(raise_error { |error| expect(error.class.name).to eq('Crm::Leadsquared::Api::BaseClient::ApiError') }) + end + end + + context 'when request fails with a non-duplicate 500 error' do + let(:lead_data) do + { 'FirstName' => 'John', 'Mobile' => '+91-7507684392' } + end + let(:formatted_lead_data) do + lead_data.map { |key, value| { 'Attribute' => key, 'Value' => value } } + end + + before do + stub_request(:post, full_url) + .with(body: formatted_lead_data.to_json, headers: headers) + .to_return( + status: 500, + body: { 'Status' => 'Error', 'ExceptionType' => 'SomeOtherException', 'ExceptionMessage' => 'Something else' }.to_json, + headers: { 'Content-Type' => 'application/json' } + ) + end + + it 'raises ApiError without retrying' do + expect { client.create_or_update_lead(lead_data) } + .to(raise_error { |error| expect(error.class.name).to eq('Crm::Leadsquared::Api::BaseClient::ApiError') }) + end + end + + context 'when retry with SearchBy=Phone also fails' do + let(:lead_data) do + { 'FirstName' => 'John', 'Mobile' => '+91-7507684392' } + end + let(:formatted_lead_data) do + lead_data.map { |key, value| { 'Attribute' => key, 'Value' => value } } + end + let(:formatted_lead_data_with_search_by) do + formatted_lead_data + [{ 'Attribute' => 'SearchBy', 'Value' => 'Phone' }] + end + let(:duplicate_error_response) do + { + 'Status' => 'Error', + 'ExceptionType' => 'MXDuplicateEntryException', + 'ExceptionMessage' => 'A Lead with same Primary Mobile Number already exists.', + 'IsMXException' => true + } + end + + before do + stub_request(:post, full_url) + .with(body: formatted_lead_data.to_json, headers: headers) + .to_return(status: 500, body: duplicate_error_response.to_json, headers: { 'Content-Type' => 'application/json' }) + stub_request(:post, full_url) + .with(body: formatted_lead_data_with_search_by.to_json, headers: headers) + .to_return( + status: 500, + body: { 'Status' => 'Error', 'ExceptionMessage' => 'Still failing' }.to_json, + headers: { 'Content-Type' => 'application/json' } + ) + end + + it 'raises ApiError from the retry attempt' do + expect { client.create_or_update_lead(lead_data) } + .to(raise_error { |error| expect(error.class.name).to eq('Crm::Leadsquared::Api::BaseClient::ApiError') }) + end + end + # Add test for update_lead method describe '#update_lead' do let(:path) { 'LeadManagement.svc/Lead.Update' }