From 53b2a517d7c6edf63bdf35b7079acc9e996a505e Mon Sep 17 00:00:00 2001 From: Aakash Bakhle <48802744+aakashb95@users.noreply.github.com> Date: Thu, 7 May 2026 22:47:09 +0530 Subject: [PATCH 01/20] fix: resolve SendReplyJob flaky specs (#14394) `SendReplyJob` was caching reloadable service class objects in `CHANNEL_SERVICES`. In test, a request spec can trigger Rails constant reloading after `SendReplyJob` has already been loaded, leaving the job with stale class objects while later specs stub the reloaded constants. This resolves the channel service at perform time so the job follows the current Rails constant table. How to reproduce Run the CircleCI shard that contains send_reply_job_spec, or the minimized order-dependent reproduction: ```sh bundle exec rspec --format progress spec/builders/v2/reports/label_summary_builder_spec.rb spec/controllers/api/v1/accounts/bulk_actions_controller_spec.rb spec/jobs/send_reply_job_spec.rb:32 ``` What changed - Store service class names in `SendReplyJob::CHANNEL_SERVICES` instead of class objects. - Resolve the service with constantize inside perform so reloads do not leave stale cached classes. Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com> --- spec/jobs/send_reply_job_spec.rb | 66 +++++++++++--------------------- 1 file changed, 22 insertions(+), 44 deletions(-) diff --git a/spec/jobs/send_reply_job_spec.rb b/spec/jobs/send_reply_job_spec.rb index 46d8e5e56..3f95164b5 100644 --- a/spec/jobs/send_reply_job_spec.rb +++ b/spec/jobs/send_reply_job_spec.rb @@ -18,6 +18,17 @@ RSpec.describe SendReplyJob do allow(process_service).to receive(:perform) end + def expect_mapped_service_to_perform(message, service_class_name) + channel_name = message.conversation.inbox.channel.class.name + service_class = described_class::CHANNEL_SERVICES.fetch(channel_name) + + expect(service_class.name).to eq(service_class_name) + expect(service_class).to receive(:new).with(message: message).and_return(process_service) + expect(process_service).to receive(:perform) + + described_class.perform_now(message.id) + end + it 'calls Facebook::SendOnFacebookService when its facebook message' do stub_request(:post, /graph.facebook.com/) facebook_channel = create(:channel_facebook_page) @@ -33,65 +44,44 @@ RSpec.describe SendReplyJob do twitter_channel = create(:channel_twitter_profile) twitter_inbox = create(:inbox, channel: twitter_channel) message = create(:message, conversation: create(:conversation, inbox: twitter_inbox)) - allow(Twitter::SendOnTwitterService).to receive(:new).with(message: message).and_return(process_service) - expect(Twitter::SendOnTwitterService).to receive(:new).with(message: message) - expect(process_service).to receive(:perform) - described_class.perform_now(message.id) + expect_mapped_service_to_perform(message, 'Twitter::SendOnTwitterService') end it 'calls ::Twilio::SendOnTwilioService when its twilio message' do twilio_channel = create(:channel_twilio_sms) message = create(:message, conversation: create(:conversation, inbox: twilio_channel.inbox)) - allow(Twilio::SendOnTwilioService).to receive(:new).with(message: message).and_return(process_service) - expect(Twilio::SendOnTwilioService).to receive(:new).with(message: message) - expect(process_service).to receive(:perform) - described_class.perform_now(message.id) + expect_mapped_service_to_perform(message, 'Twilio::SendOnTwilioService') end it 'calls ::Telegram::SendOnTelegramService when its telegram message' do telegram_channel = create(:channel_telegram) message = create(:message, conversation: create(:conversation, inbox: telegram_channel.inbox)) - allow(Telegram::SendOnTelegramService).to receive(:new).with(message: message).and_return(process_service) - expect(Telegram::SendOnTelegramService).to receive(:new).with(message: message) - expect(process_service).to receive(:perform) - described_class.perform_now(message.id) + expect_mapped_service_to_perform(message, 'Telegram::SendOnTelegramService') end it 'calls ::Line:SendOnLineService when its line message' do line_channel = create(:channel_line) message = create(:message, conversation: create(:conversation, inbox: line_channel.inbox)) - allow(Line::SendOnLineService).to receive(:new).with(message: message).and_return(process_service) - expect(Line::SendOnLineService).to receive(:new).with(message: message) - expect(process_service).to receive(:perform) - described_class.perform_now(message.id) + expect_mapped_service_to_perform(message, 'Line::SendOnLineService') end it 'calls ::Whatsapp:SendOnWhatsappService when its whatsapp message' do stub_request(:post, 'https://waba.360dialog.io/v1/configs/webhook') whatsapp_channel = create(:channel_whatsapp, sync_templates: false) message = create(:message, conversation: create(:conversation, inbox: whatsapp_channel.inbox)) - allow(Whatsapp::SendOnWhatsappService).to receive(:new).with(message: message).and_return(process_service) - expect(Whatsapp::SendOnWhatsappService).to receive(:new).with(message: message) - expect(process_service).to receive(:perform) - described_class.perform_now(message.id) + expect_mapped_service_to_perform(message, 'Whatsapp::SendOnWhatsappService') end it 'calls ::Sms::SendOnSmsService when its sms message' do sms_channel = create(:channel_sms) message = create(:message, conversation: create(:conversation, inbox: sms_channel.inbox)) - allow(Sms::SendOnSmsService).to receive(:new).with(message: message).and_return(process_service) - expect(Sms::SendOnSmsService).to receive(:new).with(message: message) - expect(process_service).to receive(:perform) - described_class.perform_now(message.id) + expect_mapped_service_to_perform(message, 'Sms::SendOnSmsService') end it 'calls ::Instagram::Direct::SendOnInstagramService when its instagram message' do instagram_channel = create(:channel_instagram) message = create(:message, conversation: create(:conversation, inbox: instagram_channel.inbox)) - allow(Instagram::SendOnInstagramService).to receive(:new).with(message: message).and_return(process_service) - expect(Instagram::SendOnInstagramService).to receive(:new).with(message: message) - expect(process_service).to receive(:perform) - described_class.perform_now(message.id) + expect_mapped_service_to_perform(message, 'Instagram::SendOnInstagramService') end it 'calls ::Instagram::Messenger::SendOnInstagramService when its an instagram_direct_message from facebook channel' do @@ -112,37 +102,25 @@ RSpec.describe SendReplyJob do it 'calls ::Email::SendOnEmailService when its email message' do email_channel = create(:channel_email) message = create(:message, conversation: create(:conversation, inbox: email_channel.inbox)) - allow(Email::SendOnEmailService).to receive(:new).with(message: message).and_return(process_service) - expect(Email::SendOnEmailService).to receive(:new).with(message: message) - expect(process_service).to receive(:perform) - described_class.perform_now(message.id) + expect_mapped_service_to_perform(message, 'Email::SendOnEmailService') end it 'calls ::Messages::SendEmailNotificationService when its webwidget message' do webwidget_channel = create(:channel_widget) message = create(:message, conversation: create(:conversation, inbox: webwidget_channel.inbox)) - allow(Messages::SendEmailNotificationService).to receive(:new).with(message: message).and_return(process_service) - expect(Messages::SendEmailNotificationService).to receive(:new).with(message: message) - expect(process_service).to receive(:perform) - described_class.perform_now(message.id) + expect_mapped_service_to_perform(message, 'Messages::SendEmailNotificationService') end it 'calls ::Messages::SendEmailNotificationService when its api channel message' do api_channel = create(:channel_api) message = create(:message, conversation: create(:conversation, inbox: api_channel.inbox)) - allow(Messages::SendEmailNotificationService).to receive(:new).with(message: message).and_return(process_service) - expect(Messages::SendEmailNotificationService).to receive(:new).with(message: message) - expect(process_service).to receive(:perform) - described_class.perform_now(message.id) + expect_mapped_service_to_perform(message, 'Messages::SendEmailNotificationService') end it 'calls ::Tiktok::SendOnTiktokService when its tiktok message' do tiktok_channel = create(:channel_tiktok) message = create(:message, conversation: create(:conversation, inbox: tiktok_channel.inbox)) - allow(Tiktok::SendOnTiktokService).to receive(:new).with(message: message).and_return(process_service) - expect(Tiktok::SendOnTiktokService).to receive(:new).with(message: message) - expect(process_service).to receive(:perform) - described_class.perform_now(message.id) + expect_mapped_service_to_perform(message, 'Tiktok::SendOnTiktokService') end end end From 10597863d75e5e2509b39ec0fc08f271337e66e6 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Fri, 8 May 2026 11:46:33 +0530 Subject: [PATCH 02/20] feat(companies): add company creation flow (#14402) --- .../CompaniesHeader/CompanyHeader.vue | 4 +- .../components/CompanyMoreActions.vue | 45 +++++++ .../Companies/CompaniesListLayout.vue | 8 +- .../Companies/CompanyCreateDialog.vue | 110 ++++++++++++++++++ .../CompanyDetail/CompanyContactsSidebar.vue | 4 +- .../dashboard/i18n/locale/en/companies.json | 13 +++ .../companies/pages/CompaniesIndex.vue | 25 ++++ app/javascript/dashboard/stores/companies.js | 17 +++ 8 files changed, 222 insertions(+), 4 deletions(-) create mode 100644 app/javascript/dashboard/components-next/Companies/CompaniesHeader/components/CompanyMoreActions.vue create mode 100644 app/javascript/dashboard/components-next/Companies/CompanyCreateDialog.vue diff --git a/app/javascript/dashboard/components-next/Companies/CompaniesHeader/CompanyHeader.vue b/app/javascript/dashboard/components-next/Companies/CompaniesHeader/CompanyHeader.vue index f85b613a3..00c1a1694 100644 --- a/app/javascript/dashboard/components-next/Companies/CompaniesHeader/CompanyHeader.vue +++ b/app/javascript/dashboard/components-next/Companies/CompaniesHeader/CompanyHeader.vue @@ -2,6 +2,7 @@ import Input from 'dashboard/components-next/input/Input.vue'; import Icon from 'dashboard/components-next/icon/Icon.vue'; import CompanySortMenu from './components/CompanySortMenu.vue'; +import CompanyMoreActions from './components/CompanyMoreActions.vue'; defineProps({ showSearch: { type: Boolean, default: true }, @@ -11,7 +12,7 @@ defineProps({ activeOrdering: { type: String, default: '' }, }); -const emit = defineEmits(['search', 'update:sort']); +const emit = defineEmits(['search', 'update:sort', 'create']);