Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45c65aa10c | ||
|
|
6812417043 | ||
|
|
15d47f94a1 | ||
|
|
961ec86ba9 | ||
|
|
e86222034e | ||
|
|
2b37fef0e0 | ||
|
|
44b32eacec | ||
|
|
5b7430f376 | ||
|
|
5c00a1601a |
+3
-3
@@ -193,7 +193,7 @@ GEM
|
||||
climate_control (1.2.0)
|
||||
coderay (1.1.3)
|
||||
commonmarker (0.23.10)
|
||||
concurrent-ruby (1.3.5)
|
||||
concurrent-ruby (1.3.7)
|
||||
connection_pool (2.5.5)
|
||||
crack (1.0.0)
|
||||
bigdecimal
|
||||
@@ -304,7 +304,7 @@ GEM
|
||||
railties (>= 5.0.0)
|
||||
faker (3.2.0)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (2.14.2)
|
||||
faraday (2.14.3)
|
||||
faraday-net_http (>= 2.0, < 3.5)
|
||||
json
|
||||
logger
|
||||
@@ -474,7 +474,7 @@ GEM
|
||||
rails-dom-testing (>= 1, < 3)
|
||||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
json (2.19.8)
|
||||
json (2.19.9)
|
||||
json_refs (0.1.8)
|
||||
hana
|
||||
json_schemer (0.2.24)
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<script setup>
|
||||
import { computed, onMounted } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import BaseBubble from './Base.vue';
|
||||
import AudioChip from 'next/message/chips/Audio.vue';
|
||||
import Icon from 'next/icon/Icon.vue';
|
||||
import { useLoadWithRetry } from 'dashboard/composables/loadWithRetry';
|
||||
import { useMessageContext } from '../provider.js';
|
||||
|
||||
const { attachments } = useMessageContext();
|
||||
@@ -11,28 +9,11 @@ const { attachments } = useMessageContext();
|
||||
const attachment = computed(() => {
|
||||
return attachments.value[0];
|
||||
});
|
||||
|
||||
const { isLoaded, hasError, loadWithRetry } = useLoadWithRetry({
|
||||
mediaType: 'audio',
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (attachment.value?.dataUrl) {
|
||||
loadWithRetry(attachment.value.dataUrl);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseBubble class="bg-transparent" data-bubble-name="audio">
|
||||
<div v-if="hasError" class="flex items-center gap-1 text-center rounded-lg">
|
||||
<Icon icon="i-lucide-circle-off" class="text-n-slate-11" />
|
||||
<p class="mb-0 text-n-slate-11">
|
||||
{{ $t('COMPONENTS.MEDIA.AUDIO_UNAVAILABLE') }}
|
||||
</p>
|
||||
</div>
|
||||
<AudioChip
|
||||
v-else-if="isLoaded"
|
||||
:attachment="attachment"
|
||||
class="p-2 text-n-slate-12 skip-context-menu"
|
||||
/>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
export const useLoadWithRetry = (config = {}) => {
|
||||
const maxRetry = (config.max_retry ?? config.maxRetry) || 3;
|
||||
const backoff = (config.backoff ?? config.backOff) || 1000;
|
||||
const mediaType = (config.mediaType ?? config.media_type) || 'image';
|
||||
const maxRetry = config.max_retry || 3;
|
||||
const backoff = config.backoff || 1000;
|
||||
|
||||
const isLoaded = ref(false);
|
||||
const hasError = ref(false);
|
||||
@@ -11,25 +10,19 @@ export const useLoadWithRetry = (config = {}) => {
|
||||
const loadWithRetry = async url => {
|
||||
const attemptLoad = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const onSuccess = () => {
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
isLoaded.value = true;
|
||||
hasError.value = false;
|
||||
resolve();
|
||||
};
|
||||
|
||||
let element;
|
||||
if (mediaType === 'audio') {
|
||||
element = new Audio();
|
||||
element.preload = 'metadata';
|
||||
element.onloadedmetadata = onSuccess;
|
||||
} else {
|
||||
element = new Image();
|
||||
element.onload = onSuccess;
|
||||
}
|
||||
element.onerror = () => reject(new Error('Failed to load resource'));
|
||||
const cacheBustedUrl = new URL(url);
|
||||
cacheBustedUrl.searchParams.set('t', Date.now());
|
||||
element.src = cacheBustedUrl.toString();
|
||||
img.onerror = () => {
|
||||
reject(new Error('Failed to load image'));
|
||||
};
|
||||
|
||||
img.src = url;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -163,6 +163,11 @@ export const FORMATTING = {
|
||||
nodes: [],
|
||||
menu: [],
|
||||
},
|
||||
'Context::NoToolbar': {
|
||||
marks: ['strong', 'em', 'link'],
|
||||
nodes: ['bulletList', 'orderedList'],
|
||||
menu: [],
|
||||
},
|
||||
};
|
||||
|
||||
// Editor menu options for Full Editor
|
||||
|
||||
@@ -296,7 +296,6 @@
|
||||
},
|
||||
"MEDIA": {
|
||||
"IMAGE_UNAVAILABLE": "This image is no longer available.",
|
||||
"AUDIO_UNAVAILABLE": "This audio is no longer available.",
|
||||
"LOADING_FAILED": "Loading failed"
|
||||
}
|
||||
},
|
||||
|
||||
+6
-2
@@ -5,7 +5,7 @@ import { useI18n } from 'vue-i18n';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import WithLabel from 'v3/components/Form/WithLabel.vue';
|
||||
import TextArea from 'next/textarea/TextArea.vue';
|
||||
import Editor from 'next/Editor/Editor.vue';
|
||||
import Switch from 'next/switch/Switch.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import DurationInput from 'next/input/DurationInput.vue';
|
||||
@@ -162,9 +162,13 @@ const toggleAutoResolve = async () => {
|
||||
:label="t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE.MESSAGE.LABEL')"
|
||||
:help-message="t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE.MESSAGE.HELP')"
|
||||
>
|
||||
<TextArea
|
||||
<Editor
|
||||
v-model="message"
|
||||
class="w-full"
|
||||
channel-type="Context::NoToolbar"
|
||||
enable-variables
|
||||
:enable-canned-responses="false"
|
||||
:show-character-count="false"
|
||||
:placeholder="
|
||||
t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE.MESSAGE.PLACEHOLDER')
|
||||
"
|
||||
|
||||
@@ -95,13 +95,20 @@ class Crm::Leadsquared::ProcessorService < Crm::BaseProcessorService
|
||||
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}"
|
||||
context = {
|
||||
activity_type: activity_type,
|
||||
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}"
|
||||
context.merge!(
|
||||
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})")
|
||||
ChatwootExceptionTracker.new(error, account: @account, additional_context: { leadsquared: context }).capture_exception
|
||||
end
|
||||
|
||||
def get_activity_code(key)
|
||||
|
||||
@@ -49,7 +49,7 @@ class Api::V1::Accounts::CompaniesController < Api::V1::Accounts::EnterpriseAcco
|
||||
end
|
||||
|
||||
def destroy
|
||||
@company.destroy!
|
||||
Companies::DeleteJob.perform_later(company_id: @company.id)
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
||||
+13
@@ -29,6 +29,19 @@ module Enterprise::DeviseOverrides::OmniauthCallbacksController
|
||||
|
||||
private
|
||||
|
||||
def create_account_for_user
|
||||
super
|
||||
record_marketing_attribution
|
||||
end
|
||||
|
||||
def record_marketing_attribution
|
||||
return if @account.blank?
|
||||
|
||||
Internal::Accounts::MarketingAttributionService.new(account: @account, cookies: cookies).perform
|
||||
rescue StandardError => e
|
||||
ChatwootExceptionTracker.new(e).capture_exception
|
||||
end
|
||||
|
||||
def handle_saml_auth
|
||||
account_id = extract_saml_account_id
|
||||
relay_state = saml_relay_state
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
class Companies::DeleteJob < ApplicationJob
|
||||
queue_as :low
|
||||
|
||||
BATCH_SIZE = 1000
|
||||
CONTACT_COMPANY_CLEAR_SQL = <<~SQL.squish.freeze
|
||||
company_id = NULL,
|
||||
additional_attributes = COALESCE(additional_attributes, '{}'::jsonb) - 'company_name'
|
||||
SQL
|
||||
|
||||
def perform(company_id:)
|
||||
company = Company.find_by(id: company_id)
|
||||
return if company.blank?
|
||||
|
||||
clear_contact_company_names(company)
|
||||
company.destroy!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Avoid contact callbacks so this cleanup does not dispatch contact automations/webhooks.
|
||||
# rubocop:disable Rails/SkipsModelValidations
|
||||
def clear_contact_company_names(company)
|
||||
company.contacts.in_batches(of: BATCH_SIZE) do |contacts|
|
||||
contacts.update_all(CONTACT_COMPANY_CLEAR_SQL)
|
||||
end
|
||||
end
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
end
|
||||
@@ -0,0 +1,33 @@
|
||||
class Companies::SyncContactNamesJob < ApplicationJob
|
||||
queue_as :low
|
||||
|
||||
BATCH_SIZE = 1000
|
||||
CONTACT_COMPANY_NAME_UPDATE_SQL = <<~SQL.squish.freeze
|
||||
additional_attributes = jsonb_set(
|
||||
COALESCE(additional_attributes, '{}'::jsonb),
|
||||
'{company_name}',
|
||||
?::jsonb,
|
||||
true
|
||||
)
|
||||
SQL
|
||||
|
||||
def perform(company_id:)
|
||||
return if company_id.blank?
|
||||
|
||||
company = Company.find_by(id: company_id)
|
||||
return if company.blank?
|
||||
|
||||
sync_company_name(company)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Denormalized display field sync; avoid contact validations, callbacks, and webhook/automation side effects.
|
||||
# rubocop:disable Rails/SkipsModelValidations
|
||||
def sync_company_name(company)
|
||||
company.contacts.in_batches(of: BATCH_SIZE) do |contacts|
|
||||
contacts.update_all([CONTACT_COMPANY_NAME_UPDATE_SQL, company.name.to_json])
|
||||
end
|
||||
end
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
end
|
||||
@@ -39,6 +39,7 @@ class Company < ApplicationRecord
|
||||
has_many :contacts, dependent: :nullify
|
||||
before_validation :prepare_jsonb_attributes
|
||||
after_create_commit :fetch_favicon, if: -> { domain.present? }
|
||||
after_update_commit :enqueue_contact_company_name_sync, if: :saved_change_to_name?
|
||||
|
||||
scope :ordered_by_name, -> { order(:name) }
|
||||
scope :search_by_name_or_domain, lambda { |query|
|
||||
@@ -76,4 +77,8 @@ class Company < ApplicationRecord
|
||||
def fetch_favicon
|
||||
Avatar::AvatarFromFaviconJob.set(wait: 5.seconds).perform_later(self)
|
||||
end
|
||||
|
||||
def enqueue_contact_company_name_sync
|
||||
Companies::SyncContactNamesJob.perform_later(company_id: id)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
############
|
||||
|
||||
class ChatwootExceptionTracker
|
||||
def initialize(exception, user: nil, account: nil)
|
||||
def initialize(exception, user: nil, account: nil, additional_context: {})
|
||||
@exception = exception
|
||||
@user = user
|
||||
@account = account
|
||||
@additional_context = additional_context
|
||||
end
|
||||
|
||||
def capture_exception
|
||||
@@ -25,6 +26,7 @@ class ChatwootExceptionTracker
|
||||
scope.set_tags(account_id: @account.id)
|
||||
end
|
||||
|
||||
@additional_context.each { |key, value| scope.set_context(key.to_s, value) }
|
||||
scope.set_user(id: @user.id, email: @user.email) if @user.is_a?(User)
|
||||
Sentry.capture_exception(@exception)
|
||||
end
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@
|
||||
"countries-and-timezones": "^3.6.0",
|
||||
"date-fns": "2.21.1",
|
||||
"date-fns-tz": "^1.3.3",
|
||||
"dompurify": "3.4.0",
|
||||
"dompurify": "3.4.11",
|
||||
"flag-icons": "^7.2.3",
|
||||
"floating-vue": "^5.2.2",
|
||||
"highlight.js": "^11.10.0",
|
||||
|
||||
Generated
+15
-7
@@ -130,8 +130,8 @@ importers:
|
||||
specifier: ^1.3.3
|
||||
version: 1.3.8(date-fns@2.21.1)
|
||||
dompurify:
|
||||
specifier: 3.4.0
|
||||
version: 3.4.0
|
||||
specifier: 3.4.11
|
||||
version: 3.4.11
|
||||
flag-icons:
|
||||
specifier: ^7.2.3
|
||||
version: 7.2.3
|
||||
@@ -1635,6 +1635,11 @@ packages:
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
acorn@8.17.0:
|
||||
resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
activestorage@5.2.8:
|
||||
resolution: {integrity: sha512-bueFOxBGIAUdrjbLyBZ8Xlkcecy8vr05sCk5VV37BbFi+RehPoEjfvKX3iYYPY7RFVhl+L43W9/ZbN3xNNLPtQ==}
|
||||
|
||||
@@ -2218,8 +2223,8 @@ packages:
|
||||
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
dompurify@3.4.0:
|
||||
resolution: {integrity: sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==}
|
||||
dompurify@3.4.11:
|
||||
resolution: {integrity: sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==}
|
||||
|
||||
domutils@3.1.0:
|
||||
resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==}
|
||||
@@ -6380,6 +6385,9 @@ snapshots:
|
||||
|
||||
acorn@8.16.0: {}
|
||||
|
||||
acorn@8.17.0:
|
||||
optional: true
|
||||
|
||||
activestorage@5.2.8:
|
||||
dependencies:
|
||||
spark-md5: 3.0.2
|
||||
@@ -6999,7 +7007,7 @@ snapshots:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
|
||||
dompurify@3.4.0:
|
||||
dompurify@3.4.11:
|
||||
optionalDependencies:
|
||||
'@types/trusted-types': 2.0.7
|
||||
|
||||
@@ -9511,7 +9519,7 @@ snapshots:
|
||||
terser@5.33.0:
|
||||
dependencies:
|
||||
'@jridgewell/source-map': 0.3.11
|
||||
acorn: 8.16.0
|
||||
acorn: 8.17.0
|
||||
commander: 2.20.3
|
||||
source-map-support: 0.5.21
|
||||
optional: true
|
||||
@@ -9898,7 +9906,7 @@ snapshots:
|
||||
|
||||
vue-dompurify-html@5.3.0(vue@3.5.12(typescript@5.6.2)):
|
||||
dependencies:
|
||||
dompurify: 3.4.0
|
||||
dompurify: 3.4.11
|
||||
vue: 3.5.12(typescript@5.6.2)
|
||||
|
||||
vue-eslint-parser@9.4.3(eslint@8.57.0):
|
||||
|
||||
@@ -385,13 +385,13 @@ RSpec.describe 'Companies API', type: :request do
|
||||
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||
let(:company) { create(:company, account: account) }
|
||||
|
||||
it 'deletes the company' do
|
||||
company
|
||||
it 'enqueues company deletion' do
|
||||
expect do
|
||||
delete "/api/v1/accounts/#{account.id}/companies/#{company.id}",
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
end.to change(Company, :count).by(-1)
|
||||
end.to have_enqueued_job(Companies::DeleteJob).with(company_id: company.id)
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
end
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
require 'rails_helper'
|
||||
require 'base64'
|
||||
|
||||
RSpec.describe 'Enterprise Google OAuth attribution', type: :request do
|
||||
let(:email_validation_service) { instance_double(Account::SignUpEmailValidationService) }
|
||||
let(:email) { 'oauth-attribution@example.com' }
|
||||
let(:account_builder) { double }
|
||||
let(:account) { create(:account) }
|
||||
let(:first_touch_cookie) { encoded_cookie('source' => 'reddit', 'source_type' => 'paid_social') }
|
||||
let(:last_touch_cookie) { encoded_cookie('source' => 'github', 'source_type' => 'referral') }
|
||||
|
||||
before do
|
||||
allow(ChatwootApp).to receive(:enterprise?).and_return(true)
|
||||
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true)
|
||||
allow(Account::SignUpEmailValidationService).to receive(:new).and_return(email_validation_service)
|
||||
allow(email_validation_service).to receive(:perform).and_return(true)
|
||||
allow(AccountBuilder).to receive(:new).and_return(account_builder)
|
||||
allow(account_builder).to receive(:perform) do
|
||||
[create(:user, email: email, account: account), account]
|
||||
end
|
||||
|
||||
OmniAuth.config.test_mode = true
|
||||
OmniAuth.config.mock_auth[:google_oauth2] = OmniAuth::AuthHash.new(
|
||||
provider: 'google',
|
||||
uid: '123545',
|
||||
info: {
|
||||
name: 'OAuth Attribution',
|
||||
email: email,
|
||||
image: 'https://example.com/image.jpg'
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
it 'records marketing attribution for Google OAuth signups' do
|
||||
cookies[Internal::Accounts::MarketingAttributionService::FIRST_TOUCH_COOKIE] = first_touch_cookie
|
||||
cookies[Internal::Accounts::MarketingAttributionService::LAST_TOUCH_COOKIE] = last_touch_cookie
|
||||
|
||||
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true', FRONTEND_URL: 'http://www.example.com' do
|
||||
get '/omniauth/google_oauth2/callback'
|
||||
follow_redirect!
|
||||
end
|
||||
|
||||
attribution = account.reload.internal_attributes['marketing_attribution']
|
||||
|
||||
expect(attribution['captured_from']).to eq('cookie')
|
||||
expect(attribution['first_touch']).to include('source' => 'reddit', 'source_type' => 'paid_social')
|
||||
expect(attribution['last_touch']).to include('source' => 'github', 'source_type' => 'referral')
|
||||
end
|
||||
|
||||
def encoded_cookie(payload)
|
||||
Base64.urlsafe_encode64(payload.to_json, padding: false)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,19 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Companies::DeleteJob, type: :job do
|
||||
describe '#perform' do
|
||||
it 'unlinks contacts, clears company names, and deletes the company' do
|
||||
account = create(:account)
|
||||
company = create(:company, account: account, name: 'Acme')
|
||||
contact = create(:contact, account: account, company: company, additional_attributes: { 'company_name' => 'Acme', 'city' => 'Berlin' })
|
||||
other_contact = create(:contact, account: account, additional_attributes: { 'company_name' => 'Acme' })
|
||||
|
||||
described_class.perform_now(company_id: company.id)
|
||||
|
||||
expect { company.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect(contact.reload.company_id).to be_nil
|
||||
expect(contact.additional_attributes).to eq('city' => 'Berlin')
|
||||
expect(other_contact.reload.additional_attributes).to eq('company_name' => 'Acme')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,38 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Companies::SyncContactNamesJob, type: :job do
|
||||
let(:account) { create(:account) }
|
||||
let(:company) { create(:company, account: account, name: 'Acme') }
|
||||
|
||||
describe '#perform' do
|
||||
it 'updates linked contact company names' do
|
||||
contact = create(:contact, account: account, company: company, additional_attributes: { 'company_name' => 'Acme', 'city' => 'Berlin' })
|
||||
|
||||
company.update!(name: 'Acme Labs')
|
||||
|
||||
described_class.perform_now(company_id: company.id)
|
||||
|
||||
expect(contact.reload.additional_attributes).to eq('company_name' => 'Acme Labs', 'city' => 'Berlin')
|
||||
end
|
||||
|
||||
it 'uses the current company name when a stale rename job runs' do
|
||||
contact = create(:contact, account: account, company: company, additional_attributes: { 'company_name' => 'Acme' })
|
||||
company.update!(name: 'Acme Labs')
|
||||
|
||||
described_class.perform_now(company_id: company.id)
|
||||
|
||||
expect(contact.reload.additional_attributes).to eq('company_name' => 'Acme Labs')
|
||||
end
|
||||
|
||||
it 'does not save contacts while syncing the denormalized company name' do
|
||||
contact = create(:contact, account: account, company: company, additional_attributes: { 'company_name' => 'Acme' })
|
||||
original_updated_at = contact.reload.updated_at
|
||||
|
||||
company.update!(name: 'Acme Labs')
|
||||
|
||||
described_class.perform_now(company_id: company.id)
|
||||
|
||||
expect(contact.reload.updated_at).to eq(original_updated_at)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -46,4 +46,15 @@ RSpec.describe Company, type: :model do
|
||||
expect(company.reload.last_activity_at).to be_within(1.second).of(original_activity_at)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'contact company name sync' do
|
||||
let(:account) { create(:account) }
|
||||
let(:company) { create(:company, account: account, name: 'Acme') }
|
||||
|
||||
it 'enqueues contact company name sync when the company name changes' do
|
||||
expect do
|
||||
company.update!(name: 'Acme Labs')
|
||||
end.to have_enqueued_job(Companies::SyncContactNamesJob).with(company_id: company.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -151,13 +151,20 @@ RSpec.describe Crm::Leadsquared::ProcessorService do
|
||||
allow(activity_client).to receive(:post_activity)
|
||||
.with('test_lead_id', 1001, activity_note)
|
||||
.and_raise(StandardError.new('Activity error'))
|
||||
|
||||
allow(Rails.logger).to receive(:error)
|
||||
end
|
||||
|
||||
it 'logs the error' do
|
||||
it 'captures the exception with leadsquared context' do
|
||||
tracker = instance_double(ChatwootExceptionTracker, capture_exception: nil)
|
||||
allow(ChatwootExceptionTracker).to receive(:new).and_return(tracker)
|
||||
|
||||
service.handle_conversation_created(conversation)
|
||||
expect(Rails.logger).to have_received(:error).with(/LeadSquared conversation activity failed/)
|
||||
|
||||
expect(ChatwootExceptionTracker).to have_received(:new).with(
|
||||
instance_of(StandardError),
|
||||
account: account,
|
||||
additional_context: { leadsquared: hash_including(activity_type: 'conversation', conversation_display_id: conversation.display_id) }
|
||||
)
|
||||
expect(tracker).to have_received(:capture_exception)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user