fix: clear whatsapp webhook override when manual cloud inbox is deleted (#15010)
Deleting a manually-configured WhatsApp Cloud inbox left its
phone-number-level webhook override still pointing at Chatwoot on Meta's
side. The number kept routing inbound events to us after the inbox was
gone, which blocked the customer's own app — subscribed separately on
the same WABA — from receiving messages, since the phone-level override
takes priority over the app-level subscription. Deleting the inbox now
releases the override, as it already did for embedded-signup inboxes.
## What changed
The setup and teardown paths gated on opposite halves of the same
condition. `Channel::Whatsapp#should_auto_setup_webhooks?` sets the
override for `whatsapp_cloud` inboxes where `source !=
'embedded_signup'` (i.e. manual ones), while
`Whatsapp::WebhookTeardownService#should_teardown_webhook?` only cleared
it when `source == 'embedded_signup'`. The two sets are disjoint, so
manual inboxes were exactly the ones that set an override on create and
never cleared it on destroy. Embedded-signup inboxes were unaffected
because `EmbeddedSignupService` calls `setup_webhooks` explicitly.
Dropping the `source` check from the teardown guard is the whole fix.
Manual `whatsapp_cloud` channels can't persist without `api_key`,
`phone_number_id` and `business_account_id` (`validate_provider_config`
verifies all three against Meta), so the remaining presence guards and
both API calls have everything they need. The WABA-level `DELETE
/subscribed_apps` now also fires for manual inboxes when the last one on
a WABA is removed, which is symmetric with manual setup subscribing the
app in the first place; the token only unsubscribes the app it belongs
to, so a customer's separate app subscription is untouched.
This fixes the leak going forward. Numbers already stranded still need
the override cleared with the customer's own token, since we no longer
hold their `api_key` once the inbox is deleted.
## How to reproduce
1. Create a WhatsApp Cloud inbox using manual API keys (not embedded
signup).
2. Confirm the override is set: `GET
/v22.0/{phone_number_id}?fields=webhook_configuration` shows
`phone_number` pointing at your Chatwoot install.
3. Delete the inbox.
4. Before this change, the override still points at Chatwoot. After it,
`webhook_configuration` no longer carries the phone-level override and
events fall back to the WABA/app-level subscription.
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
co-authored by
Muhsin Keloth
parent
3e03f8da1e
commit
9328f8739c
@@ -23,7 +23,6 @@ class Whatsapp::WebhookTeardownService
|
||||
|
||||
def should_teardown_webhook?
|
||||
@channel.provider == 'whatsapp_cloud' &&
|
||||
provider_config['source'] == 'embedded_signup' &&
|
||||
provider_config['api_key'].present? &&
|
||||
(provider_config['phone_number_id'].present? || provider_config['business_account_id'].present?)
|
||||
end
|
||||
@@ -38,8 +37,11 @@ class Whatsapp::WebhookTeardownService
|
||||
Rails.logger.error "[WHATSAPP] Phone-level webhook clear failed for channel #{@channel.id}: #{e.message}"
|
||||
end
|
||||
|
||||
# The app subscription is shared by every inbox on the WABA, so only unsubscribe when this is the last one.
|
||||
# Embedded signup only — a manual token's subscribed app is the customer's, not ours to unsubscribe.
|
||||
# The subscription is shared across the WABA, so only unsubscribe when this is the last inbox.
|
||||
def unsubscribe_app_if_last_inbox(api_client)
|
||||
return unless provider_config['source'] == 'embedded_signup'
|
||||
|
||||
waba_id = provider_config['business_account_id']
|
||||
return if waba_id.blank?
|
||||
return if waba_sibling_exists?(waba_id)
|
||||
|
||||
@@ -51,18 +51,41 @@ RSpec.describe Whatsapp::WebhookTeardownService do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when channel is whatsapp_cloud but not embedded_signup' do
|
||||
context 'when channel is whatsapp_cloud with manual setup' do
|
||||
before do
|
||||
allow(channel).to receive(:setup_webhooks).and_return(true)
|
||||
|
||||
channel.update!(
|
||||
provider: 'whatsapp_cloud',
|
||||
provider_config: { 'source' => 'manual' }
|
||||
provider_config: {
|
||||
'source' => 'manual',
|
||||
'phone_number_id' => 'manual_phone_id',
|
||||
'business_account_id' => 'manual_waba_id',
|
||||
'api_key' => 'manual_api_key'
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not attempt to unsubscribe webhook' do
|
||||
expect(Whatsapp::FacebookApiClient).not_to receive(:new)
|
||||
it 'clears the phone number callback override' do
|
||||
api_client = instance_double(Whatsapp::FacebookApiClient)
|
||||
allow(Whatsapp::FacebookApiClient).to receive(:new).with('manual_api_key').and_return(api_client)
|
||||
allow(api_client).to receive(:clear_phone_number_callback_override).with('manual_phone_id')
|
||||
|
||||
service.perform
|
||||
|
||||
expect(api_client).to have_received(:clear_phone_number_callback_override).with('manual_phone_id')
|
||||
end
|
||||
|
||||
# The manual token belongs to the customer's own Meta app, so its WABA subscription is not ours to remove.
|
||||
it 'does not unsubscribe the app from the WABA' do
|
||||
api_client = instance_double(Whatsapp::FacebookApiClient)
|
||||
allow(Whatsapp::FacebookApiClient).to receive(:new).and_return(api_client)
|
||||
allow(api_client).to receive(:clear_phone_number_callback_override)
|
||||
allow(api_client).to receive(:unsubscribe_app_from_waba)
|
||||
|
||||
service.perform
|
||||
|
||||
expect(api_client).not_to have_received(:unsubscribe_app_from_waba)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user