From 63a2c70667193a922bd18738d3ed5c967f57384f Mon Sep 17 00:00:00 2001 From: Gabriel Jablonski Date: Thu, 6 Nov 2025 23:41:58 -0300 Subject: [PATCH] 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 --- .../channels/whatsapp/zapi_qr_code_job.rb | 2 +- .../providers/whatsapp_zapi_service.rb | 4 +- .../zapi_handlers/received_callback.rb | 24 +++++++-- .../whatsapp/zapi_qr_code_job_spec.rb | 12 +++++ .../providers/whatsapp_zapi_service_spec.rb | 12 +++++ .../zapi_handlers/received_callback_spec.rb | 53 +++++++++++++++++++ 6 files changed, 102 insertions(+), 5 deletions(-) diff --git a/app/jobs/channels/whatsapp/zapi_qr_code_job.rb b/app/jobs/channels/whatsapp/zapi_qr_code_job.rb index 7ce6cffb1..7f60a2bbf 100644 --- a/app/jobs/channels/whatsapp/zapi_qr_code_job.rb +++ b/app/jobs/channels/whatsapp/zapi_qr_code_job.rb @@ -2,7 +2,7 @@ class Channels::Whatsapp::ZapiQrCodeJob < ApplicationJob queue_as :default 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' if attempt > 3 diff --git a/app/services/whatsapp/providers/whatsapp_zapi_service.rb b/app/services/whatsapp/providers/whatsapp_zapi_service.rb index dfc1ab2b8..e3653fbab 100644 --- a/app/services/whatsapp/providers/whatsapp_zapi_service.rb +++ b/app/services/whatsapp/providers/whatsapp_zapi_service.rb @@ -46,7 +46,9 @@ class Whatsapp::Providers::WhatsappZapiService < Whatsapp::Providers::BaseServic 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 end diff --git a/app/services/whatsapp/zapi_handlers/received_callback.rb b/app/services/whatsapp/zapi_handlers/received_callback.rb index cdbb341f8..e89edf5e5 100644 --- a/app/services/whatsapp/zapi_handlers/received_callback.rb +++ b/app/services/whatsapp/zapi_handlers/received_callback.rb @@ -73,12 +73,18 @@ module Whatsapp::ZapiHandlers::ReceivedCallback # rubocop:disable Metrics/Module def set_contact 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[:phone_number] = "+#{@raw_message[:phone]}" unless @raw_message[:phone].ends_with?('@lid') + contact_attributes = { name: push_name, identifier: identifier } + + 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( - source_id: @raw_message[:chatLid].to_s.gsub(/[^\d]/, ''), + source_id: source_id, inbox: inbox, contact_attributes: contact_attributes ).perform @@ -91,6 +97,18 @@ module Whatsapp::ZapiHandlers::ReceivedCallback # rubocop:disable Metrics/Module try_update_contact_avatar 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 return if @contact.phone_number.present? return if @raw_message[:phone].ends_with?('@lid') diff --git a/spec/jobs/channels/whatsapp/zapi_qr_code_job_spec.rb b/spec/jobs/channels/whatsapp/zapi_qr_code_job_spec.rb index 911c93644..6b04489e0 100644 --- a/spec/jobs/channels/whatsapp/zapi_qr_code_job_spec.rb +++ b/spec/jobs/channels/whatsapp/zapi_qr_code_job_spec.rb @@ -37,6 +37,18 @@ RSpec.describe Channels::Whatsapp::ZapiQrCodeJob do described_class.perform_now(whatsapp_channel, 2) 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 whatsapp_channel.update_provider_connection!(connection: 'close') allow(zapi_service).to receive(:qr_code_image).and_return('data:image/png;base64,test-qr-code') diff --git a/spec/services/whatsapp/providers/whatsapp_zapi_service_spec.rb b/spec/services/whatsapp/providers/whatsapp_zapi_service_spec.rb index 265c1da88..1e045f0b3 100644 --- a/spec/services/whatsapp/providers/whatsapp_zapi_service_spec.rb +++ b/spec/services/whatsapp/providers/whatsapp_zapi_service_spec.rb @@ -70,6 +70,18 @@ describe Whatsapp::Providers::WhatsappZapiService do expect(service.setup_channel_provider).to be(true) 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 whatsapp_channel.update!(provider_connection: { 'connection' => 'close' }) diff --git a/spec/services/whatsapp/zapi_handlers/received_callback_spec.rb b/spec/services/whatsapp/zapi_handlers/received_callback_spec.rb index e0860cddf..3954c9467 100644 --- a/spec/services/whatsapp/zapi_handlers/received_callback_spec.rb +++ b/spec/services/whatsapp/zapi_handlers/received_callback_spec.rb @@ -376,6 +376,59 @@ describe Whatsapp::ZapiHandlers::ReceivedCallback do 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 let(:params) do base_params.merge(