From 774c168d9404e4841090219775b00e7ca50c1122 Mon Sep 17 00:00:00 2001 From: Gabriel Jablonski Date: Fri, 12 Dec 2025 09:58:12 -0300 Subject: [PATCH] fix: z-api read message (#165) * fix: z-api read messages * fix: use threads for parallel requests * feat: use jobs instead * fix: refactor ZapiReadMessageJob to use service method for sending read messages --- .../whatsapp/zapi_read_message_job.rb | 8 +++ .../providers/whatsapp_zapi_service.rb | 21 ++++-- .../whatsapp/zapi_read_message_job_spec.rb | 71 +++++++++++++++++++ .../providers/whatsapp_zapi_service_spec.rb | 42 +++++++---- 4 files changed, 123 insertions(+), 19 deletions(-) create mode 100644 app/jobs/channels/whatsapp/zapi_read_message_job.rb create mode 100644 spec/jobs/channels/whatsapp/zapi_read_message_job_spec.rb diff --git a/app/jobs/channels/whatsapp/zapi_read_message_job.rb b/app/jobs/channels/whatsapp/zapi_read_message_job.rb new file mode 100644 index 000000000..afcd6438e --- /dev/null +++ b/app/jobs/channels/whatsapp/zapi_read_message_job.rb @@ -0,0 +1,8 @@ +class Channels::Whatsapp::ZapiReadMessageJob < ApplicationJob + queue_as :default + + def perform(whatsapp_channel, phone, message_source_id) + service = Whatsapp::Providers::WhatsappZapiService.new(whatsapp_channel: whatsapp_channel) + service.send_read_message(phone, message_source_id) + end +end diff --git a/app/services/whatsapp/providers/whatsapp_zapi_service.rb b/app/services/whatsapp/providers/whatsapp_zapi_service.rb index 01df1015c..bb29bb00c 100644 --- a/app/services/whatsapp/providers/whatsapp_zapi_service.rb +++ b/app/services/whatsapp/providers/whatsapp_zapi_service.rb @@ -81,21 +81,28 @@ class Whatsapp::Providers::WhatsappZapiService < Whatsapp::Providers::BaseServic end def read_messages(messages, recipient_id:, **) - # NOTE: Z-API will handle marking previous messages as read. - last_message = messages.last + phone = recipient_id.delete('+') + messages.each do |message| + next if message.source_id.blank? + + Channels::Whatsapp::ZapiReadMessageJob.perform_later(whatsapp_channel, phone, message.source_id) + end + + true + end + + def send_read_message(phone, message_source_id) response = HTTParty.post( "#{api_instance_path_with_token}/read-message", headers: api_headers, body: { - phone: recipient_id.delete('+'), - messageId: last_message.source_id + phone: phone, + messageId: message_source_id }.to_json ) - raise ProviderUnavailableError unless process_response(response) - - true + process_response(response) end def on_whatsapp(phone_number) diff --git a/spec/jobs/channels/whatsapp/zapi_read_message_job_spec.rb b/spec/jobs/channels/whatsapp/zapi_read_message_job_spec.rb new file mode 100644 index 000000000..6dfe7ac4b --- /dev/null +++ b/spec/jobs/channels/whatsapp/zapi_read_message_job_spec.rb @@ -0,0 +1,71 @@ +require 'rails_helper' + +RSpec.describe Channels::Whatsapp::ZapiReadMessageJob do + let(:whatsapp_channel) do + create(:channel_whatsapp, + provider: 'zapi', + sync_templates: false, + validate_provider_config: false, + provider_config: { + 'instance_id' => 'test-instance', + 'token' => 'test-token', + 'client_token' => 'test-client-token' + }) + end + + let(:phone) { '551187654321' } + let(:message_source_id) { 'msg_123' } + let(:api_base_path) { Whatsapp::Providers::WhatsappZapiService::API_BASE_PATH } + let(:api_instance_path_with_token) { "#{api_base_path}/instances/test-instance/token/test-token" } + + def stub_headers + { + 'Accept' => '*/*', + 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', + 'Content-Type' => 'application/json', + 'Client-Token' => 'test-client-token', + 'User-Agent' => 'Ruby' + } + end + + describe '#perform' do + context 'when response is successful' do + it 'sends a read message request to Z-API' do + stub_request(:post, "#{api_instance_path_with_token}/read-message") + .with( + headers: stub_headers, + body: { + phone: phone, + messageId: message_source_id + }.to_json + ) + .to_return(status: 200) + + described_class.perform_now(whatsapp_channel, phone, message_source_id) + + expect(a_request(:post, "#{api_instance_path_with_token}/read-message") + .with( + headers: stub_headers, + body: { + phone: phone, + messageId: message_source_id + }.to_json + )).to have_been_made.once + end + end + + context 'when response is unsuccessful' do + it 'logs the error' do + stub_request(:post, "#{api_instance_path_with_token}/read-message") + .with(headers: stub_headers) + .to_return(status: 400, body: 'error message') + + allow(Rails.logger).to receive(:error) + + described_class.perform_now(whatsapp_channel, phone, message_source_id) + + expect(Rails.logger).to have_received(:error).with('error message') + end + end + end +end diff --git a/spec/services/whatsapp/providers/whatsapp_zapi_service_spec.rb b/spec/services/whatsapp/providers/whatsapp_zapi_service_spec.rb index c08996520..3eedb796e 100644 --- a/spec/services/whatsapp/providers/whatsapp_zapi_service_spec.rb +++ b/spec/services/whatsapp/providers/whatsapp_zapi_service_spec.rb @@ -194,37 +194,55 @@ describe Whatsapp::Providers::WhatsappZapiService do end describe '#read_messages' do - let(:messages) { [create(:message), message] } + let(:first_message) { create(:message, source_id: 'msg_first') } + let(:second_message) { create(:message, source_id: 'msg_second') } + let(:messages) { [first_message, second_message] } + + it 'enqueues a job for each message' do + service.read_messages(messages, recipient_id: "+#{test_send_phone_number}") + + expect(Channels::Whatsapp::ZapiReadMessageJob).to have_been_enqueued.with(whatsapp_channel, test_send_phone_number, first_message.source_id) + expect(Channels::Whatsapp::ZapiReadMessageJob).to have_been_enqueued.with(whatsapp_channel, test_send_phone_number, second_message.source_id) + end + + it 'returns true' do + result = service.read_messages(messages, recipient_id: "+#{test_send_phone_number}") + + expect(result).to be(true) + end + end + + describe '#send_read_message' do + let(:phone) { test_send_phone_number } + let(:message_source_id) { 'msg_123' } context 'when response is successful' do - it 'marks messages as read by referencing last message id' do + it 'sends a read message request to Z-API' do stub_request(:post, "#{api_instance_path_with_token}/read-message") .with( headers: stub_headers, - body: { - phone: test_send_phone_number, - messageId: message.source_id - }.to_json + body: { phone: phone, messageId: message_source_id }.to_json ) .to_return(status: 200) - result = service.read_messages(messages, recipient_id: "+#{test_send_phone_number}") + result = service.send_read_message(phone, message_source_id) expect(result).to be(true) end end context 'when response is unsuccessful' do - it 'raises ProviderUnavailableError' do + it 'logs the error and returns false' do stub_request(:post, "#{api_instance_path_with_token}/read-message") .with(headers: stub_headers) - .to_return(status: 400, body: 'error message', headers: {}) + .to_return(status: 400, body: 'error message') allow(Rails.logger).to receive(:error) - expect do - service.read_messages(messages, recipient_id: "+#{test_send_phone_number}") - end.to raise_error(Whatsapp::Providers::WhatsappZapiService::ProviderUnavailableError) + result = service.send_read_message(phone, message_source_id) + + expect(result).to be(false) + expect(Rails.logger).to have_received(:error).with('error message') end end end