Files
chatwoot/app/services/crm/base_processor_service.rb
299bc6c0a4 fix: recover from stale LeadSquared lead ids on activity sync (#14818)
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>
2026-06-29 16:38:08 +05:30

101 lines
3.2 KiB
Ruby

class Crm::BaseProcessorService
def initialize(hook)
@hook = hook
@account = hook.account
end
# Class method to be overridden by subclasses
def self.crm_name
raise NotImplementedError, 'Subclasses must define self.crm_name'
end
# Instance method that calls the class method
def crm_name
self.class.crm_name
end
def process_event(event_name, event_data)
case event_name
when 'contact.created'
handle_contact_created(event_data)
when 'contact.updated'
handle_contact_updated(event_data)
when 'conversation.created'
handle_conversation_created(event_data)
when 'conversation.updated'
handle_conversation_updated(event_data)
else
{ success: false, error: "Unsupported event: #{event_name}" }
end
rescue StandardError => e
Rails.logger.error "#{crm_name} Processor Error: #{e.message}"
Rails.logger.error e.backtrace.join("\n")
{ success: false, error: e.message }
end
# Abstract methods that subclasses must implement
def handle_contact_created(contact)
raise NotImplementedError, 'Subclasses must implement #handle_contact_created'
end
def handle_contact_updated(contact)
raise NotImplementedError, 'Subclasses must implement #handle_contact_updated'
end
def handle_conversation_created(conversation)
raise NotImplementedError, 'Subclasses must implement #handle_conversation_created'
end
def handle_conversation_resolved(conversation)
raise NotImplementedError, 'Subclasses must implement #handle_conversation_resolved'
end
# Common helper methods for all CRM processors
protected
def identifiable_contact?(contact)
has_social_profile = contact.additional_attributes['social_profiles'].present?
contact.present? && (contact.email.present? || contact.phone_number.present? || has_social_profile)
end
def get_external_id(contact)
return nil if contact.additional_attributes.blank?
return nil if contact.additional_attributes['external'].blank?
contact.additional_attributes.dig('external', "#{crm_name}_id")
end
def store_external_id(contact, external_id)
# Initialize additional_attributes if it's nil
contact.additional_attributes = {} if contact.additional_attributes.nil?
# Initialize external hash if it doesn't exist
contact.additional_attributes['external'] = {} if contact.additional_attributes['external'].blank?
# Store the external ID
contact.additional_attributes['external']["#{crm_name}_id"] = external_id
contact.save!
end
def clear_external_id(contact)
return if contact.additional_attributes.blank?
return if contact.additional_attributes['external'].blank?
contact.additional_attributes['external'].delete("#{crm_name}_id")
contact.save!
end
def store_conversation_metadata(conversation, metadata)
# Initialize additional_attributes if it's nil
conversation.additional_attributes = {} if conversation.additional_attributes.nil?
# Initialize CRM-specific hash in additional_attributes
conversation.additional_attributes[crm_name] = {} if conversation.additional_attributes[crm_name].blank?
# Store the metadata
conversation.additional_attributes[crm_name].merge!(metadata)
conversation.save!
end
end