LeadSquared sync now recovers automatically when a contact's cached lead has been deleted or merged on the LeadSquared side. Previously the stale lead id was never cleared, so every new conversation or contact update for that contact failed with "Lead not found" (`MXInvalidEntityReferenceException`) indefinitely. ## What changed - Activity sync: on a "Lead not found" error while posting a conversation/transcript activity, clear the cached `leadsquared_id`, re-resolve the contact to a fresh lead, and retry the activity once (guarded against loops and duplicate leads). - Contact sync: on the same error while updating an existing lead, clear the cached id and create a fresh lead instead. - Fix `get_lead_id` to actually return early for unidentifiable contacts (the guard previously fell through). ## How to reproduce 1. For a LeadSquared-enabled account, point a contact's cached lead id at a lead that no longer exists in LeadSquared. 2. Update the contact, or create/resolve a conversation for it. 3. Before: the sync fails repeatedly with "Lead not found" and never self-corrects. After: the stale id is cleared, a fresh lead is resolved/created, and subsequent syncs reuse the healed id. --------- Co-authored-by: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com>
157 lines
5.9 KiB
Ruby
157 lines
5.9 KiB
Ruby
class Crm::Leadsquared::ProcessorService < Crm::BaseProcessorService
|
|
def self.crm_name
|
|
'leadsquared'
|
|
end
|
|
|
|
def initialize(hook)
|
|
super(hook)
|
|
@access_key = hook.settings['access_key']
|
|
@secret_key = hook.settings['secret_key']
|
|
@endpoint_url = hook.settings['endpoint_url']
|
|
|
|
@allow_transcript = hook.settings['enable_transcript_activity']
|
|
@allow_conversation = hook.settings['enable_conversation_activity']
|
|
|
|
# Initialize API clients
|
|
@lead_client = Crm::Leadsquared::Api::LeadClient.new(@access_key, @secret_key, @endpoint_url)
|
|
@activity_client = Crm::Leadsquared::Api::ActivityClient.new(@access_key, @secret_key, @endpoint_url)
|
|
@lead_finder = Crm::Leadsquared::LeadFinderService.new(@lead_client)
|
|
end
|
|
|
|
def handle_contact(contact)
|
|
contact.reload
|
|
unless identifiable_contact?(contact)
|
|
Rails.logger.info("Contact not identifiable. Skipping handle_contact for ##{contact.id}")
|
|
return
|
|
end
|
|
|
|
stored_lead_id = get_external_id(contact)
|
|
create_or_update_lead(contact, stored_lead_id)
|
|
end
|
|
|
|
def handle_conversation_created(conversation)
|
|
return unless @allow_conversation
|
|
|
|
create_conversation_activity(
|
|
conversation: conversation,
|
|
activity_type: 'conversation',
|
|
activity_code_key: 'conversation_activity_code',
|
|
metadata_key: 'created_activity_id',
|
|
activity_note: Crm::Leadsquared::Mappers::ConversationMapper.map_conversation_activity(@hook, conversation)
|
|
)
|
|
end
|
|
|
|
def handle_conversation_resolved(conversation)
|
|
return unless @allow_transcript
|
|
return unless conversation.status == 'resolved'
|
|
|
|
create_conversation_activity(
|
|
conversation: conversation,
|
|
activity_type: 'transcript',
|
|
activity_code_key: 'transcript_activity_code',
|
|
metadata_key: 'transcript_activity_id',
|
|
activity_note: Crm::Leadsquared::Mappers::ConversationMapper.map_transcript_activity(@hook, conversation)
|
|
)
|
|
end
|
|
|
|
private
|
|
|
|
def create_or_update_lead(contact, lead_id)
|
|
lead_data = Crm::Leadsquared::Mappers::ContactMapper.map(contact)
|
|
|
|
# Why can't we use create_or_update_lead here?
|
|
# In LeadSquared, it's possible that the email field
|
|
# may not be marked as unique, same with the phone number field
|
|
# So we just use the update API if we already have a lead ID
|
|
if lead_id.present?
|
|
with_stale_lead_recovery(contact, lead_id) { |id| @lead_client.update_lead(lead_data, id) }
|
|
else
|
|
new_lead_id = @lead_client.create_or_update_lead(lead_data)
|
|
store_external_id(contact, new_lead_id)
|
|
end
|
|
rescue Crm::Leadsquared::Api::BaseClient::ApiError => e
|
|
ChatwootExceptionTracker.new(e, account: @account).capture_exception
|
|
Rails.logger.error "LeadSquared API error processing contact: #{e.message}"
|
|
rescue StandardError => e
|
|
ChatwootExceptionTracker.new(e, account: @account).capture_exception
|
|
Rails.logger.error "Error processing contact in LeadSquared: #{e.message}"
|
|
end
|
|
|
|
def create_conversation_activity(conversation:, activity_type:, activity_code_key:, metadata_key:, activity_note:)
|
|
lead_id = get_lead_id(conversation.contact)
|
|
return if lead_id.blank?
|
|
|
|
activity_code = get_activity_code(activity_code_key)
|
|
activity_id = with_stale_lead_recovery(conversation.contact, lead_id) do |id|
|
|
@activity_client.post_activity(id, activity_code, activity_note)
|
|
end
|
|
return if activity_id.blank?
|
|
|
|
metadata = {}
|
|
metadata[metadata_key] = activity_id
|
|
store_conversation_metadata(conversation, metadata)
|
|
rescue Crm::Leadsquared::Api::BaseClient::ApiError => e
|
|
log_activity_error(e, activity_type, conversation, payload: { lead_id: lead_id, activity_code: activity_code, activity_note: activity_note })
|
|
rescue StandardError => e
|
|
log_activity_error(e, activity_type, conversation)
|
|
end
|
|
|
|
# The cached lead id can become stale when the lead is deleted/merged in LeadSquared,
|
|
# making LeadSquared reject the call with "Lead not found". When that happens, clear the
|
|
# stored id, re-resolve the contact to a fresh lead, and run the operation again once.
|
|
def with_stale_lead_recovery(contact, lead_id)
|
|
yield(lead_id)
|
|
rescue Crm::Leadsquared::Api::BaseClient::ApiError => e
|
|
raise unless lead_not_found_error?(e)
|
|
|
|
Rails.logger.warn("LeadSquared stale lead #{lead_id} for contact ##{contact.id}, clearing and retrying")
|
|
clear_external_id(contact)
|
|
fresh_lead_id = get_lead_id(contact)
|
|
raise if fresh_lead_id.blank? || fresh_lead_id == lead_id
|
|
|
|
yield(fresh_lead_id)
|
|
end
|
|
|
|
def lead_not_found_error?(error)
|
|
return false if error.response.blank?
|
|
|
|
parsed = error.response.parsed_response
|
|
parsed.is_a?(Hash) && parsed['ExceptionType'] == 'MXInvalidEntityReferenceException'
|
|
rescue StandardError
|
|
false
|
|
end
|
|
|
|
def log_activity_error(error, activity_type, conversation, payload: nil)
|
|
ChatwootExceptionTracker.new(error, account: @account).capture_exception
|
|
context = "account_id=#{conversation.account_id}, conversation_display_id=#{conversation.display_id}"
|
|
if payload
|
|
context += ", http_status=#{error.code}, prospect_id=#{payload[:lead_id]}, " \
|
|
"activity_event=#{payload[:activity_code]}, note_bytes=#{payload[:activity_note].to_s.bytesize}"
|
|
end
|
|
Rails.logger.error("LeadSquared #{activity_type} activity failed: #{error.message} (#{context})")
|
|
end
|
|
|
|
def get_activity_code(key)
|
|
activity_code = @hook.settings[key]
|
|
raise StandardError, "LeadSquared #{key} activity code not found for hook ##{@hook.id}." if activity_code.blank?
|
|
|
|
activity_code
|
|
end
|
|
|
|
def get_lead_id(contact)
|
|
contact.reload # reload to ensure all the attributes are up-to-date
|
|
|
|
unless identifiable_contact?(contact)
|
|
Rails.logger.info("Contact not identifiable. Skipping activity for ##{contact.id}")
|
|
return nil
|
|
end
|
|
|
|
lead_id = @lead_finder.find_or_create(contact)
|
|
return nil if lead_id.blank?
|
|
|
|
store_external_id(contact, lead_id) unless get_external_id(contact)
|
|
|
|
lead_id
|
|
end
|
|
end
|