Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
190a09c
feat: add is_active index and scopes to ConversationGroupMember model
CayoPOliveira Feb 11, 2026
39ed63d
feat: implement GroupConversationHandler for managing group conversat…
CayoPOliveira Feb 11, 2026
5c73e1f
feat: add group_type attribute to contact creation
CayoPOliveira Feb 12, 2026
a1041aa
fix: update WHATSAPP_CHANNEL_REGEX to allow up to 20 digits to handle…
CayoPOliveira Feb 12, 2026
dc5f8dd
feat: handle group JID format in remote_jid method
CayoPOliveira Feb 12, 2026
da82216
chore: update group contact info when finding or creating group contact
CayoPOliveira Feb 12, 2026
2c6319e
chore: refactor and implement contact message handling and message cr…
CayoPOliveira Feb 12, 2026
d150a07
feat: implement group message handling and metadata fetching in Whats…
CayoPOliveira Feb 12, 2026
b45b0cb
chore: add spec for group type handling in contact creation for indiv…
CayoPOliveira Feb 12, 2026
3015d56
chore: add specs for test scopes in conversation group members
CayoPOliveira Feb 12, 2026
7fa221b
chore: update documentation for sender phone extraction in group conv…
CayoPOliveira Feb 12, 2026
85b91d8
chore: move GroupConversationHandler concern to correct dir
CayoPOliveira Feb 12, 2026
fea412f
chore: implement specs for recipient_id handling to individual and gr…
CayoPOliveira Feb 12, 2026
9401b0d
chore: add group message handling specs for incoming messages
CayoPOliveira Feb 12, 2026
d703338
chore: enhance group message handling to prevent race conditions
CayoPOliveira Feb 13, 2026
00863c0
chore: add group_metadata method to with error handling
CayoPOliveira Feb 13, 2026
889f559
chore: add test for sending messages to group recipients in WhatsappB…
CayoPOliveira Feb 13, 2026
f0b3f59
chore: raise error for unsuccessful response in group_metadata method
CayoPOliveira Feb 13, 2026
d35e629
chore: adds tests for group metadata retrieval and error handling
CayoPOliveira Feb 13, 2026
f923143
chore: refactor build_sender_contact_attributes to avoid double call …
CayoPOliveira Feb 13, 2026
dfd2b12
chore: update error handling for attachment download failure in messa…
CayoPOliveira Feb 13, 2026
376a459
chore: optimize update_contact_info method to use compact hash for up…
CayoPOliveira Feb 13, 2026
ac1f946
chore: simplify find_or_create_sender_contact method return values
CayoPOliveira Feb 19, 2026
b611f0c
chore: rename group and individual contact message handlers
CayoPOliveira Feb 19, 2026
afc4c47
chore: remove pointless comments from group contact message handler m…
CayoPOliveira Feb 19, 2026
a6feebd
chore: refine sender JID extraction logic to remove unnecessary checks
CayoPOliveira Feb 19, 2026
e117933
chore: remove phone number in spec for group contact attributes
CayoPOliveira Feb 19, 2026
c7648b8
Merge branch 'Cayo-Oliveira/CU-86af00yvg/2-Backend-Models-Main-PR' in…
CayoPOliveira Feb 19, 2026
bd8eb28
chore: implement sync_group route
CayoPOliveira Feb 20, 2026
1204541
chore: implement get group_members route
CayoPOliveira Feb 20, 2026
bab1e5c
fix: sync_group participants creation handling
CayoPOliveira Feb 20, 2026
a16d384
chore: update contact avatar handling in group message processing
CayoPOliveira Feb 23, 2026
cc98ef3
chore: move sync_group functionality for conversation model
CayoPOliveira Feb 23, 2026
d06285c
feat: add sync_group action to ConversationsController and route
CayoPOliveira Feb 23, 2026
eb6d7db
fix: set contact name to phone in group message processing
CayoPOliveira Feb 23, 2026
d0c5ffb
chore: refine group member retrieval logic in sync_group service and …
CayoPOliveira Feb 23, 2026
d3f9379
feat: implement group participants update handling
CayoPOliveira Feb 24, 2026
cbc83d2
feat: implement group updates handling and localization for group act…
CayoPOliveira Feb 24, 2026
014c077
chore: add handling for group membership requests and icon changes
CayoPOliveira Feb 24, 2026
007656c
chore: add authorization for sync_group action in ContactsController
CayoPOliveira Feb 24, 2026
678b52a
chore: add sync_group endpoint specs for contact management
CayoPOliveira Feb 24, 2026
67f88d0
chore: add authorization for sync_group action in ConversationsContro…
CayoPOliveira Feb 24, 2026
5e253b8
chore: add specs for sync_group endpoint in ConversationsController
CayoPOliveira Feb 24, 2026
86178f1
chore: refactor index action in GroupMembersController for improved c…
CayoPOliveira Feb 24, 2026
5c5b11e
chore: add request specs for group_members endpoint in ContactsContro…
CayoPOliveira Feb 24, 2026
74ba3aa
chore: add specs for sync_group method in Conversation model
CayoPOliveira Feb 24, 2026
23642e9
chore: add specs for sync_group method in Channel::Whatsapp model
CayoPOliveira Feb 24, 2026
5ed3683
chore: remove comments in find_or_create_group_conversation method
CayoPOliveira Feb 24, 2026
ae239f1
chore: add specs for Contacts::SyncGroupService to validate group con…
CayoPOliveira Feb 24, 2026
057f88c
chore: add specs for Whatsapp::BaileysHandlers::GroupsUpdate to valid…
CayoPOliveira Feb 25, 2026
4b020b5
chore: add specs for Whatsapp::BaileysHandlers::GroupParticipantsUpda…
CayoPOliveira Feb 25, 2026
504e1e3
chore: add fallback for identifier when contact has no phone_number i…
CayoPOliveira Feb 25, 2026
953f187
chore: add specs for group membership request and icon change handlin…
CayoPOliveira Feb 25, 2026
097bcca
chore: add specs for sync_group method to handle group metadata and p…
CayoPOliveira Feb 25, 2026
b06121b
chore: update sync_group method to retrieve group members and adjust …
CayoPOliveira Feb 25, 2026
f5067fd
chore: update conversation query to filter by group type in GroupMemb…
CayoPOliveira Feb 25, 2026
778b72f
chore: update conversation creation in group_members_controller_spec …
CayoPOliveira Feb 25, 2026
2d8ccfc
chore: update find_or_create_group_conversation to include pending co…
CayoPOliveira Feb 25, 2026
c78fdc9
chore: refactor sync_group method and enhance specs for group convers…
CayoPOliveira Feb 25, 2026
4963dd9
feat: add GroupEventHelper module for managing group activities and c…
CayoPOliveira Feb 25, 2026
2bcf1a3
chore: refactor group contact inbox and conversation creation methods…
CayoPOliveira Feb 25, 2026
16b50ce
chore: remove unnecessary check for blank participant contacts in syn…
CayoPOliveira Feb 25, 2026
cb72db7
feat: implement message receipt update handling for WhatsApp integration
CayoPOliveira Feb 26, 2026
26a8196
chore: resolve rubocop rule for update_last_seen_at method
CayoPOliveira Feb 26, 2026
fa34ff3
chore: update swagger with endpoints for syncing group information an…
CayoPOliveira Feb 26, 2026
049239d
chore: integrate Contacts::SyncGroupService in group members controll…
CayoPOliveira Feb 26, 2026
e8870a6
chore: include participant information in reaction and quoted message…
CayoPOliveira Feb 26, 2026
7613493
chore: enhance whatsapp_baileys_service with participant handling for…
CayoPOliveira Feb 27, 2026
efd953e
feat: add skill for writing RSpec tests in the project
CayoPOliveira Feb 27, 2026
44d772c
fix: update recipient_id logic to directly use contact identifier for…
CayoPOliveira Feb 27, 2026
b17e9ec
chore: implement group stub message handling for membership requests …
CayoPOliveira Feb 27, 2026
b9494e8
fix: update whatsapp inbox source_id validation regex spec
CayoPOliveira Feb 27, 2026
ea3a94f
chore: fix spec for contact syncing group
CayoPOliveira Feb 27, 2026
d5cb1a9
Merge branch 'Cayo-Oliveira/CU-86af01932/4-Backend-Gerenciamento-dos-…
CayoPOliveira Feb 27, 2026
16b267f
chore: remove readTimestamp handling and related tests for message re…
CayoPOliveira Feb 27, 2026
462bbfa
fix: optimize update_last_seen_at method to use update_columns
CayoPOliveira Feb 27, 2026
62c7ac3
Merge branch 'Cayo-Oliveira/CU-86af00yvg/2-Backend-Models-Main-PR' in…
CayoPOliveira Feb 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions .claude/skills/rspec-tests/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
---
name: rspec-tests
description: Guidelines for writing RSpec tests in this Rails project. Covers test structure, conventions, anti-patterns, and project-specific tooling. Use when creating or reviewing Ruby specs.
---

# RSpec Testing Skill

## Core Principles

### 1. Test Each Behavior Exactly Once

Every spec should verify one expected behavior. Do not write multiple specs that assert the same thing in different ways. Redundant specs slow the suite and add maintenance burden without extra confidence.

### 2. Avoid `before(:each)`

Prefer `let`, `let!`, `let_it_be`, `before_all`, or inline setup inside the `it` block. Only use `before(:each)` when there is truly no alternative (e.g., stateful mutation needed before every example in a tightly scoped context). When a `before` block is unavoidable, keep it as close to the examples that need it as possible.

### 3. Follow Set → Action → Expect Ordering

Structure every test with three clearly separated stages, each divided by a blank line:

- **Set** (Setup): Build the context — `let`, `let_it_be`, `create`, variable assignment.
- **Action**: Execute the behavior under test — call the method, make the request, trigger the job.
- **Expect**: Assert the outcome — `expect(...)`.

Only break this order when the assertion must come before the action (e.g., `expect { action }.to change(...)`).

```ruby
it 'assigns the conversation to the agent' do
agent = create(:user, account: account)

service.perform

expect(conversation.reload.assignee).to eq(agent)
end
```

### 4. Never Assert on Translated Strings

Translations vary by locale. Instead of matching user-facing text like `'enviado'` or `'sent'`, assert on the underlying status, enum value, or state change that produces that text.

```ruby
# Bad
expect(message.status_text).to eq('sent')

# Good
expect(message.status).to eq('sent')
expect(message).to be_sent
```

---

## Project Conventions

### Stubs and Mocks

- Stub with `allow(object).to receive(:method).and_return(value)`.
- Verify calls with `have_received(:method)` (message spies style is acceptable — `RSpec/MessageSpies` is disabled).
- For ENV variables, use the `with_modified_env` helper instead of stubbing `ENV` directly.

### Assertions

- Use Shoulda matchers for model validations and associations as one-liners:
```ruby
it { is_expected.to belong_to(:account) }
it { is_expected.to validate_presence_of(:name) }
```
- For HTTP responses: `expect(response).to have_http_status(:success)`.
- For error classes in parallel/reloading environments, compare `error.class.name` (string) instead of the constant directly.

### Style

- **Hash syntax**: Use explicit `key: value`. Do not use Ruby 3.1 shorthand (`{key:}`).
- **Module/class style**: Use compact definitions (`class Foo::Bar`) — never nested.
- **Describe/context naming**: `describe '#method_name'` for instance methods, `describe '.method_name'` for class methods, `context 'when condition'` for scenarios.

---

## Structure Examples

### Service Spec

```ruby
require 'rails_helper'

RSpec.describe MyService do
subject(:service) { described_class.new(account: account) }

let_it_be(:account) { create(:account) }

describe '#perform' do
context 'when the input is valid' do
let(:params) { { name: 'Test' } }

it 'creates the resource' do
result = service.perform(params)

expect(result).to be_persisted
expect(result.name).to eq('Test')
end
end

context 'when the input is invalid' do
let(:params) { { name: '' } }

it 'raises a validation error' do
expect { service.perform(params) }.to raise_error(ActiveRecord::RecordInvalid)
end
end
end
end
```

### Request Spec

```ruby
require 'rails_helper'

RSpec.describe 'Widgets API', type: :request do
let_it_be(:account) { create(:account) }
let_it_be(:agent) { create(:user, account: account, role: :agent) }

describe 'POST /api/v1/accounts/{account.id}/widgets' do
context 'when authenticated' do
it 'creates a widget' do
params = { name: 'Support', website_url: 'https://example.com' }

post "/api/v1/accounts/#{account.id}/widgets",
headers: { api_access_token: agent.access_token.token },
params: params,
as: :json

expect(response).to have_http_status(:success)
expect(JSON.parse(response.body)['name']).to eq('Support')
end
end
end
end
```
40 changes: 40 additions & 0 deletions app/services/whatsapp/baileys_handlers/message_receipt_update.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
module Whatsapp::BaileysHandlers::MessageReceiptUpdate
include Whatsapp::BaileysHandlers::Helpers

private

def process_message_receipt_update
receipts = processed_params[:data]
receipts.each do |receipt|
@message = nil
@raw_message = receipt

next handle_receipt_update if incoming?

# NOTE: Shared lock with Whatsapp::SendOnWhatsappService
# Avoids race conditions when sending messages.
with_baileys_channel_lock_on_outgoing_message(inbox.channel.id) { handle_receipt_update }
end
end

def handle_receipt_update
return unless find_message_by_source_id(raw_message_id)

new_status = receipt_status
return if new_status.nil?
return unless receipt_status_transition_allowed?(new_status)

@message.update!(status: new_status)
end

def receipt_status
'delivered' if @raw_message.dig(:receipt, :receiptTimestamp).present?
end

def receipt_status_transition_allowed?(new_status)
return false if @message.status == 'read'
return false if @message.status == 'delivered' && new_status == 'delivered'

true
end
end
1 change: 1 addition & 0 deletions app/services/whatsapp/incoming_message_baileys_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ class Whatsapp::IncomingMessageBaileysService < Whatsapp::IncomingMessageBaseSer
include Whatsapp::BaileysHandlers::ConnectionUpdate
include Whatsapp::BaileysHandlers::MessagesUpsert
include Whatsapp::BaileysHandlers::MessagesUpdate
include Whatsapp::BaileysHandlers::MessageReceiptUpdate
include Whatsapp::BaileysHandlers::GroupParticipantsUpdate
include Whatsapp::BaileysHandlers::GroupsUpdate

Expand Down
66 changes: 27 additions & 39 deletions app/services/whatsapp/providers/whatsapp_baileys_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -161,13 +161,7 @@ def read_messages(messages, recipient_id:, **)
"#{provider_url}/connections/#{whatsapp_channel.phone_number}/read-messages",
headers: api_headers,
body: {
keys: messages.map do |message|
{
id: message.source_id,
remoteJid: remote_jid,
fromMe: message.message_type == 'outgoing'
}
end
keys: messages.map { |message| message_key_for(message) }
}.to_json
)

Expand All @@ -176,7 +170,7 @@ def read_messages(messages, recipient_id:, **)
true
end

def unread_message(recipient_id, message) # rubocop:disable Metrics/MethodLength
def unread_message(recipient_id, message)
@recipient_id = recipient_id

response = HTTParty.post(
Expand All @@ -187,11 +181,7 @@ def unread_message(recipient_id, message) # rubocop:disable Metrics/MethodLength
mod: {
markRead: false,
lastMessages: [{
key: {
id: message.source_id,
remoteJid: remote_jid,
fromMe: message.message_type == 'outgoing'
},
key: message_key_for(message),
messageTimestamp: message.content_attributes[:external_created_at]
}]
}
Expand All @@ -210,13 +200,7 @@ def received_messages(recipient_id, messages)
"#{provider_url}/connections/#{whatsapp_channel.phone_number}/send-receipts",
headers: api_headers,
body: {
keys: messages.map do |message|
{
id: message.source_id,
remoteJid: remote_jid,
fromMe: message.message_type == 'outgoing'
}
end
keys: messages.map { |message| message_key_for(message) }
}.to_json
)

Expand Down Expand Up @@ -275,11 +259,7 @@ def delete_message(recipient_id, message)
headers: api_headers,
body: {
jid: remote_jid,
key: {
id: message.source_id,
remoteJid: remote_jid,
fromMe: message.message_type == 'outgoing'
}
key: message_key_for(message)
}.to_json
)

Expand All @@ -296,11 +276,7 @@ def edit_message(recipient_id, message, new_content)
headers: api_headers,
body: {
jid: remote_jid,
key: {
id: message.source_id,
remoteJid: remote_jid,
fromMe: message.message_type == 'outgoing'
},
key: message_key_for(message),
messageContent: { text: new_content }
}.to_json
)
Expand All @@ -323,10 +299,10 @@ def api_key
def reaction_message_content
reply_to = Message.find(@message.in_reply_to)
{
react: { key: { id: reply_to.source_id,
remoteJid: remote_jid,
fromMe: reply_to.message_type == 'outgoing' },
text: @message.outgoing_content }
react: {
key: message_key_for(reply_to),
text: @message.outgoing_content
}
}
end

Expand All @@ -339,16 +315,28 @@ def reply_context

{
quotedMessage: {
key: {
id: reply_to_external_id,
remoteJid: remote_jid,
fromMe: reply_to_message.message_type == 'outgoing'
},
key: message_key_for(reply_to_message),
message: quoted_message_content(reply_to_message)
}
}
end

def message_key_for(message)
{
id: message.source_id,
remoteJid: remote_jid,
fromMe: message.message_type == 'outgoing',
participant: group_participant_jid(message)
}.compact
end

def group_participant_jid(message)
return unless remote_jid.ends_with?('@g.us')
return if message.message_type == 'outgoing'

message.sender&.identifier
end

def quoted_message_content(message)
if message.attachments.present?
attachment = message.attachments.first
Expand Down
61 changes: 61 additions & 0 deletions spec/services/whatsapp/incoming_message_baileys_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,67 @@
end
end
end

context 'when processing message-receipt.update event' do
let(:conversation) do
agent = create(:user, account: inbox.account, role: :agent)
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: agent.id)
end
let!(:message) { create(:message, inbox: inbox, conversation: conversation, source_id: '123ABCDE1234567', status: 'sent') }
let(:receipt_payload) do
{
key: { remoteJid: '123456789123456789@g.us', id: '123ABCDE1234567', fromMe: true,
participant: '12345678@lid' },
receipt: receipt_data
}
end
let(:receipt_data) { { userJid: '12345678@lid', receiptTimestamp: 1_772_056_268 } }
let(:params) do
{
webhookVerifyToken: webhook_verify_token,
event: 'message-receipt.update',
data: [receipt_payload]
}
end

it 'updates message from sent to delivered on receiptTimestamp' do
described_class.new(inbox: inbox, params: params).perform

expect(message.reload.status).to eq('delivered')
end

it 'ignores readTimestamp and does not update status' do
receipt_data.replace(userJid: '12345678@lid', readTimestamp: 1_772_056_497)

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

expect(message.reload.status).to eq('sent')
end

it 'does not downgrade a delivered message on receiptTimestamp' do
message.update!(status: 'delivered')

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

expect(message.reload.status).to eq('delivered')
end

it 'does not downgrade a read message on receiptTimestamp' do
message.update!(status: 'read')

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

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

it 'does not raise error when message is not found' do
receipt_payload[:key][:id] = 'NONEXISTENT_MSG_ID'

expect { described_class.new(inbox: inbox, params: params).perform }.not_to raise_error
end
end
end

def format_message_source_key(message_id)
Expand Down
Loading