## Summary One-off SMS and WhatsApp campaigns now show a `Processing` state while the audience send is in progress. The campaign moves to `Completed` after processing finishes, and already-processing campaigns are skipped by the scheduler to avoid duplicate sends. ## Closes - [CW-6037: feat: Introduce an in-progress status for campaigns](https://linear.app/chatwoot/issue/CW-6037/feat-introduce-an-in-progress-status-for-campaigns) ## Screenshot SMS campaign card showing the new `Processing` status. <img width="3840" height="2160" alt="framed-campaign-processing-status" src="https://github.com/user-attachments/assets/de7913b5-65fb-4121-9034-24a568eb0382" /> ## What changed - Added `processing` as a campaign status. - Mark one-off campaigns as `processing` under a row lock before the send service runs. - Complete SMS, Twilio SMS, and WhatsApp one-off campaigns after audience processing finishes. - Keep campaigns in `processing` if an unexpected service error escapes, so the scheduler does not automatically resend the audience. - Added the `Processing` label for SMS and WhatsApp campaign cards. ## Known operational behavior If a worker is interrupted or an unexpected service error escapes after a campaign is marked `processing`, the campaign can remain in `processing`. This is intentional for now to avoid automatic full-audience resends. Installation admins can decide whether to mark the campaign completed or restart it manually from the Rails console after checking what was sent. ## How to test - Create a one-off SMS or WhatsApp campaign scheduled for now. - Run the scheduled job or trigger the campaign job. - Confirm the campaign card shows `Processing` while the audience is being processed. For small audiences, refresh during processing or use a larger audience so the state is observable. - Confirm the campaign moves to `Completed` after audience processing finishes. - Confirm an already-processing campaign is not enqueued again by the scheduled job.
124 lines
4.9 KiB
Ruby
124 lines
4.9 KiB
Ruby
require 'rails_helper'
|
|
|
|
describe Twilio::OneoffSmsCampaignService do
|
|
subject(:sms_campaign_service) { described_class.new(campaign: campaign) }
|
|
|
|
let(:account) { create(:account) }
|
|
let!(:twilio_sms) { create(:channel_twilio_sms, account: account) }
|
|
let!(:twilio_inbox) { create(:inbox, channel: twilio_sms, account: account) }
|
|
let(:label1) { create(:label, account: account) }
|
|
let(:label2) { create(:label, account: account) }
|
|
let!(:campaign) do
|
|
create(:campaign, inbox: twilio_inbox, account: account,
|
|
audience: [{ type: 'Label', id: label1.id }, { type: 'Label', id: label2.id }])
|
|
end
|
|
let(:twilio_client) { double }
|
|
let(:twilio_messages) { double }
|
|
|
|
describe 'perform' do
|
|
before do
|
|
allow(Twilio::REST::Client).to receive(:new).and_return(twilio_client)
|
|
allow(twilio_client).to receive(:messages).and_return(twilio_messages)
|
|
end
|
|
|
|
it 'raises error if the campaign is completed' do
|
|
campaign.completed!
|
|
|
|
expect { sms_campaign_service.perform }.to raise_error 'Completed Campaign'
|
|
end
|
|
|
|
it 'raises error invalid campaign when its not a oneoff sms campaign' do
|
|
campaign = create(:campaign)
|
|
|
|
expect { described_class.new(campaign: campaign).perform }.to raise_error "Invalid campaign #{campaign.id}"
|
|
end
|
|
|
|
it 'send messages to contacts in the audience and marks the campaign completed' do
|
|
contact_with_label1, contact_with_label2, contact_with_both_labels = FactoryBot.create_list(:contact, 3, :with_phone_number, account: account)
|
|
contact_with_label1.update_labels([label1.title])
|
|
contact_with_label2.update_labels([label2.title])
|
|
contact_with_both_labels.update_labels([label1.title, label2.title])
|
|
expect(twilio_messages).to receive(:create).with(
|
|
body: campaign.message,
|
|
messaging_service_sid: twilio_sms.messaging_service_sid,
|
|
to: contact_with_label1.phone_number,
|
|
status_callback: 'http://localhost:3000/twilio/delivery_status'
|
|
).once
|
|
expect(twilio_messages).to receive(:create).with(
|
|
body: campaign.message,
|
|
messaging_service_sid: twilio_sms.messaging_service_sid,
|
|
to: contact_with_label2.phone_number,
|
|
status_callback: 'http://localhost:3000/twilio/delivery_status'
|
|
).once
|
|
expect(twilio_messages).to receive(:create).with(
|
|
body: campaign.message,
|
|
messaging_service_sid: twilio_sms.messaging_service_sid,
|
|
to: contact_with_both_labels.phone_number,
|
|
status_callback: 'http://localhost:3000/twilio/delivery_status'
|
|
).once
|
|
|
|
sms_campaign_service.perform
|
|
expect(campaign.reload.completed?).to be true
|
|
end
|
|
|
|
it 'marks the campaign completed after processing the audience' do
|
|
contact = create(:contact, :with_phone_number, account: account)
|
|
contact.update_labels([label1.title])
|
|
|
|
expect(twilio_messages).to receive(:create).with(
|
|
body: campaign.message,
|
|
messaging_service_sid: twilio_sms.messaging_service_sid,
|
|
to: contact.phone_number,
|
|
status_callback: 'http://localhost:3000/twilio/delivery_status'
|
|
) do
|
|
expect(campaign.reload.completed?).to be false
|
|
end
|
|
|
|
sms_campaign_service.perform
|
|
|
|
expect(campaign.reload.completed?).to be true
|
|
end
|
|
|
|
it 'uses liquid template service to process campaign message' do
|
|
contact = create(:contact, :with_phone_number, account: account)
|
|
contact.update_labels([label1.title])
|
|
|
|
expect(Liquid::CampaignTemplateService).to receive(:new).with(campaign: campaign, contact: contact).and_call_original
|
|
expect(twilio_messages).to receive(:create).once
|
|
|
|
sms_campaign_service.perform
|
|
end
|
|
|
|
it 'continues processing contacts when Twilio raises an error' do
|
|
contact_error, contact_success = FactoryBot.create_list(:contact, 2, :with_phone_number, account: account)
|
|
contact_error.update_labels([label1.title])
|
|
contact_success.update_labels([label1.title])
|
|
|
|
error = Twilio::REST::TwilioError.new("The 'To' number #{contact_error.phone_number} is not a valid phone number.")
|
|
|
|
allow(twilio_messages).to receive(:create).and_return(nil)
|
|
|
|
expect(twilio_messages).to receive(:create).with(
|
|
body: campaign.message,
|
|
messaging_service_sid: twilio_sms.messaging_service_sid,
|
|
to: contact_error.phone_number,
|
|
status_callback: 'http://localhost:3000/twilio/delivery_status'
|
|
).and_raise(error)
|
|
|
|
expect(twilio_messages).to receive(:create).with(
|
|
body: campaign.message,
|
|
messaging_service_sid: twilio_sms.messaging_service_sid,
|
|
to: contact_success.phone_number,
|
|
status_callback: 'http://localhost:3000/twilio/delivery_status'
|
|
).once
|
|
|
|
expect(Rails.logger).to receive(:error).with(
|
|
"[Twilio Campaign #{campaign.id}] Failed to send to #{contact_error.phone_number}: #{error.message}"
|
|
)
|
|
|
|
sms_campaign_service.perform
|
|
expect(campaign.reload.completed?).to be true
|
|
end
|
|
end
|
|
end
|