fix: deregister WhatsApp Cloud number on inbox delete (#14940)

## Description

When a WhatsApp Cloud inbox is deleted,
`Whatsapp::WebhookTeardownService` clears the phone-level webhook
override and unsubscribes the app from the WABA (when it's the last
inbox), but it **never deregisters the phone number**.

Because the number stays registered to the app, Meta reports that the
number is **"already registered to a partner app"** when the user later
tries to re-add it under a different app/BSP — leaving the number
effectively stuck.

This adds a deregister step so the number is released on deletion:
- New `Whatsapp::FacebookApiClient#deregister_phone_number` → `POST
/{phone_number_id}/deregister`.
- `WebhookTeardownService` calls it during teardown (alongside the
existing override-clear and app-unsubscribe), guarded on
`phone_number_id` and wrapped so a failure is logged and never blocks
the channel delete.

Docs:
https://developers.facebook.com/docs/whatsapp/cloud-api/reference/registration
(deregister)

## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)

## How has this been tested?
Unit specs for both the new API client method and the teardown service
(Meta API stubbed with WebMock), mirroring the existing
`register_phone_number` / teardown coverage.

## Checklist
- [x] My code follows the style guidelines of this project
- [x] I have added tests that prove my fix is effective
- [x] New and existing unit tests pass locally with my changes

---------

Co-authored-by: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com>
Co-authored-by: Tanmay Deep Sharma <tanmaydeepsharma21@gmail.com>
This commit is contained in:
Petterson
2026-07-27 06:06:33 -03:00
committed by GitHub
parent 5e82e971fa
commit b2716e15e1
4 changed files with 67 additions and 0 deletions

View File

@@ -52,6 +52,17 @@ class Whatsapp::FacebookApiClient
handle_response(response, 'Phone registration failed')
end
# Releases the number from this app so it can be re-added under another app/BSP. Without this,
# after an inbox is deleted the number stays registered and Meta reports "already in a partner app".
def deregister_phone_number(phone_number_id)
response = HTTParty.post(
"#{BASE_URI}/#{@api_version}/#{phone_number_id}/deregister",
headers: request_headers
)
handle_response(response, 'Phone deregistration failed')
end
def phone_number_verified?(phone_number_id)
response = HTTParty.get(
"#{BASE_URI}/#{@api_version}/#{phone_number_id}",

View File

@@ -9,6 +9,7 @@ class Whatsapp::WebhookTeardownService
api_client = Whatsapp::FacebookApiClient.new(provider_config['api_key'])
clear_phone_number_override(api_client)
deregister_phone_number(api_client)
unsubscribe_app_if_last_inbox(api_client)
rescue StandardError => e
# before_destroy must never block a channel delete — log and move on.
@@ -37,6 +38,20 @@ class Whatsapp::WebhookTeardownService
Rails.logger.error "[WHATSAPP] Phone-level webhook clear failed for channel #{@channel.id}: #{e.message}"
end
# Embedded signup only — deregistering a manually connected number disables it on the customer's own app.
# Releases the number from our app so the customer can re-add it elsewhere.
def deregister_phone_number(api_client)
return unless provider_config['source'] == 'embedded_signup'
phone_number_id = provider_config['phone_number_id']
return if phone_number_id.blank?
api_client.deregister_phone_number(phone_number_id)
Rails.logger.info "[WHATSAPP] Phone number deregistered for channel #{@channel.id}"
rescue StandardError => e
Rails.logger.error "[WHATSAPP] Phone deregistration 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)

View File

@@ -154,6 +154,34 @@ describe Whatsapp::FacebookApiClient do
end
end
describe '#deregister_phone_number' do
let(:phone_number_id) { 'test_phone_id' }
context 'when successful' do
before do
stub_request(:post, "https://graph.facebook.com/#{api_version}/#{phone_number_id}/deregister")
.with(headers: { 'Authorization' => "Bearer #{access_token}", 'Content-Type' => 'application/json' })
.to_return(status: 200, body: { success: true }.to_json, headers: { 'Content-Type' => 'application/json' })
end
it 'returns success response' do
result = api_client.deregister_phone_number(phone_number_id)
expect(result['success']).to be(true)
end
end
context 'when failed' do
before do
stub_request(:post, "https://graph.facebook.com/#{api_version}/#{phone_number_id}/deregister")
.to_return(status: 400, body: { error: 'Deregistration failed' }.to_json)
end
it 'raises an error' do
expect { api_client.deregister_phone_number(phone_number_id) }.to raise_error(/Phone deregistration failed/)
end
end
end
describe '#subscribe_phone_number_webhook' do
let(:waba_id) { 'test_waba_id' }
let(:phone_number_id) { 'test_phone_id' }

View File

@@ -24,16 +24,29 @@ RSpec.describe Whatsapp::WebhookTeardownService do
api_client = instance_double(Whatsapp::FacebookApiClient)
allow(Whatsapp::FacebookApiClient).to receive(:new).with('test_api_key').and_return(api_client)
allow(api_client).to receive(:clear_phone_number_callback_override).with('test_phone_id')
allow(api_client).to receive(:deregister_phone_number).with('test_phone_id')
service.perform
expect(api_client).to have_received(:clear_phone_number_callback_override).with('test_phone_id')
end
it 'deregisters the phone number so it is freed from the app' do
api_client = instance_double(Whatsapp::FacebookApiClient)
allow(Whatsapp::FacebookApiClient).to receive(:new).with('test_api_key').and_return(api_client)
allow(api_client).to receive(:clear_phone_number_callback_override)
allow(api_client).to receive(:deregister_phone_number).with('test_phone_id')
service.perform
expect(api_client).to have_received(:deregister_phone_number).with('test_phone_id')
end
it 'handles errors gracefully without raising' 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).and_raise(StandardError, 'API Error')
allow(api_client).to receive(:deregister_phone_number)
expect { service.perform }.not_to raise_error
end