## Description
Adds a guarded retry action for Intercom imports that have not recorded
progress for 15 minutes. The API reports stalled state, rotates the
import run identifier under an account lock, preserves existing progress
and logs, and queues a fresh worker only when no other Intercom import
is active for the account. The import details page shows Retry
immediately before Abandon only while the server reports the import as
stalled.
This is Phase 1, Task 1 of the [Intercom import optimization plan
(CW-7615)](https://linear.app/chatwoot/issue/CW-7615/optimize-intercom-import-reliability-and-bulk-message-ingestion).
This replaces #15049 with the updated 15-minute threshold and is based
directly on the latest `develop`.
## Closes
-
[CW-7519](https://linear.app/chatwoot/issue/CW-7519/explore-intercom-import)
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How to test
1. Open a processing Intercom import updated within the last 15 minutes
and confirm Retry is hidden.
2. Set its updated timestamp to more than 15 minutes ago and reload the
details page.
3. Confirm Retry appears immediately before Abandon.
4. Retry the import and confirm it returns to Pending while existing
progress, logs, and the original start time remain intact.
5. Confirm a second retry is rejected and another active Intercom import
prevents queueing.
## Checklist
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have added tests that prove the change is effective
- [x] New and existing focused tests pass locally with my changes
Agents can review recurring FAQ suggestions when they can access at
least one supporting conversation. The detail view returns only source
conversations the agent can access. Administrators can review every
suggestion and can edit, approve, or dismiss it. Approval creates one
approved Captain FAQ and removes the stored source observations.
This is the third PR in the CW-7495 stack. It is built on
[#14978](https://github.com/chatwoot/chatwoot/pull/14978), which adds
the FAQ suggestion models and generation flow.
## Closes
Closes
[CW-7495](https://linear.app/chatwoot/issue/CW-7495/backend-llm-changes-to-make-conversation-faqs-as-signalssuggestions).
## What changed
1. Added a paginated suggestion list with assistant, status, and search
filters.
2. Limited agents to suggestions that have at least one source
conversation they can access.
3. Limited the detail response to the 50 most recent source
conversations the current user can access.
4. Allowed administrators to edit, approve, and dismiss open
suggestions.
5. Added approval that creates one approved Captain FAQ, closes the
suggestion, and removes its source observations.
6. Rejected approval when the suggestion language does not match the
account language.
7. Added row locking so an edit or dismissal cannot overwrite an
approval.
8. Prevented FAQ generation from attaching a new observation after a
suggestion has closed.
## How to test
1. Sign in as an agent who has access to one inbox but not another.
2. Confirm the agent sees only suggestions with at least one source
conversation from an accessible inbox.
3. Open a suggestion and confirm the source list does not contain
conversations from restricted inboxes.
4. Sign in as an administrator and confirm all account suggestions are
available.
5. Edit an open suggestion and approve it. Confirm one approved FAQ is
created and the suggestion no longer has source observations.
6. Try to approve a suggestion in a different language from the account
language. Confirm the request is rejected.
7. Dismiss another open suggestion and confirm it leaves the open review
queue.
---------
Co-authored-by: Sony Mathew <sony@chatwoot.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Captain can now turn resolved conversations into FAQ suggestions that
people can review. When a human support agent gives a reusable answer,
Captain saves the question and answer as an observation. Captain groups
matching observations into one suggestion instead of creating a pending
FAQ for every conversation. This PR does not approve or publish FAQs.
PR [#14977](https://github.com/chatwoot/chatwoot/pull/14977) adds the
data model and should be reviewed first. The controller and UI PRs will
add the review flow. The three PRs should merge together.
## Closes
[CW-7495](https://linear.app/chatwoot/issue/CW-7495/backend-llm-changes-to-make-conversation-faqs-as-signalssuggestions).
This is PR 2 of 3. The issue is complete after the full stack lands.
## What changed
1. Runs FAQ generation in the low priority queue after a conversation is
resolved.
2. Reads only customer messages and answers written by human support
agents.
3. Uses the assistant's product details, instructions, response rules,
and guardrails to reject spam and unrelated conversations.
4. Stores each reusable question and answer as an observation.
5. Uses exact text similarity search within the conversation language to
find likely matches.
6. Asks the LLM whether both FAQs ask the same question and give the
same answer.
7. Does not create a new suggestion when an approved FAQ already covers
the observation.
8. Adds matching observations to an existing open suggestion and updates
its source count.
9. Does not suggest the same FAQ again after someone dismisses it.
10. Creates a new open suggestion only when no approved FAQ or existing
suggestion covers the observation.
## How to test
1. Resolve a conversation where a human support agent gives a reusable
answer. Confirm that Captain creates one open suggestion with one
source.
2. Resolve another conversation with the same question and answer.
Confirm that Captain adds a source to the existing suggestion instead of
creating another suggestion.
3. Resolve a conversation that is already covered by an approved FAQ.
Confirm that Captain creates no new suggestion.
4. Dismiss a suggestion, then resolve another conversation with the same
question and answer. Confirm that Captain does not suggest the FAQ
again.
5. Resolve a spam or unrelated conversation. Confirm that Captain
creates no suggestion.
---------
Co-authored-by: Sony Mathew <sony@chatwoot.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Connected Agent Bot-handled conversation creation now records the bot as
the explicit owner instead of relying on pending status alone.
Closes:
https://linear.app/chatwoot/issue/CW-7449/set-connected-agent-bot-as-owner-in-bot-handled-flows
## Why
When an inbox has a connected Agent Bot, bot-handled conversations
already move to pending for queue placement. They should also carry
explicit Agent Bot ownership so agents can see that the bot is handling
the conversation and later takeover/hand-back behavior has a real owner
to work with.
## What changed
- Sets active connected Agent Bot inbox conversations to pending.
- Sets `assignee_agent_bot` to the connected Agent Bot when no explicit
human assignee is present.
- Clears the human `assignee` when assigning the connected Agent Bot
owner.
- Preserves explicit human assignees on conversation creation.
- Sets bot-initiated campaign conversations in active Agent Bot inboxes
to pending and owned by the connected Agent Bot.
- Keeps human-sender campaign conversations open and without Agent Bot
ownership.
- Leaves Dialogflow pending behavior without Agent Bot ownership.
- Clears `assignee_agent_bot` when the bot hands the conversation off.
## Validation
- Create a conversation in an inbox with an active connected Agent Bot
and verify it is pending and owned by the Agent Bot.
- Create a conversation in that inbox with an explicit human assignee
and verify the human assignee is preserved.
- Create a bot-initiated campaign conversation in the same inbox and
verify it is pending and owned by the Agent Bot.
- Create a human-sender campaign conversation and verify it remains open
without Agent Bot ownership.
- Create a Dialogflow-handled pending conversation and verify no Agent
Bot owner is set.
- Trigger bot handoff and verify the Agent Bot owner is cleared.
Adds E2E UI tests for two core Phase 2 flows, building on the Playwright
setup from #13578.
**Agent onboarding** — validates the Agents settings page, Add Agent
modal elements, form validation (name + email required, submit disabled
until valid), and cancel behaviour.
**Inbox creation** — walks through the full API channel inbox creation
journey: channel selection → form fill → agent assignment → finish
screen.
## What changed
New UI component objects (`tests/playwright/components/ui/`):
- `agent-page.component.ts`
- `add-agent-modal.component.ts`
- `add-agents-form.component.ts`
- `settings-inbox-page.component.ts`
- `channel-selector.component.ts`
- `api-channel-form.component.ts`
- `finish-setup.component.ts`
New test specs (`tests/playwright/tests/e2e/ui/`):
- `agent-onboarding-flow-ui-validation.spec.ts`
- `inbox-creation-flow.spec.ts`
Updated `components/ui/index.ts` barrel export to include all new
components.
## How to test
```bash
cd tests/playwright
npx playwright test tests/e2e/ui/
```
All 5 tests pass locally (3 login + 2 new flows).
Closes part of the Phase 2 scope from the [Playwright E2E discussion
#13500](https://github.com/orgs/chatwoot/discussions/13500).
---------
Co-authored-by: Sony Mathew <sony@chatwoot.com>
Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
This improves the Captain overview by loading reporting metrics and FAQ
stats from separate endpoints. Range changes now refresh only the
metrics, while reopen-rate calculation reuses the resolved conversation
count to avoid redundant database queries.
## What changed
- Split Captain overview metrics and FAQ stats into separate APIs.
- Fetch FAQ stats independently from range-based metrics.
- Reuse resolved conversation totals when calculating reopen rate.
- Skip the reopen query when there are no resolved conversations.
# Pull Request Template
## Description
Locks the agent quota check to the account row while creating account
users. This fixes a race where concurrent agent-create requests could
all observe the same remaining seat before any `account_users` row was
inserted.
The API continues to return the existing `402 Account limit exceeded.
Please purchase more licenses` response when the limit is reached. Bulk
create now preflights the requested email count while holding the
account lock, then creates each agent through the same locked builder
path. The Enterprise custom-role hook now no-ops when create did not
produce an agent.
Fixes:
[CW-7039](https://linear.app/chatwoot/issue/CW-7039/race-condition-in-agent-creation-bypasses-plan-agent-seat-limit)
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
- `POSTGRES_DATABASE=chatwoot_test_c20f_agent_quota REDIS_DB=9 bundle
exec rspec spec/builders/agent_builder_spec.rb
spec/enterprise/builders/agent_builder_spec.rb
spec/controllers/api/v1/accounts/agents_controller_spec.rb
spec/enterprise/controllers/api/v1/accounts/agents_controller_spec.rb
spec/enterprise/controllers/enterprise/api/v1/accounts/agents_controller_spec.rb`
- `bundle exec rubocop app/builders/agent_builder.rb
app/controllers/api/v1/accounts/agents_controller.rb
enterprise/app/controllers/enterprise/api/v1/accounts/agents_controller.rb
spec/builders/agent_builder_spec.rb
spec/enterprise/controllers/api/v1/accounts/agents_controller_spec.rb`
- `git diff --check`
- One-off threaded Rails validation with 8 concurrent `AgentBuilder`
calls against an account with one remaining seat: `created: 1`,
`limited: 7`, final `count=2`, `limit=2`.
## 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>
Non-admin agents could delete an account's Linear, Notion, or Shopify
integration through the dedicated integration endpoints, which — unlike
the generic hooks endpoint — never checked the caller's role. This
restores the intended admin-only boundary for removing an integration.
## Closes
- https://linear.app/chatwoot/issue/CW-7383
- https://linear.app/chatwoot/issue/CW-7384
- https://linear.app/chatwoot/issue/CW-7189
## How to reproduce
As a non-admin **agent**, `DELETE
/api/v1/accounts/:id/integrations/{linear,notion,shopify}` returned
`200` and removed the account-wide integration. After this change it
returns `401` and the integration is preserved; administrators can still
remove it.
## What changed
- Route integration-hook deletion through `HookPolicy` (admin-only) via
a shared `Integrations::BaseController`, matching the generic hooks
controller.
Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
## Description
The widget email-transcript button (`ChatFooter.vue`) had no client-side
guard. Its visibility depends only on whether the contact has an email,
and the click handler fired a request on every click with no in-flight
lock, no disabled state, and no post-send handling. A user could
therefore trigger a large number of duplicate transcript emails from a
single conversation just by clicking repeatedly.
This adds a re-entry guard in the handler, disables the button while a
send is in flight, and applies a short cooldown (15s) after a successful
send. Normal use is unaffected: the button sends once, shows the success
toast, then briefly disables and automatically re-enables so a genuine
later re-request still works. On failure the button stays enabled so the
user can retry immediately. The cooldown timer is cleared on unmount.
Using a timed cooldown (rather than a permanent post-send lock) also
avoids the button getting stuck disabled if a resolved conversation is
reopened and later re-resolved.
This is the client-side complement to the server-side rate limit added
in #15085.
Fixes https://linear.app/chatwoot/issue/CW-7640
# Pull Request Template
## Description
This PR fixes a crash where opening a conversation threw `TypeError:
Cannot read properties of null (reading 'localeCompare')` and prevented
the agent assignment dropdown from rendering.
Since #14866, agent bots are included in the assignable agents list.
`AgentBot#name` is not presence-validated, so system bots (account-less,
global) can have a `null` name. Those nameless bots flowed into
name-based operations that assumed a string, causing crashes and
warnings across multiple surfaces:
* **Assignment dropdown sort:** `getAgentsByAvailability` called
`a.name.localeCompare(b.name)`, causing a `localeCompare` `TypeError`.
* **Dropdown search:** `MultiselectDropdownItems` called
`option.name.toLowerCase()`, causing a `toLowerCase` `TypeError`.
* **Agent Bots settings:** `Avatar` received `name=null` for a `String`
prop, triggering a Vue prop validation warning.
### What changed
* Keep nameless agent bots in the assignment dropdown and render a `-`
fallback label in `useAgentsList`. These are still valid,
assignable-by-ID records: the assignable agents API includes accessible
bots, and `Conversations::AssignmentService` assigns them by ID.
Preserving them avoids hiding valid assignment targets. Bots are still
included only when `includeAgentBots` is enabled.
* Make the sort in `getAgentsByAvailability` null-safe by coercing
missing names to an empty string (defense in depth).
* Make the search filter in `MultiselectDropdownItems` null-safe
(defense in depth).
* Pass a null-safe `name` prop to `Avatar` in the Agent Bots settings
list to eliminate the Vue prop validation warning.
Fixes
https://linear.app/chatwoot/issue/CW-7670/agent-assignment-dropdown-crashes-with-cannot-read-properties-of-null
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
1. Have a system agent bot (`name: null`) that is assignable to an
inbox.
2. Open any conversation in that inbox.
* The agent assignment dropdown renders without console errors.
* The nameless bot is listed with a `-` label and can be assigned.
3. Go to **Settings → Agent Bots**.
* The page renders without the `Avatar` prop validation warning.
## 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
WhatsApp automations now fail locally when they attempt to send a
free-form message after the 24-hour customer service window has closed.
This avoids sending an invalid template request to Meta and gives users
a clear, actionable error instead of “Template not found or invalid
template name.”
Template messages continue to be sent whenever template parameters are
present. Free-form messages continue to be sent normally while the
conversation is replyable.
Fixes
https://linear.app/chatwoot/issue/PLA-183/prevent-whatsapp-automations-outside-the-24-hour-window-from-producing
### How to reproduce
1. Create a WhatsApp automation that sends a message without template
parameters.
2. Trigger it on a conversation whose 24-hour customer service window is
closed.
3. Observe that the message previously reached the template send path
and failed with a misleading provider error.
### How to test
1. Trigger an automation with template parameters and confirm it sends
as a template message.
2. Trigger an automation without template parameters inside the 24-hour
window and confirm it sends as a free-form message.
3. Trigger an automation without template parameters outside the 24-hour
window and confirm it fails locally with a clear error and makes no
request to Meta.
### Things to know
This changes only the invalid closed-window, no-template path. Existing
template and in-window message behavior remains unchanged.
---------
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Fixes
https://linear.app/chatwoot/issue/PLA-99/whatsapp-messages-dropped-for-brazilargentina-numbers-due-to-phone
Fixes https://github.com/chatwoot/chatwoot/issues/14492
Meta's WhatsApp Cloud API includes `display_phone_number` in webhook
payloads, but its format can differ from the number stored in Chatwoot's
channel record.
In Brazil, Meta omits the mobile 9 prefix. For example, it sends
55419XXXXXXX (12 digits) instead of 554199XXXXXXX (13 digits). In
Argentina, Meta adds an extra 9 after the country code. For example, it
sends 549XXXXXXXXXX instead of 54XXXXXXXXXX.
The whatsapp event job uses `display_phone_number` for an exact-match
channel lookup. When the formats do not match, the lookup returns nil
and the incoming message is silently dropped, logging:
`Inactive WhatsApp channel: unknown - <phone_number>.`
The fix extends `get_channel_from_wb_payload` to fall back to normalized
phone number matching using the existing PhoneNumberNormalizationService
normalizers (Brazil, Argentina), which were previously only used for
contact-level lookups.
---------
Co-authored-by: Sojan Jose <sojan@pepalo.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
## Description
Agents can now place a WhatsApp call to a contact straight from the
contacts screen, even if that contact has never messaged in. Previously
the call only worked once a conversation already existed, so a freshly
added contact would fail with "Unable to start the call. Please try
again." — the only workaround was to get the contact to message the
channel first.
## Type of change
- [ ] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
- Manually via UI
## 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>
WhatsApp embedded signup now uses
`whatsapp_embedded_signup_inbox_creation` as the single Chatwoot Cloud
rollout gate for inbox creation, proactive reconfiguration, and
disconnected inbox reauthorization. The authorization endpoint enforces
the same gate, so the UI and backend remain consistent.
Self-hosted installations keep their existing behavior.
## Things to know
- This reuses the existing feature flag; there is no migration or schema
change.
- The feature is shown as “WhatsApp Embedded Signup Flow” in feature
management.
- `whatsapp_reconfigure` remains visible and honored for self-hosted
proactive reconfiguration to preserve existing accounts. It can be
deprecated after the self-hosted dependency is removed or migrated.
## How to test
1. On Chatwoot Cloud, enable `whatsapp_embedded_signup_inbox_creation`
for an account.
2. Confirm that new WhatsApp inbox creation, proactive reconfiguration,
and disconnected inbox reauthorization are available.
3. Disable the flag and confirm those entry points are hidden and
authorization requests are rejected.
4. On self-hosted, confirm proactive reconfiguration remains controlled
by the existing `whatsapp_reconfigure` account setting.
---------
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
WhatsApp contacts using coexistence are identified by more than one
source ID (a phone `wa_id` and a `BR.`/BSUID identity), so a single
contact ends up owning multiple `contact_inbox` records. The "reopen the
same conversation" feature scoped conversation reuse to a single
`contact_inbox`, so messages arriving under a different identity of the
same contact started a brand-new conversation — even with reopen enabled
— producing duplicate conversations.
This scopes reuse to the contact across all of its `contact_inbox`
records in the inbox instead of a single `contact_inbox`.
## Closes
- [CW-7651
](https://linear.app/chatwoot/issue/CW-7651/duplicate-conversations)
## How to reproduce
1. On a WhatsApp Cloud inbox with "reopen the same conversation" (lock
to single conversation) enabled.
2. Have a coexistence contact whose webhooks alternate between carrying
the phone `wa_id` and only the BSUID identity.
3. Before: each identity opens its own conversation → duplicates. After:
incoming messages reopen the contact's existing conversation regardless
of which identity the webhook carried.
## What changed
- `Whatsapp::IncomingMessageBaseService#set_conversation` now looks up
reusable conversations via `@contact.conversations.where(inbox_id:
@inbox.id)` instead of `@contact_inbox.conversations`.
- Updated existing specs to wire the conversation's `contact` to the
contact_inbox's contact, mirroring production data.
## Linear Ticket
-
https://linear.app/chatwoot/issue/CW-7276/bsuid-support-to-whatsapp-voice-calling
## Description
Keeps WhatsApp voice calls in the same thread as the chat when a caller
has adopted a **WhatsApp username** and hidden their phone number.
This makes the inbound-call path BSUID-aware, reusing the same
identifier the messaging pipeline keys on so calls land on the existing
`ContactInbox`/conversation.
## Type of change
- [ ] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
- Locally via UI
## 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>
Captain now saves agent session records when a user message includes an
image. The saved record keeps the image URL and excludes downloaded
image bytes, so image replies no longer report a JSON serialization
error after delivery.
Fixes:
https://chatwoot-p3.sentry.io/issues/7618423184/?alert_rule_id=13673680&alert_type=issue¬ification_uuid=d22a7ab9-95d6-4bba-85e0-733a28466775&project=6382945
## Root cause
RubyLLM downloads image attachments and caches the binary bytes inside
`RubyLLM::Content`. `SessionCaptureService` passed the live object to
the `run_context` JSON column. Rails then tried to encode the cached
JPEG bytes as UTF-8 and raised `JSON::GeneratorError`.
The error did not block replies, handoffs, or credit updates because
session capture rescues its own failures. The failed write meant that
Chatwoot lost the agent session record for the response.
## How to reproduce
1. Send an image to a Captain V2 assistant.
2. Let RubyLLM load the image during the model request.
3. Save the resulting conversation history in an agent session.
4. Observe the JSON encoding error when Rails reaches the cached image
bytes.
## What changed
`SessionCaptureService` now converts `RubyLLM::Content` to its JSON safe
hash before saving the current turn. The hash contains the message text
and attachment URL without the cached bytes. Other message content is
unchanged.
The focused service spec covers a cached JPEG byte payload and passes
with 12 examples. RuboCop reports no offenses in the changed service and
spec.
# Pull Request Template
## Description
Moves the WhatsApp & Twilio content-template logic to the shared
[`@chatwoot/utils`](https://github.com/chatwoot/utils)
([PR](https://github.com/chatwoot/utils/pull/62)) package so web and
mobile share one implementation. The neutral core takes the raw template
and returns `processed_params`, the same shape the web parsers already
use, so it's a drop-in with no behavior change.
- `templateHelper.js` / `URLHelper.js` → source `MEDIA_FORMATS`,
`findComponentByType`, `processVariable`, `buildTemplateParameters`,
`extractFilenameFromUrl` from the package
- `inboxes.js` → filters with shared `isSendableTemplate`
- `WhatsAppTemplateParser.vue` / `ContentTemplateParser.vue` →
`isFormInvalid` and Twilio media helpers now use the shared
`isWhatsAppComplete` / `isTwilioComplete` / `applyTwilioMediaFilename`
> Depends on the `@chatwoot/utils` release adding the shared template
API, bump `package.json` from `^0.0.55` to the published version before
merge.
Fixes
[CW-7540](https://linear.app/chatwoot/issue/CW-7540/web-templates-integration-with-utils)
## Type of change
- [x] Breaking change (Refactor)
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
## Description
The widget conversation transcript endpoint (`POST
/api/v1/widget/conversations/transcript`) has no rate limit. Every other
comparable endpoint does: the agent-facing transcript API and the widget
conversation-create and contact-update endpoints are all throttled. This
gap lets a single client trigger a large burst of transcript emails from
one conversation.
This adds an IP-based throttle (5 requests/hour) for the endpoint,
placed inside the existing widget-API throttle block so it inherits the
`ENABLE_RACK_ATTACK_WIDGET_API` opt-out used by embedded/iframe clients.
The limit is generous for legitimate use (a visitor emailing themselves
a transcript) while stopping abusive loops. Throttled requests get the
standard 429 the widget already handles.
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
`config/initializers/rack_attack.rb` throttles have no existing specs in
this file, so this follows the established convention (no new spec).
Verified `ruby -c` and `rubocop` pass on the file. The new throttle
mirrors the sibling widget throttles directly above it (same IP key,
path guard, and structure).
## 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
---------
Co-authored-by: Sojan Jose <sojan@pepalo.com>
Token-authenticated requests to Agent Bots, Labels, and affected Captain
endpoints return normal responses again. The regression was caused by
duplicate `current_account` callbacks in subclasses moving account
resolution behind the API entitlement check, leaving `Current.account`
unset.
## Closes
- https://linear.app/chatwoot/issue/CW-7641/5xx-errors-in-agent-bot-apis
## How to reproduce
1. Send `GET /api/v1/accounts/:account_id/agent_bots` with a valid
administrator API access token.
2. Observe a `500` from `validate_token_api_access` because
`Current.account` is `nil`.
3. With this change, account resolution runs in the base-controller
order and the request succeeds.
## What changed
- Removed redundant `current_account` callbacks from account-scoped
controllers that already inherit the callback from
`Api::V1::Accounts::BaseController`.
- Kept the standalone direct-upload controller callback unchanged.
- Added regression coverage for administrator API-token access to Agent
Bots.
Enables Uzbek as a selectable Chatwoot language and wires the `uz`
locale into the dashboard, widget, and survey translation loaders.
## Closes
Closes https://github.com/chatwoot/chatwoot/issues/15030
## Why
Uzbek is now available in the Chatwoot Crowdin project. The widget
translation has been proofread and is 100% translated and approved.
## What changed
- Adds Uzbek to the supported language registry
- Registers the `uz` locale with the dashboard, widget, and survey i18n
loaders
## Validation
- Proofread the Uzbek widget translation in Crowdin
- Verified placeholder preservation and corrected wording where needed
- Confirmed the widget file is 100% translated and 100% approved
This draft intentionally remains blocked until the Crowdin sync adds the
generated Uzbek locale files. It should only be marked ready and merged
after that sync lands.
Updates the vulnerable Ruby dependencies reported by bundle audit so the
application resolves to patched versions.
## Why
The latest Ruby advisory database reports known vulnerabilities in
Datadog, Loofah, and Rails HTML Sanitizer on `develop`, including a
high-severity denial-of-service advisory in Datadog.
## What changed
- Updates `datadog` from 2.19.0 to 2.38.0
- Updates `loofah` from 2.23.1 to 2.25.2
- Updates `rails-html-sanitizer` from 1.6.1 to 1.7.1
- Refreshes Datadog's required native packages
## Validation
Verified the resolved bundle is satisfied, bundle audit reports no known
vulnerabilities, and Datadog 2.38.0 loads successfully.
## Description
Adds Swagger API documentation for branded email layouts, including the
account-level layout endpoint, Email inbox update field, and generated
API schemas. This stacked PR keeps the implementation review in #14936
focused on the product change.
Related:
[CW-7514](https://linear.app/chatwoot/issue/CW-7514/branded-html-email-templates-per-inboxbrand)
Depends on #14936
## Type of change
- [x] This change requires a documentation update
## How to test
1. Run `bundle exec rake swagger:build`.
2. Start Chatwoot locally and open `http://localhost:3000/swagger`.
3. Verify the branded email layout account endpoint and Email inbox
`branded_email_layout` field appear in the API reference.
## Checklist
- [x] I have performed a self-review of my changes
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
WhatsApp inbox creation now shows Embedded Signup for Chatwoot Cloud
accounts only when the new `whatsapp_embedded_signup_inbox_creation`
feature flag is enabled. Cloud accounts without the flag go directly to
manual WhatsApp Cloud API setup, while self-hosted installations with a
configured WhatsApp App ID retain their existing Embedded Signup flow.
---------
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
## Description
Adds an API-only branded email layout feature for Email inbox replies.
Administrators can configure an account-level fallback layout and
per-email-inbox overrides with Liquid HTML using `{{ content_for_layout
}}`, and eligible outbound email replies/transcripts render through the
scoped layout when the account feature flag `branded_email_templates` is
enabled.
The feature is disabled by default and is manually controlled through
the normal account feature flag mechanism.
Fixes
https://linear.app/chatwoot/issue/CW-7514/branded-html-email-templates-per-inboxbrand
## Type of change
- [x] New feature (non-breaking change which adds functionality)
- [x] This change requires a documentation update
## How to test
1. Start Chatwoot locally and sign in as an administrator.
2. Enable the account feature flag for the account you are testing:
```ruby
account = Account.find(<account_id>)
account.enable_features!(:branded_email_templates)
```
3. Create or pick an Email inbox, then note the `account_id` and
`inbox_id`.
4. Configure an account-level fallback layout through the API using
authenticated admin headers:
```http
PATCH /api/v1/accounts/:account_id/branded_email_layout
Content-Type: application/json
{
"branded_email_layout": "<html><body><header>Account Brand</header>{{
content_for_layout }}<footer>Account footer</footer></body></html>"
}
```
5. Confirm `GET /api/v1/accounts/:account_id/branded_email_layout`
returns the saved account layout.
6. Configure an inbox-level override for the Email inbox:
```http
PATCH /api/v1/accounts/:account_id/inboxes/:inbox_id
Content-Type: application/json
{
"branded_email_layout": "<html><body><header>Inbox Brand</header>{{
content_for_layout }}<footer>Inbox footer</footer></body></html>"
}
```
7. Confirm `GET /api/v1/accounts/:account_id/inboxes/:inbox_id` returns
the inbox `branded_email_layout`.
8. Send an Email inbox reply and verify the outbound email body is
wrapped with the inbox layout around the generated reply content.
9. Clear the inbox layout by sending a blank value, then send another
reply and verify it falls back to the account layout:
```http
PATCH /api/v1/accounts/:account_id/inboxes/:inbox_id
Content-Type: application/json
{
"branded_email_layout": ""
}
```
10. Clear the account layout with a blank value and verify Email replies
return to the existing no-layout behavior.
11. Verify validation behavior:
- Updating either API with a layout that omits `{{ content_for_layout
}}` returns `422`.
- Updating either API with invalid Liquid returns `422`.
- Updating a non-Email inbox with `branded_email_layout` returns `422`.
- Disabling `branded_email_templates` and updating a layout returns
`422`.
## How Has This Been Tested?
Validation:
- `bundle exec rspec spec/models/email_template_spec.rb
spec/controllers/api/v1/accounts/branded_email_layouts_controller_spec.rb
spec/controllers/api/v1/accounts/inboxes_controller_spec.rb
spec/lib/email_templates/db_resolver_service_spec.rb
spec/mailers/conversation_reply_mailer_spec.rb`
- `bundle exec rspec
spec/enterprise/services/enterprise/billing/handle_stripe_event_service_spec.rb
spec/enterprise/services/internal/reconcile_plan_config_service_spec.rb`
- `bundle exec rubocop` on changed Ruby files, excluding generated
`db/schema.rb`
- `git diff --check` and `git diff --cached --check`
- YAML parsing for changed config/Swagger files
- `bundle exec rails routes -g branded_email_layout`
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have made corresponding changes to the documentation
- [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
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] My changes generate no new warnings
- [ ] Any dependent changes have been merged and published in downstream
modules
Restores the Inbox Channels dialog’s Facebook-gating test coverage by
isolating account feature dependencies that are unrelated to these
cases. This keeps the onboarding checks focused on whether Facebook is
configured and avoids initializing router and account-store state.
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Suspended accounts no longer participate in scheduled WhatsApp template
syncs. This avoids unnecessary external API calls while keeping the
existing refresh behavior unchanged for active accounts.
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>