From 7cd2ea1258dfdf7601bce44c3da9ea3847964e63 Mon Sep 17 00:00:00 2001 From: Rodribm10 Date: Sun, 26 Apr 2026 11:35:08 -0300 Subject: [PATCH] =?UTF-8?q?fix(reports):=20tratar=20resposta=20humana=20vi?= =?UTF-8?q?a=20WhatsApp=20como=20intera=C3=A7=C3=A3o=20humana?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug do BotReports descoberto pelo Rodrigo: - A regra de "conversation_bot_resolved" só desqualificava conversas com outgoing.sender_type='User' (atendente respondendo pelo Chatwoot UI) - Mas mensagem outgoing vinda do webhook WhatsApp com IsFromMe=true (atendente respondeu direto pelo celular do hotel) é gravada com sender=nil - Resultado: a Jasmine ganhava crédito mesmo quando humano respondia fora do Chatwoot. Taxa de resolução pelo bot inflada. Fix prospectivo: - ReportingEventListener#create_bot_resolved_event agora desqualifica via human_outgoing_messages? (sender_type='User' OU sender_type IS NULL) - Captain::Assistant (a Jasmine) usa sender_type='Captain::Assistant' e segue fora do filtro, como antes - Spec novo cobrindo o caso WhatsApp echo Retroativo: - lib/tasks/rebuild_bot_resolved.rake — task idempotente que purga reporting_events de conversation_bot_resolved gerados sob a regra antiga. - DRY-RUN por padrão, APPLY=true pra deletar, ACCOUNT_ID pra restringir, SNAPSHOT_PATH pra trilha de auditoria Co-Authored-By: Claude Opus 4.7 (1M context) --- app/listeners/reporting_event_listener.rb | 15 +++- lib/tasks/rebuild_bot_resolved.rake | 79 +++++++++++++++++++ .../reporting_event_listener_spec.rb | 8 ++ 3 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 lib/tasks/rebuild_bot_resolved.rake diff --git a/app/listeners/reporting_event_listener.rb b/app/listeners/reporting_event_listener.rb index 9f683a97f..bd27a8aba 100644 --- a/app/listeners/reporting_event_listener.rb +++ b/app/listeners/reporting_event_listener.rb @@ -133,11 +133,22 @@ class ReportingEventListener < BaseListener def create_bot_resolved_event(conversation, reporting_event) return unless conversation.inbox.active_bot? - # We don't want to create a bot_resolved event if there is user interaction on the conversation - return if conversation.messages.exists?(message_type: :outgoing, sender_type: 'User') + # We don't want to create a bot_resolved event if there is human interaction on the conversation. + # Human interaction = outgoing message either from a User (replied via Chatwoot UI) OR from a + # nil sender (replied directly via the connected WhatsApp app — webhook echo with IsFromMe=true, + # see app/services/whatsapp/incoming_message_wuzapi_service.rb#build_message). + # The bot itself uses sender_type 'Captain::Assistant' (or 'AgentBot'), so it stays excluded from this filter. + return if human_outgoing_messages?(conversation) bot_resolved_event = reporting_event.dup bot_resolved_event.name = 'conversation_bot_resolved' bot_resolved_event.save! end + + def human_outgoing_messages?(conversation) + conversation.messages + .where(message_type: :outgoing) + .where('sender_type = ? OR sender_type IS NULL', 'User') + .any? + end end diff --git a/lib/tasks/rebuild_bot_resolved.rake b/lib/tasks/rebuild_bot_resolved.rake new file mode 100644 index 000000000..1ccf745e8 --- /dev/null +++ b/lib/tasks/rebuild_bot_resolved.rake @@ -0,0 +1,79 @@ +# Rebuild conversation_bot_resolved retroactively +# +# Removes 'conversation_bot_resolved' reporting events that were created with the +# old (buggy) classification rule, where outgoing messages with sender_type=NULL +# (= human replied directly via the connected WhatsApp app, not via Chatwoot UI) +# were not considered as human interaction. The bot was incorrectly credited. +# +# What this task does: +# 1. Finds all conversation_bot_resolved events whose conversations contain at +# least one outgoing message with sender_id IS NULL (= replied externally). +# 2. (Optional) Saves the count + ids to a snapshot file before deleting. +# 3. Deletes those events. The classification will be re-evaluated correctly +# for any *future* resolution because the listener was fixed. +# +# Idempotent: re-running on a clean dataset is a no-op. +# +# Usage: +# # Dry-run (default) — counts only, no delete +# bundle exec rake reports:rebuild_bot_resolved +# +# # Actually delete +# APPLY=true bundle exec rake reports:rebuild_bot_resolved +# +# # Restrict to a specific account (recommended on multi-tenant prod) +# APPLY=true ACCOUNT_ID=1 bundle exec rake reports:rebuild_bot_resolved +# +# # Snapshot ids to a file before deleting (audit trail) +# APPLY=true SNAPSHOT_PATH=/tmp/bot_resolved_purge.csv bundle exec rake reports:rebuild_bot_resolved + +namespace :reports do + desc 'Remove buggy conversation_bot_resolved events created before the sender-NIL fix' + task rebuild_bot_resolved: :environment do + apply = ENV['APPLY'].to_s.casecmp('true').zero? + account_id = ENV['ACCOUNT_ID'].presence&.to_i + snapshot_path = ENV['SNAPSHOT_PATH'].presence + + base_scope = ReportingEvent.where(name: 'conversation_bot_resolved') + base_scope = base_scope.where(account_id: account_id) if account_id + + affected_conversation_ids = Message + .where(message_type: :outgoing, sender_id: nil) + .distinct + .pluck(:conversation_id) + + bad_events = base_scope.where(conversation_id: affected_conversation_ids) + + total_events = base_scope.count + purge_count = bad_events.count + + puts '=== Rebuild conversation_bot_resolved ===' + puts "Account filter: #{account_id || 'ALL'}" + puts "Total bot_resolved events in scope: #{total_events}" + puts "Events to purge (had outgoing with sender_id NULL): #{purge_count}" + puts "Mode: #{apply ? 'APPLY (will delete)' : 'DRY-RUN (no changes)'}" + + if purge_count.zero? + puts 'Nothing to do — already clean.' + next + end + + if snapshot_path + File.open(snapshot_path, 'w') do |f| + f.puts 'reporting_event_id,conversation_id,account_id,inbox_id,created_at' + bad_events.find_each(batch_size: 500) do |re| + f.puts [re.id, re.conversation_id, re.account_id, re.inbox_id, re.created_at.iso8601].join(',') + end + end + puts "Snapshot written to #{snapshot_path}" + end + + unless apply + puts 'DRY-RUN finished. Re-run with APPLY=true to delete.' + next + end + + deleted = bad_events.delete_all + puts "Deleted #{deleted} reporting_events. Bot resolution metrics will reflect the new count immediately." + end +end diff --git a/spec/listeners/reporting_event_listener_spec.rb b/spec/listeners/reporting_event_listener_spec.rb index 3f9276947..821cb2c1e 100644 --- a/spec/listeners/reporting_event_listener_spec.rb +++ b/spec/listeners/reporting_event_listener_spec.rb @@ -62,6 +62,14 @@ describe ReportingEventListener do listener.conversation_resolved(event) expect(account.reporting_events.where(name: 'conversation_bot_resolved').count).to be 0 end + + it 'does not create a conversation_bot_resolved event if a human replied directly via the connected WhatsApp app' do + # outgoing message with sender_type=NULL = WhatsApp echo (IsFromMe=true) — human replied outside Chatwoot UI + create(:message, :bot_message, account: account, inbox: agent_bot_inbox, conversation: bot_resolved_conversation) + event = Events::Base.new('conversation.resolved', Time.zone.now, conversation: bot_resolved_conversation) + listener.conversation_resolved(event) + expect(account.reporting_events.where(name: 'conversation_bot_resolved').count).to be 0 + end end end