Files
chatwoot/spec/services/sms/oneoff_sms_campaign_service_spec.rb
Sojan JoseandGitHub f27bbef73b feat: show processing status for one-off campaigns (#14592)
## 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.
2026-06-01 16:47:17 +05:30

87 lines
3.5 KiB
Ruby

require 'rails_helper'
describe Sms::OneoffSmsCampaignService do
subject(:sms_campaign_service) { described_class.new(campaign: campaign) }
let(:account) { create(:account) }
let!(:sms_channel) { create(:channel_sms, account: account) }
let!(:sms_inbox) { create(:inbox, channel: sms_channel, account: account) }
let(:label1) { create(:label, account: account) }
let(:label2) { create(:label, account: account) }
let!(:campaign) do
create(:campaign, inbox: sms_inbox, account: account,
audience: [{ type: 'Label', id: label1.id }, { type: 'Label', id: label2.id }])
end
describe 'perform' do
before do
stub_request(:post, 'https://messaging.bandwidth.com/api/v2/users/1/messages').to_return(
status: 200,
body: { 'id' => '1' }.to_json,
headers: {}
)
allow_any_instance_of(described_class).to receive(:channel).and_return(sms_channel) # rubocop:disable RSpec/AnyInstance
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])
sms_campaign_service.perform
assert_requested(:post, 'https://messaging.bandwidth.com/api/v2/users/1/messages', times: 3)
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(sms_channel).to receive(:send_text_message) 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
sms_campaign_service.perform
end
it 'continues processing contacts when sending message 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_message = 'SMS provider error'
expect(sms_channel).to receive(:send_text_message).with(contact_error.phone_number, anything).and_raise(StandardError, error_message)
expect(sms_channel).to receive(:send_text_message).with(contact_success.phone_number, anything).and_return(nil)
expect(Rails.logger).to receive(:error).with("[SMS 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