fix: Reply time calculation for re-opened conversations (#11787)

This PR fixes the reply time calculation for reopened conversations.
Previously, when a customer sent a message to reopen a resolved
conversation, the reply time metric would be calculated incorrectly
because the `waiting_since` timestamp was not properly set before the
reply event was dispatched. This would create a case where you'd have
reporting events like the following

```
[[33955732, "reply_time", 19.0],
 [33955847, "reply_time", 24.0],
 [33955666, "reply_time", 89.0],
 [33955530, "conversation_bot_handoff", 4.0],
 [33955567, "first_response", 42.0],
 [33955745, "reply_time", 21.0],
 [33955934, "reply_time", 49.0],
 [33955906, "reply_time", 121.0],
 [33987938, "conversation_resolved", 26285.0],
 [35571005, "reply_time", 985492.0]]
```
Note the `reply_time` after `conversation_resolved`

The fix ensures that `waiting_since` is correctly updated when
conversations are reopened, either through incoming messages or manual
status changes, resulting in accurate reply time metrics that measure
only the time from the customer's new message to the agent's response.

## Type of change

Please delete options that are not relevant.

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

## How Has This Been Tested?

The changes have been tested with comprehensive specs that verify:

1. **Reply time calculation after conversation reopening** - Ensures
correct timestamps are used when calculating reply times for reopened
conversations
2. **Waiting since updates on status changes** - Verifies that
`waiting_since` is properly set when conversation status changes from
resolved to open
3. **Test the happy path** - Happy path is tested to ensure the
`reply_time` and `first_response_time` is correctly calculated

Test instructions:
1. Create a conversation with the last message from a customer and
resolve it
2. Have an agent reopen it and reply to it
4. When an agent replies, verify that the agent reply_time event is not
created for this message

To fix any existing data, I've written a small script:
https://gist.github.com/scmmishra/fdf458863f2d971978327bbfd5232d0c

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
Shivam Mishra
2025-06-27 10:48:07 +05:30
committed by GitHub
parent b26862e3d8
commit b7f3f72b9c
4 changed files with 237 additions and 0 deletions

View File

@@ -836,4 +836,117 @@ RSpec.describe Conversation do
expect(message_window_service).to have_received(:can_reply?)
end
end
describe 'reply time calculation flows' do
include ActiveJob::TestHelper
let(:account) { create(:account) }
let(:inbox) { create(:inbox, account: account) }
let(:contact) { create(:contact, account: account) }
let(:agent) { create(:user, account: account, role: :agent) }
let(:conversation) { create(:conversation, account: account, inbox: inbox, contact: contact, assignee: agent, waiting_since: nil) }
let(:conversation_start_time) { 5.hours.ago }
before do
create(:inbox_member, user: agent, inbox: inbox)
# rubocop:disable Rails/SkipsModelValidations
conversation.update_column(:waiting_since, nil)
conversation.update_column(:created_at, conversation_start_time)
# rubocop:enable Rails/SkipsModelValidations
conversation.messages.destroy_all
conversation.reporting_events.destroy_all
conversation.reload
end
def create_customer_message(conversation, created_at: Time.current)
message = nil
perform_enqueued_jobs do
message = create(:message,
message_type: 'incoming',
account: conversation.account,
inbox: conversation.inbox,
conversation: conversation,
sender: conversation.contact,
created_at: created_at)
end
message
end
def create_agent_message(conversation, created_at: Time.current)
message = nil
perform_enqueued_jobs do
message = create(:message,
message_type: 'outgoing',
account: conversation.account,
inbox: conversation.inbox,
conversation: conversation,
sender: conversation.assignee,
created_at: created_at)
end
message
end
it 'correctly tracks waiting_since and creates first response time events' do
create_customer_message(conversation, created_at: conversation_start_time)
conversation.reload
expect(conversation.waiting_since).to be_within(1.second).of(conversation_start_time)
# Agent replies - this should create first response event
agent_reply1_time = 4.hours.ago
create_agent_message(conversation, created_at: agent_reply1_time)
first_response_events = account.reporting_events.where(name: 'first_response', conversation_id: conversation.id)
expect(first_response_events.count).to eq(1)
expect(first_response_events.first.value).to be_within(1.second).of(1.hour)
# the first response should also clear the waiting_since
conversation.reload
expect(conversation.waiting_since).to be_nil
end
it 'does not reset waiting_since if customer sends another message' do
create_customer_message(conversation, created_at: conversation_start_time)
conversation.reload
expect(conversation.waiting_since).to be_within(1.second).of(conversation_start_time)
create_customer_message(conversation, created_at: 3.hours.ago)
conversation.reload
expect(conversation.waiting_since).to be_within(1.second).of(conversation_start_time)
end
it 'records the correct reply_time for subsequent messages' do
create_customer_message(conversation, created_at: conversation_start_time)
create_agent_message(conversation, created_at: 4.hours.ago)
create_customer_message(conversation, created_at: 3.hours.ago)
create_agent_message(conversation, created_at: 2.hours.ago)
reply_events = account.reporting_events.where(name: 'reply_time', conversation_id: conversation.id)
expect(reply_events.count).to eq(1)
expect(reply_events.first.value).to be_within(1.second).of(1.hour)
conversation.reload
expect(conversation.waiting_since).to be_nil
end
it 'records zero reply time if an agent sends a message after resolution' do
create_customer_message(conversation, created_at: conversation_start_time)
create_agent_message(conversation, created_at: 4.hours.ago)
create_customer_message(conversation, created_at: 3.hours.ago)
conversation.toggle_status
expect(conversation.status).to eq('resolved')
conversation.toggle_status
expect(conversation.status).to eq('open')
conversation.reload
expect(conversation.waiting_since).to be_nil
create_agent_message(conversation, created_at: 1.hour.ago)
# update_waiting_since will ensure that no events were created since the waiting_since was nil
# if the event is created it should log zero value, we have handled that in the reporting_event_listener
reply_events = account.reporting_events.where(name: 'reply_time', conversation_id: conversation.id)
expect(reply_events.count).to eq(0)
end
end
end