feat: integrate LeadSquared CRM (#11284)

This commit is contained in:
Shivam Mishra
2025-04-29 09:14:00 +05:30
committed by GitHub
parent c63b583f90
commit 1a2e6dc4ee
36 changed files with 2577 additions and 7 deletions

View File

@@ -163,9 +163,16 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
@contact.custom_attributes
end
def contact_additional_attributes
return @contact.additional_attributes.merge(permitted_params[:additional_attributes]) if permitted_params[:additional_attributes]
@contact.additional_attributes
end
def contact_update_params
# we want the merged custom attributes not the original one
permitted_params.except(:custom_attributes, :avatar_url).merge({ custom_attributes: contact_custom_attributes })
permitted_params.except(:custom_attributes, :avatar_url)
.merge({ custom_attributes: contact_custom_attributes })
.merge({ additional_attributes: contact_additional_attributes })
end
def set_include_contact_inboxes

View File

@@ -80,7 +80,7 @@ export default {
}, {});
this.formItems.forEach(item => {
if (item.validation.includes('JSON')) {
if (item.validation?.includes('JSON')) {
hookPayload.settings[item.name] = JSON.parse(
hookPayload.settings[item.name]
);
@@ -117,7 +117,7 @@ export default {
<div class="flex flex-col h-auto overflow-auto integration-hooks">
<woot-modal-header
:header-title="integration.name"
:header-content="integration.description"
:header-content="integration.short_description || integration.description"
/>
<FormKit
v-model="values"
@@ -169,6 +169,10 @@ export default {
@apply hidden;
}
.formkit-form .formkit-help {
@apply text-n-slate-10 text-sm font-normal mt-2 w-full;
}
/* equivalent of .reset-base */
.formkit-input {
margin-bottom: 0px !important;

36
app/jobs/crm/setup_job.rb Normal file
View File

@@ -0,0 +1,36 @@
class Crm::SetupJob < ApplicationJob
queue_as :default
def perform(hook_id)
hook = Integrations::Hook.find_by(id: hook_id)
return if hook.blank? || hook.disabled?
begin
setup_service = create_setup_service(hook)
return if setup_service.nil?
setup_service.setup
rescue StandardError => e
ChatwootExceptionTracker.new(e, account: hook.account).capture_exception
Rails.logger.error "Error in CRM setup for hook ##{hook_id} (#{hook.app_id}): #{e.message}"
end
end
private
def create_setup_service(hook)
case hook.app_id
when 'leadsquared'
Crm::Leadsquared::SetupService.new(hook)
# Add cases for future CRMs here
# when 'hubspot'
# Crm::Hubspot::SetupService.new(hook)
# when 'zoho'
# Crm::Zoho::SetupService.new(hook)
else
Rails.logger.error "Unsupported CRM app_id: #{hook.app_id}"
nil
end
end
end

View File

@@ -1,4 +1,6 @@
class HookJob < ApplicationJob
class HookJob < MutexApplicationJob
retry_on LockAcquisitionError, wait: 3.seconds, attempts: 3
queue_as :medium
def perform(hook, event_name, event_data = {})
@@ -11,6 +13,8 @@ class HookJob < ApplicationJob
process_dialogflow_integration(hook, event_name, event_data)
when 'google_translate'
google_translate_integration(hook, event_name, event_data)
when 'leadsquared'
process_leadsquared_integration_with_lock(hook, event_name, event_data)
end
rescue StandardError => e
Rails.logger.error e
@@ -41,4 +45,39 @@ class HookJob < ApplicationJob
message = event_data[:message]
Integrations::GoogleTranslate::DetectLanguageService.new(hook: hook, message: message).perform
end
def process_leadsquared_integration_with_lock(hook, event_name, event_data)
# Why do we need a mutex here? glad you asked
# When a new conversation is created. We get a contact created event, immediately followed by
# a contact updated event, and then a conversation created event.
# This all happens within milliseconds of each other.
# Now each of these subsequent event handlers need to have a leadsquared lead created and the contact to have the ID.
# If the lead data is not present, we try to search the API and create a new lead if it doesn't exist.
# This gives us a bad race condition that allows the API to create multiple leads for the same contact.
#
# This would have not been a problem if the email and phone number were unique identifiers for contacts at LeadSquared
# But then this is configurable in the LeadSquared settings, and may or may not be unique.
valid_event_names = ['contact.updated', 'conversation.created', 'conversation.resolved']
return unless valid_event_names.include?(event_name)
return unless hook.feature_allowed?
key = format(::Redis::Alfred::CRM_PROCESS_MUTEX, hook_id: hook.id)
with_lock(key) do
process_leadsquared_integration(hook, event_name, event_data)
end
end
def process_leadsquared_integration(hook, event_name, event_data)
# Process the event with the processor service
processor = Crm::Leadsquared::ProcessorService.new(hook)
case event_name
when 'contact.updated'
processor.handle_contact(event_data[:contact])
when 'conversation.created'
processor.handle_conversation_created(event_data[:conversation])
when 'conversation.resolved'
processor.handle_conversation_resolved(event_data[:conversation])
end
end
end

View File

@@ -11,6 +11,29 @@ class HookListener < BaseListener
execute_hooks(event, message)
end
def contact_created(event)
contact = extract_contact_and_account(event)[0]
execute_account_hooks(event, contact.account, contact: contact)
end
def contact_updated(event)
contact = extract_contact_and_account(event)[0]
execute_account_hooks(event, contact.account, contact: contact)
end
def conversation_created(event)
conversation = extract_conversation_and_account(event)[0]
execute_account_hooks(event, conversation.account, conversation: conversation)
end
def conversation_resolved(event)
conversation = extract_conversation_and_account(event)[0]
# Only trigger for status changes is resolved
return unless conversation.status == 'resolved'
execute_account_hooks(event, conversation.account, conversation: conversation)
end
private
def execute_hooks(event, message)
@@ -22,4 +45,10 @@ class HookListener < BaseListener
HookJob.perform_later(hook, event.name, message: message)
end
end
def execute_account_hooks(event, account, event_data = {})
account.hooks.account_hooks.find_each do |hook|
HookJob.perform_later(hook, event.name, event_data)
end
end
end

View File

@@ -18,6 +18,10 @@ class Integrations::App
I18n.t("integration_apps.#{params[:i18n_key]}.description")
end
def short_description
I18n.t("integration_apps.#{params[:i18n_key]}.short_description")
end
def logo
params[:logo]
end
@@ -51,6 +55,8 @@ class Integrations::App
GlobalConfigService.load('LINEAR_CLIENT_ID', nil).present?
when 'shopify'
account.feature_enabled?('shopify_integration') && GlobalConfigService.load('SHOPIFY_CLIENT_ID', nil).present?
when 'leadsquared'
account.feature_enabled?('crm_integration')
else
true
end

View File

@@ -19,11 +19,13 @@ class Integrations::Hook < ApplicationRecord
attr_readonly :app_id, :account_id, :inbox_id, :hook_type
before_validation :ensure_hook_type
after_create :trigger_setup_if_crm
validates :account_id, presence: true
validates :app_id, presence: true
validates :inbox_id, presence: true, if: -> { hook_type == 'inbox' }
validate :validate_settings_json_schema
validate :ensure_feature_enabled
validates :app_id, uniqueness: { scope: [:account_id], unless: -> { app.present? && app.params[:allow_multiple_hooks].present? } }
# TODO: This seems to be only used for slack at the moment
@@ -36,6 +38,9 @@ class Integrations::Hook < ApplicationRecord
enum hook_type: { account: 0, inbox: 1 }
scope :account_hooks, -> { where(hook_type: 'account') }
scope :inbox_hooks, -> { where(hook_type: 'inbox') }
def app
@app ||= Integrations::App.find(id: app_id)
end
@@ -61,8 +66,21 @@ class Integrations::Hook < ApplicationRecord
end
end
def feature_allowed?
return true if app.blank?
flag = app.params[:feature_flag]
return true unless flag
account.feature_enabled?(flag)
end
private
def ensure_feature_enabled
errors.add(:feature_flag, 'Feature not enabled') unless feature_allowed?
end
def ensure_hook_type
self.hook_type = app.params[:hook_type] if app.present?
end
@@ -72,4 +90,17 @@ class Integrations::Hook < ApplicationRecord
errors.add(:settings, ': Invalid settings data') unless JSONSchemer.schema(app.params[:settings_json_schema]).valid?(settings)
end
def trigger_setup_if_crm
# we need setup services to create data prerequisite to functioning of the integration
# in case of Leadsquared, we need to create a custom activity type for capturing conversations and transcripts
# https://apidocs.leadsquared.com/create-new-activity-type-api/
return unless crm_integration?
::Crm::SetupJob.perform_later(id)
end
def crm_integration?
%w[leadsquared].include?(app_id)
end
end

View File

@@ -0,0 +1,92 @@
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 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

View File

@@ -0,0 +1,36 @@
class Crm::Leadsquared::Api::ActivityClient < Crm::Leadsquared::Api::BaseClient
# https://apidocs.leadsquared.com/post-an-activity-to-lead/#api
def post_activity(prospect_id, activity_event, activity_note)
raise ArgumentError, 'Prospect ID is required' if prospect_id.blank?
raise ArgumentError, 'Activity event code is required' if activity_event.blank?
path = 'ProspectActivity.svc/Create'
body = {
'RelatedProspectId' => prospect_id,
'ActivityEvent' => activity_event,
'ActivityNote' => activity_note
}
response = post(path, {}, body)
response['Message']['Id']
end
def create_activity_type(name:, score:, direction: 0)
raise ArgumentError, 'Activity name is required' if name.blank?
path = 'ProspectActivity.svc/CreateType'
body = {
'ActivityEventName' => name,
'Score' => score.to_i,
'Direction' => direction.to_i
}
response = post(path, {}, body)
response['Message']['Id']
end
def fetch_activity_types
get('ProspectActivity.svc/ActivityTypes.Get')
end
end

View File

@@ -0,0 +1,84 @@
class Crm::Leadsquared::Api::BaseClient
include HTTParty
class ApiError < StandardError
attr_reader :code, :response
def initialize(message = nil, code = nil, response = nil)
@code = code
@response = response
super(message)
end
end
def initialize(access_key, secret_key, endpoint_url)
@access_key = access_key
@secret_key = secret_key
@base_uri = endpoint_url
end
def get(path, params = {})
full_url = URI.join(@base_uri, path).to_s
options = {
query: params,
headers: headers
}
response = self.class.get(full_url, options)
handle_response(response)
end
def post(path, params = {}, body = {})
full_url = URI.join(@base_uri, path).to_s
options = {
query: params,
headers: headers
}
options[:body] = body.to_json if body.present?
response = self.class.post(full_url, options)
handle_response(response)
end
private
def headers
{
'Content-Type': 'application/json',
'x-LSQ-AccessKey': @access_key,
'x-LSQ-SecretKey': @secret_key
}
end
def handle_response(response)
case response.code
when 200..299
handle_success(response)
else
error_message = "LeadSquared API error: #{response.code} - #{response.body}"
Rails.logger.error error_message
raise ApiError.new(error_message, response.code, response)
end
end
def handle_success(response)
parse_response(response)
rescue JSON::ParserError, TypeError => e
error_message = "Failed to parse LeadSquared API response: #{e.message}"
raise ApiError.new(error_message, response.code, response)
end
def parse_response(response)
body = response.parsed_response
if body.is_a?(Hash) && body['Status'] == 'Error'
error_message = body['ExceptionMessage'] || 'Unknown API error'
raise ApiError.new(error_message, response.code, response)
else
body
end
end
end

View File

@@ -0,0 +1,50 @@
class Crm::Leadsquared::Api::LeadClient < Crm::Leadsquared::Api::BaseClient
# https://apidocs.leadsquared.com/quick-search/#api
def search_lead(key)
raise ArgumentError, 'Search key is required' if key.blank?
path = 'LeadManagement.svc/Leads.GetByQuickSearch'
params = { key: key }
get(path, params)
end
# 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
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['Message']['Id']
end
def update_lead(lead_data, lead_id)
raise ArgumentError, 'Lead ID is required' if lead_id.blank?
raise ArgumentError, 'Lead data is required' if lead_data.blank?
path = "LeadManagement.svc/Lead.Update?leadId=#{lead_id}"
formatted_data = format_lead_data(lead_data)
response = post(path, {}, formatted_data)
response['Message']['AffectedRows']
end
private
def format_lead_data(lead_data)
lead_data.map do |key, value|
{
'Attribute' => key,
'Value' => value
}
end
end
end

View File

@@ -0,0 +1,59 @@
class Crm::Leadsquared::LeadFinderService
def initialize(lead_client)
@lead_client = lead_client
end
def find_or_create(contact)
lead_id = get_stored_id(contact)
return lead_id if lead_id.present?
lead_id = find_by_contact(contact)
return lead_id if lead_id.present?
create_lead(contact)
end
private
def find_by_contact(contact)
lead_id = find_by_email(contact)
lead_id = find_by_phone_number(contact) if lead_id.blank?
lead_id
end
def find_by_email(contact)
return if contact.email.blank?
search_by_field(contact.email)
end
def find_by_phone_number(contact)
return if contact.phone_number.blank?
search_by_field(contact.phone_number)
end
def search_by_field(value)
leads = @lead_client.search_lead(value)
return nil unless leads.is_a?(Array)
leads.first['ProspectID'] if leads.any?
end
def create_lead(contact)
lead_data = Crm::Leadsquared::Mappers::ContactMapper.map(contact)
lead_id = @lead_client.create_or_update_lead(lead_data)
raise StandardError, 'Failed to create lead - no ID returned' if lead_id.blank?
lead_id
end
def get_stored_id(contact)
return nil if contact.additional_attributes.blank?
return nil if contact.additional_attributes['external'].blank?
contact.additional_attributes.dig('external', 'leadsquared_id')
end
end

View File

@@ -0,0 +1,35 @@
class Crm::Leadsquared::Mappers::ContactMapper
def self.map(contact)
new(contact).map
end
def initialize(contact)
@contact = contact
end
def map
base_attributes
end
private
attr_reader :contact
def base_attributes
{
'FirstName' => contact.name.presence,
'LastName' => contact.last_name.presence,
'EmailAddress' => contact.email.presence,
'Mobile' => contact.phone_number.presence,
'Source' => brand_name
}.compact
end
def brand_name
::GlobalConfig.get('BRAND_NAME')['BRAND_NAME'] || 'Chatwoot'
end
def brand_name_without_spaces
brand_name.gsub(/\s+/, '')
end
end

View File

@@ -0,0 +1,108 @@
class Crm::Leadsquared::Mappers::ConversationMapper
include ::Rails.application.routes.url_helpers
# https://help.leadsquared.com/what-is-the-maximum-character-length-supported-for-lead-and-activity-fields/
# the rest of the body of the note is around 200 chars
# so this limits it
ACTIVITY_NOTE_MAX_SIZE = 1800
def self.map_conversation_activity(conversation)
new(conversation).conversation_activity
end
def self.map_transcript_activity(conversation, messages = nil)
new(conversation, messages).transcript_activity
end
def initialize(conversation, messages = nil)
@conversation = conversation
@messages = messages
end
def conversation_activity
I18n.t('crm.created_activity',
brand_name: brand_name,
channel_info: conversation.inbox.name,
formatted_creation_time: formatted_creation_time,
display_id: conversation.display_id,
url: conversation_url)
end
def transcript_activity
return I18n.t('crm.no_message') if transcript_messages.empty?
I18n.t('crm.transcript_activity',
brand_name: brand_name,
channel_info: conversation.inbox.name,
display_id: conversation.display_id,
url: conversation_url,
format_messages: format_messages)
end
private
attr_reader :conversation, :messages
def formatted_creation_time
conversation.created_at.strftime('%Y-%m-%d %H:%M:%S')
end
def transcript_messages
@transcript_messages ||= messages || conversation.messages.chat.select(&:conversation_transcriptable?)
end
def format_messages
selected_messages = []
separator = "\n\n"
current_length = 0
# Reverse the messages to have latest on top
transcript_messages.reverse_each do |message|
formatted_message = format_message(message)
required_length = formatted_message.length + separator.length # the last one does not need to account for separator, but we add it anyway
break unless (current_length + required_length) <= ACTIVITY_NOTE_MAX_SIZE
selected_messages << formatted_message
current_length += required_length
end
selected_messages.join(separator)
end
def format_message(message)
<<~MESSAGE.strip
[#{message_time(message)}] #{sender_name(message)}: #{message_content(message)}#{attachment_info(message)}
MESSAGE
end
def message_time(message)
# TODO: Figure out what timezone to send the time in
message.created_at.strftime('%Y-%m-%d %H:%M')
end
def sender_name(message)
return 'System' if message.sender.nil?
message.sender.name.presence || "#{message.sender_type} #{message.sender_id}"
end
def message_content(message)
message.content.presence || I18n.t('crm.no_content')
end
def attachment_info(message)
return '' unless message.attachments.any?
attachments = message.attachments.map { |a| I18n.t('crm.attachment', type: a.file_type) }.join(', ')
"\n#{attachments}"
end
def conversation_url
app_account_conversation_url(account_id: conversation.account.id, id: conversation.display_id)
end
def brand_name
::GlobalConfig.get('BRAND_NAME')['BRAND_NAME'] || 'Chatwoot'
end
end

View File

@@ -0,0 +1,121 @@
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(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(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?
@lead_client.update_lead(lead_data, lead_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 = @activity_client.post_activity(lead_id, activity_code, activity_note)
return if activity_id.blank?
metadata = {}
metadata[metadata_key] = activity_id
store_conversation_metadata(conversation, metadata)
rescue Crm::Leadsquared::Api::BaseClient::ApiError => e
ChatwootExceptionTracker.new(e, account: @account).capture_exception
Rails.logger.error "LeadSquared API error in #{activity_type} activity: #{e.message}"
rescue StandardError => e
ChatwootExceptionTracker.new(e, account: @account).capture_exception
Rails.logger.error "Error creating #{activity_type} activity in LeadSquared: #{e.message}"
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}")
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

View File

@@ -0,0 +1,108 @@
class Crm::Leadsquared::SetupService
def initialize(hook)
@hook = hook
credentials = @hook.settings
@access_key = credentials['access_key']
@secret_key = credentials['secret_key']
@client = Crm::Leadsquared::Api::BaseClient.new(@access_key, @secret_key, 'https://api.leadsquared.com/v2/')
@activity_client = Crm::Leadsquared::Api::ActivityClient.new(@access_key, @secret_key, 'https://api.leadsquared.com/v2/')
end
def setup
setup_endpoint
setup_activity
rescue Crm::Leadsquared::Api::BaseClient::ApiError => e
ChatwootExceptionTracker.new(e, account: @hook.account).capture_exception
Rails.logger.error "LeadSquared API error in setup: #{e.message}"
rescue StandardError => e
ChatwootExceptionTracker.new(e, account: @hook.account).capture_exception
Rails.logger.error "Error during LeadSquared setup: #{e.message}"
end
def setup_endpoint
response = @client.get('Authentication.svc/UserByAccessKey.Get')
endpoint_host = response['LSQCommonServiceURLs']['api']
app_host = response['LSQCommonServiceURLs']['app']
endpoint_url = "https://#{endpoint_host}/v2/"
app_url = "https://#{app_host}/"
update_hook_settings({ :endpoint_url => endpoint_url, :app_url => app_url })
# replace the clients
@client = Crm::Leadsquared::Api::BaseClient.new(@access_key, @secret_key, endpoint_url)
@activity_client = Crm::Leadsquared::Api::ActivityClient.new(@access_key, @secret_key, endpoint_url)
end
private
def setup_activity
existing_types = @activity_client.fetch_activity_types
return if existing_types.blank?
activity_codes = setup_activity_types(existing_types)
return if activity_codes.blank?
update_hook_settings(activity_codes)
activity_codes
end
def setup_activity_types(existing_types)
activity_codes = {}
activity_types.each do |activity_type|
activity_id = find_or_create_activity_type(activity_type, existing_types)
if activity_id.present?
activity_codes[activity_type[:setting_key]] = activity_id
else
Rails.logger.error "Failed to find or create activity type: #{activity_type[:name]}"
end
end
activity_codes
end
def find_or_create_activity_type(activity_type, existing_types)
existing = existing_types.find { |t| t['ActivityEventName'] == activity_type[:name] }
if existing
existing['ActivityEvent'].to_i
else
@activity_client.create_activity_type(
name: activity_type[:name],
score: activity_type[:score],
direction: activity_type[:direction]
)
end
end
def update_hook_settings(params)
@hook.settings = @hook.settings.merge(params)
@hook.save!
end
def activity_types
[
{
name: "#{brand_name} Conversation Started",
score: @hook.settings['conversation_activity_score'].to_i || 0,
direction: 0,
setting_key: 'conversation_activity_code'
},
{
name: "#{brand_name} Conversation Transcript",
score: @hook.settings['transcript_activity_score'].to_i || 0,
direction: 0,
setting_key: 'transcript_activity_code'
}
].freeze
end
def brand_name
::GlobalConfig.get('BRAND_NAME')['BRAND_NAME'].presence || 'Chatwoot'
end
end

View File

@@ -1,6 +1,7 @@
json.id resource.id
json.name resource.name
json.description resource.description
json.short_description resource.short_description.presence
json.enabled resource.enabled?(@current_account)
if Current.account_user&.administrator?