Ralph/group conversations full feature#231
Ralph/group conversations full feature#231CayoPOliveira wants to merge 93 commits intoCayo-Oliveira/CU-86af00yvg/2-Backend-Models-Main-PRfrom
Conversation
…ions - Added PRD for group conversations detailing frontend and backend requirements. - Created new Baileys TypeScript definitions for group-related functions. - Renamed `conversation_type` to `group_type` in the database and updated all references. - Implemented API serialization for `group_type` in conversation and contact responses. - Developed Vuex store module for managing group members. - Created UI components for group management, including group creation, member management, and metadata editing. - Integrated @mention functionality for group conversations and real-time updates via ActionCable.
- Add migration to rename column and indexes - Update Conversation model enum to group_type - Update GroupConversationHandler concern - Update controllers (contacts, group_members) - Update all backend specs
- Add POST /api/v1/accounts/:account_id/groups endpoint - Add Groups::CreateService to orchestrate Baileys group creation - Extend WhatsappBaileysService and BaseService with group management methods - Add routes for group members, metadata, invite, and join requests - Returns 403 when agent lacks inbox access, 422 when provider is unavailable
- Add create/destroy/update actions to GroupMembersController - Delegate group management methods from Channel::Whatsapp to provider_service - create adds members via Baileys and creates ConversationGroupMember records - destroy removes a member by ID and sets is_active false - update promotes/demotes a member and updates their role
- Add PATCH /contacts/:id/group_metadata endpoint - Updates group subject via Baileys and syncs contact name - Updates group description via Baileys and syncs additional_attributes.description - Returns 422 when provider is unavailable
- Add GET /contacts/:id/group_invite to retrieve current invite code/url - Add POST /contacts/:id/group_invite/revoke to revoke and get new invite code/url - Returns 422 when provider is unavailable
- Add GET /contacts/:id/group_join_requests to list pending join requests - Add POST /contacts/:id/group_join_requests/handle to approve/reject requests - Uses request_action param to avoid conflict with Rails reserved params[:action] - Returns 422 when provider is unavailable
- Extract mention://contact/ID/Name URIs from message content - Store mentioned contact IDs in message.content_attributes[mentioned_contacts] - Existing user/team mention handling unchanged
- Add app/javascript/dashboard/api/groupMembers.js - Exports 11 methods: getGroupMembers, syncGroup, createGroup, updateGroupMetadata, addMembers, removeMembers, updateMemberRole, getInviteLink, revokeInviteLink, getPendingRequests, handleJoinRequest
- Add groupMembers store module with fetch, sync, addMembers, removeMembers, updateMemberRole actions - Add SET_GROUP_MEMBERS and SET_GROUP_MEMBERS_UI_FLAG mutation types - Register module in store index
- Add groups.json with keys for group info, filter, creation modal, metadata editing, invite link, member management, join requests, and mention dropdown - Register groups.json in i18n locale en/index.js Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add chatGroupTypeFilter state, getter, mutation, and action to conversations store - Add getChatGroupTypeFilter getter - Add group_type param to ConversationApi.get() - Add Type filter section to ConversationBasicFilter with All/Individual/Group options - Persist group_type to UI settings under conversations_filter_by.group_type - Restore group_type from UI settings on page load - Include groupType in conversationFilters and pass as group_type param to API Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…cFilter All implementation was already in place from prior work: - ConversationBasicFilter.vue has Type section with All/Individual/Group options - ChatList.vue handles group_type in conversationFilters and restores from UI settings - Store has setChatGroupTypeFilter action, getChatGroupTypeFilter getter - API maps groupType → group_type query param Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add GROUP_TYPE to CONVERSATION_ATTRIBUTES in filterHelper.js - Add group_type filter definition in provider.js (components-next) - Add group_type to legacy advancedFilterItems/index.js and filterAttributeGroups - Add group_type to automationHelper conditionFilterMaps - Add group_type to customViewsHelper getValuesForFilter - Add group_type options to ChatList setParamsForEditFolderModal - Add GROUP_TYPE i18n key in en and pt_BR advancedFilters.json Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Import GroupContactInfo component - Conditionally render GroupContactInfo when group_type === 'group' - Keep ContactInfo for individual conversations (no regression) - Dynamic sidebar title: 'Group' for groups, 'Contact' for individual - contact_notes and contact_attributes accordion sections unchanged Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add inline editing for group name, description, and avatar in GroupContactInfo: - Click group name to edit inline, save on Enter/blur - Click description to edit inline with textarea, save on blur - Click avatar to open file picker for upload via contacts/update - Loading states on all fields during save - Success/error alerts for all operations - updateGroupMetadata action added to groupMembers store Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add sender name display above incoming message bubbles in group conversations - Deterministic color per sender using AVATAR_COLORS palette (name.length % 6) - Sender name hidden for consecutive messages from the same sender - Individual conversation bubbles unchanged - Pass groupWithPrevious and isGroupConversation props through MessageList → Message Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…d progress tracking
- Everyone item shown at top of mention dropdown - Searchable by 'all', 'todos', 'everyone' keywords - i18n keys added for en and pt-BR
- Add disabled prop to Switch component - Replace custom toggle buttons in GroupContactInfo with Switch - Loading spinner shown alongside toggle while toggling
…te error messages
… state after copying
…roup
- Remove dead conversation/{id}/sync_group swagger entry and file
- Update group_members.yml with pagination params, POST operation, and $ref schema
- Add swagger for: group_members_member (PATCH/DELETE), group_metadata,
group_invite, group_invite_revoke, group_join_requests,
group_join_requests_handle, group_settings, group_settings_leave,
group_settings_toggle_join_approval, groups/create
- Add group_member schema definition
- Add Groups tag to application tag_groups
- Register all 12 group endpoints in paths/index.yml
…DisplayName with @lid/@phone in outgoing text
- Create Contacts::SyncGroupJob that checks group_last_synced_at before calling SyncGroupService (skips if < 15 min) - Controller sync_group now enqueues the job and returns 202 Accepted - Delete sync_group.json.jbuilder (no longer needed) - Frontend sync action is fire-and-forget; results via ActionCable - Auto-trigger sync on conversation select and panel mount - Remove manual sync button from GroupContactInfo
…bers section visible in read-only mode when\ngroup_left is true. Admin actions (add member, promote,\ndemote, remove) remain hidden. Pending Join Requests and\nAdvanced Options also stay hidden.
…n\n- Remove the @all/everyone special mention from TagGroupMembers since\n no channel provider currently supports mentioning all participants\n- Fix Enter key sending message instead of inserting selected mention\n in group conversations. The root cause was Editor.vue only emitting\n toggleUserMention=true for private notes (isPrivate), leaving\n ReplyBox unaware the group mention dropdown was open. Now also\n emits for isGroupConversation.\n- Add TagGroupMembers spec covering filtering, exclusion, and emission"
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
@codex review |
|
To use Codex here, create a Codex account and connect to github. |
There was a problem hiding this comment.
Pull request overview
This PR adds end-to-end WhatsApp group conversation functionality (backend models/APIs, sync, and dashboard UI), including member management, group metadata/invite/join-requests, group-specific rendering, and new filtering by conversation group type.
Changes:
- Refactors group membership persistence from
ConversationGroupMembertoGroupMember(anchored on the group Contact) and renamesconversation_type→group_typeon conversations. - Adds/updates APIs + Swagger docs for group creation and group operations (members, metadata, invites, join requests, settings).
- Adds dashboard UX for group sidebar, group message bubbles, @mentions, and group-type filtering.
Reviewed changes
Copilot reviewed 143 out of 144 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| tasks/manual-testing.md | Adds manual test checklist for group conversations |
| swagger/tag_groups/platform_swagger.json | Adds group_member schema to platform swagger tag group |
| swagger/tag_groups/other_swagger.json | Adds group_member schema to other swagger tag group |
| swagger/tag_groups/client_swagger.json | Adds group_member schema to client swagger tag group |
| swagger/tag_groups/application_swagger.json | Removes old endpoints and adds group_member schema in app swagger tag group |
| swagger/tag_groups/application.yml | Adds Groups tag and fixes formatting |
| swagger/paths/index.yml | Adds new group-related paths and groups create path; removes conversation sync_group path |
| swagger/paths/application/groups/create.yml | Documents group creation endpoint |
| swagger/paths/application/conversation/sync_group.yml | Removes deprecated conversation sync_group swagger path |
| swagger/paths/application/contacts/group_settings_toggle_join_approval.yml | Documents join approval toggle endpoint |
| swagger/paths/application/contacts/group_settings_leave.yml | Documents leave group endpoint |
| swagger/paths/application/contacts/group_settings.yml | Documents group settings update endpoint |
| swagger/paths/application/contacts/group_metadata.yml | Documents group metadata update endpoint |
| swagger/paths/application/contacts/group_members_member.yml | Documents per-member update/remove endpoints |
| swagger/paths/application/contacts/group_members.yml | Updates group members list + add members; adds pagination/meta |
| swagger/paths/application/contacts/group_join_requests_handle.yml | Documents join request handling endpoint |
| swagger/paths/application/contacts/group_join_requests.yml | Documents join request listing endpoint |
| swagger/paths/application/contacts/group_invite_revoke.yml | Documents invite revoke endpoint |
| swagger/paths/application/contacts/group_invite.yml | Documents invite show endpoint |
| swagger/definitions/resource/group_member.yml | Adds reusable group_member schema |
| swagger/definitions/index.yml | Registers group_member schema |
| spec/services/whatsapp/providers/whatsapp_baileys_service_spec.rb | Updates provider response expectations and adds provider group API specs |
| spec/services/whatsapp/baileys_handlers/messages_upsert_spec.rb | Updates specs for group_type and GroupMember |
| spec/services/whatsapp/baileys_handlers/groups_update_spec.rb | Updates specs to use group_type |
| spec/services/whatsapp/baileys_handlers/group_participants_update_spec.rb | Updates specs to use GroupMember and group_type |
| spec/services/messages/mention_service_spec.rb | Adds specs for contact mentions storage behavior |
| spec/services/contacts/sync_group_service_spec.rb | Updates sync behavior to use channel-based sync and conversation creation |
| spec/models/conversation_spec.rb | Removes old sync/group member association specs; adds group_type enum spec |
| spec/models/conversation_group_member_spec.rb | Removes specs for removed model |
| spec/models/contact_spec.rb | Removes old conversation_group_member association specs |
| spec/jobs/contacts/sync_group_job_spec.rb | Adds specs for new SyncGroupJob cooldown/rescue behavior |
| spec/factories/group_members.rb | Adds factory for GroupMember |
| spec/factories/conversation_group_members.rb | Removes old factory |
| spec/controllers/api/v1/accounts/groups_controller_spec.rb | Adds request specs for group creation endpoint |
| spec/controllers/api/v1/accounts/conversations_controller_spec.rb | Removes request specs for removed conversation sync_group endpoint |
| spec/controllers/api/v1/accounts/contacts_controller_spec.rb | Updates sync_group to enqueue job and return 202 |
| spec/controllers/api/v1/accounts/contacts/group_metadata_controller_spec.rb | Adds request specs for group metadata update endpoint |
| spec/controllers/api/v1/accounts/contacts/group_join_requests_controller_spec.rb | Adds request specs for join requests endpoints |
| spec/controllers/api/v1/accounts/contacts/group_invite_controller_spec.rb | Adds request specs for invite endpoints |
| ralph.sh | Adds long-running agent loop script (Ralph) |
| lib/filters/filter_keys.yml | Adds group_type as a standard conversation filter key |
| enterprise/app/models/company.rb | Updates schema comment formatting |
| db/migrate/20260303120000_drop_conversation_group_members.rb | Drops old conversation_group_members table |
| db/migrate/20260303114847_create_group_members.rb | Adds group_members table |
| db/migrate/20260227135739_rename_conversation_type_to_group_type_on_conversations.rb | Renames conversations column/indexes to group_type |
| config/routes.rb | Removes conversation sync_group route; adds groups + contact group subresources |
| app/views/api/v1/models/_inbox.json.jbuilder | Exposes allow_group_creation on inbox payload |
| app/views/api/v1/models/_contact.json.jbuilder | Exposes group_type on contact payload |
| app/views/api/v1/conversations/partials/_conversation.json.jbuilder | Exposes group_type on conversation payload |
| app/views/api/v1/accounts/contacts/sync_group.json.jbuilder | Removes old sync_group response body |
| app/views/api/v1/accounts/contacts/group_members/index.json.jbuilder | Updates member payload and adds pagination meta |
| app/services/whatsapp/providers/base_service.rb | Adds abstract group operations to provider base |
| app/services/whatsapp/mention_converter_service.rb | Adds WhatsApp mention extraction/conversion utilities |
| app/services/whatsapp/baileys_handlers/messages_upsert.rb | Adds group-create stub handling |
| app/services/whatsapp/baileys_handlers/helpers.rb | Converts incoming mentions on text messages |
| app/services/whatsapp/baileys_handlers/groups_update.rb | Persists invite/settings changes from group updates |
| app/services/whatsapp/baileys_handlers/group_participants_update.rb | Updates group member tracking and resolves conversations when inbox leaves |
| app/services/whatsapp/baileys_handlers/concerns/group_stub_message_handler.rb | Handles GROUP_CREATE and icon updates (sync + avatar refresh) |
| app/services/whatsapp/baileys_handlers/concerns/group_contact_message_handler.rb | Updates member sync to use GroupMember |
| app/services/messages/mention_service.rb | Adds parsing/storing of contact mentions |
| app/services/messages/markdown_renderers/whats_app_renderer.rb | Avoids rendering mention links as raw URLs |
| app/services/groups/create_service.rb | Adds service to create WhatsApp groups via provider |
| app/services/filter_service.rb | Adds group_type to filter value coercion |
| app/services/contacts/sync_group_service.rb | Refactors sync to use channel + ensured conversation creation |
| app/presenters/conversations/event_data_presenter.rb | Adds group_type to conversation event payload |
| app/policies/conversation_policy.rb | Removes sync_group authorization method |
| app/models/user.rb | Updates schema comment formatting |
| app/models/super_admin.rb | Updates schema comment formatting |
| app/models/reporting_event.rb | Updates schema comment formatting |
| app/models/group_member.rb | Adds new GroupMember model |
| app/models/csat_survey_response.rb | Updates schema comment formatting |
| app/models/conversation_group_member.rb | Removes old ConversationGroupMember model |
| app/models/conversation.rb | Renames enum to group_type and removes group member associations/sync method |
| app/models/contact.rb | Adds group membership associations and group_channel helper |
| app/models/concerns/group_conversation_handler.rb | Updates group conversation/member management to use GroupMember |
| app/models/channel/whatsapp.rb | Adds group-related delegation + allow_group_creation |
| app/listeners/action_cable_listener.rb | Broadcasts group members on contact.group_synced events |
| app/jobs/contacts/sync_group_job.rb | Adds background job for group sync with cooldown |
| app/javascript/shared/helpers/markdownIt/link.js | Adds mention://contact support in markdown-it plugin |
| app/javascript/shared/constants/busEvents.js | Adds NAVIGATE_TO_GROUP bus event |
| app/javascript/dashboard/store/mutation-types.js | Adds group member and group-type filter mutations |
| app/javascript/dashboard/store/modules/groupMembers.spec.js | Adds unit tests for groupMembers Vuex module |
| app/javascript/dashboard/store/modules/groupMembers.js | Adds Vuex module for group member operations and group creation |
| app/javascript/dashboard/store/modules/conversations/index.js | Adds group-type filter state/mutation |
| app/javascript/dashboard/store/modules/conversations/helpers/filterHelpers.js | Adds group_type to frontend filtering helpers |
| app/javascript/dashboard/store/modules/conversations/getters.js | Adds getter for group-type filter |
| app/javascript/dashboard/store/modules/conversations/actions.js | Adds action to set group-type filter |
| app/javascript/dashboard/store/index.js | Registers groupMembers Vuex module |
| app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue | Switches sidebar to GroupContactInfo for group chats and triggers sync |
| app/javascript/dashboard/i18n/locale/pt_BR/index.js | Registers new groups locale bundle |
| app/javascript/dashboard/i18n/locale/pt_BR/groups.json | Adds pt-BR group UI strings |
| app/javascript/dashboard/i18n/locale/pt_BR/contact.json | Adds pt-BR compose tabs for group mode |
| app/javascript/dashboard/i18n/locale/pt_BR/advancedFilters.json | Adds pt-BR label for group_type advanced filter |
| app/javascript/dashboard/i18n/locale/en/index.js | Registers new groups locale bundle |
| app/javascript/dashboard/i18n/locale/en/groups.json | Adds en group UI strings |
| app/javascript/dashboard/i18n/locale/en/contact.json | Adds en compose tabs for group mode |
| app/javascript/dashboard/i18n/locale/en/advancedFilters.json | Adds en label for group_type advanced filter |
| app/javascript/dashboard/helper/specs/actionCable.spec.js | Adds tests for contact.group_synced handler |
| app/javascript/dashboard/helper/pendingGroupNavigation.js | Adds helper for post-create navigation handshake |
| app/javascript/dashboard/helper/customViewsHelper.js | Adds support for group_type values in custom views |
| app/javascript/dashboard/helper/automationHelper.js | Adds group_type condition options in automations |
| app/javascript/dashboard/helper/actionCable.js | Handles contact.group_synced + group navigation on conversation.created |
| app/javascript/dashboard/components/widgets/conversation/specs/TagGroupMembers.spec.js | Adds tests for group member mention dropdown |
| app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/index.js | Adds group_type to advanced filter UI |
| app/javascript/dashboard/components/widgets/conversation/TagGroupMembers.vue | Adds group member dropdown for @mentions |
| app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue | Passes group context into editor for group mentions |
| app/javascript/dashboard/components/widgets/conversation/ConversationBasicFilter.vue | Adds group type selection to basic filter dropdown |
| app/javascript/dashboard/components/widgets/WootWriter/Editor.vue | Enables group-member mention UI in group conversations |
| app/javascript/dashboard/components/ChatList.vue | Adds group_type filtering to conversation list fetching |
| app/javascript/dashboard/components-next/switch/Switch.vue | Adds disabled prop support to switch component |
| app/javascript/dashboard/components-next/message/bubbles/Text/FormattedContent.vue | Styles contact mentions distinctly |
| app/javascript/dashboard/components-next/message/MessageList.vue | Passes group context for bubble grouping/name rendering |
| app/javascript/dashboard/components-next/message/Message.vue | Renders sender avatar/name in group incoming messages + click-to-contact |
| app/javascript/dashboard/components-next/filter/provider.js | Adds group_type to new filter provider |
| app/javascript/dashboard/components-next/filter/helper/filterHelper.js | Adds GROUP_TYPE constant |
| app/javascript/dashboard/components-next/NewConversation/components/ComposeNewGroupForm.vue | Adds UI to create a new WhatsApp group |
| app/javascript/dashboard/components-next/NewConversation/ComposeConversation.vue | Adds “Conversation/Group” compose tabs and group creation flow |
| app/javascript/dashboard/api/inbox/conversation.js | Sends group_type query param when fetching conversations |
| app/javascript/dashboard/api/groupMembers.js | Adds API client for group endpoints |
| app/helpers/filters/filter_helper.rb | Adds mapping for conversation group_type filter values |
| app/finders/conversation_finder.rb | Adds backend filtering by group_type param |
| app/controllers/api/v1/accounts/groups_controller.rb | Adds group creation endpoint |
| app/controllers/api/v1/accounts/conversations_controller.rb | Removes conversation sync_group action |
| app/controllers/api/v1/accounts/contacts_controller.rb | Changes sync_group to enqueue background job and return 202 |
| app/controllers/api/v1/accounts/contacts/group_settings_controller.rb | Adds group settings endpoints (leave/update/toggle join approval) |
| app/controllers/api/v1/accounts/contacts/group_metadata_controller.rb | Adds group metadata update endpoint |
| app/controllers/api/v1/accounts/contacts/group_members_controller.rb | Adds paginated member listing + add/remove/promote/demote |
| app/controllers/api/v1/accounts/contacts/group_join_requests_controller.rb | Adds join request list/handle endpoints |
| app/controllers/api/v1/accounts/contacts/group_invites_controller.rb | Adds invite show/revoke endpoints |
| AGENTS.md | Adds Ralph agent instructions |
| .last-branch | Stores last Ralph branch name |
| .claude/skills/ralph/SKILL.md | Adds Ralph PRD converter skill docs |
| .claude/skills/prd/SKILL.md | Adds PRD generator skill docs |
app/controllers/api/v1/accounts/contacts/group_join_requests_controller.rb
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 7
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
app/models/csat_survey_response.rb (2)
36-40:⚠️ Potential issue | 🟠 MajorAdd model-level validation for
message_iduniqueness/presence.
message_idhas a UNIQUE DB index, but the model does not validate it. Addvalidates :message_id, presence: true, uniqueness: true(or scoped uniqueness if that is the domain rule) to fail fast at the model layer.Proposed change
validates :rating, presence: true, inclusion: { in: [1, 2, 3, 4, 5] } validates :account_id, presence: true validates :contact_id, presence: true validates :conversation_id, presence: true + validates :message_id, presence: true, uniqueness: trueAs per coding guidelines,
app/models/**/*.rb: "Always validate presence and uniqueness in Rails models, and add proper database indexes".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/models/csat_survey_response.rb` around lines 36 - 40, Add a model-level validation for message_id on the CsatSurveyResponse model: ensure the model (CsatSurveyResponse) validates presence and uniqueness of :message_id (or uniqueness scoped to the relevant attribute if domain rules require, e.g. uniqueness: { scope: :conversation_id }). Update the validates block to include validates :message_id, presence: true, uniqueness: true (or scoped uniqueness) so model-level checks mirror the UNIQUE DB index and fail fast before DB constraint errors.
45-46:⚠️ Potential issue | 🟠 MajorBack
filter_by_ratingwith a database index.A new rating filter scope was added, but schema annotations do not show an index on
rating. This can cause table scans as data grows; add an index (often[:account_id, :rating]in multi-tenant queries).As per coding guidelines,
app/models/**/*.rb: "Always validate presence and uniqueness in Rails models, and add proper database indexes".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/models/csat_survey_response.rb` around lines 45 - 46, Add a DB index and model validation for the new rating filter: create a Rails migration that adds an index on [:account_id, :rating] for the csat_survey_responses table (e.g., add_index :csat_survey_responses, [:account_id, :rating]) to avoid table scans in multi-tenant queries, and update the CsAtSurveyResponse model to validate presence of :rating (e.g., validates :rating, presence: true) so the scope filter_by_rating operates against an indexed, validated column.app/javascript/dashboard/api/inbox/conversation.js (1)
19-33:⚠️ Potential issue | 🟡 MinorAdd
groupTypeparameter toConversationApi.meta()to match backend support and frontend list filtering.The backend
conversation_finderfilters bygroup_typefor the meta endpoint, andget()already acceptsgroupTypefor filtered results. However,meta()lacks this parameter, causing meta counts (used in conversationStats) to desynchronize when conversations are filtered by group type. Update the method signature to acceptgroupTypeand pass it to the API.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/javascript/dashboard/api/inbox/conversation.js` around lines 19 - 33, ConversationApi.meta() is missing the groupType parameter causing meta counts to desync when filtering by group_type; update the meta method signature to accept groupType and include it in the axios GET params (mirror how get() passes groupType) so the backend receives group_type; specifically modify the ConversationApi.meta function to add the groupType argument and add group_type: groupType to the params object.app/services/whatsapp/providers/whatsapp_baileys_service.rb (1)
773-788:⚠️ Potential issue | 🟠 MajorSeveral new network methods bypass centralized error recovery.
Methods added in this PR (e.g., group create/update/invite/join-request operations) are not included in
with_error_handling, so provider reconnection logic won’t run for those failures.💡 Proposed fix
with_error_handling :setup_channel_provider, :disconnect_channel_provider, :send_message, :toggle_typing_status, :update_presence, :read_messages, :unread_message, :received_messages, :group_metadata, :sync_group, :on_whatsapp, :delete_message, :edit_message, + :create_group, + :update_group_subject, + :update_group_description, + :update_group_participants, + :group_invite_code, + :revoke_group_invite, + :group_join_requests, + :handle_group_join_requests, :group_leave, :group_setting_update, :group_join_approval_mode🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/services/whatsapp/providers/whatsapp_baileys_service.rb` around lines 773 - 788, The new group-related network methods (e.g., group_create, group_update, group_invite, group_join_request — whatever exact method names were added in this PR) are not wrapped by the existing with_error_handling call, so failures there skip the provider reconnection/error recovery; update the with_error_handling invocation to include each new group method name (add the exact method identifiers you introduced) so they use the centralized error handler (refer to the with_error_handling symbol to locate the call and add the new method symbols to its argument list).
🟠 Major comments (15)
tasks/references/baileys.d.ts-61-61 (1)
61-61:⚠️ Potential issue | 🟠 MajorFix typo in API type names:
Bussines→BusinessLines 61 and 192 use
Bussines(double 's'), but upstream@whiskeysockets/baileysdocumentation usesBusiness(single 's') for the correct spelling. This causes broken imports and incorrect API names.Proposed fix
- updateBussinesProfile: (args: import("../Types/Bussines.js").UpdateBussinesProfileProps) => Promise<any>; + updateBusinessProfile: (args: import("../Types/Business.js").UpdateBusinessProfileProps) => Promise<any>; @@ - addOrEditQuickReply: (quickReply: import("../Types/Bussines.js").QuickReplyAction) => Promise<void>; + addOrEditQuickReply: (quickReply: import("../Types/Business.js").QuickReplyAction) => Promise<void>;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tasks/references/baileys.d.ts` at line 61, The API type and import are misspelled: change all occurrences of "Bussines" to "Business" so the type and import match upstream; specifically update the declaration updateBussinesProfile to use import("../Types/Business.js").UpdateBusinessProfileProps and rename the type/export in the Types file (and any other references like UpdateBussinesProfileProps or ../Types/Bussines.js) to UpdateBusinessProfileProps / ../Types/Business.js to restore correct imports and API names.ralph.sh-51-53 (1)
51-53:⚠️ Potential issue | 🟠 MajorSanitize archive folder names derived from branch input.
FOLDER_NAMEis built from branch text (sourced fromprd.jsonvia.last-branchfile) and used in filesystem paths without sanitization. Branch strings containing/or other unsafe characters can create directory traversal vulnerabilities when passed tomkdir -p. For example, a branch name likeralph/../../../tmpwould escape the intended archive directory.Suggested fix
- FOLDER_NAME=$(echo "$LAST_BRANCH" | sed 's|^ralph/||') + FOLDER_NAME=$(echo "$LAST_BRANCH" | sed 's|^ralph/||' | sed 's/[^A-Za-z0-9._-]/-/g') + FOLDER_NAME=${FOLDER_NAME:-unknown-branch} ARCHIVE_FOLDER="$ARCHIVE_DIR/$DATE-$FOLDER_NAME"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ralph.sh` around lines 51 - 53, FOLDER_NAME is taken directly from LAST_BRANCH and used to build ARCHIVE_FOLDER, allowing path traversal or unsafe characters; sanitize LAST_BRANCH before using it: strip any leading "ralph/" prefix, remove any "../" or "/" segments, replace all characters except [A-Za-z0-9._-] with a safe separator (e.g., "-"), ensure the resulting FOLDER_NAME is non-empty and does not start with "-" (prepend a safe prefix if needed), then use that sanitized FOLDER_NAME to construct ARCHIVE_FOLDER and call mkdir -p; update references to the symbols LAST_BRANCH, FOLDER_NAME, and ARCHIVE_FOLDER accordingly.app/services/whatsapp/mention_converter_service.rb-107-113 (1)
107-113:⚠️ Potential issue | 🟠 MajorPrevent partial-token mention replacements
Current regexes can replace substrings inside longer mentions (e.g.,
@anainside@anabel), which can map mentions to the wrong contact.Proposed fix
+ def mention_token_pattern(value, case_insensitive: false) + options = case_insensitive ? Regexp::IGNORECASE : nil + Regexp.new("@#{Regexp.escape(value)}(?=$|\\s|[[:punct:]])", options) + end + def apply_lid_mention(text, lid, account, inbox) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity contact = find_contact_by_lid(lid, account, inbox) return text unless contact contact_phone = contact.phone_number&.delete('+') display_name = contact.name.presence || contact_phone || lid encoded_name = ERB::Util.url_encode(display_name) mention_uri = "[@#{display_name}](mention://contact/#{contact.id}/#{encoded_name})" # Try `@lid` first, then `@phone`, then `@displayName` - patterns = [/@#{Regexp.escape(lid)}/] - patterns << /@#{Regexp.escape(contact_phone)}/ if contact_phone.present? - patterns << /@#{Regexp.escape(display_name)}/i if display_name != lid && display_name != contact_phone + patterns = [mention_token_pattern(lid)] + patterns << mention_token_pattern(contact_phone) if contact_phone.present? + patterns << mention_token_pattern(display_name, case_insensitive: true) if display_name != lid && display_name != contact_phone patterns.each do |pattern| return text.sub(pattern, mention_uri) if text.match?(pattern) end @@ def replace_mention_in_text(text, phone, display_name, mention_uri) # Try `@phone` first, then `@DisplayName` patterns = [ - /@#{Regexp.escape(phone)}/, - /@#{Regexp.escape(display_name)}/i + mention_token_pattern(phone), + mention_token_pattern(display_name, case_insensitive: true) ] patterns.each do |pattern| return text.sub(pattern, mention_uri) if text.match?(pattern) endAlso applies to: 148-157
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/services/whatsapp/mention_converter_service.rb` around lines 107 - 113, The regexes for building mention patterns allow partial matches (e.g., matching `@ana` inside `@anabel`); update the patterns in the block that builds `patterns = [/@#{Regexp.escape(lid)}/]` (and the similar block later at lines ~148-157) to require that the mention token is not followed by a word character by appending a negative lookahead, e.g. use /@#{Regexp.escape(lid)}(?!\w)/, /@#{Regexp.escape(contact_phone)}(?!\w)/ and /@#{Regexp.escape(display_name)}(?!\w)/i so the `patterns.each` loop and `text.sub(pattern, mention_uri)` only replace whole mention tokens, not substrings inside longer mentions.swagger/paths/application/contacts/group_invite.yml-3-8 (1)
3-8:⚠️ Potential issue | 🟠 MajorChange
contact_idtype fromnumbertointegerin group-related endpoints.The
numbertype in OpenAPI permits decimal values, which is inappropriate for identifiers. This issue affects multiple group-related endpoints:group_invite.yml,group_metadata.yml,group_invite_revoke.yml,group_join_requests.yml,group_join_requests_handle.yml,group_settings.yml,group_settings_leave.yml, andgroup_settings_toggle_join_approval.ymlall usetype: numberfor thecontact_idpath parameter, while request/response payloads correctly usetype: integer(e.g., inmerge.yml).Suggested fix
- name: contact_id in: path required: true schema: - type: number + type: integer description: ID of the group contact🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@swagger/paths/application/contacts/group_invite.yml` around lines 3 - 8, The path parameter "contact_id" is currently defined with schema type: number in several group-related endpoints; change each occurrence to schema type: integer (and optionally add format: int64 if you want explicit sizing) so the path param cannot accept decimals—update contact_id in the group-related YAMLs (group_invite.yml, group_metadata.yml, group_invite_revoke.yml, group_join_requests.yml, group_join_requests_handle.yml, group_settings.yml, group_settings_leave.yml, group_settings_toggle_join_approval.yml) to use type: integer to match request/response payloads.swagger/paths/application/contacts/group_join_requests.yml-3-8 (1)
3-8:⚠️ Potential issue | 🟠 MajorUse
integer(notnumber) forcontact_idpath parameters.Path parameters should consistently use
integerfor ID fields. Currently,type: numberallows decimals, which is inappropriate for database IDs and can leak into generated SDK types and validation behavior. This applies to all 8 contact group endpoints:
- group_join_requests.yml
- group_join_requests_handle.yml
- group_invite.yml
- group_invite_revoke.yml
- group_settings.yml
- group_settings_leave.yml
- group_settings_toggle_join_approval.yml
- group_metadata.yml
Align with the
account_idreference parameter inswagger/parameters/account_id.yml, which correctly usestype: integer.Suggested fix
- name: contact_id in: path required: true schema: - type: number + type: integer description: ID of the group contactApply this change to all 8 contact group endpoint files listed above.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@swagger/paths/application/contacts/group_join_requests.yml` around lines 3 - 8, The path parameter "contact_id" in the contacts group endpoints is declared as type: number (allowing decimals); change it to type: integer for all listed files (group_join_requests.yml, group_join_requests_handle.yml, group_invite.yml, group_invite_revoke.yml, group_settings.yml, group_settings_leave.yml, group_settings_toggle_join_approval.yml, group_metadata.yml) so the contact_id path parameter uses type: integer (matching the account_id parameter reference) to prevent decimal IDs and fix generated SDK/validation behavior.app/javascript/dashboard/helper/actionCable.js-93-96 (1)
93-96:⚠️ Potential issue | 🟠 MajorConsume the pending JID only after a successful match.
Line 93 currently clears pending navigation before validating the sender identifier. A non-matchingconversation.createdevent can discard the token and prevent later navigation to the intended group.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/javascript/dashboard/helper/actionCable.js` around lines 93 - 96, The code currently calls pendingGroupNavigation.consume() before verifying the sender, which can discard the token on a non-matching event; change the flow to read the pending JID without consuming it (e.g., use a peek/get method on pendingGroupNavigation instead of consume, or otherwise inspect the token) and compare it to data.meta?.sender?.identifier first, and only call pendingGroupNavigation.consume() after a successful match, then call emitter.emit(BUS_EVENTS.NAVIGATE_TO_GROUP, { conversationId: data.id }); using the same pendingJid for the check.app/controllers/api/v1/accounts/groups_controller.rb-3-10 (1)
3-10:⚠️ Potential issue | 🟠 MajorUse strong params before passing request data to the service.
Lines 3, 8–9 consume raw
paramsdirectly, bypassing controller-layer type and shape validation. Add agroup_paramsmethod and use it instead.🔧 Suggested fix
def create - inbox = Current.account.inboxes.find_by(id: params[:inbox_id]) + inbox = Current.account.inboxes.find_by(id: group_params[:inbox_id]) return render json: { error: 'Access Denied' }, status: :forbidden unless inbox_accessible?(inbox) result = Groups::CreateService.new( inbox: inbox, - subject: params[:subject], - participants: Array(params[:participants]) + subject: group_params[:subject], + participants: Array(group_params[:participants]) ).perform render json: result rescue Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError => e render json: { error: e.message }, status: :unprocessable_entity end private + +def group_params + params.permit(:inbox_id, :subject, participants: []) +end🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/controllers/api/v1/accounts/groups_controller.rb` around lines 3 - 10, Request data is consumed directly from params when finding the inbox and calling Groups::CreateService; add a strong params method and use it instead. Implement a private group_params (e.g. params.permit(:inbox_id, :subject, participants: [] ) or params.require(:group).permit(...)) and change inbox lookup to use group_params[:inbox_id] and pass subject: group_params[:subject], participants: Array(group_params[:participants]) into Groups::CreateService; keep using inbox_accessible? and ensure group_params is used everywhere you read these incoming values.app/controllers/api/v1/accounts/contacts_controller.rb-87-88 (1)
87-88:⚠️ Potential issue | 🟠 MajorReplace
ActionController::BadRequestwith a custom exception fromlib/custom_exceptions/contacts.rb.Lines 87–88 use
ActionController::BadRequest, a framework exception. Per project guidelines (**/*.rb), validation errors must use custom exceptions fromlib/custom_exceptions/. Create a newlib/custom_exceptions/contacts.rbmodule with appropriate exception classes (e.g.,InvalidGroupType,MissingIdentifier) that inherit fromCustomExceptions::Baseand overridehttp_statusto return 400 for validation failures.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/controllers/api/v1/accounts/contacts_controller.rb` around lines 87 - 88, Add a custom exceptions file and raise those instead of framework errors: create lib/custom_exceptions/contacts.rb defining module CustomExceptions::Contacts with classes InvalidGroupType and MissingIdentifier that inherit from CustomExceptions::Base and override http_status to return 400; then update app/controllers/api/v1/accounts/contacts_controller.rb to replace the two raises (the checks using `@contact.group_type_individual`? and `@contact.identifier.blank`?) to raise CustomExceptions::Contacts::InvalidGroupType and CustomExceptions::Contacts::MissingIdentifier respectively (ensure the new module is loaded/autoloaded).app/controllers/api/v1/accounts/contacts/group_join_requests_controller.rb-12-12 (1)
12-12:⚠️ Potential issue | 🟠 MajorUse strong params for
participantsandrequest_actionbefore forwarding to channel.Raw
paramsare passed directly into provider logic without whitelisting.Suggested fix
def handle authorize `@contact`, :update? - channel.handle_group_join_requests(`@contact.identifier`, params[:participants], params[:request_action]) + channel.handle_group_join_requests( + `@contact.identifier`, + handle_params[:participants], + handle_params[:request_action] + ) head :ok rescue Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError => e render json: { error: e.message }, status: :unprocessable_entity end private +def handle_params + params.permit(:request_action, participants: []) +endAs per coding guidelines
app/controllers/**/*.rb: Use strong params in Rails controllers for type safety and parameter validation.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/controllers/api/v1/accounts/contacts/group_join_requests_controller.rb` at line 12, The controller currently forwards raw params to channel.handle_group_join_requests; fix by whitelisting and validating participants and request_action via strong params before calling the channel. Add a private method in GroupJoinRequestsController (e.g., group_join_request_params) that does params.require(:some_parent_if_needed).permit(participants: [], :request_action) and normalizes/validates types (ensure participants is an Array and request_action is a permitted value), then call channel.handle_group_join_requests(`@contact.identifier`, group_join_request_params[:participants], group_join_request_params[:request_action]).app/services/whatsapp/baileys_handlers/groups_update.rb-84-90 (1)
84-90:⚠️ Potential issue | 🟠 MajorMake invite-code removal logic consistent with description-update pattern.
The
persist_invite_code_updatemethod skips database updates wheninviteCodeis blank, which differs from howupdate_group_descriptionhandles blank values (line 48 uses.presenceto store nil). For consistency and robustness, handle invite-code removal by storing nil or removing the key rather than skipping the update entirely.Suggested fix
def persist_invite_code_update(conversation, update) contact = conversation.contact - invite_code = update[:inviteCode] - return if invite_code.blank? - - new_attrs = (contact.additional_attributes || {}).merge('invite_code' => invite_code) + new_attrs = (contact.additional_attributes || {}).dup + if update[:inviteCode].present? + new_attrs['invite_code'] = update[:inviteCode] + else + new_attrs.delete('invite_code') + end contact.update!(additional_attributes: new_attrs) if new_attrs != contact.additional_attributes end🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/services/whatsapp/baileys_handlers/groups_update.rb` around lines 84 - 90, persist_invite_code_update currently returns early when update[:inviteCode] is blank; instead follow the update_group_description pattern by normalizing invite_code = update[:inviteCode].presence (so blank becomes nil) and then build new_attrs from (contact.additional_attributes || {}).merge('invite_code' => invite_code) or remove the 'invite_code' key when invite_code is nil, then call contact.update! when new_attrs != contact.additional_attributes; reference persist_invite_code_update, contact.additional_attributes and 'invite_code' when making the change.swagger/tag_groups/application_swagger.json-7942-7991 (1)
7942-7991: 🛠️ Refactor suggestion | 🟠 MajorReplace inline
group_members.itemsdefinition with$refto the new schema.The new
group_memberschema (lines 7942–7991) is unused. Update line 2045 (items) to use{ "$ref": "#/components/schemas/group_member" }instead of the current inline object definition. This eliminates duplication and ensures consistency between schema and endpoint documentation.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@swagger/tag_groups/application_swagger.json` around lines 7942 - 7991, Update the schema so the existing inline definition used for the group's members list is replaced with a reference to the new reusable schema: locate the schema named "group_members" (the property that has an "items" definition) and change its items value to use { "$ref": "#/components/schemas/group_member" }; ensure the referenced schema name is exactly "group_member" to match the newly added components schema and remove the duplicated inline object.app/services/whatsapp/providers/whatsapp_baileys_service.rb-6-6 (1)
6-6: 🛠️ Refactor suggestion | 🟠 MajorUse a shared custom exception class instead of defining a new local
StandardError.Please move this to
lib/custom_exceptions/and reference that class here for consistency with the project error model.As per coding guidelines "Use custom exceptions from
lib/custom_exceptions/for error handling".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/services/whatsapp/providers/whatsapp_baileys_service.rb` at line 6, The local exception class GroupParticipantNotAllowedError should be removed and replaced with the shared exception from lib/custom_exceptions; delete the local "class GroupParticipantNotAllowedError < StandardError" declaration, add the appropriate require/import for the shared exception (e.g. require 'custom_exceptions/group_participant_not_allowed_error' or require 'custom_exceptions' depending on project convention), and update any references in this file (whatsapp_baileys_service.rb) to use the shared GroupParticipantNotAllowedError class instead of the local definition to align with the project's error model.app/services/whatsapp/providers/whatsapp_baileys_service.rb-558-567 (1)
558-567:⚠️ Potential issue | 🟠 MajorMention extraction is using the wrong source field.
This path sends
@message.outgoing_content, but mention extraction/replacement reads@message.content. Ifcontentis empty, mention metadata won’t be applied.💡 Proposed fix
def merge_mention_data - return if `@message.content.blank`? + source_text = `@message.outgoing_content.presence` || `@message.content` + return if source_text.blank? - mention_data = Whatsapp::MentionConverterService.extract_mentions_for_whatsapp(`@message.content`, whatsapp_channel.account) + mention_data = Whatsapp::MentionConverterService.extract_mentions_for_whatsapp(source_text, whatsapp_channel.account) `@message_content.merge`!(mention_data) if mention_data.present? # Replace `@DisplayName` with `@lid/`@phone in text so Baileys can match mentions `@message_content`[:text] = Whatsapp::MentionConverterService.replace_mentions_in_outgoing_text( - `@message.content`, `@message_content`[:text], whatsapp_channel.account + source_text, `@message_content`[:text], whatsapp_channel.account ) end🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/services/whatsapp/providers/whatsapp_baileys_service.rb` around lines 558 - 567, The code is reading the wrong source field for mentions: change the blank check and both calls to use `@message.outgoing_content` instead of `@message.content`; specifically, in merge_mention_data update the guard to return if `@message.outgoing_content.blank`?, call Whatsapp::MentionConverterService.extract_mentions_for_whatsapp(`@message.outgoing_content`, whatsapp_channel.account) and pass `@message.outgoing_content` into Whatsapp::MentionConverterService.replace_mentions_in_outgoing_text so mention metadata and text replacements are derived from outgoing_content.db/schema.rb-880-890 (1)
880-890:⚠️ Potential issue | 🟠 MajorAdd foreign keys for
inbox_signaturesreferences.
user_idandinbox_idare required but currently not FK-constrained in schema, which can leave orphan records.💡 Migration follow-up
+ add_foreign_key :inbox_signatures, :users + add_foreign_key :inbox_signatures, :inboxes🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@db/schema.rb` around lines 880 - 890, Add foreign key constraints for inbox_signatures.user_id and inbox_signatures.inbox_id to prevent orphan records: create a migration that adds add_foreign_key :inbox_signatures, :users, column: :user_id and add_foreign_key :inbox_signatures, :inboxes, column: :inbox_id (or use change_table with t.foreign_key), ensure the migration references the existing indexes index_inbox_signatures_on_inbox_id and index_inbox_signatures_on_user_id_and_inbox_id and sets appropriate on_delete behavior (e.g., :cascade or :nullify) consistent with your app rules, then run migrations and update schema.rb accordingly.app/javascript/dashboard/store/modules/groupMembers.js-47-49 (1)
47-49:⚠️ Potential issue | 🟠 MajorPreserve original API errors instead of re-wrapping them.
throw new Error(error)strips useful fields from axios errors (likeresponse.status/response.data). Re-throw the original error object.💡 Proposed fix
- } catch (error) { - throw new Error(error); + } catch (error) { + throw error; } finally {Apply the same change to all four catch blocks in this file (lines 48, 72, 98, 119).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/javascript/dashboard/store/modules/groupMembers.js` around lines 47 - 49, Replace the re-wrapping of caught errors with re-throwing the original error object so Axios-specific fields are preserved: change any "throw new Error(error)" in groupMembers.js to "throw error" (apply this to all catch blocks in the file so response.status/response.data and other error properties remain intact).
🟡 Minor comments (20)
app/javascript/dashboard/helper/automationHelper.js-154-157 (1)
154-157:⚠️ Potential issue | 🟡 MinorAvoid hard-coded English labels in
group_typeoptions.Line 154-Line 157 hard-code display labels (
Individual,Group), which can bypass i18n and show mixed-language UI in localized installs. Keep IDs static, but source names from translated options passed into this helper.Proposed fix
export const getConditionOptions = ({ agents, booleanFilterOptions, campaigns, contacts, countries, customAttributes, inboxes, languages, labels, statusFilterOptions, teams, type, priorityOptions, messageTypeOptions, + groupTypeOptions, }) => { @@ message_type: messageTypeOptions, priority: priorityOptions, - group_type: [ - { id: 'individual', name: 'Individual' }, - { id: 'group', name: 'Group' }, - ], + group_type: groupTypeOptions || [], labels: generateConditionOptions(labels, 'title'), };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/javascript/dashboard/helper/automationHelper.js` around lines 154 - 157, The group_type array currently contains hard-coded English labels; change it to keep static ids but pull the display names from the translations passed into this helper (e.g., a translations/options param) instead of literal strings. Replace the name values in the group_type entries with something like translatedOptions.group_type.individual and translatedOptions.group_type.group (or use i18n.t('...') if the helper receives an i18n instance), ensuring the keys remain { id: 'individual' } and { id: 'group' } so callers provide localized labels rather than embedding English text in group_type.AGENTS.md-126-135 (1)
126-135:⚠️ Potential issue | 🟡 MinorAdd language identifiers to fenced code blocks.
Two fenced blocks are missing language tags, which triggers MD040.
Suggested fix
-``` +```text ## [Date/Time] - [Story ID] - What was implemented - Files changed - **Learnings for future iterations:** - Patterns discovered (e.g., "this codebase uses X for Y") - Gotchas encountered (e.g., "don't forget to update Z when changing W") - Useful context (e.g., "the evaluation panel is in component X") ---@@
-+textCodebase Patterns
- Example: Use
sql<number>template for aggregations- Example: Always use
IF NOT EXISTSfor migrations- Example: Export types from actions.ts for UI components
Also applies to: 143-148
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@AGENTS.md` around lines 126 - 135, Two fenced code blocks (the one starting with "## [Date/Time] - [Story ID]" and the one starting with "## Codebase Patterns") are missing language identifiers which triggers MD040; update each opening fence from ``` to a fenced block with a language tag (e.g., ```text) so the Markdown linter recognizes them. Locate the blocks containing those headings and replace their triple-backtick openers with language-tagged openers (consistent tag like "text") for both occurrences mentioned.app/services/whatsapp/baileys_handlers/concerns/group_stub_message_handler.rb-40-48 (1)
40-48:⚠️ Potential issue | 🟡 MinorUse
presencefor group name fallback.If
messageStubParameters.firstis an empty string,group_name || group_jidkeeps an empty name instead of falling back to the JID.Suggested fix
- name: group_name || group_jid, + name: group_name.presence || group_jid,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/services/whatsapp/baileys_handlers/concerns/group_stub_message_handler.rb` around lines 40 - 48, The code uses group_name = `@raw_message`[:messageStubParameters]&.first then sets name: group_name || group_jid which keeps an empty string instead of falling back; change to use presence so empty string falls back (e.g. use group_name.presence || group_jid or compute group_name = `@raw_message`[:messageStubParameters]&.first.presence) and keep the rest of the ContactInboxWithContactBuilder call (group_jid, inbox, group_contact_inbox) unchanged..claude/skills/prd/SKILL.md-34-52 (1)
34-52:⚠️ Potential issue | 🟡 MinorSpecify a language on the fenced example block.
The example block should include a language identifier to satisfy markdown lint (MD040).
Suggested fix
-``` +```text 1. What is the primary goal of this feature? A. Improve user onboarding experience B. Increase user retention C. Reduce support burden D. Other: [please specify] @@ 3. What is the scope? A. Minimal viable version B. Full-featured implementation C. Just the backend/API D. Just the UI</details> <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against the current code and only fix it if needed.
In @.claude/skills/prd/SKILL.md around lines 34 - 52, The fenced code block
starting with "1. What is the primary goal of this feature?" is missing a
language identifier, which triggers MD040; update the opening triple-backtick to
include a language (for example "text") so the block becomes "text" and
keep the block contents unchanged; locate the fenced block by searching for the
lines that begin "1. What is the primary goal..." and the closing
triple-backticks and add the language token to the opening fence.</details> </blockquote></details> <details> <summary>tasks/prd-group-conversations-frontend.md-5-16 (1)</summary><blockquote> `5-16`: _⚠️ Potential issue_ | _🟡 Minor_ **Align data-model references with current GroupMember architecture.** This PRD still instructs `ConversationGroupMember`-based behavior and includes `conversation_id` in member payload examples, which is out of sync with the GroupMember/contact-level model adopted in this feature set. Please update those sections to avoid implementation drift. Also applies to: 190-207, 424-425 <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@tasks/prd-group-conversations-frontend.md` around lines 5 - 16, Update the PRD to replace all references and examples that mention the deprecated ConversationGroupMember join-table and any payloads containing conversation_id with the current contact-level GroupMember model: use GroupMember (or Contact-level group member) terminology, show payload examples that omit conversation_id and instead reference contact_id/group_id as appropriate, and adjust descriptions of member attributes (role, is_active) to reflect they are stored on GroupMember/Contact not on a conversation join record; also update any procedures that mention ConversationGroupMember, Conversation ↔ contact join semantics, and the sync endpoint POST /api/v1/accounts/:account_id/contacts/:id/sync_group to clarify it operates against Contact/GroupMember metadata. ``` </details> </blockquote></details> <details> <summary>.claude/skills/ralph/SKILL.md-100-112 (1)</summary><blockquote> `100-112`: _⚠️ Potential issue_ | _🟡 Minor_ **Add language identifiers to fenced code blocks (MD040).** Three fenced blocks are missing a language tag. <details> <summary>Suggested fix</summary> ````diff -``` +```text "Typecheck passes" ``` @@ -``` +```text "Tests pass" ``` @@ -``` +```text "Verify in browser using dev-browser skill" ```🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.claude/skills/ralph/SKILL.md around lines 100 - 112, The three fenced code blocks containing the literal lines "Typecheck passes", "Tests pass", and "Verify in browser using dev-browser skill" are missing language identifiers; update each opening fence to include a language (e.g., change ``` to ```text) so the blocks become fenced with a language tag and satisfy MD040 for the blocks in SKILL.md that contain those exact strings.spec/services/contacts/sync_group_service_spec.rb-17-21 (1)
17-21:⚠️ Potential issue | 🟡 MinorUse class-name assertion for parallel-safe error checks.
Line 20 still asserts constant equality directly. Prefer asserting
error.class.nameto avoid reloading/parallel-class identity edge cases in specs.Suggested adjustment
- expect { described_class.new(contact: contact).perform }.to raise_error(ActionController::BadRequest) + expect { described_class.new(contact: contact).perform }.to raise_error { |error| + expect(error.class.name).to eq('ActionController::BadRequest') + }As per coding guidelines, "In parallel/reloading test environments, prefer comparing
error.class.nameover constant class equality when asserting raised errors".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@spec/services/contacts/sync_group_service_spec.rb` around lines 17 - 21, The spec currently asserts raised error by comparing to the constant ActionController::BadRequest which can fail under parallel/reload class identity; change the assertion to inspect the raised error's class name instead: wrap the call to described_class.new(contact: contact).perform in an expect { ... }.to raise_error block and assert error.class.name equals "ActionController::BadRequest" (i.e., use error.class.name string comparison rather than constant equality) so the test is parallel/reload safe.app/javascript/dashboard/routes/dashboard/conversation/contact/GroupContactInfo.vue-66-74 (1)
66-74:⚠️ Potential issue | 🟡 MinorPhone matching heuristic may produce false positives.
The
phonesMatchfunction compares only the last 8 digits, which could incorrectly match different phone numbers that happen to share the same suffix (e.g., different country codes with the same local number). Consider a stricter comparison or documenting the known limitations.// Example of false positive: // +1-555-123-4567 (US) // +44-555-123-4567 (UK) // Both would match because last 8 digits are "1234567" (or close)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/javascript/dashboard/routes/dashboard/conversation/contact/GroupContactInfo.vue` around lines 66 - 74, phonesMatch currently compares only the last 8 digits which can yield false positives; update the phonesMatch function to normalize digits and then prefer exact equality of normalized numbers (a === b), and if not equal only allow a suffix match when the original normalized lengths are the same or when country/area code equality can be established (e.g., compare leading length or leading digits), or alternatively tighten the suffix check to more digits (e.g., last 10) and add a clear comment about the heuristic limitation; reference the phonesMatch function and its local variables a and b when making this change.app/services/whatsapp/mention_converter_service.rb-75-77 (1)
75-77:⚠️ Potential issue | 🟡 MinorGuard against malformed
mentionedJidentriesIf
mentionedJidcontainsnil/blank elements,jid.split('@')can crash this path. Add a defensive skip before split.Proposed guard
mentioned_jids.reduce(text) do |result, jid| - jid_user, jid_server = jid.split('@') + next result if jid.blank? + jid_user, jid_server = jid.to_s.split('@', 2) + next result if jid_user.blank? if jid_server == 'lid'🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/services/whatsapp/mention_converter_service.rb` around lines 75 - 77, The reduce over mentioned_jids can raise when a jid is nil/blank because jid.split('@') is called unguarded; update the reducer in MentionConverterService (the mentioned_jids.reduce block) to skip any nil/blank or malformed jid entries before calling split (e.g., check jid.present? and that it contains '@' or convert to string and guard), returning the current result when skipping so the loop continues safely.spec/services/whatsapp/providers/whatsapp_baileys_service_spec.rb-1570-1573 (1)
1570-1573:⚠️ Potential issue | 🟡 MinorUse
error.class.nameassertions for raised errors in these new specsSwitch these
raise_error(SomeConstant)assertions to checkingerror.class.nameto avoid constant reloading flakes in parallel/reloading environments.Proposed spec update
expect do service.update_group_participants(group_jid, [participant_jid], 'add') - end.to raise_error(Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError) + end.to raise_error { |error| + expect(error.class.name).to eq('Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError') + } expect do service.group_invite_code(group_jid) - end.to raise_error(Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError) + end.to raise_error { |error| + expect(error.class.name).to eq('Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError') + }Applies to lines 1570-1573 and 1599-1602.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@spec/services/whatsapp/providers/whatsapp_baileys_service_spec.rb` around lines 1570 - 1573, Replace direct raise_error(SomeConstant) expectations with assertions that check the raised exception's class name string to avoid constant reloading flakes; for the spec around calling service.update_group_participants(group_jid, [participant_jid], 'add') (and the similar block at lines ~1599-1602), wrap the call in an expect { ... }.to raise_error, capture the rescued error (or use expect { ... }.to raise_error { |err| ... }) and assert err.class.name == 'Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError' (use the exact class name string used in the original assertion) instead of referring to the constant directly.swagger/paths/application/contacts/group_metadata.yml-15-30 (1)
15-30:⚠️ Potential issue | 🟡 MinorSchema does not enforce the “at least one field required” contract.
The description states one of
subjectordescriptionmust be present, but the request schema allows an empty object.Schema fix
application/json: schema: type: object + anyOf: + - required: [subject] + - required: [description] properties: subject: type: string description: New group subject (name) description:🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@swagger/paths/application/contacts/group_metadata.yml` around lines 15 - 30, The request schema currently allows an empty object but the description requires at least one of subject or description; update the schema under the requestBody → content → application/json → schema to enforce that by adding a oneOf (or anyOf) constraint that lists two alternatives: one requiring ["subject"] and one requiring ["description"], keeping the existing properties (subject, description) as-is so the validator enforces "at least one present" for the group metadata update.spec/controllers/api/v1/accounts/contacts/group_invite_controller_spec.rb-70-71 (1)
70-71:⚠️ Potential issue | 🟡 MinorAssert the error payload in revoke failure path.
Line 70 currently validates only status. Add an assertion for
response.parsed_body['error'](as done in the GET unavailable test) to protect the error contract.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@spec/controllers/api/v1/accounts/contacts/group_invite_controller_spec.rb` around lines 70 - 71, The revoke-failure spec currently only checks HTTP status; update the test (in spec/controllers/api/v1/accounts/contacts/group_invite_controller_spec.rb) to also assert the error payload by adding an assertion on response.parsed_body['error'] (mirror the style used in the "GET unavailable" test) — e.g. expect(response.parsed_body['error']).to be_present or to eq(...) depending on the expected message — so the test validates the error contract as well as the status.app/javascript/dashboard/components/ChatList.vue-378-385 (1)
378-385:⚠️ Potential issue | 🟡 MinorNormalize persisted
group_typebefore applying it.Line 378 reads
group_typefrom UI settings, but Line 385 accepts it without validation. This can leave the list filtered by an invalid value after stale/corrupted settings.🔧 Suggested patch
function setFiltersFromUISettings() { const { conversations_filter_by: filterBy = {} } = uiSettings.value; const { status, order_by: orderBy, group_type: groupType } = filterBy; + const validGroupTypes = ['', 'individual', 'group']; activeStatus.value = status || wootConstants.STATUS_TYPE.OPEN; activeSortBy.value = Object.values(wootConstants.SORT_BY_TYPE).includes( orderBy ) ? orderBy : wootConstants.SORT_BY_TYPE.LAST_ACTIVITY_AT_DESC; - activeGroupType.value = groupType || ''; + activeGroupType.value = validGroupTypes.includes(groupType) ? groupType : ''; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/javascript/dashboard/components/ChatList.vue` around lines 378 - 385, Persisted filterBy.group_type is accepted without validation; normalize and validate it before setting activeGroupType. In the block that reads filterBy (use symbols filterBy, groupType, activeGroupType), coerce groupType to a string (e.g., String(groupType).trim()) and only assign it to activeGroupType.value if it is present in the allowed set (use Object.values(wootConstants.GROUP_TYPE).includes(...)); otherwise set activeGroupType.value to ''. This prevents stale/invalid persisted values from becoming active filters.app/javascript/dashboard/i18n/locale/en/groups.json-24-24 (1)
24-24:⚠️ Potential issue | 🟡 MinorFix grammar in inbox placeholder.
"Select a inbox"should be"Select an inbox".Suggested fix
- "INBOX_PLACEHOLDER": "Select a inbox", + "INBOX_PLACEHOLDER": "Select an inbox",🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/javascript/dashboard/i18n/locale/en/groups.json` at line 24, The INBOX_PLACEHOLDER string currently reads "Select a inbox" and needs correct grammar; update the value for the "INBOX_PLACEHOLDER" key in groups.json to "Select an inbox" (replace the existing string literal) so the placeholder uses "an" before "inbox".app/services/groups/create_service.rb-18-20 (1)
18-20:⚠️ Potential issue | 🟡 MinorAdd validation to prevent creating groups with empty participants.
The
participants!parameter only ensures the value is notnil, not that it contains elements. An empty array can pass through the controller and service without validation, reaching the external WhatsApp API. Either validate that participants is non-empty inGroups::CreateServiceor in the controller before instantiating the service, or confirm the API gracefully handles empty participant lists.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/services/groups/create_service.rb` around lines 18 - 20, The service currently assumes participants is non-nil but allows an empty array to reach WhatsApp; add a presence check in Groups::CreateService (e.g., in initialize or the perform/create method) to raise or return a validation error when participants is empty (use participants.blank? or participants.empty?) before calling format_participants, so the service refuses to proceed with an empty participant list; reference the participants parameter and the format_participants method when implementing this check.app/javascript/dashboard/components-next/NewConversation/components/ComposeNewGroupForm.vue-86-88 (1)
86-88:⚠️ Potential issue | 🟡 MinorClose contacts dropdown on search failure as well.
If a prior query opened the dropdown, Line 87 clears results but leaves visibility unchanged. Explicitly closing it avoids stale empty-dropdown UI.
✅ Small fix
} catch { contactResults.value = []; + showContactsDropdown.value = false; } finally { isSearching.value = false; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/javascript/dashboard/components-next/NewConversation/components/ComposeNewGroupForm.vue` around lines 86 - 88, The catch block currently clears contactResults.value but doesn't close the contacts dropdown; update the catch in ComposeNewGroupForm.vue to also set the contacts-dropdown visibility flag to false (e.g., set contactDropdownVisible.value = false or call setShowContactsDropdown(false) — whatever visibility state is used in this component) so the dropdown is explicitly closed on search failure.swagger/paths/application/contacts/group_join_requests_handle.yml-3-8 (1)
3-8:⚠️ Potential issue | 🟡 MinorUse integer schema for
contact_idpath parameter.Line 7 currently allows non-integer numeric values. For ID semantics, this should be an integer type.
🛠️ Suggested schema fix
- name: contact_id in: path required: true schema: - type: number + type: integer + format: int64🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@swagger/paths/application/contacts/group_join_requests_handle.yml` around lines 3 - 8, The path parameter "contact_id" currently uses schema type: number which allows non-integer values; update the parameter definition for contact_id (the path param in group_join_requests_handle) to use an integer schema (e.g., type: integer, optionally with format: int64) so it enforces integer ID semantics in the OpenAPI spec.swagger/paths/application/contacts/group_members.yml-66-66 (1)
66-66:⚠️ Potential issue | 🟡 MinorParticipant phone format docs are inconsistent.
The text says “E.164 without
+”, while E.164 canonical examples include+, and this PR’s specs use+values. Please document a single accepted format (or explicitly state both are accepted).📝 Suggested doc adjustment
- description: Adds new participants to the WhatsApp group. Expects an array of phone numbers (E.164 format without the + prefix). + description: Adds new participants to the WhatsApp group. Expects E.164 phone numbers (for example, "+5511999999999"). - description: Phone numbers to add (e.g. ["5511999999999"]) + description: Phone numbers to add (e.g. ["+5511999999999"])Also applies to: 80-80
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@swagger/paths/application/contacts/group_members.yml` at line 66, The description for the group participant add endpoint is inconsistent about phone format; update the description string for the "Adds new participants..." endpoint and the related parameter (the phone numbers/participants parameter) to clearly state a single accepted E.164 format (either "E.164 with leading +, e.g. +14155552671" or "E.164 without +, e.g. 14155552671") — choose one to match the rest of the API spec and examples, or explicitly state both are accepted and show both examples; ensure the wording and examples in this description and the matching sibling description are updated to be identical and consistent with the PR specs.app/javascript/dashboard/components/widgets/conversation/TagGroupMembers.vue-97-99 (1)
97-99:⚠️ Potential issue | 🟡 MinorGuard
onSelectagainst empty selections.
onSelectcan emitundefinedwhen the list is empty or index is out of bounds, which may break consumers expecting a member object.✅ Suggested fix
const onSelect = () => { - emit('selectAgent', selectableItems.value[selectedIndex.value]); + const selected = selectableItems.value[selectedIndex.value]; + if (!selected) return; + emit('selectAgent', selected); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/javascript/dashboard/components/widgets/conversation/TagGroupMembers.vue` around lines 97 - 99, The onSelect handler can emit undefined when selectableItems is empty or selectedIndex is out of range; update onSelect to first check that selectableItems.value is non-empty and that selectedIndex.value is a valid index (>=0 and < selectableItems.value.length) before calling emit('selectAgent', ...); if the check fails, return early (or optionally emit a clear/no-op event) so consumers never receive undefined. Ensure you reference the existing symbols onSelect, selectableItems, selectedIndex, and the emit('selectAgent', ...) call when making the change.swagger/swagger.json-3616-3617 (1)
3616-3617:⚠️ Potential issue | 🟡 MinorRegister the new
Groupstag in top-level tag metadata.The
Groupstag is used in the API operations (e.g., line 3616) but is missing from the top-leveltagsdeclaration andx-tagGroupsgrouping. This causes weaker docs grouping and discoverability in OpenAPI UIs.🧭 Proposed fix
"tags": [ + { + "name": "Groups", + "description": "Group conversation management APIs" + }, { "name": "Accounts", "description": "Account management APIs" @@ { "name": "Application", "tags": [ "Account AgentBots", "Account", "Agents", "Audit Logs", "Canned Responses", "Contacts", "Contact Labels", "Conversation Assignments", "Conversation Labels", "Conversations", "Custom Attributes", "Custom Filters", + "Groups", "Inboxes", "Integrations", "Messages",🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@swagger/swagger.json` around lines 3616 - 3617, The OpenAPI document uses the "Groups" tag in operations but it's missing from the top-level tags and tag grouping; add a new tag entry named "Groups" to the top-level "tags" array (with a short description) and include "Groups" inside the appropriate "x-tagGroups" group so UIs can display it; update the top-level "tags" and "x-tagGroups" structures to reference the "Groups" tag (ensure the exact tag string "Groups" matches the operations).
app/controllers/api/v1/accounts/contacts/group_invites_controller.rb
Outdated
Show resolved
Hide resolved
app/controllers/api/v1/accounts/contacts/group_join_requests_controller.rb
Outdated
Show resolved
Hide resolved
- Fix nil safety in group_invites and group_join_requests controllers by replacing group_conversation.inbox.channel with @contact.group_channel - Add before_action guard in group_members_controller to validate contact is a group with identifier before create/update/destroy - Persist metadata locally in group_metadata_controller after provider calls (subject -> name, description -> additional_attributes) - Add server-side allow_group_creation? check in groups_controller - Add word boundary to mention regex to prevent matching inside words - Remove useless catch clauses in groupMembers store (try/finally only) - Default groupType to [] in customViewsHelper to prevent crash - Fix swagger parameter name mismatch (contact_id -> id) across all group endpoint YML files for consistency
PR Review Feedback — All Critical Items Addressed ✅All critical review comments from Copilot and CodeRabbit have been addressed in commit
Intentionally skipped:
|
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.
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:
This change is
Summary by CodeRabbit
Release Notes