## Linear Ticket
https://linear.app/chatwoot/issue/CW-4569/nomethoderror-undefined-method-blocked-for-nil-nomethoderror
## Description
Fixes NoMethodError in ConversationMuteHelpers that occurs during
contact deletion race condition.
When a contact is deleted, there's a brief window (~50-150ms) where
contact_id becomes nil but conversations still exist. If ResolutionJob
runs during this window, the muted? method crashes trying to call
blocked? on nil.Fixes # (issue)
## Type of change
- [ ] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
- Created orphaned conversations (contact_id = nil)
- Called muted?, mute!, unmute! - all return gracefully
- Verified async deletion still works correctly
## Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
Co-authored-by: Sojan Jose <sojan@pepalo.com>
CSAT scores are helpful, but on their own they rarely tell the full
story. A drop in rating can come from delayed timelines, unclear
expectations, or simple misunderstandings, even when the issue itself
was handled correctly.
Review Notes for CSAT let admins/report manager roles add internal-only
context next to each CSAT response. This makes it easier to interpret
scores properly and focus on patterns and root causes, not just numbers.
<img width="2170" height="1680" alt="image"
src="https://github.com/user-attachments/assets/56df7fab-d0a7-4a94-95b9-e4c459ad33d5"
/>
### Why this matters
* Capture the real context behind individual CSAT ratings
* Clarify whether a low score points to a genuine service issue or a
process gap
* Spot recurring themes across conversations and teams
* Make CSAT reviews more useful for leadership reviews and
retrospectives
### How Review Notes work
**View CSAT responses**
Open the CSAT report to see overall metrics, rating distribution, and
individual responses.
**Add a Review Note**
For any CSAT entry, managers can add a Review Note directly below the
customer’s feedback.
**Document internal insights**
Use Review Notes to capture things like:
* Why a score was lower or higher than expected
* Patterns you are seeing across similar cases
* Observations around communication, timelines, or customer expectations
Review Notes are visible only to administrators and people with report
access only. We may expand visibility to agents in the future based on
feedback. However, customers never see them.
Each note clearly shows who added it and when, making it easy to review
context and changes over time.
This API gives you how many conversations exist per channel, broken down
by status in a given time period. The max time period is capped to 6
months for now.
**Input Params:**
- **since:** Unix timestamp (seconds) - start of date range
- **until:** Unix timestamp (seconds) - end of date range
**Response Payload:**
```json
{
"Channel::Sms": {
"resolved": 85,
"snoozed": 10,
"open": 5,
"pending": 5,
"total": 100
},
"Channel::Email": {
"resolved": 72,
"snoozed": 15,
"open": 13,
"pending": 13,
"total": 100
},
"Channel::WebWidget": {
"resolved": 90,
"snoozed": 7,
"open": 3,
"pending": 3,
"total": 100
}
}
```
**Definitons:**
resolved = Number of conversations created within the selected time
period that are currently marked as resolved.
snoozed = Number of conversations created within the selected time
period that are currently marked as snoozed.
pending = Number of conversations created within the selected time
period that are currently marked as pending.
open = Number of conversations created within the selected time period
that are currently open.
total = Total number of conversations created within the selected time
period, across all statuses.
https://one.newrelic.com/alerts/issue?account=3437125&duration=259200000&state=d088e9b7-d0ce-3fcf-fda5-145df8b9cb2a
## Description
Pass serialized data instead of ActiveRecord object in
dispatch_destroy_event to prevent ActiveJob::DeserializationError when
the notification is already deleted.
This error occurs frequently because RemoveDuplicateNotificationJob
deletes notifications, and by the time the async EventDispatcherJob
runs, the record no longer exists.
## Type of change
- [ ] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
## Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> Avoids ActiveJob deserialization failures by sending serialized data
for notification deletion and updating the listener accordingly.
>
> - `Notification#dispatch_destroy_event` now dispatches
`NOTIFICATION_DELETED` with serialized `notification_data` (`id`,
`user_id`, `account_id`) instead of the AR object
> - `ActionCableListener#notification_deleted` reads
`notification_data`, finds `User`/`Account`, computes
`unread_count`/`count` via `NotificationFinder`, and broadcasts using
the user’s pubsub token
> - Specs updated to pass `notification_data` and assert payload
(including `unread_count`/`count`)
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
e2ffbe765b148fdfd2cd2e031c657c36e423c1f5. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
## Description
Adds the ability to sort companies by the number of contacts they have
(contacts_count) in ascending or descending order. This is part of the
Chatwoot 5.0 release requirements for the companies feature.
The implementation uses a scope-based approach consistent with other
sorting implementations in the codebase (e.g., contacts sorting by
last_activity_at).
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## Available Sorting Options
After this change, the Companies API supports the following sorting
options:
| Sort Field | Type | Ascending | Descending |
|------------|------|-----------|------------|
| `name` | string | `?sort=name` | `?sort=-name` |
| `domain` | string | `?sort=domain` | `?sort=-domain` |
| `created_at` | datetime | `?sort=created_at` | `?sort=-created_at` |
| `contacts_count` | integer (scope) | `?sort=contacts_count` |
`?sort=-contacts_count` |
**Note:** Prefix with `-` for descending order. Companies with NULL
contacts_count will appear last (NULLS LAST).
## CURL Examples
**Sort by contacts count (ascending):**
```bash
curl -X GET 'https://app.chatwoot.com/api/v1/accounts/{account_id}/companies?sort=contacts_count' \
-H 'api_access_token: YOUR_API_TOKEN'
```
**Sort by contacts count (descending):**
```bash
curl -X GET 'https://app.chatwoot.com/api/v1/accounts/{account_id}/companies?sort=-contacts_count' \
-H 'api_access_token: YOUR_API_TOKEN'
```
**Sort by name (ascending):**
```bash
curl -X GET 'https://app.chatwoot.com/api/v1/accounts/{account_id}/companies?sort=name' \
-H 'api_access_token: YOUR_API_TOKEN'
```
**Sort by created_at (descending):**
```bash
curl -X GET 'https://app.chatwoot.com/api/v1/accounts/{account_id}/companies?sort=-created_at' \
-H 'api_access_token: YOUR_API_TOKEN'
```
**With pagination:**
```bash
curl -X GET 'https://app.chatwoot.com/api/v1/accounts/{account_id}/companies?sort=-contacts_count&page=2' \
-H 'api_access_token: YOUR_API_TOKEN'
```
## How Has This Been Tested?
- Added RSpec tests for both ascending and descending sort
- All 24 existing specs pass
- Manually tested the sorting functionality with test data
**Test configuration:**
- Ruby 3.4.4
- Rails 7.1.5.2
- PostgreSQL (test database)
**To reproduce:**
1. Run `bundle exec rspec
spec/enterprise/controllers/api/v1/accounts/companies_controller_spec.rb`
2. All tests should pass (24 examples, 0 failures)
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
## Technical Details
**Backend changes:**
- Controller: Added `sort_on :contacts_count` with scope-based sorting
- Model: Added `order_on_contacts_count` scope using
`Arel::Nodes::SqlLiteral` and `sanitize_sql_for_order` with `NULLS LAST`
for consistent NULL handling
- Specs: Added 2 new tests for ascending/descending sort validation
**Files changed:**
- `enterprise/app/controllers/api/v1/accounts/companies_controller.rb`
- `enterprise/app/models/company.rb`
-
`spec/enterprise/controllers/api/v1/accounts/companies_controller_spec.rb`
**Note:** This PR only includes the backend implementation. Frontend
changes (sort menu UI + i18n) will follow in a separate commit.
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
## Description
Implements comprehensive search functionality with advanced filtering
capabilities for Chatwoot (Linear: CW-5956).
This PR adds:
1. **Time-based filtering** for contacts and conversations (SQL-based
search)
2. **Advanced message search** with multiple filters
(OpenSearch/Elasticsearch-based)
- **`from` filter**: Filter messages by sender (format: `contact:42` or
`agent:5`)
- **`inbox_id` filter**: Filter messages by specific inbox
- **Time range filters**: Filter messages using `since` and `until`
parameters (Unix timestamps in seconds)
- **90-day limit enforcement**: Automatically limits searches to the
last 90 days to prevent performance issues
The implementation extends the existing `Enterprise::SearchService`
module for advanced features and adds time filtering to the base
`SearchService` for SQL-based searches.
## API Documentation
### Base URL
All search endpoints follow this pattern:
```
GET /api/v1/accounts/{account_id}/search/{resource}
```
### Authentication
All requests require authentication headers:
```
api_access_token: YOUR_ACCESS_TOKEN
```
---
## 1. Search All Resources
**Endpoint:** `GET /api/v1/accounts/{account_id}/search`
Returns results from all searchable resources (contacts, conversations,
messages, articles).
### Parameters
| Parameter | Type | Description | Required |
|-----------|------|-------------|----------|
| `q` | string | Search query | Yes |
| `page` | integer | Page number (15 items per page) | No |
| `since` | integer | Unix timestamp (contacts/conversations only) | No
|
| `until` | integer | Unix timestamp (contacts/conversations only) | No
|
### Example Request
```bash
curl -X GET "https://app.chatwoot.com/api/v1/accounts/1/search?q=customer" \
-H "api_access_token: YOUR_ACCESS_TOKEN"
```
### Example Response
```json
{
"payload": {
"contacts": [...],
"conversations": [...],
"messages": [...],
"articles": [...]
}
}
```
---
## 2. Search Contacts
**Endpoint:** `GET /api/v1/accounts/{account_id}/search/contacts`
Search contacts by name, email, phone number, or identifier with
optional time filtering.
### Parameters
| Parameter | Type | Description | Required |
|-----------|------|-------------|----------|
| `q` | string | Search query | Yes |
| `page` | integer | Page number (15 items per page) | No |
| `since` | integer | Unix timestamp - filter by last_activity_at | No |
| `until` | integer | Unix timestamp - filter by last_activity_at | No |
### Example Requests
**Basic search:**
```bash
curl -X GET "https://app.chatwoot.com/api/v1/accounts/1/search/contacts?q=john" \
-H "api_access_token: YOUR_ACCESS_TOKEN"
```
**Search contacts active in the last 7 days:**
```bash
SINCE=$(date -v-7d +%s)
curl -X GET "https://app.chatwoot.com/api/v1/accounts/1/search/contacts?q=john&since=${SINCE}" \
-H "api_access_token: YOUR_ACCESS_TOKEN"
```
**Search contacts active between 30 and 7 days ago:**
```bash
SINCE=$(date -v-30d +%s)
UNTIL=$(date -v-7d +%s)
curl -X GET "https://app.chatwoot.com/api/v1/accounts/1/search/contacts?q=john&since=${SINCE}&until=${UNTIL}" \
-H "api_access_token: YOUR_ACCESS_TOKEN"
```
### Example Response
```json
{
"payload": {
"contacts": [
{
"id": 42,
"email": "john@example.com",
"name": "John Doe",
"phone_number": "+1234567890",
"identifier": "user_123",
"additional_attributes": {},
"created_at": 1701234567
}
]
}
}
```
---
## 3. Search Conversations
**Endpoint:** `GET /api/v1/accounts/{account_id}/search/conversations`
Search conversations by display ID, contact name, email, phone number,
or identifier with optional time filtering.
### Parameters
| Parameter | Type | Description | Required |
|-----------|------|-------------|----------|
| `q` | string | Search query | Yes |
| `page` | integer | Page number (15 items per page) | No |
| `since` | integer | Unix timestamp - filter by last_activity_at | No |
| `until` | integer | Unix timestamp - filter by last_activity_at | No |
### Example Requests
**Basic search:**
```bash
curl -X GET "https://app.chatwoot.com/api/v1/accounts/1/search/conversations?q=billing" \
-H "api_access_token: YOUR_ACCESS_TOKEN"
```
**Search conversations active in the last 24 hours:**
```bash
SINCE=$(date -v-1d +%s)
curl -X GET "https://app.chatwoot.com/api/v1/accounts/1/search/conversations?q=billing&since=${SINCE}" \
-H "api_access_token: YOUR_ACCESS_TOKEN"
```
**Search conversations from last month:**
```bash
SINCE=$(date -v-30d +%s)
UNTIL=$(date +%s)
curl -X GET "https://app.chatwoot.com/api/v1/accounts/1/search/conversations?q=billing&since=${SINCE}&until=${UNTIL}" \
-H "api_access_token: YOUR_ACCESS_TOKEN"
```
### Example Response
```json
{
"payload": {
"conversations": [
{
"id": 123,
"display_id": 45,
"inbox_id": 1,
"status": "open",
"messages": [...],
"meta": {...}
}
]
}
}
```
---
## 4. Search Messages (Advanced)
**Endpoint:** `GET /api/v1/accounts/{account_id}/search/messages`
Advanced message search with multiple filters powered by
OpenSearch/Elasticsearch.
### Prerequisites
- OpenSearch/Elasticsearch must be running (`OPENSEARCH_URL` env var
configured)
- Account must have `advanced_search` feature flag enabled
- Messages must be indexed in OpenSearch
### Parameters
| Parameter | Type | Description | Required |
|-----------|------|-------------|----------|
| `q` | string | Search query | Yes |
| `page` | integer | Page number (15 items per page) | No |
| `from` | string | Filter by sender: `contact:{id}` or `agent:{id}` |
No |
| `inbox_id` | integer | Filter by specific inbox ID | No |
| `since` | integer | Unix timestamp - searches from this time (max 90
days ago) | No |
| `until` | integer | Unix timestamp - searches until this time | No |
### Important Notes
- **90-Day Limit**: If `since` is not provided, searches default to the
last 90 days
- If `since` exceeds 90 days, returns `422` error: "Search is limited to
the last 90 days"
- All time filters use message `created_at` timestamp
### Example Requests
**Basic message search:**
```bash
curl -X GET "https://app.chatwoot.com/api/v1/accounts/1/search/messages?q=refund" \
-H "api_access_token: YOUR_ACCESS_TOKEN"
```
**Search messages from a specific contact:**
```bash
curl -X GET "https://app.chatwoot.com/api/v1/accounts/1/search/messages?q=refund&from=contact:42" \
-H "api_access_token: YOUR_ACCESS_TOKEN"
```
**Search messages from a specific agent:**
```bash
curl -X GET "https://app.chatwoot.com/api/v1/accounts/1/search/messages?q=refund&from=agent:5" \
-H "api_access_token: YOUR_ACCESS_TOKEN"
```
**Search messages in a specific inbox:**
```bash
curl -X GET "https://app.chatwoot.com/api/v1/accounts/1/search/messages?q=refund&inbox_id=3" \
-H "api_access_token: YOUR_ACCESS_TOKEN"
```
**Search messages from the last 7 days:**
```bash
SINCE=$(date -v-7d +%s)
curl -X GET "https://app.chatwoot.com/api/v1/accounts/1/search/messages?q=refund&since=${SINCE}" \
-H "api_access_token: YOUR_ACCESS_TOKEN"
```
**Search messages between specific dates:**
```bash
SINCE=$(date -v-30d +%s)
UNTIL=$(date -v-7d +%s)
curl -X GET "https://app.chatwoot.com/api/v1/accounts/1/search/messages?q=refund&since=${SINCE}&until=${UNTIL}" \
-H "api_access_token: YOUR_ACCESS_TOKEN"
```
**Combine all filters:**
```bash
SINCE=$(date -v-14d +%s)
curl -X GET "https://app.chatwoot.com/api/v1/accounts/1/search/messages?q=refund&from=contact:42&inbox_id=3&since=${SINCE}" \
-H "api_access_token: YOUR_ACCESS_TOKEN"
```
**Attempt to search beyond 90 days (returns error):**
```bash
SINCE=$(date -v-120d +%s)
curl -X GET "https://app.chatwoot.com/api/v1/accounts/1/search/messages?q=refund&since=${SINCE}" \
-H "api_access_token: YOUR_ACCESS_TOKEN"
```
### Example Response (Success)
```json
{
"payload": {
"messages": [
{
"id": 789,
"content": "I need a refund for my purchase",
"message_type": "incoming",
"created_at": 1701234567,
"conversation_id": 123,
"inbox_id": 3,
"sender": {
"id": 42,
"type": "contact"
}
}
]
}
}
```
### Example Response (90-day limit exceeded)
```json
{
"error": "Search is limited to the last 90 days"
}
```
**Status Code:** `422 Unprocessable Entity`
---
## 5. Search Articles
**Endpoint:** `GET /api/v1/accounts/{account_id}/search/articles`
Search help center articles by title or content.
### Parameters
| Parameter | Type | Description | Required |
|-----------|------|-------------|----------|
| `q` | string | Search query | Yes |
| `page` | integer | Page number (15 items per page) | No |
### Example Request
```bash
curl -X GET "https://app.chatwoot.com/api/v1/accounts/1/search/articles?q=installation" \
-H "api_access_token: YOUR_ACCESS_TOKEN"
```
### Example Response
```json
{
"payload": {
"articles": [
{
"id": 456,
"title": "Installation Guide",
"slug": "installation-guide",
"portal_slug": "help",
"account_id": 1,
"category_name": "Getting Started",
"status": "published",
"updated_at": 1701234567
}
]
}
}
```
---
## Technical Implementation
### SQL-Based Search (Contacts, Conversations, Articles)
- Uses PostgreSQL `ILIKE` queries by default
- Optional GIN index support via `search_with_gin` feature flag for
better performance
- Time filtering uses `last_activity_at` for contacts/conversations
- Returns paginated results (15 per page)
### Advanced Search (Messages)
- Powered by OpenSearch/Elasticsearch via Searchkick gem
- Requires `OPENSEARCH_URL` environment variable
- Requires `advanced_search` account feature flag
- Enforces 90-day lookback limit via
`Limits::MESSAGE_SEARCH_TIME_RANGE_LIMIT_DAYS`
- Validates inbox access permissions before filtering
- Returns paginated results (15 per page)
---
## Type of change
- [x] New feature (non-breaking change which adds functionality)
- [x] Enhancement (improves existing functionality)
---
## How Has This Been Tested?
### Unit Tests
- **Contact Search Tests**: 3 new test cases for time filtering
(`since`, `until`, combined)
- **Conversation Search Tests**: 3 new test cases for time filtering
- **Message Search Tests**: 10+ test cases covering:
- Individual filters (`from`, `inbox_id`, time range)
- Combined filters
- Permission validation for inbox access
- Feature flag checks
- 90-day limit enforcement
- Error handling for exceeded time limits
### Test Commands
```bash
# Run all search controller tests
bundle exec rspec spec/controllers/api/v1/accounts/search_controller_spec.rb
# Run search service tests (includes enterprise specs)
bundle exec rspec spec/services/search_service_spec.rb
```
### Manual Testing Setup
A rake task is provided to create 50,000 test messages across multiple
inboxes:
```bash
# 1. Create test data
bundle exec rake search:setup_test_data
# 2. Start OpenSearch
mise elasticsearch-start
# 3. Reindex messages
rails runner "Message.search_index.import Message.all"
# 4. Enable feature flag
rails runner "Account.first.enable_features('advanced_search')"
# 5. Test via API or Rails console
```
---
## Checklist
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [x] I have made corresponding changes to the documentation (this PR
description)
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---
## Additional Notes
### Requirements
- **OpenSearch/Elasticsearch**: Required for advanced message search
- Set `OPENSEARCH_URL` environment variable
- Example: `export OPENSEARCH_URL=http://localhost:9200`
- **Feature Flags**:
- `advanced_search`: Account-level flag for message advanced search
- `search_with_gin` (optional): Account-level flag for GIN-based SQL
search
### Performance Considerations
- 90-day limit prevents expensive long-range queries on large datasets
- GIN indexes recommended for high-volume search on SQL-based resources
- OpenSearch/Elasticsearch provides faster full-text search for messages
### Breaking Changes
- None. All new parameters are optional and backward compatible.
### Frontend Integration
- Frontend PR tracking advanced search UI will consume these endpoints
- Time range pickers should convert JavaScript `Date` to Unix timestamps
(seconds)
- Date conversion: `Math.floor(date.getTime() / 1000)`
### Error Handling
- Invalid `from` parameter format is silently ignored (filter not
applied)
- Time range exceeding 90 days returns `422` with error message
- Missing `q` parameter returns `422` (existing behavior)
- Unauthorized inbox access is filtered out (no error, just excluded
from results)
---------
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
When AgentBot responds to customer messages, the `waiting_since`
timestamp is not reset, causing inflated reply time metrics when a human
agent eventually responds. This results in inaccurate reporting that
incorrectly includes periods when customers were satisfied with bot
responses.
### Timeline from Production Data
```
Dec 12, 16:20:14 - Customer sends message (ID: 368451924)
↓ waiting_since = Dec 12, 16:20:14
Dec 12, 16:20:17 - AgentBot replies (ID: 368451960)
↓ waiting_since STILL = Dec 12, 16:20:14 ❌
↓ (Bot response doesn't clear it)
14-day gap - Customer satisfied, no messages
↓ waiting_since STILL = Dec 12, 16:20:14 ❌
Dec 26, 22:25:45 - Customer sends new message (ID: 383522275)
↓ waiting_since STILL = Dec 12, 16:20:14 ❌
↓ (New message doesn't reset it)
Dec 26-27 - More AgentBot interactions
↓ waiting_since STILL = Dec 12, 16:20:14 ❌
Dec 27, 07:36:53 - Human agent finally replies (ID: 383799517)
↓ Reply time calculated: 1,268,404 seconds
↓ = 14.7 DAYS ❌
```
## Root Cause
The core issues is in `app/models/message.rb`, where **AgentBot messages
does not clear `waiting_since`** - The `human_response?` method only
returns true for `User` senders, so bot replies never trigger the
clearing logic. This means once `waiting_since` is set, it stays set
even when customers send new messages after receiving bot responses.
The solution is to simply reset `waiting_since` **after a bot has
responded**. This ensures reply time metrics reflect actual human agent
response times, not bot-handled periods.
### What triggers the rest
This is an intentional "gotcha", that only `AgentBot` and
`Captain::Assistant` messages trigger the waiting time reset. Automation
and campaign messages maintain current behavior (no reset). This is
because interactive bot assistants provide conversational help that
might satisfy customers. Automation and campaigns are one-way
communications and shouldn't affect waiting time calculations.
## Related Work
Extends PR #11787 which fixed `waiting_since` clearing on conversation
resolution. This PR addresses the bot interaction scenario which was not
covered by that fix.
Scripts to clean data:
https://gist.github.com/scmmishra/bd133208e219d0ab52fbfdf03036c48a
Problem: SystemStackError: stack level too deep occurred when rendering
messages with indented content (4+ spaces) for WhatsApp, Instagram, and
Facebook channels.
Root Cause: CommonMarker::Renderer#code_block contains a self-recursive
placeholder that must be overridden:
```
def code_block(node)
code_block(node) # calls itself infinitely
end
```
WhatsAppRenderer and InstagramRenderer were missing this override,
causing infinite recursion when markdown with 4-space indentation
(interpreted as code blocks) was rendered.
Fix: Added code_block method to both renderers that outputs the node
content as plain text:
```
def code_block(node)
out(node.string_content)
end
```
Fix https://linear.app/chatwoot/issue/CW-6217/systemstackerror-stack-level-too-deep-systemstackerror
Previously, attachments relied only on blob_id, which made it possible
to attach blobs across accounts by enumerating IDs. We now require both
blob_id and blob_key, add cross-account validation to prevent blob
reuse, and centralize the logic in a shared BlobOwnershipValidation
concern.
It also fixes a frontend bug where mixed-type action params (number +
string) were incorrectly dropped, causing attachment uploads to fail.
fixes: #11834
This pull request introduces TikTok channel integration, enabling users
to connect and manage TikTok business accounts similarly to other
supported social channels. The changes span backend API endpoints,
authentication helpers, webhook handling, configuration, and frontend
components to support TikTok as a first-class channel.
**Key Notes**
* This integration is only compatible with TikTok Business Accounts
* Special permissions are required to access the TikTok [Business
Messaging
API](https://business-api.tiktok.com/portal/docs?id=1832183871604753).
* The Business Messaging API is region-restricted and is currently
unavailable to users in the EU.
* Only TEXT, IMAGE, and POST_SHARE messages are currently supported due
to limitations in the TikTok Business Messaging API
* A message will be successfully sent only if it contains text alone or
one image attachment. Messages with multiple attachments or those
combining text and attachments will fail and receive a descriptive error
status.
* Messages sent directly from the TikTok App will be synced into the
system
* Initiating a new conversation from the system is not permitted due to
limitations from the TikTok Business Messaging API.
**Backend: TikTok Channel Integration**
* Added `Api::V1::Accounts::Tiktok::AuthorizationsController` to handle
TikTok OAuth authorization initiation, returning the TikTok
authorization URL.
* Implemented `Tiktok::CallbacksController` to handle TikTok OAuth
callback, process authorization results, create or update channel/inbox,
and handle errors or denied scopes.
* Added `Webhooks::TiktokController` to receive and verify TikTok
webhook events, including signature verification and event dispatching.
* Created `Tiktok::IntegrationHelper` module for JWT-based token
generation and verification for secure TikTok OAuth state management.
**Configuration and Feature Flags**
* Added TikTok app credentials (`TIKTOK_APP_ID`, `TIKTOK_APP_SECRET`) to
allowed configs and app config, and registered TikTok as a feature in
the super admin features YAML.
[[1]](diffhunk://#diff-5e46e1d248631a1147521477d84a54f8ba6846ea21c61eca5f70042d960467f4R43)
[[2]](diffhunk://#diff-8bf37a019cab1dedea458c437bd93e34af1d6e22b1672b1d43ef6eaa4dcb7732R69)
[[3]](diffhunk://#diff-123164bea29f3c096b0d018702b090d5ae670760c729141bd4169a36f5f5c1caR74-R79)
**Frontend: TikTok Channel UI and Messaging Support**
* Added `TiktokChannel` API client for frontend TikTok authorization
requests.
* Updated channel icon mappings and tests to include TikTok
(`Channel::Tiktok`).
[[1]](diffhunk://#diff-b852739ed45def61218d581d0de1ba73f213f55570aa5eec52aaa08f380d0e16R16)
[[2]](diffhunk://#diff-3cd3ae32e94ef85f1f2c4435abf0775cc0614fb37ee25d97945cd51573ef199eR64-R69)
* Enabled TikTok as a supported channel in contact forms, channel
widgets, and feature toggles.
[[1]](diffhunk://#diff-ec59c85e1403aaed1a7de35971fe16b7033d5cd763be590903ebf8f1ca25a010R47)
[[2]](diffhunk://#diff-ec59c85e1403aaed1a7de35971fe16b7033d5cd763be590903ebf8f1ca25a010R69)
[[3]](diffhunk://#diff-725b90ca7e3a6837ec8291e9f57094f6a46b3ee00e598d16564f77f32cf354b0R26-R29)
[[4]](diffhunk://#diff-725b90ca7e3a6837ec8291e9f57094f6a46b3ee00e598d16564f77f32cf354b0R51-R54)
[[5]](diffhunk://#diff-725b90ca7e3a6837ec8291e9f57094f6a46b3ee00e598d16564f77f32cf354b0R68)
* Updated message meta logic to support TikTok-specific message statuses
(sent, delivered, read).
[[1]](diffhunk://#diff-e41239cf8dda36c1bd1066dbb17588ae8868e56289072c74b3a6d7ef5abdd696R23)
[[2]](diffhunk://#diff-e41239cf8dda36c1bd1066dbb17588ae8868e56289072c74b3a6d7ef5abdd696L63-R65)
[[3]](diffhunk://#diff-e41239cf8dda36c1bd1066dbb17588ae8868e56289072c74b3a6d7ef5abdd696L81-R84)
[[4]](diffhunk://#diff-e41239cf8dda36c1bd1066dbb17588ae8868e56289072c74b3a6d7ef5abdd696L103-R107)
* Added support for embedded message attachments (e.g., TikTok embeds)
with a new `EmbedBubble` component and updated message rendering logic.
[[1]](diffhunk://#diff-c3d701caf27d9c31e200c6143c11a11b9d8826f78aa2ce5aa107470e6fdb9d7fR31)
[[2]](diffhunk://#diff-047859f9368a46d6d20177df7d6d623768488ecc38a5b1e284f958fad49add68R1-R19)
[[3]](diffhunk://#diff-c3d701caf27d9c31e200c6143c11a11b9d8826f78aa2ce5aa107470e6fdb9d7fR316)
[[4]](diffhunk://#diff-cbc85e7c4c8d56f2a847d0b01cd48ef36e5f87b43023bff0520fdfc707283085R52)
* Adjusted reply policy and UI messaging for TikTok's 48-hour reply
window.
[[1]](diffhunk://#diff-0d691f6a983bd89502f91253ecf22e871314545d1e3d3b106fbfc76bf6d8e1c7R208-R210)
[[2]](diffhunk://#diff-0d691f6a983bd89502f91253ecf22e871314545d1e3d3b106fbfc76bf6d8e1c7R224-R226)
These changes collectively enable end-to-end TikTok channel support,
from configuration and OAuth flow to webhook processing and frontend
message handling.
------------
# TikTok App Setup & Configuration
1. Grant access to the Business Messaging API
([Documentation](https://business-api.tiktok.com/portal/docs?id=1832184145137922))
2. Set the app authorization redirect URL to
`https://FRONTEND_URL/tiktok/callback`
3. Update the installation config with TikTok App ID and Secret
4. Create a Business Messaging Webhook configuration and set the
callback url to `https://FRONTEND_URL/webhooks/tiktok`
([Documentation](https://business-api.tiktok.com/portal/docs?id=1832190670631937))
. You can do this by calling
`Tiktok::AuthClient.update_webhook_callback` from rails console once you
finish Tiktok channel configuration in super admin ( will be automated
in future )
5. Enable TikTok channel feature in an account
---------
Co-authored-by: Sojan Jose <sojan@pepalo.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Extracted the backend changes from
https://github.com/chatwoot/chatwoot/pull/13040
- Added the support for saving `conversation_required_attributes` in
account
- Delete `conversation_required_attributes` if custom attribute deleted.
Co-authored-by: Vinay Keerthi <11478411+stonecharioteer@users.noreply.github.com>
- Linear::CallbacksController: Replace broken
`described_class.new`mocking with proper `GlobalConfigService` stubbing
and real JWT token generation. The old pattern doesn't work in request
specs since Rails instantiates controllers internally.
- User associations: Remove `.class_name('Conversation')` assertion that
fails intermittently due to enterprise `prepend_mod_with` timing in
parallel tests. The class_name is already enforced by Rails at runtime -
if wrong, the app would crash immediately. No need to explicitly test
for this
Fixes
https://linear.app/chatwoot/issue/CW-6138/debug-linear-and-user-spec-failures-in-ci
* fix: z-api read messages
* fix: use threads for parallel requests
* feat: use jobs instead
* fix: refactor ZapiReadMessageJob to use service method for sending read messages
## Summary
Fixes the issue where double newlines (paragraph breaks) were collapsing
to single newlines in text-based messaging channels (Telegram, WhatsApp,
Instagram, Facebook, LINE, SMS).
### Root Cause
The `preserve_multiple_newlines` method only preserved 3+ consecutive
newlines using the regex `/\n{3,}/`. When users pressed Enter twice
(creating a paragraph break with 2 newlines), CommonMarker would parse
this as separate paragraphs, which then collapsed to a single newline in
the output.
This caused:
- ❌ Normal Enter: Double newlines collapsed to single newline
- ✅ Shift+Enter: Worked (created hard breaks)
### Fix
Changed the regex from `/\n{3,}/` to `/\n{2,}/` to preserve 2+
consecutive newlines. This prevents CommonMarker from collapsing
paragraph breaks.
Now:
- ✅ Single newline (`\n`) → Single newline (handled by softbreak)
- ✅ Double newline (`\n\n`) → Double newline (preserved with
placeholders)
- ✅ Triple+ newlines → Preserved as before
### Test Coverage
Added comprehensive tests for:
- Single newlines preservation
- Double newlines (paragraph breaks) preservation
- Multiple consecutive newlines
- Newlines with varying amounts of whitespace between them (1 space, 3
spaces, 5 spaces, tabs)
All 66 tests passing.
### Impact
This fix affects all text-based messaging channels that use the markdown
renderer:
- Telegram
- WhatsApp
- Instagram
- Facebook
- LINE
- SMS
- Twilio SMS (when configured for WhatsApp)
Fixes
https://linear.app/chatwoot/issue/CW-6135/double-newline-is-breaking
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This PR add the backend changes for the feature [sending CSAT surveys
via WhatsApp message templates
](https://github.com/chatwoot/chatwoot/pull/12787)
---------
Co-authored-by: Vinay Keerthi <11478411+stonecharioteer@users.noreply.github.com>
## Description
Fixes an issue where multiple newlines with whitespace between them
(e.g., `\n \n \n`) were being collapsed to single newlines in text-based
messaging channels (Telegram, WhatsApp, Instagram, Facebook, Line, SMS).
The frontend was sending messages with spaces/tabs between newlines, and
the markdown renderer was treating these as paragraph content,
collapsing them during rendering.
### Changes:
1. Added whitespace normalization in `render_telegram_html`,
`render_whatsapp`, `render_instagram`, `render_line`, and
`render_plain_text` methods
2. Strips whitespace from whitespace-only lines before markdown
processing
3. Added comprehensive regression tests for all affected channels
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
1. **Unit Tests**: Added 7 new specs testing multiple newlines with
whitespace between them for all text-based channels
2. **Manual Testing**: Verified with actual frontend payload containing
`\n \n \n` patterns
3. **Regression Testing**: All existing 63 specs pass
### Test Results:
- ✅ All 63 markdown renderer specs pass (56 original + 7 new)
- ✅ All 12 Telegram channel specs pass
- ✅ All 27 WhatsApp + Instagram specs pass
- ✅ Verified with real-world payload: 18 newlines preserved (previously
collapsed to 1)
### Test Command:
```bash
RAILS_ENV=test bundle exec rspec spec/services/messages/markdown_renderer_service_spec.rb
RAILS_ENV=test bundle exec rspec spec/models/channel/telegram_spec.rb
```
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective
- [x] New and existing unit tests pass locally with my changes
This PR fixes the HTTP 500 timeout errors occurring when deleting SLA
policies that have large volumes of historical data.
The fix moves the deletion workflow to asynchronous background
processing using the existing `DeleteObjectJob`.
By offloading heavy cascaded deletions (applied SLAs, SLA events,
conversation nullifications) from the request cycle, the API can now
return immediately while the cleanup continues in the background
avoiding the `Rack::Timeout::RequestTimeoutException`. This ensures that
SLA policies can be deleted reliably, regardless of data size.
### Problem
Deleting an SLA policy via `DELETE
/api/v1/accounts/{account_id}/sla_policies/{id}` fails consistently with
`Rack::Timeout::RequestTimeoutException (15s)` for policies with large
amounts of related data.
Because the current implementation performs all dependent deletions
**synchronously**, Rails processes:
- `has_many :applied_slas, dependent: :destroy` (thousands)
- Each `AppliedSla#destroy` → triggers destruction of many `SlaEvent`
records
- `has_many :conversations, dependent: :nullify` (thousands)
This processing far exceeds the Rack timeout window and consistently
triggers HTTP 500 errors for users.
### Solution
This PR applies the same pattern used successfully in Inbox deletion.
**Move deletion to async background jobs**
- Uses `DeleteObjectJob` for centralized, reliable cleanup.
- Allows the DELETE API call to respond immediately.
**Chunk large datasets**
- Records are processed in **batches of 5,000** to reduce DB load and
avoid job timeouts.
## Description
This PR fixes an issue where multiple consecutive newlines (blank lines
for visual spacing) were being collapsed in text-based messaging
channels like WhatsApp, Instagram, and SMS.
When users send messages via API with intentional spacing using multiple
newlines (e.g., `\n\n\n\n`), the markdown renderer was following
standard Markdown spec and collapsing them into single blank lines.
While this is correct for document formatting, messaging platforms like
WhatsApp and Instagram support and preserve multiple blank lines for
visual spacing.
The fix adds preprocessing to preserve multiple consecutive newlines
(3+) by converting them to placeholder tokens before CommonMarker
processing, then restoring the exact number of newlines in the final
output.
## Changes
- Added `preserve_multiple_newlines` and `restore_multiple_newlines`
helper methods to `MarkdownRendererService`
- Updated `render_whatsapp` to preserve multiple consecutive newlines
- Updated `render_instagram` to preserve multiple consecutive newlines
- Updated `render_plain_text` (affects SMS, Twilio SMS, Twitter) to
preserve multiple consecutive newlines
- Updated `render_line` to preserve multiple consecutive newlines
- HTML-based renderers (Email, Telegram, WebWidget) remain unchanged as
they handle spacing via HTML tags
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update
## How Has This Been Tested?
Added comprehensive test coverage:
- 3 new tests for multi-newline preservation across WhatsApp, Instagram,
and SMS channels
- All 56 tests passing (up from 53)
Testing scenarios:
- Single newlines preserved: `"Line 1\nLine 2"` remains `"Line 1\nLine
2"`
- Multiple newlines preserved: `"Para 1\n\n\n\nPara 2"` remains `"Para
1\n\n\n\nPara 2"`
- Standard paragraph breaks (2 newlines) work as before
- Markdown formatting (bold, italic, links) continues to work correctly
- Backward compatibility maintained for all channels
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
## Description
This PR fixes an issue where Twilio WhatsApp messages were losing
newlines and markdown formatting. The problem had two root causes:
1. Text-based renderers (WhatsApp, Instagram, SMS) were converting
newlines to spaces when processing plain text without markdown list
markers
2. Twilio WhatsApp channels were incorrectly using the plain text
renderer instead of the WhatsApp renderer, stripping all markdown
formatting
The fix updates the markdown rendering system to:
- Preserve newlines by overriding the `softbreak` method in WhatsApp,
Instagram, and PlainText renderers
- Detect Twilio WhatsApp channels (via the `medium` field) and route
them to use the WhatsApp renderer
- Maintain backward compatibility with existing code
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update
## How Has This Been Tested?
Added comprehensive test coverage:
- 3 new tests for newline preservation in WhatsApp, Instagram, and SMS
channels
- 4 new tests for Twilio WhatsApp specific behavior (medium detection,
formatting preservation, backward compatibility)
- All 53 tests passing (up from 50)
Manual testing verified:
- Twilio WhatsApp messages with plain text preserve newlines
- Twilio WhatsApp messages with markdown preserve formatting (bold,
italic, links)
- Regular WhatsApp, Instagram, and SMS channels continue to work
correctly
- Backward compatibility maintained when channel parameter is not
provided
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
# Pull Request Template
## Description
This PR includes,
1. **Channel-specific formatting and menu options** for the rich reply
editor.
2. **Removal of the plain reply editor** and full **standardization** on
the rich reply editor across all channels.
3. **Fix for multiple canned responses insertion:**
* **Before:** The plain editor only allowed inserting canned responses
at the beginning of a message, making it impossible to combine multiple
canned responses in a single reply. This caused inconsistent behavior
across the app.
* **Solution:** Replaced the plain reply editor with the rich
(ProseMirror) editor to ensure a unified experience. Agents can now
insert multiple canned responses at any cursor position.
4. **Floating editor menu** for the reply box to improve accessibility
and overall user experience.
5. **New Strikethrough formatting option** added to the editor menu.
---
**Editor repo PR**:
https://github.com/chatwoot/prosemirror-schema/pull/36
Fixes https://github.com/chatwoot/chatwoot/issues/12517,
[CW-5924](https://linear.app/chatwoot/issue/CW-5924/standardize-the-editor),
[CW-5679](https://linear.app/chatwoot/issue/CW-5679/allow-inserting-multiple-canned-responses-in-a-single-message)
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
### Screenshot
**Dark**
<img width="850" height="345" alt="image"
src="https://github.com/user-attachments/assets/47748e6c-380f-44a3-9e3b-c27e0c830bd0"
/>
**Light**
<img width="850" height="345" alt="image"
src="https://github.com/user-attachments/assets/6746cf32-bf63-4280-a5bd-bbd42c3cbe84"
/>
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
Co-authored-by: Vinay Keerthi <11478411+stonecharioteer@users.noreply.github.com>
## Description
- Replaces Stripe Checkout session flow with direct card charging for AI
credit top-ups
- Adds a two-step confirmation modal (select package → confirm purchase)
for better UX
- Creates Stripe invoice directly and charges the customer's default
payment method immediately
## Type of change
- [ ] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
- Using the specs
- UI manual test cases
<img width="945" height="580" alt="image"
src="https://github.com/user-attachments/assets/52bdad46-cd0e-4927-b13f-54c6b6353bcc"
/>
<img width="945" height="580" alt="image"
src="https://github.com/user-attachments/assets/231bc7e9-41ac-440d-a93d-cba45a4d3e3e"
/>
## Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
We’ve been watching Sidekiq workers climb from ~600 MB at boot to
1.4–1.5 GB after an hour whenever attachment-heavy jobs run. This PR is
an experiment to curb that growth by streaming attachments instead of
loading the whole blob into Ruby: reply-mailer inline attachments,
Telegram uploads, and audio transcriptions now read/write in chunks. If
this keeps RSS stable in production we’ll keep it; otherwise we’ll roll
it back and keep digging
## 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 false new relic alerts set due to hardcoding an error code
## Type of change
Bug fix (non-breaking change which fixes an issue)
## 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.
Before
<img width="776" height="666" alt="image"
src="https://github.com/user-attachments/assets/f086890d-eaf1-4e83-b383-fe3675b24159"
/>
the 500 was hardcoded.
RubyLLM doesn't send any error codes, so i removed the error code
argument and just pass the error message
Langfuse gets just the error message
<img width="883" height="700" alt="image"
src="https://github.com/user-attachments/assets/fc8c3907-b9a5-4c87-bfc6-8e05cfe9c8b0"
/>
local logs only show error
<img width="1434" height="200" alt="image"
src="https://github.com/user-attachments/assets/716c6371-78f0-47b8-88a4-03e4196c0e9a"
/>
Better fix is to handle each case and show the user wherever necessary
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: aakashb95 <aakash@chatwoot.com>
* fix: enhance filename extraction for document messages with captions
* test: add specs for filename extraction from document messages with and without captions
* fix: update media URL stubbing for multiple message types in filename extraction tests
## Description
When a user tries creating a new account through the Super Admin
dashboard, and they forget to fill in the account name, they're faced
with an ugly error (generic "Something went wrong" on production).
This PR simply adds the `validates :name, presence: true` model
validation on `Account` model, which is translated as a proper error
message on the Super Admin UI.
- Enables outbound voice calls in voice channel . We are only caring
about wiring the logic to trigger outgoing calls to the call button
introduced in previous PRs. We will connect it to call component in
subsequent PRs
ref: #11602
## Screens
<img width="2304" height="1202" alt="image"
src="https://github.com/user-attachments/assets/b91543a8-8d4e-4229-bd80-9727b42c7b0f"
/>
<img width="2304" height="1200" alt="image"
src="https://github.com/user-attachments/assets/1a1dad2a-8cb2-4aa2-9702-c062416556a7"
/>
---------
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Vishnu Narayanan <vishnu@chatwoot.com>
* fix: do not toggle typing status when typing private message
* feat: toggle typing status off when sending message
* Revert "fix: do not toggle typing status when typing private message"
This reverts commit 3c74b7c5df0960be37c3e8726c2538ae8ebc4d60.
* fix: ensure typing off event is triggered for all message types
* fix: add success status expectation for typing off events in message controller specs
* fix: ensure typing off event is dispatched correctly for both private and non-private messages
This PR adds LLM instrumentation on langfuse for ai-editor feature
## Type of change
New feature (non-breaking change which adds functionality)
Needs langfuse account and env vars to be set
## How Has This Been Tested?
I configured personal langfuse credentials and instrumented the app,
traces can be seen in langfuse.
each conversation is one session.
<img width="1683" height="714" alt="image"
src="https://github.com/user-attachments/assets/3fcba1c9-63cf-44b9-a355-fd6608691559"
/>
<img width="1446" height="172" alt="image"
src="https://github.com/user-attachments/assets/dfa6e98f-4741-4e04-9a9e-078d1f01e97b"
/>
## Checklist:
- [x ] My code follows the style guidelines of this project
- [ x] I have performed a self-review of my code
- [ x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: aakashb95 <aakash@chatwoot.com>
Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
* chore: Hide "Learn More" button in feature spotlight for self-hosted (#12675)
* feat: single query for reporting event stats (#12664)
This PR collapses multiple queries fetching stats from a single table to
a single query
```sql
SELECT
user_id as user_id,
COUNT(CASE WHEN name = 'conversation_resolved' THEN 1 END) as resolved_count,
AVG(CASE WHEN name = 'conversation_resolved' THEN value END) as avg_resolution_time,
AVG(CASE WHEN name = 'first_response' THEN value END) as avg_first_response_time,
AVG(CASE WHEN name = 'reply_time' THEN value END) as avg_reply_time
FROM "reporting_events"
WHERE
"reporting_events"."account_id" = <account_id> AND
"reporting_events"."created_at" >= '2025-09-14 18:30:00' AND
"reporting_events"."created_at" < '2025-10-14 18:29:59'
GROUP BY "reporting_events"."user_id";
```
### Why this works?
Here's why this optimization is faster based on PostgreSQL internals:
- Single Table Scan vs Multiple Scans: Earlier we did 4 sequential scans
(or 4 index scans) of the same data, with the same where clause, now in
a single scan all 4 `CASE` expressions are evaluated in a single pass.
- Shared Buffer Cache Efficiency: PostgreSQL's shared buffer cache
stores recently accessed pages, with this, pages are loaded once and
re-used for all aggregation, earlier with separate queries we were
forced to re-read all from the disk each time
- Reduced planning and network overhead (4 vs 1 query)
### How is it tested
1. The specs all pass without making any changes
2. Verified the reports side by side after generating from report seeder
#### How to test
Generate seed data using the following command
```bash
ACCOUNT_ID=1 ENABLE_ACCOUNT_SEEDING=true bundle exec rake db:seed:reports_data
```
Once done download the reports, checkout to this branch and download the
reports again and compare them
* chore: Update translations (#12625)
* chore: Migrate mailers from the worker to jobs (#12331)
Previously, email replies were handled inside workers. There was no
execution logs. This meant if emails silently failed (as reported by a
customer), we had no way to trace where the issue happened, the only
assumption was “no error = mail sent.”
By moving email handling into jobs, we now have proper execution logs
for each attempt. This makes it easier to debug delivery issues and
would have better visibility when investigating customer reports.
Fixes
https://linear.app/chatwoot/issue/CW-5538/emails-are-not-sentdelivered-to-the-contact
---------
Co-authored-by: Sojan Jose <sojan@pepalo.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
* chore(deps-dev): bump vite from 5.4.20 to 5.4.21 (#12700)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite)
from 5.4.20 to 5.4.21.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/releases">vite's
releases</a>.</em></p>
<blockquote>
<h2>v5.4.21</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v5.4.21/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/blob/v5.4.21/packages/vite/CHANGELOG.md">vite's
changelog</a>.</em></p>
<blockquote>
<h2><!-- raw HTML omitted -->5.4.21 (2025-10-20)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix(dev): trim trailing slash before <code>server.fs.deny</code>
check (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/20968">#20968</a>)
(<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/20970">#20970</a>)
(<a
href="cad1d31d06">cad1d31</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/20968">#20968</a>
<a
href="https://redirect.github.com/vitejs/vite/issues/20970">#20970</a></li>
<li>chore: update CHANGELOG (<a
href="ca88ed7398">ca88ed7</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="adce3c22c6"><code>adce3c2</code></a>
release: v5.4.21</li>
<li><a
href="cad1d31d06"><code>cad1d31</code></a>
fix(dev): trim trailing slash before <code>server.fs.deny</code> check
(<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/20968">#20968</a>)
(<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/20970">#20970</a>)</li>
<li><a
href="ca88ed7398"><code>ca88ed7</code></a>
chore: update CHANGELOG</li>
<li>See full diff in <a
href="https://github.com/vitejs/vite/commits/v5.4.21/packages/vite">compare
view</a></li>
</ul>
</details>
<br />
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
<details>
<summary>Dependabot commands and options</summary>
<br />
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chatwoot/chatwoot/network/alerts).
</details>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* chore: Update translations (#12708)
* chore(sidekiq): log ActiveJob class and job_id on dequeue (#12704)
## Context
Sidekiq logs only showed the Sidekiq wrapper class and JID, which wasn’t
helpful when debugging ActiveJobs.
## Changes
- Updated `ChatwootDequeuedLogger` to log the actual `ActiveJob class`
and `job_id` instead of the generic Sidekiq wrapper and JID.
> Example
> ```
> Dequeued ActionMailer::MailDeliveryJob
123e4567-e89b-12d3-a456-426614174000 from default
> ```
- Remove sidekiq worker and unify everything to `ActiveJob`
* chore: Enforce custom role permissions on conversation access (#12583)
## Summary
- ensure conversation lookup uses the permission filter before fetching
records
- add request specs covering custom role access to unassigned
conversations
## Testing
- bundle exec rspec
spec/enterprise/controllers/api/v1/accounts/conversations_controller_spec.rb
------
https://chatgpt.com/codex/tasks/task_e_68de1f62b9b883268a54882e608a8bb8
* fix: parameterize agent name (#12709)
* chore: Remove channel icons from the create inbox page (#12727)
# Pull Request Template
## Description
This PR removes the frame containing all channel icons from the “Create
Inbox” page.
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
### Screenshots
**Before**
<img width="1314" height="1016" alt="image"
src="https://github.com/user-attachments/assets/2b773495-9ddb-48b4-b15d-9aef18259ce1"
/>
**After**
<img width="1314" height="979" alt="image"
src="https://github.com/user-attachments/assets/f4dc64cf-516c-4faf-a45c-2f7de05cc29b"
/>
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
* fix: Use gap-4 instead of margins to define space between elements (#12728)
We should avoid using margins to define space between elements, instead
use the gap utility.
The problem with this particular instance was that if Google auth was
turned off and SSO is available, there is a weird spacing at the top
caused by the margin from the SSO element.
This PR will fix that. It also introduces a gap between the divider and
the button, but that should be okay.
* feat(ee): Add a service to fetch website content and prepare a persona of Captain Assistant (#12732)
This PR is the first of many to simplify the process of building an
assistant. The new flow will only require the user’s website. We’ll
automatically crawl it, identify the business name and what the business
does, and then generate a suggested assistant persona, complete with a
proposed name and description.
This service returns the following.
Example: tooljet.com
<img width="795" height="217" alt="Screenshot 2025-10-25 at 2 55 04 PM"
src="https://github.com/user-attachments/assets/9cb3594a-9c9c-4970-a0a1-4c9c8869c193"
/>
Example: replit.com
<img width="797" height="176" alt="Screenshot 2025-10-25 at 2 56 42 PM"
src="https://github.com/user-attachments/assets/6a1b4266-aab6-455f-a5e3-696d3a8243c9"
/>
* chore: Adds URL-based search and tab selection (#12663)
# Pull Request Template
## Description
This PR enables URL-based search and tab selection, allowing search
queries and active tabs to persist in the URL for easy sharing.
Fixes
[CW-5766](https://linear.app/chatwoot/issue/CW-5766/cannot-impersonate-an-account),
https://github.com/chatwoot/chatwoot/issues/12623
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
### Loom video
https://www.loom.com/share/422a1d61f3fe4278a88e352ef98d2b78?sid=35fabee7-652f-4e17-83bd-e066a3bb804c
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
* chore: Add tab params for inbox configuration (#12665)
# Pull Request Template
## Description
This PR enables active tabs in inbox settings to persist in the URL for
easy sharing.
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
### Loom video
https://www.loom.com/share/63820ecb17ea491a9082339f8bb457b6?sid=4fef1acd-b4fd-431f-855c-7647015a330f
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Muhsin <muhsinkeramam@gmail.com>
* feat: Changelog card components (#12673)
# Pull Request Template
## Description
This PR introduces a new changelog component that can be used in the
sidebar.
Fixes
https://linear.app/chatwoot/issue/CW-5776/changelog-card-ui-component
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
### Screencast
https://github.com/user-attachments/assets/42e77e82-388a-4fc9-9b37-f3d0ea1a9d7f
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Muhsin <muhsinkeramam@gmail.com>
* chore: Remove linear integration feature flag (#12716)
This PR removes the linear integration feature flag since the
integration is pretty much stable and we do display the Linear CTA for
users who aren't connected.
Fixes
https://linear.app/chatwoot/issue/CW-5819/remove-linear-feature-flag-from-front-end
* chore: Update translations (#12722)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
* perf: Add database index on conversations identifier (#12715)
**Problem**
Slack webhook processing was failing with 500 errors due to database
timeouts. The query `Conversation.where(identifier:
params[:event][:thread_ts]).first` was performing full table scans and
hitting PostgreSQL statement timeout.
**Solution**
Added database index on conversations.identifier and account_id.
* fix: Extend phone number normalization to Twilio WhatsApp (#12655)
### Problem
WhatsApp Cloud channels already handle Brazil/Argentina phone number
format mismatches (PRs #12492, #11173), but Twilio WhatsApp channels
were creating duplicate contacts
when:
- Template sent to new format: `whatsapp:+5541988887777` (13 digits)
- User responds from old format: `whatsapp:+554188887777` (12 digits)
### Solution
The solution extends the existing phone number normalization
infrastructure to support both WhatsApp providers while handling their
different payload formats:
### Provider Format Differences
- **WhatsApp Cloud**: `wa_id: "919745786257"` (clean number)
- **Twilio WhatsApp**: `From: "whatsapp:+919745786257"` (prefixed
format)
### Test Coverage
#### Brazil Phone Number Tests
**Case 1: New Format (13 digits with "9")**
- **Test 1**: No existing contact → Creates new contact with original
format
- **Test 2**: Contact exists in same format → Appends to existing
conversation
**Case 2: Old Format (12 digits without "9")**
- **Test 3**: Contact exists in old format → Appends to existing
conversation
- **Test 4** *(Critical)*: Contact exists in new format, message in old
format → Finds existing contact, prevents duplicate
- **Test 5**: No contact exists → Creates new contact with incoming
format
#### Argentina Phone Number Tests
**Case 3: With "9" after country code**
- **Test 6**: No existing contact → Creates new contact
- **Test 7**: Contact exists in normalized format → Uses existing
contact
**Case 4: Without "9" after country code**
- **Test 8**: Contact exists in same format → Appends to existing
- **Test 9**: No contact exists → Creates new contact
Fixes
https://linear.app/chatwoot/issue/CW-5565/inconsistencies-for-mobile-numbersargentina-brazil-and-mexico-numbers
* fix: Timezone offset reports broken by DST transition (#12747)
## Description
Fixes timezone offset parameter in V2 reports API that was broken by DST
transitions. The issue occurred when UK DST ended on October 26, 2025,
causing the test to fail starting October 27th.
~~**Initial diagnosis:** The root cause was that
`timezone_name_from_offset` used `zone.now.utc_offset` to match
timezones, which changes based on the current date's DST status rather
than the data being queried.~~
**Actual root cause:** The test was accidentally passing before DST
transition. During BST, `timezone_name_from_offset(0)` matched "Azores"
(UTC-1) instead of "Edinburgh" (UTC+0), and the -1 hour offset
coincidentally split midnight data into [1,5]. After DST ended, it
correctly matched "Edinburgh" (UTC+0), but this grouped all
conversations into one day [6], exposing that the test data was flawed.
The real issue: Test data created all 6 conversations starting at
midnight on a single day, which cannot produce a [1,5] split in true
UTC.
Fixes CW-5846
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update
## How Has This Been Tested?
**Test that was failing:**
```bash
bundle exec rspec spec/controllers/api/v2/accounts/reports_controller_spec.rb:25
```
**Changes:**
~~1. Fixed `timezone_name_from_offset` to use January 1st as reference
date instead of current date~~
~~2. Converted timezone string to `ActiveSupport::TimeZone` object for
`group_by_period` compatibility~~
**Revised approach:**
1. Freeze test time to January 2024 using `travel_to`, making timezone
matching deterministic and aligned with test data period
2. Start test conversations at 23:00 instead of midnight to properly
span two days and test timezone boundary grouping
3. Keep `zone.now.utc_offset` (correct behavior for real users during
DST)
**Why this works:**
- Test runs "in January 2024" → `zone.now.utc_offset` returns January
offsets consistently
- Offset `-8` correctly matches Pacific Standard Time (UTC-8 in January)
- Real users in PDT (summer) with offset `-7` → correctly match Pacific
Daylight Time
- No production impact, test is deterministic year-round
**Verification:**
- Test now passes consistently regardless of current DST status
- Timezone matching works correctly for real users during DST periods
- Reports correctly group data by timezone offset across all seasons
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
* fix: Captain response builder not getting triggered (#12729)
## Summary
- Fix captain response builder not getting triggered for cases where
responses are created as completed.
## Testing Instructions
- Test articles with firecrawl
- Test articles without firecrawl
- Test PDF documents
---------
Co-authored-by: Pranav <pranav@chatwoot.com>
* chore: Update captain pending FAQ interface (#12752)
# Pull Request Template
## Description
**This PR includes,**
- Added new pending FAQs view with approve/edit/delete actions for each
response.
- Implemented banner notification showing pending FAQ count on main
approved responses page.
- Created dedicated route for pending FAQs review at
/captain/responses/pending.
- Added automatic pending count updates when switching assistants or
routes.
- Modified ResponseCard component to show action buttons instead of
dropdown in pending view.
Fixes
https://linear.app/chatwoot/issue/CW-5833/pending-faqs-in-a-different-ux
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
### Loom video
https://www.loom.com/share/5fe8f79b04cd4681b9360c48710b9373
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Pranav <pranav@chatwoot.com>
* fix: Exclude authentication templates from WhatsApp template selection (#12753)
This PR add the changes for excluding the authentication templates from
the WhatsApp template selection in the frontend, as these templates are
not supported at the moment. Reference:
https://www.chatwoot.com/hc/user-guide/articles/1754940076-whatsapp-templates#what-is-not-supported
* feat: Template types components (#12714)
# Pull Request Template
## Description
Fixes
https://linear.app/chatwoot/issue/CW-5806/create-the-story-book-components-for-template-typestext-media-list
**Pending**
Need to standardize the structure to match the template/campaigns.
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
### Screenshots
<img width="669" height="179" alt="image"
src="https://github.com/user-attachments/assets/42efd292-8520-4b05-81ec-8bc526fc12db"
/>
<img width="646" height="304" alt="image"
src="https://github.com/user-attachments/assets/431dd964-006c-4877-a693-dae39b90df4c"
/>
<img width="646" height="380" alt="image"
src="https://github.com/user-attachments/assets/9052e31f-9292-4afb-8897-13931655fa00"
/>
<img width="646" height="272" alt="image"
src="https://github.com/user-attachments/assets/873d2488-e856-4a0d-8579-cc1bcc61cc8e"
/>
<img width="646" height="490" alt="image"
src="https://github.com/user-attachments/assets/14c2aa42-bf27-475f-aa70-fe59c1d00e9b"
/>
<img width="646" height="281" alt="image"
src="https://github.com/user-attachments/assets/1f42408e-03e8-4863-b4c7-715d13d67686"
/>
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
* fix: update omniauth to latest to resolve heroku deployment issues (#12749)
# Pull Request Template
## Description
Fixes https://github.com/chatwoot/chatwoot/issues/12553
Heroku build was failing due to `omniauth` version mismatch. Also, added
`NODE_OPTIONS=--max-old-space-size=4096` to handle OOM during Vite
build.
## Type of change
Please delete options that are not relevant.
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
- Tested on heroku
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
* chore: Improvements in pending FAQs (#12755)
# Pull Request Template
## Description
**This PR includes:**
1. Added URL-based filter persistence for the responses pages, including
page and search parameters.
2. Introduced a new empty state variant for pending FAQs — without a
backdrop and with a “Clear Filters” option.
3. Made the actions, filter, and search row remain fixed at the top
while scrolling.
Fixes
https://linear.app/chatwoot/issue/CW-5852/improvements-in-pending-faqs
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
### Loom video
https://www.loom.com/share/1d9eee68c0684f0ab05e08b4ca1e0ce9
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
* fix: run captain v2 outside the transaction (#12756)
* feat: Always process email content (#12734)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
* feat: Bulk actions for contacts (#12763)
Introduces APIs and UI for bulk actions in contacts table. The initial
action available will be assign labels
Fixes: #8536#12253
## Screens
<img width="1350" height="747" alt="Screenshot 2025-10-29 at 4 05 08 PM"
src="https://github.com/user-attachments/assets/0792dff5-0371-4b2e-bdfb-cd32db773402"
/>
<img width="1345" height="717" alt="Screenshot 2025-10-29 at 4 05 19 PM"
src="https://github.com/user-attachments/assets/ae510404-c6de-4c15-a720-f6d10cdac25b"
/>
---------
Co-authored-by: Muhsin <muhsinkeramam@gmail.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
* feat: Enable opensearch on paid plans automatically (#12770)
- enable `advanced_search feature` on all paid plans automatically
ref: https://github.com/chatwoot/chatwoot/pull/12503
* chore: Make contacts bulk action bar sticky (#12773)
# Pull Request Template
## Description
This PR makes the contacts bulk action bar sticky while scrolling.
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
### Screenshots
<img width="1080" height="300" alt="image"
src="https://github.com/user-attachments/assets/21f8f3c6-813e-4ef6-b40a-8dd14e6ffb26"
/>
<img width="1080" height="300" alt="image"
src="https://github.com/user-attachments/assets/bb939f1d-9a13-4f9f-953d-b9872c984b74"
/>
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
* chore: Add dependant destroy_async for sla events (#12774)
Added the destroy_async to prevent timeout during SLA policy deletion by
processing SLA events asynchronously.
* chore: Update translations (#12748)
* feat: Add company backfill migration for existing contacts (Part 1) (#12657)
## Description
Implements company backfill migration infrastructure for existing
contacts. This is **Part 1 of 2** for the company model production
rollout as described in
[CW-5726](https://linear.app/chatwoot/issue/CW-5726/company-model-setting-it-up-on-production).
Creates jobs and services to associate existing contacts with companies
based on their email domains, filtering out free email providers (gmail,
yahoo, etc.) and disposable addresses.
**What's included:**
- Business email detector service with ValidEmail2 (uses
`disposable_domain?` to avoid DNS lookups)
- Per-account batch job to process contacts for one account
- Orchestrator job to iterate all accounts
- Rake task: `bundle exec rake companies:backfill`
~~*NOTE*: I'm using a hard-coded approach to determine if something is a
"business" email by filtering out emails that are usually personal. I've
also added domains that are common to some of our customers' regions.
This should be simpler. I looked into `Valid_Email2` and I couldn't find
anything to dictate whether an email is a personal email or a business
one. I don't think the approach used in the frontend is valid here.~~
UPDATE: Using `email_provider_info` gem instead.
**Pending - Part 2 (separate PR):** Real-time company creation for new
contacts
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
```bash
# Run all new tests
bundle exec rspec spec/enterprise/services/companies/business_email_detector_service_spec.rb \\
spec/enterprise/jobs/migration/company_account_batch_job_spec.rb \\
spec/enterprise/jobs/migration/company_backfill_job_spec.rb
# Run RuboCop
bundle exec rubocop enterprise/app/services/companies/business_email_detector_service.rb \\
enterprise/app/jobs/migration/company_account_batch_job.rb \\
enterprise/app/jobs/migration/company_backfill_job.rb \\
lib/tasks/companies.rake
```
**Performance optimization:**
- Uses `disposable_domain?` instead of `disposable?` to avoid DNS MX
lookups (discovered via tcpdump analysis - `disposable?` was making
network calls for every email, causing 100x slowdown)
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Sojan Jose <sojan@pepalo.com>
* feat: Add company auto-association for contacts (CW-5726 Part 2) (#12711)
## Description
Implements real-time company auto-association for contacts based on
email domains. This is **Part 2** of the company model production
rollout (CW-5726).
**Task:**
- When a contact is created with a business email, automatically create
and associate a company from the email domain
- When a contact is updated with an email for the first time (email was
previously nil), associate with a company
- Preserve existing company associations when email changes to avoid
user confusion
- Skip free email providers and disposable domains
**Dependencies:**
⚠️ Requires PR #12657 (Part 1: Backfill migration) to be merged first
**Linear ticket:**
[CW-5726](https://linear.app/chatwoot/issue/CW-5726/company-model-setting-it-up-on-production)
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
- Service specs: Tests business email detection, company creation,
association logic, edge cases (existing companies, free emails, nil
emails)
- Integration specs: Tests full callback flow for contact create/update
scenarios
- All tests passing: 10 examples, 0 failures
- RuboCop: 0 offenses
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules (PR #12657 pending)
---------
Co-authored-by: Sojan Jose <sojan@pepalo.com>
* fix: Optimize Message search_data to prevent OpenSearch field explosion (#12786)
## Description
Refactored the `Message#search_data` method to prevent exceeding
OpenSearch's 1000 field limit during reindex operations.
**Problem:** The previous implementation serialized entire ActiveRecord
objects (Inbox, Sender, Conversation) with all their JSONB fields,
causing dynamic field explosion in OpenSearch. This resulted in
`Searchkick::ImportError` with "Limit of total fields [1000] has been
exceeded".
**Solution:** Whitelisted only necessary fields for search and
filtering, and flattened JSONB `custom_attributes` into key-value pair
arrays to prevent unbounded field creation.
Linked to: CW-5861
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [x] This change requires a documentation update
## How Has This Been Tested?
- Verified rubocop passes with no offenses
- Code review of search field usage from
`enterprise/app/services/enterprise/search_service.rb`
- Analyzed actual search queries to determine required indexed fields
**Still needed:**
- Full reindex test on staging/production environment
- Verify search functionality still works after reindex
- Confirm field count is under 1000 limit
## Changes Made
### Before
- Indexed 1000+ fields (entire AR objects with JSONB)
- `inbox` = full Inbox object (23+ fields + JSONB)
- `sender` = full Contact/User/AgentBot object (10+ fields + JSONB)
- `conversation` = full push_event_data
- Dynamic JSONB keys creating unlimited fields
### After
- ~35-40 controlled fields
- Whitelisted search fields: `content`, `attachment_transcribed_text`,
`email_subject`
- Filter fields: `account_id`, `inbox_id`, `conversation_id`,
`sender_id`, `sender_type`, etc.
- Flattened `custom_attributes`: `[{key, value, value_type}]` format
- Helper methods: `search_conversation_data`, `search_inbox_data`,
`search_sender_data`, `search_additional_data`
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
## Post-merge Steps
After merging, the following steps are required:
1. **Reindex all messages:**
```bash
bundle exec rails runner "Message.reindex"
```
2. **Verify field count:**
```bash
bundle exec rails runner "
client = Searchkick.client
index_name = Message.searchkick_index.name
mapping = client.indices.get_mapping(index: index_name)
fields = mapping.dig(index_name, 'mappings', 'properties')
puts 'Total fields: ' + fields.keys.count.to_s
"
```
3. **Test search functionality** to ensure queries still work as
expected
---------
Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
* fix: Avoid introducing new attributes in search (#12791)
Fix `Limit of total fields [1000] has been exceeded`
https://linear.app/chatwoot/issue/CW-5861/searchkickimporterror-type-=-illegal-argument-exception-reason-=-limit#comment-6b6e41bd
* fix: Gate Sidekiq dequeue logger behind env (#12790)
## Summary
- wrap the dequeue middleware registration in a boolean env flag
- document the ENABLE_SIDEKIQ_DEQUEUE_LOGGER option in .env.example
* feat: Bulk delete for contacts (#12778)
Introduces a new bulk action `delete` for contacts
ref: https://github.com/chatwoot/chatwoot/pull/12763
## Screens
<img width="1492" height="973" alt="Screenshot 2025-10-31 at 6 27 21 PM"
src="https://github.com/user-attachments/assets/30dab1bb-2c2c-4168-9800-44e0eb5f8e3a"
/>
<img width="1492" height="985" alt="Screenshot 2025-10-31 at 6 27 32 PM"
src="https://github.com/user-attachments/assets/5be610c4-b19e-4614-a164-103b22337382"
/>
* fix: Video bubble click and play issue (#12764)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
* feat: Differentiate bot and user in the summary (#12801)
While generating the summary, use the appropriate sender type for the
message.
* fix: Invalid image URL issue in Help Center articles (#12806)
* feat: allow bots to handle campaigns when sender_id is nil (#12805)
* fix: Add empty line before signature in compose conversation editor (#12702)
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
* feat: Enhance button interactions (#12738)
* fix: Remove the same account validation for whatsapp channels (#12811)
## Description
Modified the phone number validation in Whatsapp::ChannelCreationService
to check for duplicate phone numbers across ALL accounts, not just
within the current account.
## Type of change
- [ ] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
- Added test coverage for cross-account phone number validation
- Using actual UI flow
<img width="1493" height="532" alt="image"
src="https://github.com/user-attachments/assets/67d2bb99-2eb9-4115-8d56-449e4785e0d8"
/>
## Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
* feat: Update Captain navigation structure (#12761)
# Pull Request Template
## Description
This PR includes an update to the Captain navigation structure.
## Route Structure
```javascript
1. captain_assistants_responses_index → /captain/:assistantId/faqs
2. captain_assistants_documents_index → /captain/:assistantId/documents
3. captain_assistants_scenarios_index → /captain/:assistantId/scenarios
4. captain_assistants_playground_index → /captain/:assistantId/playground
5. captain_assistants_inboxes_index → /captain/:assistantId/inboxes
6. captain_tools_index → /captain/tools
7. captain_assistants_settings_index → /captain/:assistantId/settings
8. captain_assistants_guardrails_index → /captain/:assistantId/settings/guardrails
9. captain_assistants_guidelines_index → /captain/:assistantId/settings/guidelines
10. captain_assistants_index → /captain/:navigationPath
```
**How it works:**
1. User clicks sidebar item → Routes to `captain_assistants_index` with
`navigationPath`
2. `AssistantsIndexPage` validates route and gets last active assistant,
if not redirects to assistant create page.
3. Routes to actual page: `/captain/:assistantId/:page`
4. Page loads with correct assistant context
Fixes
https://linear.app/chatwoot/issue/CW-5832/updating-captain-navigation
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Pranav <pranav@chatwoot.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
* fix: Handle login when there are no accounts (#12816)
* chore: Update translations (#12794)
* chore(docs): Fix typos in some files (#12817)
This PR fixes typos in the file file using codespell.
* refactor: strategy pattern for mailbox conversation finding (#12766)
Co-authored-by: Pranav <pranav@chatwoot.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
* fix: Issue with processing variables in outgoing email content (#12799)
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Vinay Keerthi <11478411+stonecharioteer@users.noreply.github.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
* fix: hide pdf citations in captain faq responses (#12839)
* fix: Use contact_id instead of sender_id for Instagram message locks (#12841)
Previously, the lock key for Instagram used sender_id, which for echo
messages (outgoing) would be the account's own ID. This caused all
outgoing messages to compete for the same lock, creating a bottleneck
during bulk messaging.
The fix introduces contact_instagram_id method that correctly identifies
the contact's ID regardless of message direction:
- For echo messages (outgoing): uses recipient.id (the contact)
- For incoming messages: uses sender.id (the contact)
This ensures each conversation has a unique lock, allowing parallel
processing of webhooks while maintaining race condition protection
within individual conversations.
Fixes lock acquisition errors in Sidekiq when processing bulk Instagram
messages.
Fixes
https://linear.app/chatwoot/issue/CW-5931/p0-mutexapplicationjoblockacquisitionerror-failed-to-acquire-lock-for
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
* fix: label tags for contactable inboxes (#12838)
* chore: Improve captain layout (#12820)
* feat: allow selecting month range in overview reports (#12701)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
* fix: respect status parameter when creating articles via API (#12846)
## Description
The Articles API was ignoring the `status` parameter when creating new
articles. All articles were forced to be drafts due to a hardcoded
`@article.draft!` call in the controller, even when users explicitly
sent `status: 1` (published) in their API request.
This PR removes the hardcoded draft enforcement and allows the status
parameter to be respected while maintaining backward compatibility.
Fixes#12063
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
**Before:**
- API POST with `status: 1` → Created as draft (ignored parameter)
- API POST without status → Created as draft
**After:**
- API POST with `status: 1` → Created as published ✅
- API POST without status → Created as draft (backward compatible) ✅
- UI creates articles → Still creates as draft (UI doesn't send status)
✅
**Tests run:**
```bash
bundle exec rspec spec/controllers/api/v1/accounts/articles_controller_spec.rb
# 17 examples, 0 failures
```
Updated tests:
1. Changed 2 existing tests that were verifying the broken behavior
(expecting draft when published was sent)
2. Added new test to verify articles default to draft when status is not
provided
3. All existing tests pass, confirming backward compatibility
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
Co-authored-by: Sojan Jose <sojan@pepalo.com>
* feat: allow querying reporting events via the API (#12832)
* feat(webhooks): add name to webhook (#12641)
## Description
When working with webhooks, it's easy to lose track of which URL is
which. Adding a `name` (optional) column to the webhook model is a
straight-forward solution to make it significantly easier to identify
webhooks.
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
Model and controller specs, and also running in production over several
months without any issues.
| Before | After |
| --- | --- |
| <img width="949" height="990" alt="image copy 3"
src="https://github.com/user-attachments/assets/6b33c072-7d16-4a9c-a129-f9c0751299f5"
/> | <img width="806" height="941" alt="image"
src="https://github.com/user-attachments/assets/77f3cb3a-2eb0-41ac-95bf-d02915589690"
/> |
| <img width="1231" height="650" alt="image copy 2"
src="https://github.com/user-attachments/assets/583374af-96e0-4436-b026-4ce79b7f9321"
/> | <img width="1252" height="650" alt="image copy"
src="https://github.com/user-attachments/assets/aa81fb31-fd18-4e21-a40e-d8ab0dc76b4e"
/> |
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
* perf: speed up docker builds (#12859)
- Use separate keys to avoid cache overwrites across different
architecture builds
https://linear.app/chatwoot/issue/CW-5945/perf-speed-up-docker-builds
### 25 mins ---> 5mins
## before
<img width="971" height="452" alt="image"
src="https://github.com/user-attachments/assets/535cebd6-6c16-48d1-a62d-ffb6f2fc9b08"
/>
## after
<img width="940" height="428" alt="image"
src="https://github.com/user-attachments/assets/359eb313-4bb5-4e0e-9492-a8ad48645159"
/>
* chore: Update missing places with new colors (#12862)
# Pull Request Template
## Description
This PR updates the colors in places that were missed during the color
update migration.
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
* fix: Brand installation name not showing (#12861)
# Pull Request Template
## Description
Fixes
https://linear.app/chatwoot/issue/CW-5946/fix-brand-installation-name-issue-in-dyte
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
* fix: migrate from deprecated annotate gem to annotaterb (#12845)
## Description
The `annotate` gem has been deprecated and users are experiencing
annotation errors with the new Rails 7 `serialize` syntax. This PR
migrates to `annotaterb`, the actively maintained fork.
Users reported errors when running `make db`:
```
Unable to annotate app/models/installation_config.rb: no implicit conversion of Hash into String
Unable to annotate app/models/installation_config.rb: no implicit conversion of nil into Array
```
This PR updates the Gemfile and rake configuration to use `annotaterb`
instead.
Fixes#11673
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
Tested locally with the following steps:
1. Run `bundle install` - successfully installed annotaterb 4.20.0
2. Run `RAILS_ENV=development bundle exec rails db:chatwoot_prepare` -
completed without annotation errors
3. Run `RAILS_ENV=development bundle exec rails annotate_rb:models` -
successfully annotated all models including InstallationConfig
4. Verified InstallationConfig model annotations are present and correct
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] My changes generate no new warnings
- [x] New and existing unit tests pass locally with my changes
* chore: disable worker MemoryHigh throttling in systemd unit (#12871)
- set MemoryHigh to infinity in deployment/chatwoot-worker.1.service so
the worker is throttled only by the existing
MemoryMax hard limit
- prevents cgroup reclaim from slowing Sidekiq under transient spikes
while still keeping the hard stop at 1.5 GB
* chore: Update translations (#12818)
* fix: revert annotaterb migration due to persistent annotation errors (#12881)
## Description
This PR reverts the migration from the `annotate` gem to `annotaterb`
introduced in PR #12845. The annotation errors reported in #11673
persist with both gems, and the old `annotate` gem handles the errors
more gracefully by continuing to process other models instead of
crashing.
**Testing reveals both gems fail with the same underlying issue:**
**Old annotate gem (3.2.0):**
```
Unable to annotate app/models/installation_config.rb: no implicit conversion of Hash into String
Unable to annotate app/models/installation_config.rb: no implicit conversion of nil into Array
Model files unchanged.
```
(Logs error but continues processing)
**New annotaterb gem (4.20.0):**
```
❯ bundle exec annotaterb models
ruby/3.4.4/lib/ruby/gems/3.4.0/gems/reline-0.3.6/lib/reline/terminfo.rb:2: warning: ruby/3.4.4/lib/ruby/3.4.0/fiddle.rb was loaded from the standard library, but will no longer be part of the default gems starting from Ruby 3.5.0.
You can add fiddle to your Gemfile or gemspec to silence this warning.
Also please contact the author of reline-0.3.6 to request adding fiddle into its gemspec.
Annotating models
bundler: failed to load command: annotaterb (ruby/3.4.4/bin/annotaterb)
ruby/3.4.4/lib/ruby/3.4.0/psych/parser.rb:62:in 'Psych::Parser#_native_parse': no implicit conversion of Hash into String (TypeError)
_native_parse @handler, yaml, path
^^^^^^^^^^^^^^^^^^^^
from ruby/3.4.4/lib/ruby/3.4.0/psych/parser.rb:62:in 'Psych::Parser#parse'
from ruby/3.4.4/lib/ruby/3.4.0/psych.rb:457:in 'Psych.parse_stream'
from ruby/3.4.4/lib/ruby/3.4.0/psych.rb:401:in 'Psych.parse'
from ruby/3.4.4/lib/ruby/3.4.0/psych.rb:325:in 'Psych.safe_load'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/activerecord-7.1.5.2/lib/active_record/coders/yaml_column.rb:37:in 'ActiveRecord::Coders::YAMLColumn::SafeCoder#load'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/activerecord-7.1.5.2/lib/active_record/coders/column_serializer.rb:37:in 'ActiveRecord::Coders::ColumnSerializer#load'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/activerecord-7.1.5.2/lib/active_record/type/serialized.rb:22:in 'ActiveRecord::Type::Serialized#deserialize'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/activemodel-7.1.5.2/lib/active_model/attribute.rb:175:in 'ActiveModel::Attribute::FromDatabase#type_cast'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/activemodel-7.1.5.2/lib/active_model/attribute.rb:43:in 'ActiveModel::Attribute#value'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/activemodel-7.1.5.2/lib/active_model/attribute_set.rb:37:in 'block in ActiveModel::AttributeSet#to_hash'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/activesupport-7.1.5.2/lib/active_support/core_ext/enumerable.rb:78:in 'block in Enumerable#index_with'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/activesupport-7.1.5.2/lib/active_support/core_ext/enumerable.rb:78:in 'Array#each'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/activesupport-7.1.5.2/lib/active_support/core_ext/enumerable.rb:78:in 'Enumerable#index_with'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/activemodel-7.1.5.2/lib/active_model/attribute_set.rb:37:in 'ActiveModel::AttributeSet#to_hash'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/activerecord-7.1.5.2/lib/active_record/model_schema.rb:499:in 'ActiveRecord::ModelSchema::ClassMethods#column_defaults'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/model_annotator/model_wrapper.rb:68:in 'AnnotateRb::ModelAnnotator::ModelWrapper#column_defaults'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/model_annotator/model_wrapper.rb:139:in 'block in AnnotateRb::ModelAnnotator::ModelWrapper#built_attributes'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/model_annotator/model_wrapper.rb:136:in 'Array#map'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/model_annotator/model_wrapper.rb:136:in 'AnnotateRb::ModelAnnotator::ModelWrapper#built_attributes'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/model_annotator/column_annotation/annotation_builder.rb:15:in 'AnnotateRb::ModelAnnotator::ColumnAnnotation::AnnotationBuilder#build'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/model_annotator/annotation/annotation_builder.rb:52:in 'block in AnnotateRb::ModelAnnotator::Annotation::AnnotationBuilder::Annotation#columns'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/model_annotator/annotation/annotation_builder.rb:51:in 'Array#map'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/model_annotator/annotation/annotation_builder.rb:51:in 'AnnotateRb::ModelAnnotator::Annotation::AnnotationBuilder::Annotation#columns'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/model_annotator/annotation/annotation_builder.rb:26:in 'AnnotateRb::ModelAnnotator::Annotation::AnnotationBuilder::Annotation#body'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/model_annotator/annotation/annotation_builder.rb:35:in 'AnnotateRb::ModelAnnotator::Annotation::AnnotationBuilder::Annotation#build'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/model_annotator/annotation/annotation_builder.rb:71:in 'AnnotateRb::ModelAnnotator::Annotation::AnnotationBuilder#build'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/model_annotator/project_annotator.rb:43:in 'AnnotateRb::ModelAnnotator::ProjectAnnotator#build_instructions_for_file'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/model_annotator/project_annotator.rb:17:in 'block in AnnotateRb::ModelAnnotator::ProjectAnnotator#annotate'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/model_annotator/project_annotator.rb:13:in 'Array#map'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/model_annotator/project_annotator.rb:13:in 'AnnotateRb::ModelAnnotator::ProjectAnnotator#annotate'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/model_annotator/annotator.rb:21:in 'AnnotateRb::ModelAnnotator::Annotator#do_annotations'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/model_annotator/annotator.rb:8:in 'AnnotateRb::ModelAnnotator::Annotator.do_annotations'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/commands/annotate_models.rb:17:in 'AnnotateRb::Commands::AnnotateModels#call'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/runner.rb:38:in 'AnnotateRb::Runner#run'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/lib/annotate_rb/runner.rb:11:in 'AnnotateRb::Runner.run'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/annotaterb-4.20.0/exe/annotaterb:18:in '<top (required)>'
from ruby/3.4.4/bin/annotaterb:25:in 'Kernel#load'
from ruby/3.4.4/bin/annotaterb:25:in '<top (required)>'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/bundler-2.5.16/lib/bundler/cli/exec.rb:58:in 'Kernel.load'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/bundler-2.5.16/lib/bundler/cli/exec.rb:58:in 'Bundler::CLI::Exec#kernel_load'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/bundler-2.5.16/lib/bundler/cli/exec.rb:23:in 'Bundler::CLI::Exec#run'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/bundler-2.5.16/lib/bundler/cli.rb:455:in 'Bundler::CLI#exec'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/bundler-2.5.16/lib/bundler/vendor/thor/lib/thor/command.rb:28:in 'Bundler::Thor::Command#run'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/bundler-2.5.16/lib/bundler/vendor/thor/lib/thor/invocation.rb:127:in 'Bundler::Thor::Invocation#invoke_command'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/bundler-2.5.16/lib/bundler/vendor/thor/lib/thor.rb:527:in 'Bundler::Thor.dispatch'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/bundler-2.5.16/lib/bundler/cli.rb:35:in 'Bundler::CLI.dispatch'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/bundler-2.5.16/lib/bundler/vendor/thor/lib/thor/base.rb:584:in 'Bundler::Thor::Base::ClassMethods#start'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/bundler-2.5.16/lib/bundler/cli.rb:29:in 'Bundler::CLI.start'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/bundler-2.5.16/exe/bundle:28:in 'block in <top (required)>'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/bundler-2.5.16/lib/bundler/friendly_errors.rb:117:in 'Bundler.with_friendly_errors'
from ruby/3.4.4/lib/ruby/gems/3.4.0/gems/bundler-2.5.16/exe/bundle:20:in '<top (required)>'
from ruby/3.4.4/bin/bundle:25:in 'Kernel#load'
from ruby/3.4.4/bin/bundle:25:in '<main>'
```
(Crashes immediately, stops all processing)
**Root cause:** The `InstallationConfig` model uses YAML serialization
(`serialize :serialized_value, coder: YAML`) on a JSONB database column.
When annotation tools read column defaults, PostgreSQL returns JSONB as
a Hash, but YAML expects a String, causing the type error.
The migration to annotaterb doesn't solve the problem - both gems
encounter the same error. The old gem is preferable as it continues
working despite the error.
Reverts #12845
Related to #11673
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
1. Reverted commit 559d1b657
2. Ran `bundle install` to reinstall annotate gem v3.2.0
3. Ran `RAILS_ENV=development bundle exec annotate`
- Result: Logs errors for InstallationConfig but completes successfully
4. Re-applied the annotaterb changes and tested `bundle exec annotaterb
models`
- Result: Crashes with full stack trace and stops processing
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] My changes generate no new warnings
- [x] New and existing unit tests pass locally with my changes
---
*Edited to truncate environment-specific info from error dump*
* chore: Hide assistant switcher on paywall screen (#12875)
* feat: Assignment service (v2) (#12320)
## Linear Link
## Description
This PR introduces a new robust auto-assignment system for conversations
in Chatwoot. The system replaces the existing round-robin assignment
with a more sophisticated service-based architecture that supports
multiple assignment strategies, rate limiting, and Enterprise features
like capacity-based assignment and balanced distribution.
## Type of change
- [ ] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
- Unit test cases
- Test conversations getting assigned on status change to open
- Test the job directly via rails console
## Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> Adds a new service-based auto-assignment system with scheduled jobs,
rate limiting, enterprise capacity/balanced selection, and wiring via
inbox/handler; includes Redis helpers and comprehensive tests.
>
> - **Auto-assignment v2 (core services)**:
> - Add `AutoAssignment::AssignmentService` with bulk assignment,
configurable conversation priority, RR selection, and per-agent rate
limiting via `AutoAssignment::RateLimiter`.
> - Add `AutoAssignment::RoundRobinSelector` for agent selection.
> - **Jobs & scheduling**:
> - Add `AutoAssignment::AssignmentJob` (per-inbox bulk assign;
env-based limit) and `AutoAssignment::PeriodicAssignmentJob` (batch over
accounts/inboxes).
> - Schedule periodic run in `config/schedule.yml`
(`periodic_assignment_job`).
> - **Model/concerns wiring**:
> - Include `InboxAgentAvailability` in `Inbox`; add
`Inbox#auto_assignment_v2_enabled?`.
> - Update `AutoAssignmentHandler` to trigger v2 job when
`auto_assignment_v2_enabled?`, else fallback to legacy.
> - **Enterprise extensions**:
> - Add `Enterprise::InboxAgentAvailability` (capacity-aware filtering)
and `Enterprise::Concerns::Inbox` association `inbox_capacity_limits`.
> - Extend service via `Enterprise::AutoAssignment::AssignmentService`
(policy-driven config, capacity filtering, exclusion rules) and add
selectors/services: `BalancedSelector`, `CapacityService`.
> - **Infrastructure**:
> - Enhance `Redis::Alfred` with `expire`, key scan/count, and extended
ZSET helpers (`zadd`, `zcount`, `zcard`, `zrangebyscore`).
> - **Tests**:
> - Add specs for jobs, core service, rate limiter, RR selector, and
enterprise features (capacity, balanced selection, exclusions).
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
0ebe187c8aea73765b0122a44b18d6f465c2477f. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
* fix: Change contact_inboxes.source_id to text column (#12882)
## Description
Fixes CW-5961 where IMAP email processing failed with
`ActiveRecord::RecordInvalid: Validation failed: Source is too long
(maximum is 255 characters)` error.
This changes the `contact_inboxes.source_id` column from `string` (255
character limit) to `text` (unlimited) to accommodate long email message
IDs that were causing validation failures.
Fixes CW-5961
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
- Added spec test validating `source_id` values longer than 255
characters (300 chars)
- All existing `contact_inbox_spec.rb` tests pass (7 examples, 0
failures)
- Migration applied successfully with reversible up/down methods
- Verified `source_id` column type changed to `text` with `null: false`
constraint preserved
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
* feat: allow configuring attachment upload limit (#12835)
## Summary
- add a configurable MAXIMUM_FILE_UPLOAD_SIZE installation setting and
surface it through super admin and global config payloads
- apply the configurable limit to attachment validations and shared
upload helpers on dashboard and widget
- introduce a reusable helper with unit tests for parsing the limit and
extend attachment specs for configurability
------
[Codex
Task](https://chatgpt.com/codex/tasks/task_e_6912644786b08326bc8dee9401af6d0a)
---------
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
* feat: Customizable webhook timeout configuration (#12777)
## Summary
- Ability to configure the webhook timeout for Chatwoot self hosted
installations
fixes: https://github.com/chatwoot/chatwoot/issues/12754
* feat: Control the allowed login methods via Super Admin (#12892)
- Control the allowed authentication methods for a chatwoot installation
via super admin configs. [SAML, Google Auth etc]
------
[Codex
Task](https://chatgpt.com/codex/tasks/task_e_6917d503b6e48326a261672c1de91462)
---------
Co-authored-by: Pranav <pranav@chatwoot.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
* chore: Update translations (#12876)
* feat: Backend - Companies API endpoint with pagination and search (#12840)
## Description
Adds API endpoint to list companies with pagination, search, and
sorting.
Fixes
https://linear.app/chatwoot/issue/CW-5930/add-backend-routes-to-get-companies-result
Parent issue:
https://linear.app/chatwoot/issue/CW-5928/add-companies-tab-to-dashboard
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
Added comprehensive specs to
`spec/enterprise/controllers/api/v1/accounts/companies_controller_spec.rb`:
- Pagination (25 per page, multiple pages)
- Search by name and domain (case-insensitive)
- Counter cache for contacts_count
- Account scoping
- Authorization
To reproduce:
```bash
bundle exec rspec spec/enterprise/controllers/api/v1/accounts/companies_controller_spec.rb
bundle exec rubocop enterprise/app/controllers/api/v1/accounts/companies_controller.rb
```
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
* feat: Companies page (#12842)
# Pull Request Template
## Description
This PR introduces a new Companies section in the Chatwoot dashboard. It
lists all companies associated with the account and includes features
such as **search**, **sorting**, and **pagination** to enable easier
navigation and efficient management.
Fixes
https://linear.app/chatwoot/issue/CW-5928/add-companies-tab-to-dashboard
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
### Screenshot
<img width="1619" height="1200" alt="image"
src="https://github.com/user-attachments/assets/21f0a666-c3d6-4dec-bd02-1e38e0cd9542"
/>
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Vinay Keerthi <11478411+stonecharioteer@users.noreply.github.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
* feat: Add Amazon SES inbound email support (#12893)
## Summary
- add AWS ActionMailbox SES gems
- document SES as incoming email provider
- note SES option in configuration
## Testing
- `bundle exec rubocop config/initializers/mailer.rb
config/environments/production.rb Gemfile`
------
[Codex
Task](https://chatgpt.com/codex/tasks/task_e_68bbb7d482288326b8f04bb795af0322)
---------
Co-authored-by: Pranav <pranav@chatwoot.com>
Co-authored-by: Vinay Keerthi <11478411+stonecharioteer@users.noreply.github.com>
* feat: hide email forwarding address if INBOUND_EMAIL_DOMAIN is not configured (#12768)
#### Summary
- Improved email inbox setup flow to handle cases where inbound email
forwarding is not configured on the installation
- Added conditional display of email forwarding address based on
MAILER_INBOUND_EMAIL_DOMAIN environment variable availability
- Enhanced user messaging to guide users toward configuring SMTP/IMAP
settings when forwarding is unavailable
#### Changes
**Backend (app/views/api/v1/models/_inbox.json.jbuilder)**
- Added forwarding_enabled boolean flag to inbox API response based on
MAILER_INBOUND_EMAIL_DOMAIN presence
- Made forward_to_email conditional - only included when forwarding is
enabled
**Frontend - Inbox Creation Flow**
- Created new EmailInboxFinish.vue component to handle email inbox setup
completion
- Shows different messages based on whether forwarding is enabled:
- With forwarding: displays forwarding address and encourages SMTP/IMAP
configuration
- Without forwarding: warns that SMTP/IMAP configuration is required for
emails to be processed
- Added link to configuration page for easy access to SMTP/IMAP settings
<img width="988" height="312" alt="Screenshot 2025-11-18 at 3 27 27 PM"
src="https://github.com/user-attachments/assets/928aff78-df73-49fa-9a26-dbbd1297b26a"
/>
<img width="765" height="489" alt="Screenshot 2025-11-18 at 3 24 46 PM"
src="https://github.com/user-attachments/assets/6a182c7d-087f-4e88-92a5-30f147a567a7"
/>
Fixes
https://linear.app/chatwoot/issue/CW-5881/hide-forwaring-email-section-if-inbound-email-domain-is-not-configured
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
- Tested locally
## Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Pranav <pranav@chatwoot.com>
* feat: APIs to assign agents_bots as assignee in conversations (#12836)
## Summary
- add an assignee_agent_bot_id column as an initital step to prototype
this before fully switching to polymorphic assignee
- update assignment APIs and conversation list / show endpoints to
reflect assignee as agent bot
- ensure webhook payloads contains agent bot assignee
[Codex
Task](https://chatgpt.com/codex/tasks/task_e_6912833377e48326b6641b9eee32d50f)
---------
Co-authored-by: Pranav <pranav@chatwoot.com>
* Bump version to 4.8.0
* chore: remove migration
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Chatwoot Bot <92152627+chatwoot-bot@users.noreply.github.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Muhsin <muhsinkeramam@gmail.com>
Co-authored-by: Vinay Keerthi <11478411+stonecharioteer@users.noreply.github.com>
Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com>
Co-authored-by: Lê Nam Khánh <55955273+khanhkhanhlele@users.noreply.github.com>
## Summary
- add an assignee_agent_bot_id column as an initital step to prototype
this before fully switching to polymorphic assignee
- update assignment APIs and conversation list / show endpoints to
reflect assignee as agent bot
- ensure webhook payloads contains agent bot assignee
[Codex
Task](https://chatgpt.com/codex/tasks/task_e_6912833377e48326b6641b9eee32d50f)
---------
Co-authored-by: Pranav <pranav@chatwoot.com>
## Description
Adds API endpoint to list companies with pagination, search, and
sorting.
Fixes
https://linear.app/chatwoot/issue/CW-5930/add-backend-routes-to-get-companies-result
Parent issue:
https://linear.app/chatwoot/issue/CW-5928/add-companies-tab-to-dashboard
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
Added comprehensive specs to
`spec/enterprise/controllers/api/v1/accounts/companies_controller_spec.rb`:
- Pagination (25 per page, multiple pages)
- Search by name and domain (case-insensitive)
- Counter cache for contacts_count
- Account scoping
- Authorization
To reproduce:
```bash
bundle exec rspec spec/enterprise/controllers/api/v1/accounts/companies_controller_spec.rb
bundle exec rubocop enterprise/app/controllers/api/v1/accounts/companies_controller.rb
```
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
## Summary
- add a configurable MAXIMUM_FILE_UPLOAD_SIZE installation setting and
surface it through super admin and global config payloads
- apply the configurable limit to attachment validations and shared
upload helpers on dashboard and widget
- introduce a reusable helper with unit tests for parsing the limit and
extend attachment specs for configurability
------
[Codex
Task](https://chatgpt.com/codex/tasks/task_e_6912644786b08326bc8dee9401af6d0a)
---------
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
## Description
Fixes CW-5961 where IMAP email processing failed with
`ActiveRecord::RecordInvalid: Validation failed: Source is too long
(maximum is 255 characters)` error.
This changes the `contact_inboxes.source_id` column from `string` (255
character limit) to `text` (unlimited) to accommodate long email message
IDs that were causing validation failures.
Fixes CW-5961
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
- Added spec test validating `source_id` values longer than 255
characters (300 chars)
- All existing `contact_inbox_spec.rb` tests pass (7 examples, 0
failures)
- Migration applied successfully with reversible up/down methods
- Verified `source_id` column type changed to `text` with `null: false`
constraint preserved
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
## Linear Link
## Description
This PR introduces a new robust auto-assignment system for conversations
in Chatwoot. The system replaces the existing round-robin assignment
with a more sophisticated service-based architecture that supports
multiple assignment strategies, rate limiting, and Enterprise features
like capacity-based assignment and balanced distribution.
## Type of change
- [ ] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
- Unit test cases
- Test conversations getting assigned on status change to open
- Test the job directly via rails console
## Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> Adds a new service-based auto-assignment system with scheduled jobs,
rate limiting, enterprise capacity/balanced selection, and wiring via
inbox/handler; includes Redis helpers and comprehensive tests.
>
> - **Auto-assignment v2 (core services)**:
> - Add `AutoAssignment::AssignmentService` with bulk assignment,
configurable conversation priority, RR selection, and per-agent rate
limiting via `AutoAssignment::RateLimiter`.
> - Add `AutoAssignment::RoundRobinSelector` for agent selection.
> - **Jobs & scheduling**:
> - Add `AutoAssignment::AssignmentJob` (per-inbox bulk assign;
env-based limit) and `AutoAssignment::PeriodicAssignmentJob` (batch over
accounts/inboxes).
> - Schedule periodic run in `config/schedule.yml`
(`periodic_assignment_job`).
> - **Model/concerns wiring**:
> - Include `InboxAgentAvailability` in `Inbox`; add
`Inbox#auto_assignment_v2_enabled?`.
> - Update `AutoAssignmentHandler` to trigger v2 job when
`auto_assignment_v2_enabled?`, else fallback to legacy.
> - **Enterprise extensions**:
> - Add `Enterprise::InboxAgentAvailability` (capacity-aware filtering)
and `Enterprise::Concerns::Inbox` association `inbox_capacity_limits`.
> - Extend service via `Enterprise::AutoAssignment::AssignmentService`
(policy-driven config, capacity filtering, exclusion rules) and add
selectors/services: `BalancedSelector`, `CapacityService`.
> - **Infrastructure**:
> - Enhance `Redis::Alfred` with `expire`, key scan/count, and extended
ZSET helpers (`zadd`, `zcount`, `zcard`, `zrangebyscore`).
> - **Tests**:
> - Add specs for jobs, core service, rate limiter, RR selector, and
enterprise features (capacity, balanced selection, exclusions).
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
0ebe187c8aea73765b0122a44b18d6f465c2477f. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
## Description
When working with webhooks, it's easy to lose track of which URL is
which. Adding a `name` (optional) column to the webhook model is a
straight-forward solution to make it significantly easier to identify
webhooks.
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
Model and controller specs, and also running in production over several
months without any issues.
| Before | After |
| --- | --- |
| <img width="949" height="990" alt="image copy 3"
src="https://github.com/user-attachments/assets/6b33c072-7d16-4a9c-a129-f9c0751299f5"
/> | <img width="806" height="941" alt="image"
src="https://github.com/user-attachments/assets/77f3cb3a-2eb0-41ac-95bf-d02915589690"
/> |
| <img width="1231" height="650" alt="image copy 2"
src="https://github.com/user-attachments/assets/583374af-96e0-4436-b026-4ce79b7f9321"
/> | <img width="1252" height="650" alt="image copy"
src="https://github.com/user-attachments/assets/aa81fb31-fd18-4e21-a40e-d8ab0dc76b4e"
/> |
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
## Description
The Articles API was ignoring the `status` parameter when creating new
articles. All articles were forced to be drafts due to a hardcoded
`@article.draft!` call in the controller, even when users explicitly
sent `status: 1` (published) in their API request.
This PR removes the hardcoded draft enforcement and allows the status
parameter to be respected while maintaining backward compatibility.
Fixes#12063
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
**Before:**
- API POST with `status: 1` → Created as draft (ignored parameter)
- API POST without status → Created as draft
**After:**
- API POST with `status: 1` → Created as published ✅
- API POST without status → Created as draft (backward compatible) ✅
- UI creates articles → Still creates as draft (UI doesn't send status)
✅
**Tests run:**
```bash
bundle exec rspec spec/controllers/api/v1/accounts/articles_controller_spec.rb
# 17 examples, 0 failures
```
Updated tests:
1. Changed 2 existing tests that were verifying the broken behavior
(expecting draft when published was sent)
2. Added new test to verify articles default to draft when status is not
provided
3. All existing tests pass, confirming backward compatibility
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
Co-authored-by: Sojan Jose <sojan@pepalo.com>
## Description
Modified the phone number validation in Whatsapp::ChannelCreationService
to check for duplicate phone numbers across ALL accounts, not just
within the current account.
## Type of change
- [ ] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
- Added test coverage for cross-account phone number validation
- Using actual UI flow
<img width="1493" height="532" alt="image"
src="https://github.com/user-attachments/assets/67d2bb99-2eb9-4115-8d56-449e4785e0d8"
/>
## Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
## Description
Refactored the `Message#search_data` method to prevent exceeding
OpenSearch's 1000 field limit during reindex operations.
**Problem:** The previous implementation serialized entire ActiveRecord
objects (Inbox, Sender, Conversation) with all their JSONB fields,
causing dynamic field explosion in OpenSearch. This resulted in
`Searchkick::ImportError` with "Limit of total fields [1000] has been
exceeded".
**Solution:** Whitelisted only necessary fields for search and
filtering, and flattened JSONB `custom_attributes` into key-value pair
arrays to prevent unbounded field creation.
Linked to: CW-5861
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [x] This change requires a documentation update
## How Has This Been Tested?
- Verified rubocop passes with no offenses
- Code review of search field usage from
`enterprise/app/services/enterprise/search_service.rb`
- Analyzed actual search queries to determine required indexed fields
**Still needed:**
- Full reindex test on staging/production environment
- Verify search functionality still works after reindex
- Confirm field count is under 1000 limit
## Changes Made
### Before
- Indexed 1000+ fields (entire AR objects with JSONB)
- `inbox` = full Inbox object (23+ fields + JSONB)
- `sender` = full Contact/User/AgentBot object (10+ fields + JSONB)
- `conversation` = full push_event_data
- Dynamic JSONB keys creating unlimited fields
### After
- ~35-40 controlled fields
- Whitelisted search fields: `content`, `attachment_transcribed_text`,
`email_subject`
- Filter fields: `account_id`, `inbox_id`, `conversation_id`,
`sender_id`, `sender_type`, etc.
- Flattened `custom_attributes`: `[{key, value, value_type}]` format
- Helper methods: `search_conversation_data`, `search_inbox_data`,
`search_sender_data`, `search_additional_data`
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
## Post-merge Steps
After merging, the following steps are required:
1. **Reindex all messages:**
```bash
bundle exec rails runner "Message.reindex"
```
2. **Verify field count:**
```bash
bundle exec rails runner "
client = Searchkick.client
index_name = Message.searchkick_index.name
mapping = client.indices.get_mapping(index: index_name)
fields = mapping.dig(index_name, 'mappings', 'properties')
puts 'Total fields: ' + fields.keys.count.to_s
"
```
3. **Test search functionality** to ensure queries still work as
expected
---------
Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
## Description
Implements real-time company auto-association for contacts based on
email domains. This is **Part 2** of the company model production
rollout (CW-5726).
**Task:**
- When a contact is created with a business email, automatically create
and associate a company from the email domain
- When a contact is updated with an email for the first time (email was
previously nil), associate with a company
- Preserve existing company associations when email changes to avoid
user confusion
- Skip free email providers and disposable domains
**Dependencies:**
⚠️ Requires PR #12657 (Part 1: Backfill migration) to be merged first
**Linear ticket:**
[CW-5726](https://linear.app/chatwoot/issue/CW-5726/company-model-setting-it-up-on-production)
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
- Service specs: Tests business email detection, company creation,
association logic, edge cases (existing companies, free emails, nil
emails)
- Integration specs: Tests full callback flow for contact create/update
scenarios
- All tests passing: 10 examples, 0 failures
- RuboCop: 0 offenses
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules (PR #12657 pending)
---------
Co-authored-by: Sojan Jose <sojan@pepalo.com>
## Description
Implements company backfill migration infrastructure for existing
contacts. This is **Part 1 of 2** for the company model production
rollout as described in
[CW-5726](https://linear.app/chatwoot/issue/CW-5726/company-model-setting-it-up-on-production).
Creates jobs and services to associate existing contacts with companies
based on their email domains, filtering out free email providers (gmail,
yahoo, etc.) and disposable addresses.
**What's included:**
- Business email detector service with ValidEmail2 (uses
`disposable_domain?` to avoid DNS lookups)
- Per-account batch job to process contacts for one account
- Orchestrator job to iterate all accounts
- Rake task: `bundle exec rake companies:backfill`
~~*NOTE*: I'm using a hard-coded approach to determine if something is a
"business" email by filtering out emails that are usually personal. I've
also added domains that are common to some of our customers' regions.
This should be simpler. I looked into `Valid_Email2` and I couldn't find
anything to dictate whether an email is a personal email or a business
one. I don't think the approach used in the frontend is valid here.~~
UPDATE: Using `email_provider_info` gem instead.
**Pending - Part 2 (separate PR):** Real-time company creation for new
contacts
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
```bash
# Run all new tests
bundle exec rspec spec/enterprise/services/companies/business_email_detector_service_spec.rb \\
spec/enterprise/jobs/migration/company_account_batch_job_spec.rb \\
spec/enterprise/jobs/migration/company_backfill_job_spec.rb
# Run RuboCop
bundle exec rubocop enterprise/app/services/companies/business_email_detector_service.rb \\
enterprise/app/jobs/migration/company_account_batch_job.rb \\
enterprise/app/jobs/migration/company_backfill_job.rb \\
lib/tasks/companies.rake
```
**Performance optimization:**
- Uses `disposable_domain?` instead of `disposable?` to avoid DNS MX
lookups (discovered via tcpdump analysis - `disposable?` was making
network calls for every email, causing 100x slowdown)
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Sojan Jose <sojan@pepalo.com>
## Summary
- Fix captain response builder not getting triggered for cases where
responses are created as completed.
## Testing Instructions
- Test articles with firecrawl
- Test articles without firecrawl
- Test PDF documents
---------
Co-authored-by: Pranav <pranav@chatwoot.com>
Fixes timezone offset parameter in V2 reports API that was broken by DST
transitions. The issue occurred when UK DST ended on October 26, 2025,
causing the test to fail starting October 27th.
~~**Initial diagnosis:** The root cause was that
`timezone_name_from_offset` used `zone.now.utc_offset` to match
timezones, which changes based on the current date's DST status rather
than the data being queried.~~
**Actual root cause:** The test was accidentally passing before DST
transition. During BST, `timezone_name_from_offset(0)` matched "Azores"
(UTC-1) instead of "Edinburgh" (UTC+0), and the -1 hour offset
coincidentally split midnight data into [1,5]. After DST ended, it
correctly matched "Edinburgh" (UTC+0), but this grouped all
conversations into one day [6], exposing that the test data was flawed.
The real issue: Test data created all 6 conversations starting at
midnight on a single day, which cannot produce a [1,5] split in true
UTC.
Fixes CW-5846
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update
**Test that was failing:**
```bash
bundle exec rspec spec/controllers/api/v2/accounts/reports_controller_spec.rb:25
```
**Changes:**
~~1. Fixed `timezone_name_from_offset` to use January 1st as reference
date instead of current date~~
~~2. Converted timezone string to `ActiveSupport::TimeZone` object for
`group_by_period` compatibility~~
**Revised approach:**
1. Freeze test time to January 2024 using `travel_to`, making timezone
matching deterministic and aligned with test data period
2. Start test conversations at 23:00 instead of midnight to properly
span two days and test timezone boundary grouping
3. Keep `zone.now.utc_offset` (correct behavior for real users during
DST)
**Why this works:**
- Test runs "in January 2024" → `zone.now.utc_offset` returns January
offsets consistently
- Offset `-8` correctly matches Pacific Standard Time (UTC-8 in January)
- Real users in PDT (summer) with offset `-7` → correctly match Pacific
Daylight Time
- No production impact, test is deterministic year-round
**Verification:**
- Test now passes consistently regardless of current DST status
- Timezone matching works correctly for real users during DST periods
- Reports correctly group data by timezone offset across all seasons
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
## Description
Fixes timezone offset parameter in V2 reports API that was broken by DST
transitions. The issue occurred when UK DST ended on October 26, 2025,
causing the test to fail starting October 27th.
~~**Initial diagnosis:** The root cause was that
`timezone_name_from_offset` used `zone.now.utc_offset` to match
timezones, which changes based on the current date's DST status rather
than the data being queried.~~
**Actual root cause:** The test was accidentally passing before DST
transition. During BST, `timezone_name_from_offset(0)` matched "Azores"
(UTC-1) instead of "Edinburgh" (UTC+0), and the -1 hour offset
coincidentally split midnight data into [1,5]. After DST ended, it
correctly matched "Edinburgh" (UTC+0), but this grouped all
conversations into one day [6], exposing that the test data was flawed.
The real issue: Test data created all 6 conversations starting at
midnight on a single day, which cannot produce a [1,5] split in true
UTC.
Fixes CW-5846
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update
## How Has This Been Tested?
**Test that was failing:**
```bash
bundle exec rspec spec/controllers/api/v2/accounts/reports_controller_spec.rb:25
```
**Changes:**
~~1. Fixed `timezone_name_from_offset` to use January 1st as reference
date instead of current date~~
~~2. Converted timezone string to `ActiveSupport::TimeZone` object for
`group_by_period` compatibility~~
**Revised approach:**
1. Freeze test time to January 2024 using `travel_to`, making timezone
matching deterministic and aligned with test data period
2. Start test conversations at 23:00 instead of midnight to properly
span two days and test timezone boundary grouping
3. Keep `zone.now.utc_offset` (correct behavior for real users during
DST)
**Why this works:**
- Test runs "in January 2024" → `zone.now.utc_offset` returns January
offsets consistently
- Offset `-8` correctly matches Pacific Standard Time (UTC-8 in January)
- Real users in PDT (summer) with offset `-7` → correctly match Pacific
Daylight Time
- No production impact, test is deterministic year-round
**Verification:**
- Test now passes consistently regardless of current DST status
- Timezone matching works correctly for real users during DST periods
- Reports correctly group data by timezone offset across all seasons
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
### Problem
WhatsApp Cloud channels already handle Brazil/Argentina phone number
format mismatches (PRs #12492, #11173), but Twilio WhatsApp channels
were creating duplicate contacts
when:
- Template sent to new format: `whatsapp:+5541988887777` (13 digits)
- User responds from old format: `whatsapp:+554188887777` (12 digits)
### Solution
The solution extends the existing phone number normalization
infrastructure to support both WhatsApp providers while handling their
different payload formats:
### Provider Format Differences
- **WhatsApp Cloud**: `wa_id: "919745786257"` (clean number)
- **Twilio WhatsApp**: `From: "whatsapp:+919745786257"` (prefixed
format)
### Test Coverage
#### Brazil Phone Number Tests
**Case 1: New Format (13 digits with "9")**
- **Test 1**: No existing contact → Creates new contact with original
format
- **Test 2**: Contact exists in same format → Appends to existing
conversation
**Case 2: Old Format (12 digits without "9")**
- **Test 3**: Contact exists in old format → Appends to existing
conversation
- **Test 4** *(Critical)*: Contact exists in new format, message in old
format → Finds existing contact, prevents duplicate
- **Test 5**: No contact exists → Creates new contact with incoming
format
#### Argentina Phone Number Tests
**Case 3: With "9" after country code**
- **Test 6**: No existing contact → Creates new contact
- **Test 7**: Contact exists in normalized format → Uses existing
contact
**Case 4: Without "9" after country code**
- **Test 8**: Contact exists in same format → Appends to existing
- **Test 9**: No contact exists → Creates new contact
Fixes
https://linear.app/chatwoot/issue/CW-5565/inconsistencies-for-mobile-numbersargentina-brazil-and-mexico-numbers
* fix(baileys): update LID contact phone number if not present
* fix(baileys): move try_update_contact_avatar call to update_contact_information method
* refactor(baileys): consolidate contact update logic in update_contact_information method
This PR is the first of many to simplify the process of building an
assistant. The new flow will only require the user’s website. We’ll
automatically crawl it, identify the business name and what the business
does, and then generate a suggested assistant persona, complete with a
proposed name and description.
This service returns the following.
Example: tooljet.com
<img width="795" height="217" alt="Screenshot 2025-10-25 at 2 55 04 PM"
src="https://github.com/user-attachments/assets/9cb3594a-9c9c-4970-a0a1-4c9c8869c193"
/>
Example: replit.com
<img width="797" height="176" alt="Screenshot 2025-10-25 at 2 56 42 PM"
src="https://github.com/user-attachments/assets/6a1b4266-aab6-455f-a5e3-696d3a8243c9"
/>
* fix: ignore action events
* feat(zapi): allow updating instance ID
* feat: handle `chatLid` field
* test: break down handler specs into separate files
* feat: update send_message method to use recipient_id logic for zapi provider
* fix: use identifier instead of source_id for zapi
* test: fix specs
* feat: prioritize senderName over chatName for contact naming in received callback
## Context
Sidekiq logs only showed the Sidekiq wrapper class and JID, which wasn’t
helpful when debugging ActiveJobs.
## Changes
- Updated `ChatwootDequeuedLogger` to log the actual `ActiveJob class`
and `job_id` instead of the generic Sidekiq wrapper and JID.
> Example
> ```
> Dequeued ActionMailer::MailDeliveryJob
123e4567-e89b-12d3-a456-426614174000 from default
> ```
- Remove sidekiq worker and unify everything to `ActiveJob`
Previously, email replies were handled inside workers. There was no
execution logs. This meant if emails silently failed (as reported by a
customer), we had no way to trace where the issue happened, the only
assumption was “no error = mail sent.”
By moving email handling into jobs, we now have proper execution logs
for each attempt. This makes it easier to debug delivery issues and
would have better visibility when investigating customer reports.
Fixes
https://linear.app/chatwoot/issue/CW-5538/emails-are-not-sentdelivered-to-the-contact
---------
Co-authored-by: Sojan Jose <sojan@pepalo.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
## Changelog
- Added conditional Active Record encryption to every external
credential we store (SMTP/IMAP passwords, Twilio tokens,
Slack/OpenAI hook tokens, Facebook/Instagram tokens, LINE/Telegram keys,
Twitter secrets) so new writes are encrypted
whenever Chatwoot.encryption_configured? is true; legacy installs still
receive plaintext until their secrets are
updated.
- Tuned encryption settings in config/application.rb to allow legacy
reads (support_unencrypted_data) and to extend
deterministic queries so lookups continue to match plaintext rows during
the rollout; added TODOs to retire the
fallback once encryption becomes mandatory.
- Introduced an MFA-pipeline test suite
(spec/models/external_credentials_encryption_spec.rb) plus shared
examples to
verify each attribute encrypts at rest and that plaintext records
re-encrypt on update, with a dedicated Telegram case.
The existing MFA GitHub workflow now runs these tests using the
preconfigured encryption keys.
fixes:
https://linear.app/chatwoot/issue/CW-5453/encrypt-sensitive-credentials-stored-in-plain-text-in-database
## Testing Instructions
1. Instance without encryption keys
- Unset ACTIVE_RECORD_ENCRYPTION_* vars (or run in an environment where
they’re absent).
- Create at least one credentialed channel (e.g., Email SMTP).
- Confirm workflows still function (send/receive mail or a similar
sanity check).
- In the DB you should still see plaintext values—this confirms the
guard prevents encryption when keys are missing.
2. Instance with encryption keys
- Configure the three encryption env vars and restart.
- Pick a couple of representative integrations (e.g., Email SMTP +
Twilio SMS).
- Legacy channel check:
- Use existing records created before enabling keys. Trigger their
workflow (send an email / SMS, or hit the
webhook) to ensure they still authenticate.
- Inspect the raw column—value remains plaintext until changed.
- Update legacy channel:
- Edit one legacy channel’s credential (e.g., change SMTP password).
- Verify the operation still works and the stored value is now encrypted
(raw column differs, accessor returns
original).
- New channel creation:
- Create a new channel of the same type; confirm functionality and that
the stored credential is encrypted from
the start.
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
# Changelog
When an agent bot webhook fails, we now flip any pending conversation
back to an open state so a human agent can pick
it up immediately. There will be an clear activity message giving the
team clear visibility into what went
wrong. This keeps customers from getting stuck in limbo when their
connected bot goes offline.
# Testing instructions
1. Initial setup: Create an agent bot with a working webhook URL and
connect it to a test inbox. Send a message from a
contact (e.g., via the widget) so a conversation is created; it should
enter the Pending state while the bot handles
the reply.
2. Introduce failure: Edit that agent bot and swap the webhook URL for a
dummy endpoint that will fail. Have the same
contact send another message in the existing conversation. Because the
webhook call now fails, the conversation should flip from Pending back
to Open, making it visible to agents. Also verify the activity message
3. New conversation check: With the dummy URL still in place, start a
brand-new conversation from a contact. When the
bot tries (and fails) to respond, confirm that the conversation appears
immediately as Open rather than remaining Pending. Also the activity
message is visible
4. Subsequent messages in open conversations will show no change
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Fixes https://linear.app/chatwoot/issue/CW-5752/fix-nomethoderror-when-processing-video-files-in-slack-integration
#### Problem
When users shared video files (like MP4) through Slack, the `file_type`
method in `SlackMessageHelper` would return `nil` for unsupported file
types. This caused a `NoMethodError (undefined method 'to_sym' for nil)`
when the attachment was being processed, as the system expected a symbol
value for the `file_type` attribute.
#### Solution
- Added video file type support in the `file_type` method case statement
- Added `else` clause to default unknown file types to `:file` instead
of returning `nil`
- This ensures `file_type` always returns a symbol, preventing the
`to_sym` error
Changes searchkick callback behavior to check `should_index?` before
creating reindex jobs, preventing unnecessary job creation for messages
that don't need indexing (activity messages, unpaid accounts, etc.).
Previously, `callbacks: :async` created reindex jobs for all messages
(~5,100/min or 7.3M/day in production), which were then filtered by
`should_index?` inside the job worker - resulting in 98% wasted jobs,
Redis memory pressure, and avoidable p0 alerts.
Now, `should_index?` is checked before job creation via `after_commit`
callback, reducing job creation to actual incoming/outgoing messages
from paid accounts.
Changes:
- Disable automatic searchkick callbacks
- Add manual `after_commit` callback with `should_index?` condition
- Add specs to verify callback behavior
Expected impact:
- 98% reduction in sidekiq job creation (~7.3M → ~150K jobs/day)
- Reduced redis memory usage
- Same async indexing behavior for eligible messages
# Pull Request Template
## Description
* add Company model with validations for name, domain, description and
avatar
* Add database migration fo
* Implement endpoints for company CRUD operations
* Add optional company relationship for contacts
* Add test for models, controllers, factories and policies
* Add authorization policies restricting delete to admins
* support JSON API responses
Please include a summary of the change and issue(s) fixed. Also, mention
relevant motivation, context, and any dependencies that this change
requires.
Fixes #(cw-5650)
## Type of change
Please delete options that are not relevant.
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update
## How Has This Been Tested?
Tests are implemented using `RSpec`
```
$ bundle exec rails db:migrate
$ bundle exec rspec spec/models/company_spec.rb spec/controllers/api/v1/accounts/companies_controller_spec.rb
```
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
This PR fixes URL parsing errors when WhatsApp template parameters
contain URLs with spaces or special characters. The solution adds proper
URL normalization using Addressable::URI before validation, which
automatically handles space encoding and special character
normalization.
Related with https://github.com/chatwoot/chatwoot/pull/12462
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
# Pull Request Template
## Description
This PR increases the custom filter limit from 50 to 1000 per user
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
### Screenshot
<img width="1264" height="71" alt="image"
src="https://github.com/user-attachments/assets/e12667bb-147c-4115-b8a8-9113fca471db"
/>
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
Fixes
https://linear.app/chatwoot/issue/CW-5692/whatsapp-es-numbers-stuck-in-pending-due-to-premature-registration
### Problem
Multiple customers reported that their WhatsApp numbers remain stuck in
**Pending** in WhatsApp Manager even after successful onboarding.
- Our system triggers a **registration call**
(`/<PHONE_NUMBER_ID>/register`) as soon as the number is OTP verified.
- In many cases, Meta hasn’t finished **display name
review/provisioning**, so the call fails with:
```
code: 100, error_subcode: 2388001
error_user_title: "Cannot Create Certificate"
error_user_msg: "Your display name could not be processed. Please edit
your display name and try again."
```
- This leaves the number stuck in Pending, no messaging can start until
we manually retry registration.
- Some customers have reported being stuck in this state for **7+
days**.
### Root cause
- We only check `code_verification_status = VERIFIED` before attempting
registration.
- We **don’t wait** for display name provisioning (`name_status` /
`platform_type`) to be complete.
- As a result, registration fails prematurely and the number never
transitions out of Pending.
### Solution
#### 1. Health Status Monitoring
- Build a backend service to fetch **real-time health data** from Graph
API:
- `code_verification_status`
- `name_status` / `display_name_status`
- `platform_type`
- `throughput.level`
- `messaging_limit_tier`
- `quality_rating`
- Expose health data via API
(`/api/v1/accounts/:account_id/inboxes/:id/health`).
- Display this in the UI as an **Account Health tab** with clear badges
and direct links to WhatsApp Manager.
#### 2. Smarter Registration Logic
- Update `WebhookSetupService` to include a **dual-condition check**:
- Register if:
1. Phone is **not verified**, OR
2. Phone is **verified but provisioning incomplete** (`platform_type =
NOT_APPLICABLE`, `throughput.level = NOT_APPLICABLE`).
- Skip registration if number is already provisioned.
- Retry registration automatically when stuck.
- Provide a UI banner with complete registration button so customers can
retry without manual support.
### Screenshot
<img width="2292" height="1344" alt="CleanShot 2025-09-30 at 16 01
03@2x"
src="https://github.com/user-attachments/assets/1c417d2a-b11c-475e-b092-3c5671ee59a7"
/>
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
The test was failing because Current.contact was not being cleared when
testing system auto-resolution. Added Current.contact = nil to ensure
the system auto-resolution message is triggered instead of contact
resolution.
🤖 Generated with [Claude Code](https://claude.ai/code)
# Pull Request Template
## Description
Please include a summary of the change and issue(s) fixed. Also, mention
relevant motivation, context, and any dependencies that this change
requires.
Fixes # (issue)
## Type of change
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update
## How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.
## Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
We first added conversation continuity for the live chat widget, and
then carried the same logic over to email channels.
The problem was that this added a reply+conversationUUID@domain.com as
the reply-to for emails, which was unnecessary. For email channels, the
reply-to can just be the channel’s own email address.
That extra layer made things more complex than it needed to be. In this
PR, I’ve cleaned up the config so it’s simpler. The table below shows
how it’ll work going forward.
---
| Type | From Email | Reply To Email |
| -- | -- | -- |
| Standard IMAP, SMTP email channel | channel.email | channel.email |
| Google OAuth Email channel | channel.email | channel.email |
| Microsoft OAuth Email channel | channel.email | channel.email |
| Email forwarded to Chatwoot, brought their own SMTP | channel.email |
channel.email |
| Imap to fetch email, Use Chatwoot's SMTP | channel.email if verified
with Chatwoot's SMTP provider. Otherwise account support email |
channel.email |
| Email forwarded to Chatwoot, Use Chatwoot's SMTP | channel.email if
verified with Chatwoot's SMTP provider. Otherwise account support email
| channel.email |
| -- | -- | -- |
| Website Live Chat - Conversation Continuity Inbound Emails enabled|
Account Support Email | reply+{conversation-uuid}@{account_domain} |
| Website Live Chat - Conversation Continuity Inbound Emails disabled|
Account Support Email | Account Support Email |
Fixes https://github.com/chatwoot/chatwoot/issues/10614
Fixes https://github.com/chatwoot/chatwoot/issues/10521
Fixes https://github.com/chatwoot/chatwoot/issues/10300
Fixes https://github.com/chatwoot/chatwoot/issues/10091
Fixes https://github.com/chatwoot/chatwoot/issues/4890
Fixes https://github.com/chatwoot/chatwoot/issues/10676
Fixes https://github.com/chatwoot/chatwoot/issues/10756
Fixes https://github.com/chatwoot/chatwoot/issues/11515
Fixes https://github.com/chatwoot/chatwoot/issues/9471
---------
Co-authored-by: Sojan Jose <sojan@pepalo.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
With this change, the indexing would be separate from the search, so you
need to enable indexing on the cloud and run it. It should start
indexing the messages to ElasticSearch/OpenSearch. Once indexing is
completed, we can turn on the feature for the customer.
Make sure that the following is done when you deploy.
Set POSTGRES_STATEMENT_TIMEOUT=600s before you run the indexing.
1. Make sure that the account with advanced_search has
advanced_search_indexing enabled
```rb
Account.feature_advanced_search.each do |account|
account.enable_features(:advanced_search_indexing)
account.save!
end
```
2. Enable indexing for all accounts with paid subscription.
```rb
Account.where("custom_attributes ->> 'plan_name' IN (?)", ['Enterprise', 'Startups', 'Business']).each do |account|
account.enable_features(:advanced_search_indexing)
account.save!
end
```
3. Run indexing for all the messages.
```rb
Message.reindex
```
Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This PR implements the **"Lock to Single Conversation"** option for
Telegram inboxes, bringing it to parity with WhatsApp, SMS, and other
channels.
- When **enabled**: resolved conversations can be reopened (single
thread).
- When **disabled**: new messages from a resolved conversation create a
**new conversation**.
- Added **agent name display** in outgoing Telegram messages (formatted
as `Agent Name: message`).
- Updated frontend to display agent name above messages in the dashboard
(consistent with WhatsApp behavior).
This fixes [#8046](https://github.com/chatwoot/chatwoot/issues/8046).
## Type of change
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update
## How Has This Been Tested?
- Unit tests added in
`spec/services/telegram/incoming_message_service_spec.rb`
- Scenarios covered:
- Lock enabled → reopens resolved conversation
- Lock disabled → creates new conversation if resolved
- Lock disabled → appends to last open conversation
- Manual tests:
1. Create a Telegram conversation
2. Mark it as resolved
3. Send a new message from same user
4. ✅ Expected: new conversation created (if lock disabled)
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
## Additional Documentation
For full technical details of this implementation, please refer to:
[TELEGRAM_LOCK_TO_SINGLE_CONVERSATION_IMPLEMENTATION_EN.md](./TELEGRAM_LOCK_TO_SINGLE_CONVERSATION_IMPLEMENTATION_EN.md)
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Fixes
https://linear.app/chatwoot/issue/CW-5646/add-support-for-grouped-file-uploads-in-slack
Previously, when sending multiple attachments to Slack, we uploaded them
one by one. For example, sending 5 images would result in 5 separate
Slack messages. This created clutter and a poor user experience, since
Slack displayed each file as an individual message.
This PR updates the implementation to group all attachments from a
message and send them as a single Slack message. As a result,
attachments now appear together in one grouped block, providing a much
cleaner and more intuitive experience for users.
**Before:**
Each file uploaded as a separate Slack message.
<img width="400" height="800" alt="before"
src="https://github.com/user-attachments/assets/c8c7f666-549b-428f-bd19-c94e39ed2513"
/>
**After:**
All files from a single message grouped and displayed together in one
Slack message (similar to how Slack natively handles grouped uploads).
<img width="400" height="800" alt="after"
src="https://github.com/user-attachments/assets/0b1f22d5-4d37-4b84-905a-15e742317e72"
/>
**Changes**
- Upgraded Slack file upload implementation to use the new multiple
attachments API available in slack-ruby-client `v2.7.0`.
- Updated attachment handling to upload all files from a message in a
single API call.
- Enabled proper attachment grouping in Slack, ensuring related files
are presented together.
- If super admin updates a user email from super admin panel , it will
be confirmed automatically if confirmed at is present
- Also unconfirmed emails will be visible for super admins on dashboard
fixes: https://github.com/chatwoot/chatwoot/issues/8958
This PR is the fix for MFA changes, to not generate auth tokens without
MFA verification in case MFA is enabled for the account
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
## Description
This pull request introduces an optional parameter, `phone_number_id`,
to the WhatsApp API call responsible for retrieving media. The addition
of this parameter allows for greater flexibility when interacting with
the WhatsApp API, as it can now accommodate scenarios where specifying a
particular phone number ID is necessary. This change is backward
compatible and does not affect existing functionality if the parameter
is not provided.
Fixes # (issue)
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
The changes were tested locally by invoking the WhatsApp media retrieval
API with and without the `phone_number_id` parameter. Both scenarios
were verified to ensure that:
- When `phone_number_id` is provided, the API call includes the
parameter and functions as expected.
- When `phone_number_id` is omitted, the API call continues to work as
before, maintaining backward compatibility.
No errors or warnings were observed during testing, and all relevant
unit tests passed successfully.
## Checklist
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
# Pull Request Template
## Description
when filtering contacts by phone number a + is always added to the
begining of the query, this means that the filtering breaks if the
complete phone number with international code and + is entered
## Type of change
Please delete options that are not relevant.
- [X] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update
## How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.
Updated automated tests
Tested manually with contact filtering UI
## Checklist:
- [X] My code follows the style guidelines of this project
- [X] I have performed a self-review of my code
- [X] I have commented on my code, particularly in hard-to-understand
areas
- [X] I have made corresponding changes to the documentation
- [X] My changes generate no new warnings
- [X] I have added tests that prove my fix is effective or that my
feature works
- [X] New and existing unit tests pass locally with my changes
- [X] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
# Pull Request Template
## Description
Ensure messages go to correct conversation when receive multi user in 1
LINE webhook.
base on
[document](https://developers.line.biz/en/reference/messaging-api/#webhook-event-objects:~:text=There%20is%20not%20necessarily%20one%20user%20per%20webhook).
it said
```
There is not necessarily one user per webhook.
A message event from person A and a follow event from person B may be in the same webhook.
```
this PR has 1 break changes.
In old version. when receive
[follow](https://developers.line.biz/en/reference/messaging-api/#follow-event)
event, it will create conversation with no messages.
After this PR. when receive follow event, it will not create
conversation, contact and messages
## Type of change
Please delete options that are not relevant.
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [x] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update
## How Has This Been Tested?
add test case.
and follow event test by delete conversation, and block and unblock line
account
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: mix5003 <mix5003@debian.debian>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
# Pull Request Template
## Description
This PR fixes the incorrect contact access in conversations listing API.
Cause:
- `undefined method 'conversations' for nil` error because `@contact` is
not initialized
Solution:
- Using `@contact_inbox` to access `@contact`
- `@contact_inbox` is properly set in the parent controller's
`set_contact_inbox` method
Fixes
https://linear.app/chatwoot/issue/CW-4185/incorrect-contact-access-pattern-in
## Type of change
Please delete options that are not relevant.
- [x] Bug fix (non-breaking change which fixes an issue)
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
# Pull Request Template
## Description
This pull request allow LINE to receive files.
## Type of change
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update
## How Has This Been Tested?
add testcase. and test manually by myself.
in case you want to test in android, use native share method to share
files to LINE.
you can share more file types to LINE (native line share only send
image,video and audio).
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: mix5003 <mix5003@debian.debian>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
## Summary
- update the admin notification base mailer spec to ignore ordering when
verifying administrator email addresses
- extend the channel and integrations admin notification mailer specs to
cover multiple administrators without relying on recipient order
------
https://chatgpt.com/codex/tasks/task_e_68cc7457cf788326a765f116ceab1732
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
## Linear:
- https://github.com/chatwoot/chatwoot/issues/486
## Description
This PR implements Multi-Factor Authentication (MFA) support for user
accounts, enhancing security by requiring a second form of verification
during login. The feature adds TOTP (Time-based One-Time Password)
authentication with QR code generation and backup codes for account
recovery.
## Type of change
- [ ] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
- Added comprehensive RSpec tests for MFA controller functionality
- Tested MFA setup flow with QR code generation
- Verified OTP validation and backup code generation
- Tested login flow with MFA enabled/disabled
## Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Pranav <pranav@chatwoot.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
- Add support for using labels as an action event for automation
- Fix duplicated conversation_updated event dispatch for labels
Fixes https://github.com/chatwoot/chatwoot/issues/8539 and multiple
issues around duplication related to label change events.
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
## Summary
- allow help center portals to clear their associated web widget
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
This PR is part of https://github.com/chatwoot/chatwoot/pull/12259. It
adds a default expiry of 180 days for tokens issued on the widget. The
expiry can be customized based on customer requests and internal
security requirements.
Co-authored-by: Balasaheb Dubale <bdubale@entrata.com>
We recently introduced the WhatsApp Embedded Signup flow in Chatwoot to
simplify onboarding. However, we discovered two important limitations:
Some customers’ numbers are already linked to an Embedded Signup, which
blocks re-use. Tech providers cannot onboard their own numbers via
Embedded Signup.
As a result, we need to support both Manual and Embedded Signup flows to
cover all scenarios.
### Problem
- Current UI only offers the Embedded Signup option.
- Customers who need to reuse existing numbers (already connected to
WABA) or tech providers testing their own numbers get stuck.
- Manual flow exists but is no longer exposed in the UX
**Current Embedded Signup screen**
<img width="2564" height="1250" alt="CleanShot 2025-08-21 at 21 58
07@2x"
src="https://github.com/user-attachments/assets/c3de4cf1-cae6-4a0e-aa9c-5fa4e2249c0e"
/>
**Current Manual Setup screen**
<img width="2568" height="1422" alt="CleanShot 2025-08-21 at 22 00
25@2x"
src="https://github.com/user-attachments/assets/96408f97-3ffe-42d1-9019-a511e808f5ac"
/>
### Solution
- Design a dual-path UX in the Create WhatsApp Inbox step that:
- Offers Embedded Signup (default/recommended) for new numbers and
businesses.
- Offers Manual Setup for advanced users, existing linked numbers, and
tech providers.
<img width="2030" height="1376" alt="CleanShot 2025-09-01 at 14 13
16@2x"
src="https://github.com/user-attachments/assets/6f17e5a2-a2fd-40fb-826a-c9ee778be795"
/>
---------
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
I've added the account_id filter to the
`get_agent_ids_over_assignment_limit` method. This optimization will
help the query leverage the existing composite index
`conv_acid_inbid_stat_asgnid_idx (account_id, inbox_id, status,
assignee_id)` for better performance.
**Before:**
```sql
HashAggregate (cost=224238.12..224256.27 rows=484 width=4)
Group Key: assignee_id
Filter: (count(*) >= 10)
-> Index Scan using index_conversations_on_inbox_id on conversations (cost=0.44..223963.67 rows=54891 width=4)
Index Cond: (inbox_id = ???)
Filter: (status = 0)
```
**After:**
```sql
GroupAggregate (cost=0.44..5688.30 rows=476 width=4)
Group Key: assignee_id
Filter: (count(*) >= 10)
-> Index Only Scan using conv_acid_inbid_stat_asgnid_idx on conversations (cost=0.44..5640.81 rows=5928 width=4)
Index Cond: ((account_id = ??) AND (inbox_id = ??) AND (status = 0))
```
Deleting large Accounts/Inboxes with object.destroy! can time out and
create heavy destroy_async fan-out; this change adds a simple pre-purge
that batch-destroys heavy associations first .
```
Account: conversations, contacts
Inbox: conversations, contact_inboxes
```
We use find_in_batches(5000), then proceeds with destroy!, reducing DB
pressure and race conditions while preserving callbacks and leaving the
behavior for non heavy models unchanged. The change is also done in a
way to easily add additional objects or relations to the list.
fixes:
https://linear.app/chatwoot/issue/CW-3106/inbox-deletion-process-update-the-flow
The system determines a user’s active account by checking the
`active_at` field in the `account_users` table and selecting the most
recently active account:
```ruby
def active_account_user
account_users.order(active_at: :desc)&.first
end
```
This works fine when all accounts have a valid active_at timestamp.
**Problem**
When a user is added to a new account, the `active_at` value is NULL
(because the account has never been explicitly activated). Ordering by
active_at DESC produces inconsistent results across databases, since
handling of NULL values differs (sometimes treated as high, sometimes
low).
As a result:
- Mobile apps (critical impact): `/profile` returns the wrong account.
The UI keeps showing the old account even after switching, and
restarting does not fix it.
- Web app (accidentally works): Appears correct because the active
account is inferred from the browser URL, but the backend API is still
wrong.
**Root Cause**
- The ordering logic did not account for NULL `active_at`.
- New accounts without active_at sometimes get incorrectly prioritized
as the “active” account.
**Solution**
Explicitly ensure that accounts with NULL active_at are sorted after
accounts with real timestamps by using NULLS LAST:
```ruby
def active_account_user
account_users.order(Arel.sql('active_at DESC NULLS LAST, id DESC'))&.first
end
```
- Accounts with actual `active_at` values will always be prioritized.
- New accounts (with NULL active_at) will be placed at the bottom until
the user explicitly activates them.
- Adding id DESC as a secondary ordering ensures consistent tie-breaking
when multiple accounts have the same `active_at`.
# Pull Request Template
## Description
This Pull Request will provide a language selector in the Profile
Settings for each user, and allows them to change the UI language per
agent, defaulting back to the account locale.
Fixes # #678 This does PR addresses the Dashboard view but does not
change the language of the agents emails
## Type of change
Please delete options that are not relevant.
- [X ] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
1. Go to an Agents Profile settings page
2. Select a language from the Language drop down
3. the UI will update to the new i18n locale
4. navigate through the UI to make sure the appropriate language is
being used
5. Refresh the page to test that the locale persists
270
- [X] My code follows the style guidelines of this project
- [X] I have performed a self-review of my code
- [X] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [X] My changes generate no new warnings
- [X] I have added tests that prove my fix is effective or that my
feature works
- [X] New and existing unit tests pass locally with my changes
- [X] Any dependent changes have been merged and published in downstream
modules
Checklist:.724.2708
---------
Co-authored-by: Sojan Jose <sojan@pepalo.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This PR delivers the first slice of the voice channel: inbound call
handling. When a customer calls a configured voice
number, Chatwoot now creates a new conversation and shows a dedicated
call bubble in the UI. As the call progresses
(ringing, answered, completed), its status updates in real time in both
the conversation list and the call bubble, so
agents can instantly see what’s happening. This focuses on the inbound
flow and is part of breaking the larger voice
feature into smaller, functional, and testable units; further
enhancements will follow in subsequent PRs.
references: #11602 , #11481
## Testing
- Configure a Voice inbox in Chatwoot with your Twilio number.
- Place a call to that number.
- Verify a new conversation appears in the Voice inbox for the call.
- Open it and confirm a dedicated voice call message bubble is shown.
- Watch status update live (ringing/answered); hang up and see it change
to completed in both the bubble and conversation
list.
- to test missed call status, make sure to hangup the call before the
please wait while we connect you to an agent message plays
## Screens
<img width="400" alt="Screenshot 2025-09-03 at 3 11 25 PM"
src="https://github.com/user-attachments/assets/d6a1d2ff-2ded-47b7-9144-a9d898beb380"
/>
<img width="700" alt="Screenshot 2025-09-03 at 3 11 33 PM"
src="https://github.com/user-attachments/assets/c25e6a1e-a885-47f7-b3d7-c3e15eef18c7"
/>
<img width="700" alt="Screenshot 2025-09-03 at 3 11 57 PM"
src="https://github.com/user-attachments/assets/29e7366d-b1d4-4add-a062-4646d2bff435"
/>
<img width="442" height="255" alt="Screenshot 2025-09-04 at 11 55 01 PM"
src="https://github.com/user-attachments/assets/703126f6-a448-49d9-9c02-daf3092cc7f9"
/>
---------
Co-authored-by: Muhsin <muhsinkeramam@gmail.com>
This PR adds the foundation for account-level SAML SSO configuration in
Chatwoot Enterprise. It introduces a new `AccountSamlSettings` model and
management API that allows accounts to configure their own SAML identity
providers independently, this also includes the certificate generation
flow
The implementation includes a new controller
(`Api::V1::Accounts::SamlSettingsController`) that provides CRUD
operations for SAML configuration
The feature is properly gated behind the 'saml' feature flag and
includes administrator-only authorization via Pundit policies.
There was a fundamental difference in how resolution counts were
calculated between the agent summary and timeseries reports, causing
confusion for users when the numbers didn't match.
The agent summary report counted all `conversation_resolved` events
within a time period by querying the `reporting_events` table directly.
However, the timeseries report had an additional constraint that
required the conversation to currently be in resolved status
(`conversations.status = 1`). This meant that if an agent resolved a
conversation that was later reopened, the resolution action would be
counted in the summary but not in the timeseries.
This fix aligns both reports to count resolution events rather than
conversations in resolved state. When an agent resolves a conversation,
they should receive credit for that action regardless of what happens to
the conversation afterward. The same logic now applies to bot
resolutions as well.
The change removes the `conversations: { status: :resolved }` condition
from both `scope_for_resolutions_count` and
`scope_for_bot_resolutions_count` methods in CountReportBuilder, and
updates the corresponding test expectations to reflect that all
resolution events are counted.
## About timezone
When a timezone is specified via `timezone_offset` parameter, the
reporting system:
1. Converts timestamps to the target timezone before grouping
2. Groups data by local day/week/month boundaries in that timezone, but
the primary boundaries are sent by the frontend and used as-is
3. Returns timestamps representing midnight in the target timezone
This means the same events can appear in different day buckets depending
on the timezone used. For summary reports, it works fine, since the user
only needs the total count between two timestamps and the frontend sends
the timestamps adjusted for timezone.
## Testing Locally
Run the following command, this will erase all data for that account and
put in 1000 conversations over last 3 months, parameters of this can be
tweaked in `Seeders::Reports::ReportDataSeeder`
I'd suggest updating the values to generate data over 30 days, with
10000 conversations, it will take it's sweet time to run but then the
data will be really rich, great for testing.
```
ACCOUNT_ID=2 ENABLE_ACCOUNT_SEEDING=true bundle exec rake db:seed:reports_data
```
Pro Tip: Don't run the app when the seeder is active, we manually create
the reporting events anyway. So once done just use `redis-cli FLUSHALL`
to clear all sidekiq jobs. Will be easier on the system
Use the following scripts to test it
- https://gist.github.com/scmmishra/1263a922f5efd24df8e448a816a06257
- https://gist.github.com/scmmishra/ca0b861fa0139e2cccdb72526ea844b2
- https://gist.github.com/scmmishra/5fe73d1f48f35422fd1fd142ea3498f3
- https://gist.github.com/scmmishra/3b7b1f9e2ff149007170e5c329432f45
- https://gist.github.com/scmmishra/f245fa2f44cd973e5d60aac64f979162
---------
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Currently, auto-assignment runs only during conversation creation or
update events. If no agents are online when new conversations arrive,
those conversations remain unassigned.
With this change, unassigned conversations will be automatically
assigned once agents become available. The job runs every 15 minutes and
uses a fair distribution threshold of 100 to prevent a large number of
conversations from being assigned to a single available agent. This will
be customizable later.
We were using UTM params on various branding urls which weren't
compliant to standard utm params and hence were ignored by analytics
tooling. this PR ensures that the params stays compliant with defined
standard
ref: https://en.wikipedia.org/wiki/UTM_parameters
## Changes
- updated utm tags on widget and survey urls
- added utm on helpcenter branding
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
We now support searching within the actual message content, email
subject lines, and audio transcriptions. This enables a faster, more
accurate search experience going forward. Unlike the standard message
search, which is limited to the last 3 months, this search has no time
restrictions.
The search engine also accounts for small variations in queries. Minor
spelling mistakes, such as searching for slck instead of Slack, will
still return the correct results. It also ignores differences in accents
and diacritics, so searching for Deja vu will match content containing
Déjà vu.
We can also refine searches in the future by criteria such as:
- Searching within a specific inbox
- Filtering by sender or recipient
- Limiting to messages sent by an agent
Fixes https://github.com/chatwoot/chatwoot/issues/11656
Fixes https://github.com/chatwoot/chatwoot/issues/10669
Fixes https://github.com/chatwoot/chatwoot/issues/5910
---
Rake tasks to reindex all the messages.
```sh
bundle exec rake search:all
```
Rake task to reindex messages from one account only
```sh
bundle exec rake search:account ACCOUNT_ID=1
```
* feat(baileys): add status method to fetch Baileys API version and availability
* test: specs
* chore: raise error instead of returning error in version
* feat(whatsapp): add profile picture fetching for Baileys provider
- Add `get_profile_pic` method to `WhatsappBaileysService` for fetching contact avatars
- Include `avatar_url` in contact attributes during contact creation
- Update `MessagesUpsert` handler to fetch and set profile pictures
- Add proper error handling and graceful degradation when avatar fetching fails
* test(whatsapp): add test coverage for Baileys profile picture fetching
- Add test suite for `get_profile_pic` method
- Include test scenarios for successful responses, missing avatars, and error handling
- Add profile picture HTTP stubbing and avatar job expectations to message processing tests
* feat(whatsapp): optimize Baileys avatar fetching and update avatar for existing inboxes
Previously, the profile picture was fetched on every incoming message and avatar updates were only attempted for new inboxes.
This change optimizes the process by:
- Only fetching the WhatsApp profile picture if the contact does not already have an avatar.
- Attempting to update the avatar for both new and existing inboxes.
This logic is based on Telegram's version (app/services/telegram/incoming_message_service.rb).
* refactor(whatsapp): remove unnecessary safe navigation when accessing contact
This reverts an unnecessary change introduced in b918a92.
* refactor(whatsapp): manually rollback change introduced in b918a92
* refactor(whatsapp): rename endpoint to profile-picture-url
* refactor(whatsapp): simplify profile picture response handling
Replaced manual JSON parsing with HTTParty's parsed_response and removed redundant JSON parsing tests.
* test(whatsapp): separate avatar processing test from message creation test
* test(whatsapp): update profile picture test to match actual API behavior
Change test to expect a 404 response when no profile picture exists, instead of a 200 response with partial data.
* refactor: move new functions to helper module
* test: more specs
---------
Co-authored-by: gabrieljablonski <contact@gabrieljablonski.com>
## Linear reference:
https://linear.app/chatwoot/issue/CW-4649/re-imagine-assignments
## Description
This PR introduces the foundation for Assignment V2 system by
implementing agent_capacity and their association with inboxes and
users.
## Type of change
- [ ] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
Test Coverage:
- Controller specs for assignment policies CRUD operations
- Enterprise-specific specs for balanced assignment order
- Model specs for community/enterprise separation
## Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Pranav <pranav@chatwoot.com>
Added comprehensive Twilio WhatsApp content template support (Phase 1)
enabling text, media, and quick reply templates with proper parameter
conversion, sync capabilities.
**Template Types Supported**
- Basic Text Templates: Simple text with variables ({{1}}, {{2}})
- Media Templates: Image/Video/Document templates with text variables
- Quick Reply Templates: Interactive button templates
Front end changes is available via #12277
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
There were customer reported issues with FAQs which were generated in a
different langauge than what they were expecting. The reason behind this
was that the language of the account was not considered in the prompt
provided. If the language of the content was say Spanish, and the
account locale was english. The output was not predicable. The output
depends on the model and the execution time.
This PR would update the prompt to behave consistently with the account
locale. Even though the content provided is in a different language, it
would generate FAQs in the account locale.
Changes:
- Updated the prompt to include a detailed expectation of the FAQs
quality along with the language
- Added specs for the services where the prompt generator is called.
Tested the prompt using Phoenix playground across GPT 5, GPT 4.1, GPT
4.0. The reasoning setting for GPT 5 needs to be low so that it doesn't
generate random questions like "What was this updated?"
This PR improves the voice call creation flow by simplifying
configuration and automating setup with Twilio APIs.
references: #11602 , #11481
## Key changes
- Removed the requirement for twiml_app_sid – provisioning is now
automated through APIs.
- Auto-configured webhook URLs for:
- Voice number callbacks
- Status callbacks
- twiML callbacks
- Disabled business hours, help center, and related options until voice
inbox is fully supported.
- Added a configuration tab in the voice inbox to display the required
Twilio URLs (to make verification easier in Twilio console).
## Test Cases
- Provisioning
- Create a new voice inbox → verify that Twilio app provisioning happens
automatically.
- Verify twiML callback
- Webhook configuration
- Check that both voice number callback and status callback URLs are
auto-populated in Twilio.
- Disabled features
- Confirm that business hours and help center options are
hidden/disabled for voice inbox.
- Configuration tab
- Open the voice inbox configuration tab → verify that the displayed
Twilio URLs match what’s set in Twilio.
## Linear reference:
https://linear.app/chatwoot/issue/CW-4649/re-imagine-assignments
## Description
This PR introduces the foundation for Assignment V2 system by
implementing assignment policies and their association with inboxes.
Assignment policies allow configuring how conversations are distributed
among agents, with support for different assignment orders (round_robin
in community, balanced in enterprise) and conversation prioritization
strategies
Fixes # (issue)
## Type of change
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update
## How Has This Been Tested?
Test Coverage:
- Controller specs for assignment policies CRUD operations
- Enterprise-specific specs for balanced assignment order
- Model specs for community/enterprise separation
Manual Testing:
1. Create assignment policy: POST
/api/v1/accounts/{id}/assignment_policies
2. List policies: GET /api/v1/accounts/{id}/assignment_policies
3. Assign policy to inbox: POST
/api/v1/accounts/{id}/assignment_policies/{id}/inboxes
4. View inbox policy: GET
/api/v1/accounts/{id}/inboxes/{id}/assignment_policy
5. Verify community edition ignores "balanced" assignment order
6. Verify enterprise edition supports both "round_robin" and "balanced"
- testing the flows after enterprise folder deletion
## Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Pranav <pranavrajs@gmail.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
* feat: add customizable signature position and separator options
* fix: correct default value note for signatureSeparator and ensure reactivity
* fix: correct watcher boolean conversion and add immediate ui_settings updates
- Fix watchers to convert string props to boolean values for reactive refs
- Add immediate event handlers for switch changes to update ui_settings in real-time
- Ensure proper synchronization between switch states and user.ui_settings
Co-Authored-By: cayo@fazer.ai <cayoproliveira@gmail.com>
* fix: split signature content and ui_settings updates to resolve persistence bug
- Use updateUISettings store action for signature_position and signature_separator
- Keep updateProfile for message_signature content only
- Fixes FormData serialization issue that corrupted nested ui_settings object
- Add diagnostic logging to verify data flow
Co-Authored-By: cayo@fazer.ai <cayoproliveira@gmail.com>
* clean: remove diagnostic console logging from updateSignature method
- Remove temporary console.log statements added for verification
- Keep core implementation that splits signature content and ui_settings updates
- Keep console.error for proper error handling with eslint-disable comment
- Implementation now ready for production use
Co-Authored-By: cayo@fazer.ai <cayoproliveira@gmail.com>
* fix: updateUISettings call in updateSignature method
* chore: move signature application to send-time and add button highlighting (#79)
* fix: move signature application from editor manipulation to send-time
- Remove addSignature/removeSignature/toggleSignatureInEditor from WootWriter
- Remove signature logic from draft handling and canned response insertion
- Apply signatures only in getMessagePayload during message sending
- Add button highlighting for signature toggle when activated
- Prevents signature duplication and persistence in editor content
- Fixes signature position toggle bug
Co-Authored-By: cayo@fazer.ai <cayoproliveira@gmail.com>
* fix: escape signature separator to prevent markdown setext heading interpretation
- Escape '--' separator as '\--' in appendSignature to prevent H2 heading creation
- Update removeSignature to handle escaped separators correctly
- Fixes signature separator being rendered as markdown instead of plain text
- Refactor nested ternary to fix ESLint error
Co-Authored-By: cayo@fazer.ai <cayoproliveira@gmail.com>
* fix: prevent signature separator markdown interpretation in message processing
- Add fix_signature_separator_markdown method to escape '--' separators
- Update ensure_processed_message_content to fix separators before saving
- Prevents signature separators from being interpreted as setext headings
- Ensures correct message display in channels and email notifications
Co-Authored-By: cayo@fazer.ai <cayoproliveira@gmail.com>
* fix: update separator format to use \n--\n instead of escaping
- Change separator delimiter from '\--' to '\n--\n' format
- Update removeSignature function to handle new separator format correctly
- Simplify message processing since separators are already properly formatted
- Ensures consistent separator handling across frontend and backend
Co-Authored-By: cayo@fazer.ai <cayoproliveira@gmail.com>
* fix: update signature delimiter format to include extra new lines
* chore: remove comment about signature application logic
* refactor: remove unused method and comments related to signature separator markdown processing
* chore: simplify slash command detection by using updatedMessage directly
* refactor: remove signature logic from draft message handling
* refactor: simplify body empty check by removing signature manipulation logic
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: cayo@fazer.ai <cayoproliveira@gmail.com>
* refactor: extract signature settings logic into a separate method
* fix: handle nil ui_settings in signature position and separator methods
* fix: update return value of findSignatureInBody to include position information
* fix: update signature handling in findSignatureInBody and related methods
* fix: adjust delimiter length handling in removeSignature function
* test: add cases for appending, removing, and replacing signatures with various separators
* test: add cases for signature position and separator handling
* test: add cases for updating signature position and separator in ui_settings
* fix: correct typo in comment for findSignatureInBody function
* refactor: simplify translation function calls in MessageSignature component
* chore: refactoring
* chore: refactor
* feat: switch -> select
* chore: refactor and undo changes
* chore: refactor and undo changes
* chore: refactor
* fix: remove old select component usage
* chore: remove useless style
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: gabrieljablonski <contact@gabrieljablonski.com>
## Reference
https://github.com/chatwoot/chatwoot/pull/12149#issuecomment-3178108388
## Description
setup_webhook was done before the save, and hence the meta webhook
validation might fail because of a race condition where the facebook
validation is done before we saving the entry to the database.
## Type of change
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
- New inbox creation, webhook validation
- Existing inbox update, webhook validation
-
<img width="614" height="674" alt="image"
src="https://github.com/user-attachments/assets/be223945-deed-475a-82e5-3ae9c54a13fa"
/>
## Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
# Pull Request Template
## Description
Fixes
https://linear.app/chatwoot/issue/CW-5411/actionviewtemplateerror-activestorageunrepresentableerror
### Problem
API endpoints return 500 errors when conversations contain image
attachments that can't be processed by ActiveStorage (e.g., files with
non-ASCII filenames, corrupted images, or malicious XSS filenames).
Root Cause: Commit 6cab74139 removed the representable? safety check
from thumb_url, causing `ActiveStorage::UnrepresentableError` to bubble
up and crash the API when it encountered a malformed image file.
Fix: Rescue `thumb_url` method to catch UnrepresentableError and return
an empty string while logging problematic names for future debugging.
This ensures the messages/attachments api does not break due to a single
corrupted image file.
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
- Added specs
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
WhatsApp templates without parameters (body-only templates like
notifications, confirmations) were failing to send with the error:
ArgumentError (Unknown legacy format: NilClass). This affected all
parameter-less templates across marketing messages, notifications, and
utility templates.
### Summary
Fixed flaky Instagram webhook specs that failed intermittently in cloud
environments due to shared let blocks creating conflicting inboxes. The
Instagram channel factory already creates an inbox automatically, but
tests were adding extra ones in shared contexts.
Moved channel/inbox creation to isolated test contexts to prevent race
conditions between Facebook Page and Instagram Direct tests.
### Testing
```
for i in {1..30}; do
echo "=== Run $i ==="
RAILS_ENV=test bundle exec rspec spec/jobs/webhooks/instagram_events_job_spec.rb --fail-fast || break
done
```
Previously, intermittent failures could be reproduced locally. With
these changes, tests achieve ~100% pass rate.
WhatsApp template message errors were not being properly handled because
the `@message instance` variable was only set in the `send_message`
method but not in `send_template`. When template sending failed, the
`handle_error` method couldn't update the message status due to the
missing @message reference, resulting in silent failures with no user
feedback.