fix(zapi): first connection issues, and merge existing contact on received message (#138)

* fix(zapi): pairing issues on first connection

* fix(zapi): handle existing contact when receiving new message

* refactor: simplify logic
This commit is contained in:
Gabriel Jablonski 2025-11-06 23:41:58 -03:00 committed by GitHub
parent 16d7e68176
commit 63a2c70667
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 102 additions and 5 deletions

View File

@ -2,7 +2,7 @@ class Channels::Whatsapp::ZapiQrCodeJob < ApplicationJob
queue_as :default queue_as :default
def perform(whatsapp_channel, attempt = 1) def perform(whatsapp_channel, attempt = 1)
return if attempt == 1 && whatsapp_channel.provider_connection['connection'] != 'close' return if attempt == 1 && whatsapp_channel.provider_connection.present? && whatsapp_channel.provider_connection['connection'] != 'close'
return if attempt > 1 && whatsapp_channel.provider_connection['connection'] != 'connecting' return if attempt > 1 && whatsapp_channel.provider_connection['connection'] != 'connecting'
if attempt > 3 if attempt > 3

View File

@ -46,7 +46,9 @@ class Whatsapp::Providers::WhatsappZapiService < Whatsapp::Providers::BaseServic
raise ProviderUnavailableError unless process_response(response) raise ProviderUnavailableError unless process_response(response)
Channels::Whatsapp::ZapiQrCodeJob.perform_later(whatsapp_channel) if whatsapp_channel.provider_connection['connection'] == 'close' if whatsapp_channel.provider_connection.blank? || whatsapp_channel.provider_connection['connection'] == 'close'
Channels::Whatsapp::ZapiQrCodeJob.perform_later(whatsapp_channel)
end
true true
end end

View File

@ -73,12 +73,18 @@ module Whatsapp::ZapiHandlers::ReceivedCallback # rubocop:disable Metrics/Module
def set_contact def set_contact
push_name = contact_name push_name = contact_name
source_id = @raw_message[:chatLid].to_s.gsub(/[^\d]/, '')
identifier = @raw_message[:chatLid]
contact_attributes = { name: push_name, identifier: @raw_message[:chatLid] } contact_attributes = { name: push_name, identifier: identifier }
contact_attributes[:phone_number] = "+#{@raw_message[:phone]}" unless @raw_message[:phone].ends_with?('@lid')
unless @raw_message[:phone].ends_with?('@lid')
contact_attributes[:phone_number] = "+#{@raw_message[:phone]}"
update_existing_contact_inbox(@raw_message[:phone], source_id, identifier)
end
contact_inbox = ::ContactInboxWithContactBuilder.new( contact_inbox = ::ContactInboxWithContactBuilder.new(
source_id: @raw_message[:chatLid].to_s.gsub(/[^\d]/, ''), source_id: source_id,
inbox: inbox, inbox: inbox,
contact_attributes: contact_attributes contact_attributes: contact_attributes
).perform ).perform
@ -91,6 +97,18 @@ module Whatsapp::ZapiHandlers::ReceivedCallback # rubocop:disable Metrics/Module
try_update_contact_avatar try_update_contact_avatar
end end
def update_existing_contact_inbox(phone, source_id, identifier)
# NOTE: This is useful when we create a new contact manually, so we don't have information about contact LID;
# With this, when we receive a message from that contact, we can link it properly.
existing_contact_inbox = inbox.contact_inboxes.find_by(source_id: phone)
return unless existing_contact_inbox
ActiveRecord::Base.transaction do
existing_contact_inbox.update!(source_id: source_id)
existing_contact_inbox.contact.update!(identifier: identifier)
end
end
def update_contact_phone_number def update_contact_phone_number
return if @contact.phone_number.present? return if @contact.phone_number.present?
return if @raw_message[:phone].ends_with?('@lid') return if @raw_message[:phone].ends_with?('@lid')

View File

@ -37,6 +37,18 @@ RSpec.describe Channels::Whatsapp::ZapiQrCodeJob do
described_class.perform_now(whatsapp_channel, 2) described_class.perform_now(whatsapp_channel, 2)
end end
it 'fetches QR code and schedules next attempt on first attempt with blank connection' do
allow(zapi_service).to receive(:qr_code_image).and_return('data:image/png;base64,test-qr-code')
expect(described_class).to receive(:set).with(wait: 30.seconds).and_call_original
expect do
described_class.perform_now(whatsapp_channel, 1)
end.to have_enqueued_job(described_class).with(whatsapp_channel, 2)
expect(whatsapp_channel.reload.provider_connection['connection']).to eq('connecting')
expect(whatsapp_channel.provider_connection['qr_data_url']).to eq('data:image/png;base64,test-qr-code')
end
it 'fetches QR code and schedules next attempt on first attempt with close connection' do it 'fetches QR code and schedules next attempt on first attempt with close connection' do
whatsapp_channel.update_provider_connection!(connection: 'close') whatsapp_channel.update_provider_connection!(connection: 'close')
allow(zapi_service).to receive(:qr_code_image).and_return('data:image/png;base64,test-qr-code') allow(zapi_service).to receive(:qr_code_image).and_return('data:image/png;base64,test-qr-code')

View File

@ -70,6 +70,18 @@ describe Whatsapp::Providers::WhatsappZapiService do
expect(service.setup_channel_provider).to be(true) expect(service.setup_channel_provider).to be(true)
end end
it 'schedules QR code job when connection is blank' do
whatsapp_channel.update!(provider_connection: {})
stub_request(:put, "#{api_instance_path_with_token}/update-every-webhooks")
.with(headers: stub_headers)
.to_return(status: 200)
service.setup_channel_provider
expect(Channels::Whatsapp::ZapiQrCodeJob).to have_been_enqueued.with(whatsapp_channel)
end
it 'schedules QR code job when connection is closed' do it 'schedules QR code job when connection is closed' do
whatsapp_channel.update!(provider_connection: { 'connection' => 'close' }) whatsapp_channel.update!(provider_connection: { 'connection' => 'close' })

View File

@ -376,6 +376,59 @@ describe Whatsapp::ZapiHandlers::ReceivedCallback do
end end
end end
context 'when contact_inbox exists with phone as source_id (manual creation)' do
let!(:existing_contact) do
create(:contact,
account: inbox.account,
identifier: nil,
phone_number: '+5511987654321',
name: 'Manually Created Contact')
end
let!(:existing_contact_inbox) do
create(:contact_inbox,
inbox: inbox,
contact: existing_contact,
source_id: '5511987654321')
end
let(:params) do
base_params.merge(
phone: '5511987654321',
chatLid: '123456789@lid'
)
end
it 'updates existing contact_inbox source_id from phone to LID' do
service.perform
expect(existing_contact_inbox.reload.source_id).to eq('123456789')
end
it 'updates existing contact identifier with LID' do
service.perform
expect(existing_contact.reload.identifier).to eq('123456789@lid')
end
it 'does not create a new contact_inbox' do
expect do
service.perform
end.not_to change(ContactInbox, :count)
end
it 'does not create a new contact' do
expect do
service.perform
end.not_to change(Contact, :count)
end
it 'reuses the existing contact for message' do
service.perform
message = Message.last
expect(message.sender).to eq(existing_contact)
end
end
context 'when handling avatar' do context 'when handling avatar' do
let(:params) do let(:params) do
base_params.merge( base_params.merge(