Repository: fazer-ai/chatwoot
Pull request #52: feat: update message status and last seen timestamps
Author: login: cayoproliveira
Labels: name: enhancement
description: New feature or request
name: approved for merge
Created: 2025-05-24T22:04:16Z | Merged: 2025-05-29T12:55:39Z

# Pull Request Template

## Description

Please include a summary of the change and issue(s) fixed. Also, mention relevant motivation, context, and any dependencies that this change requires.
Fixes # (issue)

## Type of change

Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected)
- [ ] This change requires a documentation update

## How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration.


## Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream modules

<!-- Reviewable:start -->
- - -
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/fazer-ai/chatwoot/52)
<!-- Reviewable:end -->


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Improved tracking of when agents or assignees last viewed a conversation after a message is marked as read.
- **Tests**
  - Enhanced test coverage for message status updates and content edits, ensuring accurate tracking of conversation view timestamps and message content changes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/app/services/whatsapp/incoming_message_baileys_service.rb b/app/services/whatsapp/incoming_message_baileys_service.rb
index 2327e2791bb4e..59d1345761894 100644
--- a/app/services/whatsapp/incoming_message_baileys_service.rb
+++ b/app/services/whatsapp/incoming_message_baileys_service.rb
@@ -285,6 +285,7 @@ def handle_update
 
   def update_status
     status = status_mapper
+    update_last_seen_at if incoming? && status == 'read'
     @message.update!(status: status) if status.present? && status_transition_allowed?(status)
   end
 
@@ -314,6 +315,14 @@ def status_mapper
     end
   end
 
+  def update_last_seen_at
+    conversation = @message.conversation
+    to_update = { agent_last_seen_at: Time.current }
+    to_update[:assignee_last_seen_at] = Time.current if conversation.assignee_id.present?
+
+    conversation.update_columns(to_update) # rubocop:disable Rails/SkipsModelValidations
+  end
+
   def status_transition_allowed?(new_status)
     return false if @message.status == 'read'
     return false if @message.status == 'delivered' && new_status == 'sent'
diff --git a/spec/services/whatsapp/incoming_message_baileys_service_spec.rb b/spec/services/whatsapp/incoming_message_baileys_service_spec.rb
index e53df3eae9961..42b1affd6ec2e 100644
--- a/spec/services/whatsapp/incoming_message_baileys_service_spec.rb
+++ b/spec/services/whatsapp/incoming_message_baileys_service_spec.rb
@@ -567,10 +567,13 @@
     end
 
     context 'when processing messages.update event' do
-      let!(:message) { create(:message, source_id: 'msg_123', status: 'sent') }
-      let(:update_payload) do
-        { key: { id: 'msg_123' }, update: { status: 3 } }
+      let(:conversation) do
+        contact = create(:contact, account: inbox.account)
+        contact_inbox = create(:contact_inbox, inbox: inbox, contact: contact)
+        create(:conversation, inbox: inbox, contact_inbox: contact_inbox, assignee_id: contact.id)
       end
+      let!(:message) { create(:message, inbox: inbox, conversation: conversation, source_id: 'msg_123', status: 'sent') }
+      let(:update_payload) { { key: { id: 'msg_123' }, update: { status: 3 } } }
       let(:params) do
         {
           webhookVerifyToken: webhook_verify_token,
@@ -589,36 +592,34 @@
         end
       end
 
-      context 'when message is found' do
+      context 'when is a status update' do
         it 'updates the message status' do
           described_class.new(inbox: inbox, params: params).perform
 
           expect(message.reload.status).to eq('delivered')
         end
 
-        it 'updates the message content' do
-          update_payload[:update] = { message: { editedMessage: { message: { conversation: 'New message content' } } } }
+        it 'updates conversation agent_last_seen_at and assignee_last_seen_at' do
+          freeze_time
+          update_payload[:key][:fromMe] = false
+          update_payload[:update][:status] = 4
+
+          conversation.update!(agent_last_seen_at: 1.day.ago, assignee_last_seen_at: 1.day.ago)
 
           described_class.new(inbox: inbox, params: params).perform
 
-          original_content = message.content
-          expect(message.reload.content).to eq('New message content')
-          expect(message.is_edited).to be(true)
-          expect(message.previous_content).to eq(original_content)
+          expect(conversation.reload.agent_last_seen_at).to eq(Time.current)
+          expect(conversation.assignee_last_seen_at).to eq(Time.current)
         end
-      end
 
-      context 'when the status transition is not allowed (message already read)' do
-        it 'does not update the status' do
+        it "does not downgrade a 'read' message to delivered" do
           message.update!(status: 'read')
 
           described_class.new(inbox: inbox, params: params).perform
 
           expect(message.reload.status).to eq('read')
         end
-      end
 
-      context 'when update unsupported status' do
         it 'logs warning for unsupported played status' do
           update_payload[:update][:status] = 5
 
@@ -639,6 +640,19 @@
           expect(Rails.logger).to have_received(:warn)
         end
       end
+
+      context 'when is a content update' do
+        it 'updates the message content' do
+          original_content = message.content
+          update_payload[:update] = { message: { editedMessage: { message: { conversation: 'New message content' } } } }
+
+          described_class.new(inbox: inbox, params: params).perform
+
+          expect(message.reload.content).to eq('New message content')
+          expect(message.is_edited).to be(true)
+          expect(message.previous_content).to eq(original_content)
+        end
+      end
     end
   end
 


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR updates the business logic to modify message statuses and update last seen timestamps based on message properties.  
- Adds a condition to update the last seen timestamps when a message is marked as "read" and not sent by the current user.  
- Introduces a helper method (update_last_seen_at) to update conversation timestamps using direct database column updates.




state: COMMENTED
author_association: NONE
submitted_at: 2025-05-24T22:04:44Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>app/services/whatsapp/incoming_message_baileys_service.rb (1)</summary><blockquote>

`288-288`: **LGTM! Consider a minor style improvement.**

The condition logic is correct for tracking last seen timestamps when incoming messages are marked as read. 

Consider using the existing `incoming?` method for consistency:

```diff
-    update_last_seen_at if @raw_message.dig(:key, :fromMe) == false && status == 'read'
+    update_last_seen_at if incoming? && status == 'read'
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 37c5d317187cd6cd2b9a4577bf6d2b58ecffd0e9 and 9773c9b13e7f799e1734546876e0cfeca2a4ec07.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `app/services/whatsapp/incoming_message_baileys_service.rb` (2 hunks)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-24T22:07:23Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>spec/services/whatsapp/incoming_message_baileys_service_spec.rb (1)</summary><blockquote>

`599-612`: **Fix formatting issue and consider test isolation.**

The test correctly verifies the new timestamp update functionality, but there are a couple of issues:

1. There's a formatting issue with extra whitespace in line 601
2. The `freeze_time` call should be properly cleaned up to avoid affecting other tests



Apply this diff to fix the formatting and improve test isolation:

```diff
-        it 'updates last_seen_at on conversation' do
-          update_payload[:key][:fromMe] = false
-          update_payload[:update][ :status] = 4
-
-          conversation = message.conversation
-          conversation.update!(agent_last_seen_at: 1.day.ago, assignee_last_seen_at: 1.day.ago)
-
-          freeze_time
-
-          described_class.new(inbox: inbox, params: params).perform
-
-          expect(conversation.reload.agent_last_seen_at).to eq(Time.current)
-          expect(conversation.assignee_last_seen_at).to eq(Time.current)
-        end
+        it 'updates last_seen_at on conversation' do
+          update_payload[:key][:fromMe] = false
+          update_payload[:update][:status] = 4
+
+          conversation = message.conversation
+          conversation.update!(agent_last_seen_at: 1.day.ago, assignee_last_seen_at: 1.day.ago)
+
+          freeze_time do
+            described_class.new(inbox: inbox, params: params).perform
+
+            expect(conversation.reload.agent_last_seen_at).to eq(Time.current)
+            expect(conversation.assignee_last_seen_at).to eq(Time.current)
+          end
+        end
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 9773c9b13e7f799e1734546876e0cfeca2a4ec07 and 25f199847ffc3029638d514c9c4c2042562cb996.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `spec/services/whatsapp/incoming_message_baileys_service_spec.rb` (2 hunks)

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>spec/services/whatsapp/incoming_message_baileys_service_spec.rb (2)</summary>

`592-592`: **Good test organization improvement.**

The context rename from "when message is found" to "when is a status update" better reflects the specific behavior being tested and improves test readability.

---

`643-654`: **Comprehensive content update test coverage.**

The new test properly validates the content update functionality by:
- Testing that message content is updated with edited content
- Verifying the `is_edited` flag is set to true  
- Ensuring `previous_content` preserves the original content

This provides good coverage for the content update feature.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-24T23:21:28Z
user: login: cayoproliveira
body: +reviewer:@gabrieljablonski   

> *[Reviewable](https://reviewable.io/reviews/fazer-ai/chatwoot/52#-:-OR3_xuxBjIW6_ZCej_V:bf1mldy)* status: 0 of 2 files reviewed, 1 unresolved discussion (waiting on @gabrieljablonski)



<!-- Sent from Reviewable.io -->

state: COMMENTED
author_association: NONE
submitted_at: 2025-05-25T01:16:55Z
user: login: gabrieljablonski
body: mostly minor comments.

-(review needed) +(questions/changes requested) 

> Reviewed 1 of 1 files at r1, 2 of 2 files at r3, all commit messages.
> *[Reviewable](https://reviewable.io/reviews/fazer-ai/chatwoot/52#-:-ORDeu0X15WHL-DgFCht:blh1zzo)* status: all files reviewed, 6 unresolved discussions (waiting on @CayoPOliveira)

___
*[`spec/services/whatsapp/incoming_message_baileys_service_spec.rb` line 602 at r3](https://reviewable.io/reviews/fazer-ai/chatwoot/52#-ORDenmq573W19vvo2wj:-ORDenmq573W19vvo2wk:b2nhk2c) ([raw file](https://github.com/fazer-ai/chatwoot/blob/84e95ec0ab8f443be8ed0208eb3ea65c26646f55/spec/services/whatsapp/incoming_message_baileys_service_spec.rb#L602)):*
> ```Ruby
>         end
> 
>         it 'updates last_seen_at on conversation' do
> ```

NIT



_Suggestion:_
```Ruby
        it 'updates conversation agent_last_seen_at and assignee_last_seen_at' do
```
___
*[`spec/services/whatsapp/incoming_message_baileys_service_spec.rb` line 604 at r3](https://reviewable.io/reviews/fazer-ai/chatwoot/52#-ORDdy3h9Z1SqBfKrd0i:-ORDdy3h9Z1SqBfKrd0j:bhjy63s) ([raw file](https://github.com/fazer-ai/chatwoot/blob/84e95ec0ab8f443be8ed0208eb3ea65c26646f55/spec/services/whatsapp/incoming_message_baileys_service_spec.rb#L604)):*
> ```Ruby
>         it 'updates last_seen_at on conversation' do
>           update_payload[:key][:fromMe] = false
>           update_payload[:update][ :status] = 4
> ```

NIT

random dangling whitespace



_Suggestion:_
```Ruby
          update_payload[:update][:status] = 4
```
___
*[`spec/services/whatsapp/incoming_message_baileys_service_spec.rb` line 610 at r3](https://reviewable.io/reviews/fazer-ai/chatwoot/52#-ORDe7IR-CIe9MjJcMLo:-ORDe7IS9VghAsIiiZA6:b2nhk2c) ([raw file](https://github.com/fazer-ai/chatwoot/blob/84e95ec0ab8f443be8ed0208eb3ea65c26646f55/spec/services/whatsapp/incoming_message_baileys_service_spec.rb#L610)):*
> ```Ruby
>           freeze_time
> 
>           described_class.new(inbox: inbox, params: params).perform
> ```

NIT



_Suggestion:_
```Ruby
          freeze_time
          update_payload[:key][:fromMe] = false
          update_payload[:update][ :status] = 4
          conversation.update!(agent_last_seen_at: 1.day.ago, assignee_last_seen_at: 1.day.ago)

          described_class.new(inbox: inbox, params: params).perform
```
___
*[`spec/services/whatsapp/incoming_message_baileys_service_spec.rb` line 655 at r3](https://reviewable.io/reviews/fazer-ai/chatwoot/52#-ORDeVFFAjEfwGH-e9Tv:-ORDeVFFAjEfwGH-e9Tw:b-p3mwub) ([raw file](https://github.com/fazer-ai/chatwoot/blob/84e95ec0ab8f443be8ed0208eb3ea65c26646f55/spec/services/whatsapp/incoming_message_baileys_service_spec.rb#L655)):*
> ```Ruby
>           expect(message.is_edited).to be(true)
>           expect(message.previous_content).to eq(original_content)
>         end
> ```

NIT

so it's easier to follow the logic



_Suggestion:_
```Ruby
        it 'updates the message content' do
          original_content = message.content
          update_payload[:update] = { message: { editedMessage: { message: { conversation: 'New message content' } } } }

          described_class.new(inbox: inbox, params: params).perform

          expect(message.reload.content).to eq('New message content')
          expect(message.is_edited).to be(true)
          expect(message.previous_content).to eq(original_content)
        end
```
___
*[`app/services/whatsapp/incoming_message_baileys_service.rb` line 288 at r3](https://reviewable.io/reviews/fazer-ai/chatwoot/52#-ORDci5U9wZPscptLzuP:-ORDci5U9wZPscptLzuQ:b-6pu52m) ([raw file](https://github.com/fazer-ai/chatwoot/blob/84e95ec0ab8f443be8ed0208eb3ea65c26646f55/app/services/whatsapp/incoming_message_baileys_service.rb#L288)):*
> ```Ruby
>   def update_status
>     status = status_mapper
>     update_last_seen_at if @raw_message.dig(:key, :fromMe) == false && status == 'read'
> ```

NIT

more readable IMO



_Suggestion:_
```Ruby
    update_last_seen_at if status == 'read' && incoming?
```
___
*[`app/services/whatsapp/incoming_message_baileys_service.rb` line 324 at r3](https://reviewable.io/reviews/fazer-ai/chatwoot/52#-ORDddDz-w8Mrwi3S0xs:-ORDddDz-w8Mrwi3S0xt:bg2gbid) ([raw file](https://github.com/fazer-ai/chatwoot/blob/84e95ec0ab8f443be8ed0208eb3ea65c26646f55/app/services/whatsapp/incoming_message_baileys_service.rb#L324)):*
> ```Ruby
>     conversation.update_column(:agent_last_seen_at, Time.current)
>     conversation.update_column(:assignee_last_seen_at, Time.current) if conversation.assignee_id.present?
>     # rubocop:enable Rails/SkipsModelValidations
> ```

Avoid double-updating when possible.



_Suggestion:_
```Ruby
    to_update = { agent_last_seen_at: Time.current }
    to_update[:assignee_last_seen_at] = Time.current if conversation.assignee_id.present?
    conversation.update_columns(to_update) # rubocop:disable Rails/SkipsModelValidations
```


<!-- Sent from Reviewable.io -->

state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-26T22:17:44Z
user: login: cayoproliveira
body: -(questions/changes requested) +(review needed) 

> Reviewed 1 of 1 files at r2, 2 of 2 files at r3, all commit messages.
> *[Reviewable](https://reviewable.io/reviews/fazer-ai/chatwoot/52#-:-ORM1JOt9YTNogzMkhg4:b-1l1pr)* status: all files reviewed, 1 unresolved discussion (waiting on @gabrieljablonski)

___
*[`app/services/whatsapp/incoming_message_baileys_service.rb` line 324 at r3](https://reviewable.io/reviews/fazer-ai/chatwoot/52#-ORDddDz-w8Mrwi3S0xs:-ORM0BVB5qsgDCn7vduD:b-896fix) ([raw file](https://github.com/fazer-ai/chatwoot/blob/84e95ec0ab8f443be8ed0208eb3ea65c26646f55/app/services/whatsapp/incoming_message_baileys_service.rb#L324)):*
<details><summary><i>Previously, gabrieljablonski (Gabriel Jablonski) wrote…</i></summary><blockquote>

Avoid double-updating when possible.
</blockquote></details>

Done.


<!-- Sent from Reviewable.io -->

state: COMMENTED
author_association: NONE
submitted_at: 2025-05-28T13:18:21Z
user: login: gabrieljablonski
body: -(review needed) +(approved for merge) 

> Reviewed 2 of 2 files at r4, all commit messages.
> *[Reviewable](https://reviewable.io/reviews/fazer-ai/chatwoot/52#-:-ORNIkKUEGl8_XRPBJ89:b-y4tp9c)* status: <span>:shipit:</span> complete! all files reviewed, all discussions resolved (waiting on @CayoPOliveira)



<!-- Sent from Reviewable.io -->

state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-28T19:12:25Z

Review comments:
diff_hunk: @@ -314,6 +315,13 @@ def status_mapper
     end
   end
 
+  def update_last_seen_at
+    # rubocop:disable Rails/SkipsModelValidations
+    @message.conversation.update_column(:agent_last_seen_at, DateTime.now.utc)
+    @message.conversation.update_column(:assignee_last_seen_at, DateTime.now.utc)
path: app/services/whatsapp/incoming_message_baileys_service.rb
user: login: Copilot
body: [nitpick] Consider merging the two update_column calls into a single update_columns call to update both agent_last_seen_at and assignee_last_seen_at in one database round trip, e.g., @message.conversation.update_columns(agent_last_seen_at: DateTime.now.utc, assignee_last_seen_at: DateTime.now.utc).
```suggestion
    @message.conversation.update_columns(agent_last_seen_at: DateTime.now.utc, assignee_last_seen_at: DateTime.now.utc)
```
created_at: 2025-05-24T22:04:44Z
author_association: NONE
diff_hunk: @@ -314,6 +315,13 @@ def status_mapper
     end
   end
 
+  def update_last_seen_at
+    # rubocop:disable Rails/SkipsModelValidations
+    @message.conversation.update_column(:agent_last_seen_at, DateTime.now.utc)
+    @message.conversation.update_column(:assignee_last_seen_at, DateTime.now.utc)
path: app/services/whatsapp/incoming_message_baileys_service.rb
user: login: Copilot
body: [nitpick] Consider using Time.current instead of DateTime.now.utc to leverage Rails time zone handling and for consistency with Rails conventions.
```suggestion
    @message.conversation.update_column(:agent_last_seen_at, Time.current)
    @message.conversation.update_column(:assignee_last_seen_at, Time.current)
```
created_at: 2025-05-24T22:04:44Z
author_association: NONE
diff_hunk: @@ -314,6 +315,13 @@ def status_mapper
     end
   end
 
+  def update_last_seen_at
+    # rubocop:disable Rails/SkipsModelValidations
+    @message.conversation.update_column(:agent_last_seen_at, DateTime.now.utc)
+    @message.conversation.update_column(:assignee_last_seen_at, DateTime.now.utc)
+    # rubocop:enable Rails/SkipsModelValidations
+  end
path: app/services/whatsapp/incoming_message_baileys_service.rb
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Review timestamp update logic and consider improvements.**

Several considerations for this implementation:

1. **Semantic correctness**: Setting both `agent_last_seen_at` and `assignee_last_seen_at` to the same timestamp may not be semantically correct. These typically represent different entities - the assigned agent vs any agent who viewed the conversation.

2. **Error handling**: Consider adding a safety check for the conversation's existence.

3. **Method naming**: The method name could be more descriptive since it updates both timestamps.



Consider this improved implementation:

```diff
  def update_last_seen_at
+    return unless @message&.conversation
+    
    # rubocop:disable Rails/SkipsModelValidations
-    @message.conversation.update_column(:agent_last_seen_at, DateTime.now.utc)
-    @message.conversation.update_column(:assignee_last_seen_at, DateTime.now.utc)
+    timestamp = Time.current.utc
+    conversation = @message.conversation
+    
+    # Update agent_last_seen_at for any agent activity
+    conversation.update_column(:agent_last_seen_at, timestamp)
+    
+    # Update assignee_last_seen_at only if conversation has an assignee
+    conversation.update_column(:assignee_last_seen_at, timestamp) if conversation.assignee_id.present?
    # rubocop:enable Rails/SkipsModelValidations
  end
```

**Note**: Using `update_column` bypasses model validations and callbacks, which may be intentional for performance. Ensure this aligns with your application's requirements.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In app/services/whatsapp/incoming_message_baileys_service.rb around lines 318 to
323, the method update_last_seen_at updates both agent_last_seen_at and
assignee_last_seen_at to the same timestamp without distinguishing between the
assigned agent and the viewing agent, which may be semantically incorrect.
Refactor the method to separately update these timestamps based on the relevant
entity, add a check to ensure the conversation exists before updating to avoid
errors, and rename the method to better reflect that it updates multiple last
seen timestamps. Retain the use of update_column only if skipping validations
and callbacks is intentional for performance reasons.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-24T22:07:21Z
author_association: NONE
diff_hunk: @@ -589,36 +589,36 @@
         end
       end
 
-      context 'when message is found' do
+      context 'when is a status update' do
         it 'updates the message status' do
           described_class.new(inbox: inbox, params: params).perform
 
           expect(message.reload.status).to eq('delivered')
         end
 
-        it 'updates the message content' do
-          update_payload[:update] = { message: { editedMessage: { message: { conversation: 'New message content' } } } }
+        it 'updates last_seen_at on conversation' do
+          update_payload[:key][:fromMe] = false
+          update_payload[:update][ :status] = 4
+
+          conversation = message.conversation
+          conversation.update!(agent_last_seen_at: 1.day.ago, assignee_last_seen_at: 1.day.ago)
+
+          freeze_time
 
           described_class.new(inbox: inbox, params: params).perform
 
-          original_content = message.content
-          expect(message.reload.content).to eq('New message content')
-          expect(message.is_edited).to be(true)
-          expect(message.previous_content).to eq(original_content)
+          expect(conversation.reload.agent_last_seen_at).to eq(Time.current)
+          expect(conversation.assignee_last_seen_at).to eq(Time.current)
         end
-      end
 
-      context 'when the status transition is not allowed (message already read)' do
-        it 'does not update the status' do
+        it 'does not update the status when the transition is not allowed' do
           message.update!(status: 'read')
 
           described_class.new(inbox: inbox, params: params).perform
 
           expect(message.reload.status).to eq('read')
         end
path: spec/services/whatsapp/incoming_message_baileys_service_spec.rb
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Verify the test description accuracy.**

The test description mentions "when the transition is not allowed" but the test sets the message status to 'read' and expects it to remain 'read' when trying to update to 'delivered' (status 3). This seems to test that already-read messages don't get downgraded to delivered status.



Please verify that this test behavior aligns with the business logic in the actual service implementation:

---

🏁 Script executed:

```shell
#!/bin/bash
# Description: Check the status transition logic in the service implementation
# Expected: Find status transition validation logic

ast-grep --pattern $'def update_status
  $$$
end'

rg -A 10 -B 5 "status.*transition|transition.*status" app/services/whatsapp/
```

Length of output: 3629

---



</details>

**Clarify test description to specify a read message can’t be downgraded**

The `status_transition_allowed?` method explicitly returns false when the current status is `'read'`, so this test correctly verifies that a read message won’t be updated to delivered. To make the intent clearer, update the description:

```diff
- it 'does not update the status when the transition is not allowed' do
+ it "does not downgrade a 'read' message to delivered" do
```

• File: spec/services/whatsapp/incoming_message_baileys_service_spec.rb (Lines 614–620)  
• Change only the `it` description; the rest of the test remains valid. 

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
        it "does not downgrade a 'read' message to delivered" do
          message.update!(status: 'read')

          described_class.new(inbox: inbox, params: params).perform

          expect(message.reload.status).to eq('read')
        end
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In spec/services/whatsapp/incoming_message_baileys_service_spec.rb around lines
614 to 620, update the test description to clearly state that the test verifies
a read message cannot be downgraded to delivered status. Change the 'it' block
description to reflect this specific behavior while keeping the test code
unchanged.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 84e95ec
created_at: 2025-05-24T23:21:27Z
author_association: NONE
diff_hunk: @@ -314,6 +315,13 @@ def status_mapper
     end
   end
 
+  def update_last_seen_at
+    # rubocop:disable Rails/SkipsModelValidations
+    @message.conversation.update_column(:agent_last_seen_at, DateTime.now.utc)
+    @message.conversation.update_column(:assignee_last_seen_at, DateTime.now.utc)
path: app/services/whatsapp/incoming_message_baileys_service.rb
user: login: cayoproliveira
body: @gabrieljablonski This is the way the conversation controller performs this update, but I believe it’s important to follow Copilot’s suggestion. If we go down that path, could there be any potential issues?
<!-- Reviewable comment -OR3qdqo2_9bZgZdooZp:bv91rhq -->
<!-- Sent from Reviewable.io -->

created_at: 2025-05-25T01:16:54Z
author_association: NONE
diff_hunk: @@ -314,6 +315,13 @@ def status_mapper
     end
   end
 
+  def update_last_seen_at
+    # rubocop:disable Rails/SkipsModelValidations
+    @message.conversation.update_column(:agent_last_seen_at, DateTime.now.utc)
+    @message.conversation.update_column(:assignee_last_seen_at, DateTime.now.utc)
path: app/services/whatsapp/incoming_message_baileys_service.rb
user: login: gabrieljablonski
body: `Time.current` is the way to go.
<!-- Reviewable comment -ORDcZ-38rcQ3ihA8JUs:b-ug8foc -->
<!-- Sent from Reviewable.io -->

created_at: 2025-05-26T22:17:44Z
author_association: MEMBER

PR comments:
user: login: gabrieljablonski
created_at: 2025-05-24T22:04:22Z
body: Task linked: [CU-86a8ufv2f Atualizar agent_last_seen quando receber evento "messages.update"](https://app.clickup.com/t/86a8ufv2f)
author_association: MEMBER
user: login: coderabbitai[bot]
created_at: 2025-05-24T22:04:23Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

"""
## Walkthrough

A new method, `update_last_seen_at`, was added to the WhatsApp IncomingMessageBaileysService. The `update_status` method now calls this new method to update agent and assignee last seen timestamps when an incoming message is marked as 'read', directly updating the conversation record in the database.

## Changes

| File                                                | Change Summary                                                                                                               |
|-----------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------|
| app/services/whatsapp/incoming_message_baileys_service.rb | Added `update_last_seen_at` method; enhanced `update_status` to update last seen timestamps for agents/assignees on 'read' status for incoming messages. |
| spec/services/whatsapp/incoming_message_baileys_service_spec.rb | Reorganized and expanded tests: separated status update and content update contexts; added tests for updating conversation last seen timestamps on status update; clarified test descriptions. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant WhatsApp Service
    participant Conversation DB

    User->>WhatsApp Service: Sends/receives message
    WhatsApp Service->>WhatsApp Service: update_status called
    alt Message is incoming and status is 'read'
        WhatsApp Service->>Conversation DB: update_last_seen_at (update agent_last_seen_at, assignee_last_seen_at)
    end
```

## Possibly related PRs

- fazer-ai/chatwoot#21: Extends `update_status` in `Whatsapp::IncomingMessageBaileysService` by adding `update_last_seen_at` call to update conversation timestamps on message read status.
- fazer-ai/chatwoot#20: Adds comprehensive message update processing including status and content updates in `Whatsapp::IncomingMessageBaileysService`.
- fazer-ai/chatwoot#43: Implements dispatching read events and sending read receipts, related to handling message read status updates in WhatsApp integration.

## Suggested labels

`approved for merge`

## Suggested reviewers

- gabrieljablonski

## Poem

> A message arrives, a rabbit takes note,  
> With nimble paws, timestamps it wrote.  
> Now agents and assignees, both in the scene,  
> Know exactly when their last "read" has been.  
> Hopping through code, with updates so keen! 🐇✨
"""

<!-- walkthrough_end -->
<!-- This is an auto-generated comment: all tool run failures by coderabbit.ai -->

> [!WARNING]
> There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.
> 
> <details>
> <summary>🔧 RuboCop (1.75.5)</summary>
> 
> <details>
> <summary>spec/services/whatsapp/incoming_message_baileys_service_spec.rb</summary>
> 
> rubocop-performance extension supports plugin, specify `plugins: rubocop-performance` instead of `require: rubocop-performance` in /.rubocop.yml.
> For more information, see https://docs.rubocop.org/rubocop/plugin_migration_guide.html.
> rubocop-rails extension supports plugin, specify `plugins: rubocop-rails` instead of `require: rubocop-rails` in /.rubocop.yml.
> For more information, see https://docs.rubocop.org/rubocop/plugin_migration_guide.html.
> rubocop-rspec extension supports plugin, specify `plugins: rubocop-rspec` instead of `require: rubocop-rspec` in /.rubocop.yml.
> For more information, see https://docs.rubocop.org/rubocop/plugin_migration_guide.html.
> Error: `RSpec/FactoryBot/*` has been extracted to the `rubocop-factory_bot` gem.
> (obsolete configuration found in .rubocop.yml, please update it)
> 
> </details>
> 
> <details>
> <summary>app/services/whatsapp/incoming_message_baileys_service.rb</summary>
> 
> rubocop-performance extension supports plugin, specify `plugins: rubocop-performance` instead of `require: rubocop-performance` in /.rubocop.yml.
> For more information, see https://docs.rubocop.org/rubocop/plugin_migration_guide.html.
> rubocop-rails extension supports plugin, specify `plugins: rubocop-rails` instead of `require: rubocop-rails` in /.rubocop.yml.
> For more information, see https://docs.rubocop.org/rubocop/plugin_migration_guide.html.
> rubocop-rspec extension supports plugin, specify `plugins: rubocop-rspec` instead of `require: rubocop-rspec` in /.rubocop.yml.
> For more information, see https://docs.rubocop.org/rubocop/plugin_migration_guide.html.
> Error: `RSpec/FactoryBot/*` has been extracted to the `rubocop-factory_bot` gem.
> (obsolete configuration found in .rubocop.yml, please update it)
> 
> </details>
> 
> </details>

<!-- end of auto-generated comment: all tool run failures by coderabbit.ai -->


---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 84e95ec0ab8f443be8ed0208eb3ea65c26646f55 and e5c020c5d3b2b742ccbeca4703c6855a9233a652.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `app/services/whatsapp/incoming_message_baileys_service.rb` (2 hunks)
* `spec/services/whatsapp/incoming_message_baileys_service_spec.rb` (3 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* app/services/whatsapp/incoming_message_baileys_service.rb
* spec/services/whatsapp/incoming_message_baileys_service_spec.rb

</details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=fazer-ai/chatwoot&utm_content=52):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE