diff --git a/app/jobs/webhooks/whatsapp_events_job.rb b/app/jobs/webhooks/whatsapp_events_job.rb index f904b3723..14429e61c 100644 --- a/app/jobs/webhooks/whatsapp_events_job.rb +++ b/app/jobs/webhooks/whatsapp_events_job.rb @@ -126,12 +126,17 @@ class Webhooks::WhatsappEventsJob < MutexApplicationJob def channel_is_inactive?(channel) return true if channel.blank? - return true if channel.reauthorization_required? + # Only skip for embedded signup when reauth is required; manual flow uses API keys and should still receive webhooks + return true if channel.reauthorization_required? && embedded_signup_channel?(channel) return true unless channel.account.active? false end + def embedded_signup_channel?(channel) + (channel.provider_config || {}).to_h['source'] == 'embedded_signup' + end + def find_channel_by_url_param(params) return unless params[:phone_number] diff --git a/app/listeners/webhook_listener.rb b/app/listeners/webhook_listener.rb index 835d03661..c64b36ca0 100644 --- a/app/listeners/webhook_listener.rb +++ b/app/listeners/webhook_listener.rb @@ -68,7 +68,7 @@ class WebhookListener < BaseListener def inbox_created(event) inbox, account = extract_inbox_and_account(event) - inbox_webhook_data = Inbox::EventDataPresenter.new(inbox).push_data + inbox_webhook_data = Inbox::EventDataPresenter.new(inbox).webhook_data payload = inbox_webhook_data.merge(event: __method__.to_s) deliver_account_webhooks(payload, account) end @@ -78,7 +78,7 @@ class WebhookListener < BaseListener changed_attributes = extract_changed_attributes(event) return if changed_attributes.blank? - inbox_webhook_data = Inbox::EventDataPresenter.new(inbox).push_data + inbox_webhook_data = Inbox::EventDataPresenter.new(inbox).webhook_data payload = inbox_webhook_data.merge(event: __method__.to_s, changed_attributes: changed_attributes) deliver_account_webhooks(payload, account) end diff --git a/app/presenters/conversations/event_data_presenter.rb b/app/presenters/conversations/event_data_presenter.rb index ae0e69608..4dfa10abe 100644 --- a/app/presenters/conversations/event_data_presenter.rb +++ b/app/presenters/conversations/event_data_presenter.rb @@ -23,7 +23,10 @@ class Conversations::EventDataPresenter < SimpleDelegator # Like #push_data but with message text normalized for external integrations (webhooks). def webhook_data - push_data.merge(messages: webhook_push_messages) + push_data.merge( + account: account.webhook_data, + messages: webhook_push_messages + ) end private diff --git a/app/presenters/inbox/event_data_presenter.rb b/app/presenters/inbox/event_data_presenter.rb index a408424ae..7f832bb5a 100644 --- a/app/presenters/inbox/event_data_presenter.rb +++ b/app/presenters/inbox/event_data_presenter.rb @@ -32,4 +32,8 @@ class Inbox::EventDataPresenter < SimpleDelegator channel: channel } end + + def webhook_data + push_data.merge(account: account.webhook_data) + end end diff --git a/app/views/api/v1/models/_inbox.json.jbuilder b/app/views/api/v1/models/_inbox.json.jbuilder index 40f833b5d..a677fc11a 100644 --- a/app/views/api/v1/models/_inbox.json.jbuilder +++ b/app/views/api/v1/models/_inbox.json.jbuilder @@ -138,7 +138,11 @@ json.bot_name resource.channel.try(:bot_name) if resource.telegram? if resource.whatsapp? json.message_templates resource.channel.try(:message_templates) json.provider_config resource.channel.try(:provider_config) if Current.account_user&.administrator? - json.reauthorization_required resource.channel.try(:reauthorization_required?) + # Only show reauthorization for embedded signup; manual flow uses API keys, not OAuth + json.reauthorization_required( + (resource.channel.try(:provider_config) || {}).to_h['source'] == 'embedded_signup' && + resource.channel.try(:reauthorization_required?) + ) end ## Voice attributes for TwilioSms diff --git a/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb b/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb index 4959c1e45..32650aaff 100644 --- a/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb @@ -100,6 +100,35 @@ RSpec.describe 'Inboxes API', type: :request do expect(JSON.parse(response.body, symbolize_names: true)[:id]).to eq(inbox.id) end + it 'returns reauthorization_required for embedded signup whatsapp channel when reauth required' do + whatsapp_channel = create(:channel_whatsapp, account: account, provider: 'whatsapp_cloud', sync_templates: false, + validate_provider_config: false) + whatsapp_inbox = create(:inbox, channel: whatsapp_channel, account: account) + whatsapp_channel.prompt_reauthorization! + + get "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}", + headers: admin.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + expect(response.parsed_body['reauthorization_required']).to be(true) + end + + it 'does not flag reauthorization_required for manual whatsapp channel even when reauth required' do + whatsapp_channel = create(:channel_whatsapp, account: account, provider: 'whatsapp_cloud', sync_templates: false, + validate_provider_config: false) + whatsapp_channel.update!(provider_config: whatsapp_channel.provider_config.merge('source' => 'manual')) + whatsapp_inbox = create(:inbox, channel: whatsapp_channel, account: account) + whatsapp_channel.prompt_reauthorization! + + get "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}", + headers: admin.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + expect(response.parsed_body['reauthorization_required']).to be(false) + end + it 'returns the inbox if assigned inbox is assigned as agent' do create(:inbox_member, user: agent, inbox: inbox) get "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}", diff --git a/spec/jobs/webhooks/whatsapp_events_job_spec.rb b/spec/jobs/webhooks/whatsapp_events_job_spec.rb index d82658102..8d1b24b52 100644 --- a/spec/jobs/webhooks/whatsapp_events_job_spec.rb +++ b/spec/jobs/webhooks/whatsapp_events_job_spec.rb @@ -62,6 +62,14 @@ RSpec.describe Webhooks::WhatsappEventsJob do job.perform_now(params) end + it 'still enqueues for manual channels even when reauthorization required' do + channel.update!(provider_config: channel.provider_config.merge('source' => 'manual')) + channel.prompt_reauthorization! + allow(Whatsapp::IncomingMessageWhatsappCloudService).to receive(:new).and_return(process_service) + expect(Whatsapp::IncomingMessageWhatsappCloudService).to receive(:new) + job.perform_now(params) + end + it 'will not enqueue if channel is not present' do allow(Whatsapp::IncomingMessageWhatsappCloudService).to receive(:new).and_return(process_service) allow(Whatsapp::IncomingMessageService).to receive(:new).and_return(process_service) diff --git a/spec/listeners/agent_bot_listener_spec.rb b/spec/listeners/agent_bot_listener_spec.rb index 08deeb6c4..e3f9f0402 100644 --- a/spec/listeners/agent_bot_listener_spec.rb +++ b/spec/listeners/agent_bot_listener_spec.rb @@ -82,7 +82,7 @@ describe AgentBotListener do create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot) expect(AgentBots::WebhookJob).to receive(:perform_later).with( agent_bot.outgoing_url, - hash_including(event: 'conversation_status_changed', changed_attributes: anything), + hash_including(event: 'conversation_status_changed', account: account.webhook_data, changed_attributes: anything), :agent_bot_webhook, hash_including(secret: agent_bot.secret) ).once @@ -158,6 +158,24 @@ describe AgentBotListener do end end + describe '#conversation_resolved' do + let(:event_name) { 'conversation.resolved' } + let!(:event) { Events::Base.new(event_name, Time.zone.now, conversation: conversation) } + + context 'when agent bot is configured' do + it 'sends account details in the conversation payload' do + create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot) + expect(AgentBots::WebhookJob).to receive(:perform_later).with( + agent_bot.outgoing_url, + hash_including(event: 'conversation_resolved', account: account.webhook_data), + :agent_bot_webhook, + hash_including(secret: agent_bot.secret) + ).once + listener.conversation_resolved(event) + end + end + end + describe '#webwidget_triggered' do let(:event_name) { 'webwidget.triggered' } diff --git a/spec/listeners/webhook_listener_spec.rb b/spec/listeners/webhook_listener_spec.rb index a7a64f175..b63f43c2f 100644 --- a/spec/listeners/webhook_listener_spec.rb +++ b/spec/listeners/webhook_listener_spec.rb @@ -101,6 +101,17 @@ describe WebhookListener do ).once listener.conversation_created(conversation_created_event) end + + it 'includes account details in the conversation payload' do + webhook = create(:webhook, inbox: inbox, account: account) + expect(WebhookJob).to receive(:perform_later).with( + webhook.url, + hash_including(account: account.webhook_data), + :account_webhook, + hash_including(secret: webhook.secret) + ).once + listener.conversation_created(conversation_created_event) + end end context 'when inbox is an API Channel' do @@ -250,7 +261,7 @@ describe WebhookListener do context 'when webhook is configured' do it 'triggers webhook' do - inbox_data = Inbox::EventDataPresenter.new(inbox).push_data + inbox_data = Inbox::EventDataPresenter.new(inbox).webhook_data webhook = create(:webhook, account: account, subscriptions: ['inbox_created']) expect(WebhookJob).to receive(:perform_later).with( webhook.url, inbox_data.merge(event: 'inbox_created'), :account_webhook, @@ -258,6 +269,17 @@ describe WebhookListener do ).once listener.inbox_created(inbox_created_event) end + + it 'includes account details in the inbox payload' do + webhook = create(:webhook, account: account, subscriptions: ['inbox_created']) + expect(WebhookJob).to receive(:perform_later).with( + webhook.url, + hash_including(account: account.webhook_data), + :account_webhook, + hash_including(secret: webhook.secret) + ).once + listener.inbox_created(inbox_created_event) + end end end @@ -287,7 +309,7 @@ describe WebhookListener do it 'triggers webhook' do webhook = create(:webhook, account: account, subscriptions: ['inbox_updated']) - inbox_data = Inbox::EventDataPresenter.new(inbox).push_data + inbox_data = Inbox::EventDataPresenter.new(inbox).webhook_data changed_attributes_data = [{ 'name' => { 'previous_value': 'Inbox 1', 'current_value': inbox.name } }] expect(WebhookJob).to receive(:perform_later).with( diff --git a/spec/presenters/conversations/event_data_presenter_spec.rb b/spec/presenters/conversations/event_data_presenter_spec.rb index 76fd8f8a8..21cb26c98 100644 --- a/spec/presenters/conversations/event_data_presenter_spec.rb +++ b/spec/presenters/conversations/event_data_presenter_spec.rb @@ -46,6 +46,10 @@ RSpec.describe Conversations::EventDataPresenter do end describe '#webhook_data' do + it 'includes account details for webhook consumers' do + expect(presenter.webhook_data[:account]).to eq(conversation.account.webhook_data) + end + it 'normalizes hard-break backslashes in message content' do message = create(:message, conversation: conversation, account: conversation.account, message_type: :outgoing, content: "Hello\\\nWorld")