8.4 KiB
8.4 KiB
LeadSquared CRM Integration (Backend)
High-Level Flow
- Accounts enable the
leadsquaredapp viaconfig/integration/apps.yml; the app is gated by thecrm_integrationfeature flag and exposes settings for API keys, activity toggles, and scores. - Creating an
Integrations::Hook(app/models/integrations/hook.rb) for this app stores credentials, marks the hook asaccount-scoped, and enqueuesCrm::SetupJobto prepare LeadSquared-side prerequisites. Crm::SetupJob(app/jobs/crm/setup_job.rb) instantiatesCrm::Leadsquared::SetupServiceto resolve the tenant-specific API endpoint/timezone and ensure required custom activity types exist. The job persists setup output back into the hook’ssettingsjson.- Runtime events (
contact.updated,conversation.created,conversation.resolved) fan out throughHookListener(app/listeners/hook_listener.rb) toHookJob(app/jobs/hook_job.rb), which serialises processing per hook with a Redis mutex and delegates toCrm::Leadsquared::ProcessorService. - Processor services under
app/services/crm/leadsquared/orchestrate LeadSquared API calls, map Chatwoot data into LeadSquared payloads, and store external IDs/activity metadata on contacts and conversations to retain linkage.
Key Responsibilities by File
config/integration/apps.yml: Declares the LeadSquared app, required credentials (access_key,secret_key), optional activity toggles, score defaults, and visibility of stored settings. The schema enforces the shape of hooksettingscoming from the UI.app/models/integrations/hook.rb: Persists integration hooks, enforces uniqueness per account, validates settings via the JSON schema above, checks thecrm_integrationfeature flag, and enqueues CRM setup after creation. Stores a deterministic encryptedaccess_tokenand exposesfeature_allowed?.app/jobs/crm/setup_job.rb: Background job that loads the hook, short-circuits disabled/missing hooks, and invokes the right CRM-specific setup service (currently only LeadSquared).app/services/crm/leadsquared/setup_service.rb: Calls LeadSquared’sAuthentication.svc/UserByAccessKey.Getto derive tenant-specific API and app hosts, updates hook settings (endpoint_url,app_url,timezone), and ensures custom activity types for conversation start/transcript exist (creating them if absent) while persisting their numeric codes.app/jobs/hook_job.rb: Consumes domain events and routes them per integration. For LeadSquared it locks onRedis::Alfred::CRM_PROCESS_MUTEX(lib/redis/redis_keys.rb) to avoid creating duplicate leads during rapid event bursts, then passes processing to the CRM service.app/services/crm/base_processor_service.rb: Shared helpers for CRM processors—enforces interface (handle_contact_created/updated,handle_conversation_created/resolved), provides helpers for checking contact identifiability, reading/storing external IDs on contacts, and attaching metadata to conversations.app/services/crm/leadsquared/processor_service.rb: Main runtime handler. Wires API clients, honours the hook’senable_*toggles, processes contacts (create/update leads) and conversations (post LeadSquared activities), and records resulting IDs/metadata back to Chatwoot models. UsesChatwootExceptionTrackerfor surfaced errors.app/services/crm/leadsquared/lead_finder_service.rb: Retrieves an existing lead ID from stored contact metadata, searches LeadSquared by email or phone when absent, and creates a new lead as a last resort.app/services/crm/leadsquared/api/base_client.rb: Thin HTTParty wrapper that signs requests with LeadSquared access/secret keys, centralises response parsing, and raisesApiErroron non-success or malformed responses.app/services/crm/leadsquared/api/lead_client.rb: Exposes search/update/create operations for leads, includingcreate_or_update_lead(default LeadSquared behaviour) and an explicitupdate_leadused when the Chatwoot contact already stores the external ID.app/services/crm/leadsquared/api/activity_client.rb: Manages posting activities against leads and creating/fetching activity types.app/services/crm/leadsquared/mappers/contact_mapper.rb: Normalises Chatwoot contact fields to LeadSquared attribute keys, reformats phone numbers to+<country>-<national>form, and injects brand/source info.app/services/crm/leadsquared/mappers/conversation_mapper.rb: Builds human-readable activity notes for conversation creation and transcript events, trimming content to LeadSquared limits and linking back to the Chatwoot conversation URL.
Lifecycle by Use Case
Hook Creation & Setup
- Admin enables the integration, supplying at least
access_key/secret_key. Integrations::Hookpersists the hook (hook_type: account) and enqueuesCrm::SetupJob.SetupServiceresolves tenant metadata, resets clients to the tenant-specific base URL, and ensures two activity types:<Brand> Conversation Started<Brand> Conversation TranscriptTheir numeric codes and resolved URLs/timezone are stored back intohook.settings.
- Subsequent API calls use the tenant-specific endpoint and the hook’s stored codes/timezone.
Contact Sync (contact.updated)
- Trigger:
HookListener#contact_updateddispatches when a Chatwoot contact changes. HookJoblocks per hook, instantiatesCrm::Leadsquared::ProcessorService, and callshandle_contact.- Processor reloads the latest contact, skips if lacking email/phone/social profile, and retrieves any stored LeadSquared ID.
- With an ID it calls
LeadClient#update_lead; otherwise it callsLeadClient#create_or_update_leadand persists the returnedProspectIDintocontact.additional_attributes['external']['leadsquared_id']. - Any API/runtime error is captured via
ChatwootExceptionTrackerand logged; failures do not retry via the mutex job by default.
Conversation Created (conversation.created)
- Trigger:
HookListener#conversation_created. HookJoblocks and delegates tohandle_conversation_created.- Processor exits early if the hook has
enable_conversation_activitydisabled. - Uses
LeadFinderServiceto fetch or create the lead ID tied to the conversation’s contact (persisting it if newly discovered). - Builds the activity note via
ConversationMapper.map_conversation_activity, fetches the configuredconversation_activity_code, and posts the activity withActivityClient#post_activity. - Stores the resulting activity ID under
conversation.additional_attributes['leadsquared']['created_activity_id']for traceability.
Conversation Resolved (conversation.resolved)
- Trigger:
HookListener#conversation_resolvedwhen status transitions toresolved. HookJoblocks and callshandle_conversation_resolved.- Processor requires
enable_transcript_activityto be true and the conversation status to still beresolved. - Builds a transcript note (reverse-chronological, capped at 1,800 chars, including attachments) via
ConversationMapper.map_transcript_activity. - Posts the transcript activity using
transcript_activity_codeand records the returned ID asconversation.additional_attributes['leadsquared']['transcript_activity_id'].
Stored Metadata & Settings
- Hook settings persist credentials, resolved endpoint/app URLs, timezone, activity toggles, scores, and the created activity type codes. These values feed processor behaviour and the frontend’s “visible properties” list.
- Contacts store
additional_attributes['external']['leadsquared_id']once a LeadSquared prospect exists. - Conversations store a nested
additional_attributes['leadsquared']hash with any activity IDs created.
Error Handling & Concurrency
- All API wrappers raise
ApiErroron non-success; callers rescue to log and forward toChatwootExceptionTracker. HookJobinherits fromMutexApplicationJob; coupled with the per-hook Redis mutex it prevents concurrent processing of contact/conversation events that would otherwise create duplicate leads.- Unsupported events or disabled hooks short-circuit in
HookListener/HookJob, avoiding unnecessary API calls.
Extensibility Notes
- The LeadSquared implementation follows the
Crm::BaseProcessorServicecontract, simplifying future CRM additions. - Setup work is encapsulated so additional CRMs can hook into
Crm::SetupJob#create_setup_service. - Feature flag (
crm_integration) and hook JSON schema guardrails ensure enterprise-only exposure and predictable payloads.