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>
61 lines
2.1 KiB
Ruby
61 lines
2.1 KiB
Ruby
class Whatsapp::WebhookTeardownService
|
|
def initialize(channel)
|
|
@channel = channel
|
|
end
|
|
|
|
def perform
|
|
return unless should_teardown_webhook?
|
|
|
|
api_client = Whatsapp::FacebookApiClient.new(provider_config['api_key'])
|
|
|
|
clear_phone_number_override(api_client)
|
|
unsubscribe_app_if_last_inbox(api_client)
|
|
rescue StandardError => e
|
|
# before_destroy must never block a channel delete — log and move on.
|
|
Rails.logger.error "[WHATSAPP] Webhook teardown failed for channel #{@channel&.id}: #{e.message}"
|
|
end
|
|
|
|
private
|
|
|
|
def provider_config
|
|
@channel.provider_config || {}
|
|
end
|
|
|
|
def should_teardown_webhook?
|
|
@channel.provider == 'whatsapp_cloud' &&
|
|
provider_config['api_key'].present? &&
|
|
(provider_config['phone_number_id'].present? || provider_config['business_account_id'].present?)
|
|
end
|
|
|
|
def clear_phone_number_override(api_client)
|
|
phone_number_id = provider_config['phone_number_id']
|
|
return if phone_number_id.blank?
|
|
|
|
api_client.clear_phone_number_callback_override(phone_number_id)
|
|
Rails.logger.info "[WHATSAPP] Phone-level webhook override cleared for channel #{@channel.id}"
|
|
rescue StandardError => e
|
|
Rails.logger.error "[WHATSAPP] Phone-level webhook clear failed for channel #{@channel.id}: #{e.message}"
|
|
end
|
|
|
|
# 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)
|
|
|
|
api_client.unsubscribe_app_from_waba(waba_id)
|
|
Rails.logger.info "[WHATSAPP] WABA app subscription removed for channel #{@channel.id}"
|
|
rescue StandardError => e
|
|
Rails.logger.error "[WHATSAPP] WABA app unsubscribe failed for channel #{@channel.id}: #{e.message}"
|
|
end
|
|
|
|
def waba_sibling_exists?(waba_id)
|
|
Channel::Whatsapp
|
|
.where.not(id: @channel.id)
|
|
.exists?(["provider_config ->> 'business_account_id' = ?", waba_id])
|
|
end
|
|
end
|