Commit Graph

6514 Commits

Author SHA1 Message Date
Muhsin Keloth
9d769dfcdd fix(whatsapp): collect text header parameters (#15199)
WhatsApp templates with variables in both a text header and body
currently show inputs only for the body. Agents therefore cannot provide
the header value in the template composer, even though the API and
backend support the corresponding `processed_params.header` payload.

The composer now displays text-header variables separately, previews
their substituted values, and sends them alongside body parameters.
Templates using media headers remain unchanged.

Related: https://github.com/chatwoot/utils/pull/65

### Things to know

This PR consumes the released `@chatwoot/utils@0.0.57`, which adds
text-header parameter construction and completeness validation.

### How to reproduce

1. Open a WhatsApp template containing text header `Welcome {{1}}` and
body variables `{{1}}` and `{{2}}`.
2. Observe that the current composer displays only two body inputs and
omits the header input.

### How to test

1. Open the same template in the conversation composer.
2. Confirm one header input and two body inputs are displayed.
3. Fill the values and confirm both the header and body previews update.
4. Send the template and confirm `processed_params` contains `header.1`,
`body.1`, and `body.2`.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-07-29 20:34:33 +04:00
Sony Mathew
502c45f73b fix: freeze SLA misses after resolution (#15024)
## Description

Resolved conversations now preserve historical SLA misses without
allowing their displayed duration to keep growing. Applied SLAs record a
stable completion timestamp that is shared through REST and realtime
payloads, and the dashboard freezes FRT, NRT, and RT misses at that
point.

Legacy completed SLAs without a reliable timestamp remain visible as a
static missed state. Terminal SLAs remain frozen when a conversation is
reopened; a reopen before finalization continues the same SLA without
resetting its deadlines.

### Closes


[CW-7597](https://linear.app/chatwoot/issue/CW-7597/freeze-sla-miss-durations-after-conversation-resolution)

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How to reproduce

1. Apply an SLA with a resolution-time threshold to a conversation.
2. Let the threshold breach, then resolve the conversation.
3. Observe that the recorded miss duration continues increasing every
minute even though the conversation is resolved.

## What changed

- Added nullable `applied_slas.completed_at` and exposed it as
`sla_completed_at` in conversation, report, and websocket payloads.
- Captured completion before broadcasting resolution and preserved it
for terminal applied SLAs.
- Frozen recorded FRT, NRT, and RT durations in classic and
next-generation conversation labels, including a static fallback for
legacy rows.
- Added a dry-run-first, resumable Rails runner for account-scoped or
explicitly global historical repair without enqueuing jobs or touching
`updated_at`.

Account-scoped production rollout starts with:

```sh
ACCOUNT_ID=168154 bundle exec rails runner script/backfill_applied_sla_completed_at.rb
ACCOUNT_ID=168154 APPLY=true bundle exec rails runner script/backfill_applied_sla_completed_at.rb
```

## How Has This Been Tested?

- Verified resolution stamping, nonterminal reopen clearing, and
terminal reopen preservation.
- Verified dry-run, apply, account/global scope, resume, skip,
idempotency, and timestamp-preserving backfill behavior.
- Verified all three miss types freeze and existing conversation-card
behavior remains intact.
- 71 focused RSpec examples and 37 focused Vitest examples pass.
- RuboCop, ESLint, and diff checks pass.

## 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>
2026-07-29 18:21:26 +05:30
Aakash Bakhle
4ac2432b77 fix(captain): respect channel message limits (#14982)
Captain now keeps v2 replies within each conversation channel's delivery
limit, preventing generated responses from being rejected by providers
such as Instagram, Facebook, and WhatsApp.

## Closes

-
https://linear.app/chatwoot/issue/AI-188/captain-should-respect-whatsapp-character-limits

## How to reproduce

1. Enable Captain v2 on an Instagram inbox.
2. Ask a question that produces a response longer than 1,000 characters.
3. Observe that Meta rejects the outgoing message with error 100 because
it exceeds Instagram's character limit.

## What changed

- Resolve the outbound character limit from the conversation channel,
including provider-specific Twilio limits.
- Add the resolved limit to the assistant and scenario prompts.
- Apply the same limit to the v2 structured response schema so the model
output conforms before delivery.

---------

Co-authored-by: Sony Mathew <sony@chatwoot.com>
2026-07-29 17:56:40 +05:30
Sony Mathew
651db39765 fix: harden article author updates (#15229)
## Description

Article edits now ignore an `author_id` that does not belong to the
current account, retain the existing author, and still apply other valid
article changes. Article creation continues to reject cross-account
authors with a generic validation error and without creating a record.

The authenticated article serializers still omit authors without a
current-account membership so existing forged or stale records cannot
expose agent profile fields. The guard now checks `current_account_user`
directly to make that intent explicit.

## Closes

Follow-up to [CW-7665](https://linear.app/chatwoot/issue/CW-7665) and
[#15191](https://github.com/chatwoot/chatwoot/pull/15191).

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

- Creating an article with a cross-account author returns `422` and does
not create an article.
- Updating an article with a cross-account author retains the previous
author while applying other valid attributes.
- Articles whose previous author is no longer an account member can
still be edited without exposing that author's agent profile.
- Existing OSS and Enterprise article request coverage passes locally.

## 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
- [x] Any dependent changes have been merged and published in downstream
modules

Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
2026-07-29 17:28:45 +05:30
Sivin Varghese
754b25cd59 feat: add Video option to the article editor slash menu (#15164) 2026-07-29 17:25:58 +05:30
Sony Mathew
bc9839ed38 fix: stabilize conversation FAQ lock spec (#15236)
## Description

Backend CI no longer flakes when the conversation FAQ grouping-lock spec
runs after Rails has reloaded application constants.

The job already raises the intended lock-acquisition error and prevents
concurrent suggestion generation. This updates the assertion to compare
the error class name, preserving that behavior without depending on a
reload-sensitive Ruby `Class` object.

The failure was reproduced in [CircleCI backend job
171299](https://app.circleci.com/pipelines/github/chatwoot/chatwoot/116560/workflows/efcee2f1-5af8-4121-82c3-920002dc420b/jobs/171299).

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

The isolated job spec passes, the exact 18-way CircleCI shard selection
passes all 442 examples, and the changed spec passes Ruby lint.

## 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
2026-07-29 17:06:31 +05:30
Sony Mathew
8fcd32f442 chore(search): support Elastic Cloud API keys (#15231)
# Pull Request Template

## Description

Adds API-key authorization support for Searchkick/OpenSearch so Elastic
Cloud deployments can configure advanced search with an Elastic API key
instead of embedding basic auth in the URL.

The initializer now accepts `OPENSEARCH_API_KEY` or
`ELASTICSEARCH_API_KEY` and forwards it as an `Authorization: ApiKey
...` header. `.env.example` also documents the
OpenSearch/Elasticsearch-compatible search variables.

Refs
https://linear.app/chatwoot/issue/CW-7511/populate-test-data-set-and-run-experiments

## Type of change

- [ ] 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)
- [x] This change requires a documentation update

## How Has This Been Tested?

- `bundle exec ruby -c config/initializers/searchkick.rb`
- `bundle exec ruby -c spec/config/searchkick_spec.rb`
- `bundle exec rspec spec/config/searchkick_spec.rb`
- `bundle exec rubocop config/initializers/searchkick.rb
spec/config/searchkick_spec.rb`
- `git diff --check`

## 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
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-07-29 16:42:44 +05:30
Vishnu Narayanan
039a311327 fix: consolidate widget rack-attack throttles with per-endpoint kill switches + saner defaults (#14376)
## Description

Consolidates all widget API throttles under the single
`ENABLE_RACK_ATTACK_WIDGET_API` flag, each with its own independent kill
switch and configurable limit, with defaults tuned from real prod
traffic.

Fixes https://linear.app/chatwoot/issue/INF-83
Related to https://linear.app/chatwoot/issue/INF-77

Parent flag stays default-true; installs that disabled it keep today's
behavior (no widget throttling). Each endpoint adds
`ENABLE_RACK_ATTACK_WIDGET_<X>` + `RATE_LIMIT_WIDGET_<X>`.

- **conversations create**: keyed on (IP, website_token), 30/min (was
per-IP 6/12h)
- **messages create**: new throttle, (IP, website_token), 60/min (was
unthrottled)
- **contact update**: fixes a dormant bug. `resource :contact` gives the
singular URL `/api/v1/widget/contact`, but the throttle checked plural
`/api/v1/widget/contacts` so it never fired. Now active (60/1h per IP).
- **widget load**: 5 to 200/1h, tuned from prod (real per-IP loads top
out ~50/hr; the higher tail is crawlers/scrapers)
- **transcript**: 5/1h retained

Token precedence: the (IP, website_token) throttles read `website_token`
via ActionDispatch (query wins), matching the controller, so a
body-supplied token cannot fork the throttle bucket.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

`ruby -c` and rubocop clean. No spec added, matching this file's
existing no-spec convention for throttles.

---------

Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
2026-07-29 16:32:06 +05:30
Muhsin Keloth
59eac9a7c5 feat(whatsapp): add cloud template management token (#15218)
Chatwoot Cloud customers can now provide a dedicated WhatsApp business
management token when their Embedded Signup credential cannot access
message templates. Once validated, the token is stored securely and used
only for template synchronization.

Existing inboxes continue using their configured WhatsApp API key when
no business management token is present. Sending, receiving, webhooks,
phone-number health, and other WhatsApp operations remain unchanged.

### Things to know

- This option is available only on Chatwoot Cloud.
- Saving the token verifies that `whatsapp_business_management` is
granted through Meta's permissions endpoint; template synchronization
still verifies access to the configured WhatsApp Business Account.
- The token is encrypted using the existing external-credentials
encryption mechanism.
- Self-hosted installations continue using the existing API key flow.

### How to test

1. On Chatwoot Cloud, open a WhatsApp Cloud inbox and go to
**Configuration**.
2. Enter a token with `whatsapp_business_management` access and save it.
3. Confirm the token is accepted and the value is not exposed again in
the UI or API.
4. Select **Sync Templates** and confirm templates are fetched with the
saved business management token.
5. Remove the token and confirm template synchronization falls back to
the inbox API key.
6. Confirm the business management token controls are not shown on a
self-hosted installation.

### What changed

- Added an encrypted `business_management_token` credential to WhatsApp
channels.
- Added Cloud-only endpoints and UI controls to validate the required
permission, save, and remove the token.
- Added template-sync credential selection with API-key fallback.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-07-29 13:37:59 +04:00
Sivin Varghese
2e9423cbba fix: unread notification dot and unread preview sizing (#15216) 2026-07-29 14:16:26 +05:30
Vishnu Narayanan
d5f7a2be64 fix: don't show the self-hosted upgrade banner on chatwoot cloud (#15195)
## Description

The dashboard "update to vX.Y.Z is available" banner is a self-hosted
upgrade nudge, but nothing gates it to self-hosted installs. On a
managed cloud instance the banner can still appear:
`latest_chatwoot_version` is populated in Redis from the hub's
advertised latest stable release, and the frontend compares it
(`semver.lt`) against the per-request `appVersion` baked into the
dashboard HTML. When a browser session is holding HTML rendered before
the current deploy, `appVersion` lags the hub's advertised version and
the banner fires, even though the managed instance is already on the
newest build.

This gates the value at the source: `api/v1/accounts#show` no longer
sets `latest_chatwoot_version` when running on cloud, so
`hasAnUpdateAvailable` short-circuits (`semver.valid(null)` is false)
and the banner never renders there. Self-hosted behaviour is unchanged.

Fixes https://linear.app/chatwoot/issue/CW-7763
2026-07-29 13:32:52 +05:30
Muhsin Keloth
fe6f900db9 fix(inboxes): guide users through meta api incident (#15210)
An API-level issue on Meta’s side is preventing Chatwoot Cloud customers
from completing some Meta inbox setup flows and can affect actions in
existing Instagram conversations. This draft temporarily gates the
affected Facebook, Instagram, and WhatsApp flows behind one incident
flag and gives users clear guidance with a link to the current status
incident.

Self-hosted installations remain unchanged. While WhatsApp Embedded
Signup is unavailable, selecting WhatsApp Cloud takes eligible users
directly to manual setup and explains the current limitations.

Related: https://status.chatwoot.com/incident/976975

Fixes
https://linear.app/chatwoot/issue/CW-7771/guide-users-through-meta-api-incident

### Things to know

- `IS_META_INBOX_CREATION_DISABLED` is the single temporary rollout flag
and is currently enabled.
- The incident behavior applies only to Chatwoot Cloud.
- Facebook, Instagram, and WhatsApp Embedded Signup are removed from the
new onboarding suggestions while the incident is active.
- Existing Instagram inbox settings and conversations show incident
guidance with a status link.
- The WhatsApp Cloud provider routes to manual setup while the flag is
enabled.
- Manual setup works only for numbers already connected to WhatsApp
Cloud API; numbers using WhatsApp Business app coexistence are not
supported by this flow yet.
- The flag should be disabled or removed after the incident is resolved.

### How to test

1. Run Chatwoot with the Cloud installation configuration and open the
new inbox flow.
2. Confirm Facebook and Instagram authentication actions are disabled
and display the amber incident banner.
3. Open the WhatsApp provider selector and confirm WhatsApp Cloud is
described as manual setup with Cloud API credentials.
4. Select WhatsApp Cloud and confirm it opens the manual setup form with
the incident warning and links to the current status incident.
5. Open WhatsApp Embedded Signup directly and confirm the signup action
is disabled while the eligible manual setup option remains available.
6. Open the initial account onboarding flow and confirm Meta channels
are not offered while other configured channels remain available.
7. Open an existing Instagram inbox and conversation and confirm the
incident guidance links to the current status incident.
8. Run the same flows as a self-hosted installation and confirm they
remain available.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-07-29 09:56:03 +04:00
Sivin Varghese
0751175693 chore: register Slovenian (sl) locale in survey (#15217) 2026-07-28 22:29:32 +05:30
Matic Bončina
ce8cbf216e feat: register Slovenian (sl) locale in the live-chat widget (#15148) 2026-07-28 19:40:05 +05:30
Vishnu Narayanan
1b487f49d8 fix: reject cross-account author on help center articles (#15191)
## Description

The help center article endpoints permit `author_id` and render the
author through the agent serializer (`_agent.json.jbuilder`), which
exposes `email` plus name, role, and availability. `author_id` was never
scoped to the current account, so a forged or stale author disclosed the
profile of a user in another account. This is the article path of the
same serializer disclosure class as the conversation participants fix.

The fix has two parts:

- **Serializer guard (the disclosure fix).** The article partials now
render the author only when they are a member of the current account
(`article.author&.account`). This closes the leak on every read and for
every row, including articles that already carry a forged or stale
out-of-account author, and mirrors how the conversation assignee is
already handled.
- **Create-only validation.** `author_id` is checked against the
account's users on create, so a new article cannot be forged with an
out-of-account author. Update needs no guard: a non-member author is
simply never rendered, so editing an article whose author has left the
account continues to work.

Fixes https://linear.app/chatwoot/issue/CW-7665

Related: https://github.com/chatwoot/chatwoot/pull/15180
(https://linear.app/chatwoot/issue/CW-7746).
2026-07-28 18:37:07 +05:30
Muhsin Keloth
d04a717701 fix(whatsapp): resolve replies across scoped message ids (#15107)
WhatsApp coexistence replies can carry a BSUID-scoped `context.id` even
when Chatwoot stored the original message with a phone-scoped WAMID.
Although both identifiers refer to the same message, their complete
values differ, so incoming replies retained the external reference but
did not populate the internal `in_reply_to` relationship. Agents
consequently saw the reply text without the quoted-message preview.

This resolves the original message within the selected conversation and
stores the internal reply relationship. Exact WAMID matches remain the
primary path; scoped identifiers fall back to the unique decoded message
token.

Fixed
https://linear.app/chatwoot/issue/CW-7663/whatsapp-quoted-replies-are-not-linked-across-scoped-wamids
and https://github.com/chatwoot/chatwoot/issues/14953

## How to reproduce

1. Use a WhatsApp Cloud inbox with coexistence enabled.
2. Send a message whose source ID is stored using the phone-scoped
WAMID.
3. Reply to it from WhatsApp when the webhook carries a BSUID-scoped
`context.id` for the same message.
4. Before this change, the incoming message appears without its
quoted-message preview.
5. After this change, the reply references and displays the original
message.

## What changed

- Resolve incoming reply context IDs against messages in the selected
conversation.
- Keep exact source-ID matching as the first lookup path.
- Decode scoped WAMIDs and match only a unique 20- or 32-character
message token.
- Populate `content_attributes.in_reply_to` while preserving
`in_reply_to_external_id`.
- Leave malformed, unmatched, or ambiguous identifiers unlinked.

## How to test

1. Open a WhatsApp Cloud conversation and send a message to the contact.
2. Reply to that message from WhatsApp through a coexistence identity.
3. Confirm the incoming message displays the original message as a
quoted preview.
4. Confirm ordinary exact-ID replies continue to resolve.
5. Confirm an unknown or malformed context ID does not attach to another
message.

## Things to know

Meta documents `context.id` as the replied-to message identifier, but
does not document the internal WAMID encoding or the phone-versus-BSUID
scope transformation. The fallback is therefore limited to the selected
conversation and succeeds only when one stored message has the decoded
token.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-07-28 15:28:20 +04:00
Vishnu Narayanan
1e33bfd553 fix: add if_not_exists to conversations.created_at index migration (#15214)
## Description

Follow-up to #15147. The migration missed `if_not_exists`, so it fails
with `index already exists` on a re-run or when the index is pre-built.
Makes it idempotent, matching recent concurrent-index migrations.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)
2026-07-28 16:21:25 +05:30
Vishnu Narayanan
3a8c5117da fix(perf): lazy load super admin dashboard stats (#15147)
The super admin landing page runs all its stat queries synchronously
before rendering anything. On large installs the exact `COUNT(*)` on
conversations and the 30-day chart group-by scan the whole table and
exceed the request timeout, so the first page load fails and the console
is unreachable.
2026-07-28 15:54:51 +05:30
Sivin Varghese
b89bd613f8 chore: support opening links in the editor with Cmd/Ctrl+Click (#15130) 2026-07-28 14:46:21 +05:30
Shivam Mishra
64301495b4 fix: default Captain overview to last 7 days (#15206)
The Captain agents overview now defaults to the last 7 days instead of
this month, so the page opens on a more recent and actionable window.

## What changed

- Overview page, range selector, and welcome card default to `7`.
- Backend `Captain::AssistantStatsWindow::DEFAULT_RANGE` changed from
`30` to `7`, so requests without a `range` param (or with invalid
values) also resolve to the last 7 days.

## How to test

- Open Captain → Overview: the range selector should show "Last 7 days"
by default and metrics should reflect that window. Other ranges continue
to work as before.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-28 13:33:08 +05:30
enzogtrujillo
acae0a2acf feat(conversations): add opt-in merge for custom attributes (#15119)
Adds an opt-in merge flag to the conversation custom attributes endpoint
so integrations can update only the keys they send instead of replacing
the whole hash, matching how the contacts endpoint already behaves. Also
adds a destroy_custom_attributes endpoint to remove specific keys,
mirroring the contacts convention. Replace stays the default, so
existing integrations are unaffected.

How to test

POST /conversations/:id/custom_attributes com { "custom_attributes":
{"a":1} }, then { "custom_attributes": {"b":2}, "merge": true } results
in {a:1, b:2}; no merge, results in {b:2}.
POST /conversations/:id/destroy_custom_attributes with {
"custom_attributes": ["a"] } removes only a.

---------

Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
2026-07-28 12:43:56 +05:30
Muhsin Keloth
3479b1026e fix(whatsapp): preserve phone health when business enrichment fails (#15200)
WhatsApp phone-number health could be discarded when the separate WABA
business-information request failed due to missing business-management
permissions. This left the health snapshot empty even though Meta
successfully returned the phone’s status, quality, capacity, and other
operational fields.

This change treats WABA information as optional enrichment after a
successful phone-number request. Available phone health is persisted and
returned, while the enrichment failure remains recorded in
`phone_number_health_error`. Failures from the primary phone-number
request continue to follow the existing error path.

Related: https://github.com/chatwoot/chatwoot/pull/15100

### How to reproduce

1. Configure a WhatsApp Cloud API inbox whose token can read its
phone-number node but cannot access `owner_business_info` on the WABA.
2. Open Settings → Inbox → Account Health or run the scheduled health
sync.
3. Observe that the phone-number request succeeds while the WABA request
returns a permission error.

### How to test

1. Refresh Account Health for the affected inbox.
2. Confirm the available phone status, quality rating, messaging limit,
verification state, throughput, and coexistence information are
populated.
3. Confirm unavailable business-account and portfolio fields remain
absent.
4. Confirm the WABA permission failure is retained in the channel’s
health error field.
5. Confirm a failure from the phone-number endpoint still records the
error without replacing the last successful health snapshot.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-07-28 09:56:56 +04:00
Sony Mathew
ed5a099425 Merge branch 'release/4.16.2' into develop 2026-07-27 15:44:23 +05:30
Sony Mathew
9c202793db Bump version to 4.16.2 2026-07-27 15:42:11 +05:30
Marco Cabral
a3a961919e fix(whatsapp): send messages to business scoped user ids (BSUID) (#15150)
## Description

WhatsApp's migration to usernames / **Business-Scoped User IDs (BSUID)**
means a contact can become addressable only by a BSUID (format
`CC.<id>`, e.g. `BR.1393...`) when no phone number is exposed. Per
Meta's Cloud API, a BSUID recipient must be sent in the **`recipient`**
field (with `recipient_type: "individual"`), **not** in `to`.

Today `Whatsapp::Providers::WhatsappCloudService` always places the
recipient in `to`. When `to` carries a BSUID, the Graph API returns
**HTTP 200 with a message id** but **silently drops the message**: it
strips the country prefix and treats the remainder as a phone number
(`wa_id`), which never resolves, so nothing is delivered and **no error
is surfaced**. In the username-only era this means agents reply into the
void — Chatwoot marks the message as sent while the customer receives
nothing.

This PR adds a small helper, `recipient_params`, that routes the
outgoing identifier to the correct field:

- a **BSUID** → `{ recipient_type: "individual", recipient: <bsuid> }`
- a **phone number** → `{ to: <phone> }` (unchanged behaviour)

It is applied to all four Cloud send paths: text, attachment, template
and interactive. BSUID detection reuses the existing
`RegexHelper::WHATSAPP_BSUID_REGEX`. No Graph API version bump is
required (see testing below).

Related to #13837 (this covers the **outbound sending** part).

### References
- Meta — *Business-scoped user IDs*:
https://developers.facebook.com/documentation/business-messaging/whatsapp/business-scoped-user-ids/
— "set `recipient` to the user's BSUID or parent BSUID"; when both `to`
and `recipient` are present, `to` takes precedence.
- BSUIDs began appearing in webhooks in April 2026; sending **to** a
BSUID was enabled by Meta in July 2026.

## 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 to not work as expected)
- [ ] This change requires a documentation update

## How Has This Been Tested?

Verified **live** against a real, Meta-registered WhatsApp Cloud number,
sending to a real BSUID captured from an inbound webhook, on **both
Graph API v13.0 (the current default in this service) and v22.0**:

| # | API version | recipient field | Meta response | delivered? |
|---|-------------|-----------------|---------------|:----------:|
| 1 | v22.0 | `recipient` = BSUID | `200` · `contacts[].user_id` echoed
|  |
| 2 | v22.0 | `to` = BSUID | `200` · `contacts[].wa_id` (prefix
stripped) |  |
| 3 | v13.0 | `recipient` = BSUID | `200` · `contacts[].user_id` echoed
|  |

- When the API echoes `user_id`, the BSUID is accepted and the message
**is delivered**; when it echoes `wa_id` (prefix stripped), it is
**not** — confirmed on the receiving handset.
- **v13.0 already accepts `recipient`**, so no Graph API version bump is
needed.
- Added unit specs asserting the request body uses `recipient` +
`recipient_type` for a BSUID and `to` for a phone number, across the
send paths.

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented 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

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-07-27 13:35:22 +04:00
Petterson
b2716e15e1 fix: deregister WhatsApp Cloud number on inbox delete (#14940)
## Description

When a WhatsApp Cloud inbox is deleted,
`Whatsapp::WebhookTeardownService` clears the phone-level webhook
override and unsubscribes the app from the WABA (when it's the last
inbox), but it **never deregisters the phone number**.

Because the number stays registered to the app, Meta reports that the
number is **"already registered to a partner app"** when the user later
tries to re-add it under a different app/BSP — leaving the number
effectively stuck.

This adds a deregister step so the number is released on deletion:
- New `Whatsapp::FacebookApiClient#deregister_phone_number` → `POST
/{phone_number_id}/deregister`.
- `WebhookTeardownService` calls it during teardown (alongside the
existing override-clear and app-unsubscribe), guarded on
`phone_number_id` and wrapped so a failure is logged and never blocks
the channel delete.

Docs:
https://developers.facebook.com/docs/whatsapp/cloud-api/reference/registration
(deregister)

## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)

## How has this been tested?
Unit specs for both the new API client method and the teardown service
(Meta API stubbed with WebMock), mirroring the existing
`register_phone_number` / teardown coverage.

## Checklist
- [x] My code follows the style guidelines of this project
- [x] I have added tests that prove my fix is effective
- [x] New and existing unit tests pass locally with my changes

---------

Co-authored-by: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com>
Co-authored-by: Tanmay Deep Sharma <tanmaydeepsharma21@gmail.com>
2026-07-27 14:36:33 +05:30
Tanmay Deep Sharma
5e82e971fa fix(whatsapp): surface Meta's real error when an outbound call fails (#15178)
When an outbound WhatsApp call could not be placed, agents sometimes saw
a completely empty error (`{"error":""}`) with no indication of what
went wrong. This makes an outbound call that is blocked at Meta (for
example, a WhatsApp Business Account with a billing/eligibility problem)
look like a generic, unexplained failure. Agents now see the actual
reason Meta returned.

## Closes

No linked issue — found via a customer support investigation (outbound
calling returning `422 {"error":""}`).

## How to reproduce

1. On an account with WhatsApp calling enabled, place an outbound call
to a contact whose WhatsApp Business Account is not eligible for calling
(Meta returns error code `131044`, "Business eligibility payment issue
for calling").
2. Before: the call fails with `422 {"error":""}` — an empty message.
3. After: the call fails with Meta's actual message (e.g. "Business
eligibility payment issue for calling"), so the agent/admin knows it is
a Meta-side eligibility issue to resolve, not a Chatwoot bug.

## What changed

Meta's error responses can contain an **empty** `error_user_msg` (`""`)
while the real reason lives in `error.message` / `error_user_title` —
notably error `131044`. The previous code used `parsed.dig('error',
'error_user_msg') || 'Failed to initiate call'`, but an empty string is
truthy in Ruby, so the blank message was surfaced instead of the
fallback.

- Added a small `meta_error_message(parsed, default)` helper that
prefers the first **non-blank** field: `error_user_msg` → `message` →
`error_user_title` → default.
- Used it in both `process_initiate_call_response` (outbound call
initiate) and `update_calling_status`, which had the same pattern.

The `[WHATSAPP CALL] initiate_call failed: status=… body=…` server log
(with the full Meta body) is unchanged and remains the source of truth
for debugging.

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-07-27 14:36:21 +05:30
Sony Mathew
1fa9127a72 fix: harden conversation participant api (#15180)
Prevents the conversation participants endpoints from returning profile
details for users who cannot be assigned to the conversation's inbox.
Invalid participant IDs are now rejected before any participant records
are changed, so mixed valid and cross-account payloads fail atomically
without exposing user data.
2026-07-27 13:17:57 +05:30
Sivin Varghese
19c96fcc07 fix: prevent page overflow from hidden tooltips (#15156) 2026-07-24 20:06:33 +05:30
Sony Mathew
7c1711170b feat: Add account suspension metadata in Super Admin (#15158)
## Description

Super Admins can now record a category and reason when suspending an
account, review the complete suspension history on the account details
page, and correct the latest suspension metadata without losing its
original timestamp. Suspension events are stored internally on the
account without changing customer-facing account API payloads.

## Closes

-
[CW-7653](https://linear.app/chatwoot/issue/CW-7653/ability-to-add-notes-while-suspending-an-acocunt)
- [Implementation
plan](https://linear.app/chatwoot/document/super-admin-account-suspension-metadata-implementation-plan-e7e4eb79d078)

## Type of change

- [ ] 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

## What changed

- Require a suspension category and a reason of up to 256 characters
when an active account is suspended.
- Store append-only suspension events in `accounts.internal_attributes`,
while preserving unrelated internal metadata.
- Allow corrections to the latest event for an already suspended account
without changing its timestamp.
- Show the full suspension history, newest first, on the Super Admin
account details page.
- Add visual dividers between top-level sections on the Super Admin
account edit page.
- Keep legacy suspended-account edits and new-account creation behavior
unchanged.

## How to test

1. Open an active account in Super Admin and choose **Suspended**.
2. Confirm the category and reason controls appear, reject incomplete or
invalid values, and enforce the 256-character reason limit.
3. Suspend the account with each supported category and confirm the
event appears on the details page.
4. Reactivate and suspend the account again; confirm prior history is
retained and a new event is added.
5. Edit a suspended account's latest category or reason; confirm its
original timestamp is preserved.
6. Confirm a legacy suspended account without history can still be
edited without supplying suspension metadata.

## 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>
2026-07-24 14:48:14 +05:30
Shivam Mishra
afa6421809 feat: allow suspended accounts to access billing (#15153)
Administrators of suspended accounts can now reach the billing page to
settle payment and restore their account, instead of being fully locked
out on the suspended screen. The suspended screen shows a subtle
**Manage billing** link below the contact support button (admins on
Cloud only). Agents remain restricted to the suspended screen.

## How to test

1. On Cloud, suspend an account from Super Admin.
2. Log in as an administrator of that account — you land on the
suspended screen with a "Manage billing" link below contact support.
3. Click it — the billing settings page opens; subscription, Stripe
portal, and top-ups all work.
4. Log in as an agent — no billing link, and navigating to
`/settings/billing` manually redirects back to the suspended screen.

## What changed

- Router guard allows `billing_settings_index` for administrators when
the account is not active.
- Suspended screen gets a billing link gated by admin role and Cloud
installation.

<img width="3024" height="1718" alt="CleanShot 2026-07-24 at 12 25
56@2x"
src="https://github.com/user-attachments/assets/1905d070-5e67-4283-9c8d-6e9936bba9a3"
/>

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-07-24 14:47:58 +05:30
Sony Mathew
65499df6a3 fix(email): allow larger email templates (#15146)
## Description

Branded email layouts and other email templates can now contain up to
262,144 characters instead of the generic 20,000-character text limit.
This supports customer layouts around 41 KB and 100 KB while retaining a
defined application-level ceiling. The account and inbox API schemas now
expose the same maximum.

## Closes


[CW-7682](https://linear.app/chatwoot/issue/CW-7682/allow-larger-branded-email-templates)

Related:
[CW-7514](https://linear.app/chatwoot/issue/CW-7514/branded-html-email-templates-per-inboxbrand)

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)
- [x] This change requires a documentation update

## How Has This Been Tested?

1. As an administrator with `branded_email_templates` enabled, update an
account branded layout with a 100 KB Liquid layout containing `{{
content_for_layout }}` and confirm the request succeeds.
2. Update an Email inbox layout with more than 262,144 characters and
confirm the API returns `422`.
3. Confirm a layout at exactly 262,144 characters remains valid.

## 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
- [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
2026-07-24 14:00:01 +05:30
Sony Mathew
6baf442c28 fix: enforce email limits for agent invitations (#15082)
# Pull Request Template

## Description

Prevents Chatwoot Cloud accounts from exceeding their daily non-channel
email allowance through agent invitations. New-user invitations
atomically reserve email capacity before mail is queued; when the budget
is exhausted, agent creation rolls back and returns HTTP 429.

This covers single and bulk agent creation. Self-hosted installations
remain unaffected, and adding an existing user does not consume capacity
when no invitation is sent.

Related to
[CW-7637](https://linear.app/chatwoot/issue/CW-7637/prevent-agent-invitation-email-abuse-after-july-20-incident).

## 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?

Verified single and bulk creation at an exhausted budget, successful
invitation enqueueing below the limit, no capacity usage for existing
users, and no enforcement on self-hosted installations. A concurrent
Redis probe admitted exactly five of twenty simultaneous reservations
against a limit of five.

## 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
2026-07-24 13:59:28 +05:30
Muhsin Keloth
d644c207f0 feat: monitor whatsapp phone number health (#15100)
Adds scheduled WhatsApp Cloud API phone-number health synchronization
and expands the Account Health page with phone, capacity,
business-account, webhook, coexistence, and recovery details.
Authorization failures now guide administrators to the appropriate
Configuration flow without exposing technical Meta error codes.

Fixes
[CW-7621](https://linear.app/chatwoot/issue/CW-7621/store-whatsapp-phone-number-health-status)

**Preview**

<img width="2594" height="1676" alt="CleanShot 2026-07-22 at 10 55
01@2x"
src="https://github.com/user-attachments/assets/de606cb4-5682-4178-87e9-c18752d299b5"
/>

<img width="2576" height="1506" alt="CleanShot 2026-07-22 at 10 55
08@2x"
src="https://github.com/user-attachments/assets/4eb1810f-be12-4fbc-bcb0-9e2906785c48"
/>


<img width="1748" height="1150" alt="CleanShot 2026-07-22 at 11 10
05@2x"
src="https://github.com/user-attachments/assets/64f5be5a-c127-4372-889f-d392947c13b8"
/>



### How to test

1. Open **Settings → Inboxes → a WhatsApp Cloud API inbox → Account
Health**.
2. Confirm the page shows separate Phone number, Health and capacity,
Business account, and Webhook configuration sections.
3. Confirm the configured webhook URL can be copied and the expected URL
appears only when it differs.
4. For a coexistence number, confirm **Coexistence · Active** appears;
confirm it is hidden for standard Cloud API numbers.
5. With an invalid Embedded Signup token, confirm the page asks to
refresh the WhatsApp connection and **Go to Configuration** opens the
Configuration tab.
6. With an invalid manually configured token, confirm the page asks the
administrator to verify or replace the access token.
7. Confirm authorization states do not display Meta error codes or the
manual-migration recommendation.

### What changed

- Persists the latest successful phone health snapshot, check time, and
most recent error while retaining the last successful data after a
failed refresh.
- Refreshes stale active Cloud API channels every six hours through
low-priority jobs.
- Fetches phone, WABA, business portfolio, webhook, and coexistence
details.
- Uses Meta's current `whatsapp_business_manager_messaging_limit` field
while preserving the existing UI response key.
- Classifies authorization failures for setup-specific recovery guidance
and logs new risky quality/status transitions.
- Adds focused service, scheduler, job, trigger, and API coverage.

Internal alerts, throttling, automatic inbox disablement, and customer
notifications remain outside this PR and are tracked separately in
CW-7622.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-07-24 12:00:37 +04:00
Sony Mathew
1bff2d86ed fix: stabilize Intercom client error spec (#15154)
Stabilizes the Intercom transport-error spec when Rails reloads
application constants during the full backend suite. The production
client behavior is unchanged; the expectation now compares the exception
class name while continuing to verify its message and metadata.

## Closes

- Follow-up to
[CW-7615](https://linear.app/chatwoot/issue/CW-7615/optimize-intercom-import-reliability-and-bulk-message-ingestion)
- Follow-up to #15050

## How to reproduce

1. Run the backend shard containing the Intercom client spec after specs
that trigger Rails constant reloading.
2. Observe that the old matcher can reject an exception whose printed
class is still `DataImports::Intercom::Client::Error` because it
references a different class object.
3. Confirm the updated expectation accepts the reloaded class by name
and still verifies the transport error message and body.

## What changed

- Compare the raised Intercom client error using `error.class.name`.
- Preserve assertions for the user-facing error message and transport
error metadata.

## Checklist

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] New and existing focused unit tests pass locally with my changes
2026-07-24 13:12:27 +05:30
Aakash Bakhle
7910eabefc feat(captain): add FAQ suggestion review interface (4/4) (#15017)
Captain now groups recurring questions from resolved conversations into
FAQ suggestions and orders them by the number of source conversations.
Agents can view suggestions and open source conversations they can
access. Administrators can edit, approve, or dismiss suggestions.

The old pending FAQ flow is removed. The Captain overview and FAQ page
now use open suggestion counts and link to the same review page.
Approved FAQs remain unchanged.

## Depends on

#14979

## Closes

https://linear.app/chatwoot/issue/CW-7496/fe-and-ux

## How to test

1. Open Captain and choose an assistant with open FAQ suggestions.
2. Open FAQ suggestions from the overview or the FAQ banner. Confirm
that suggestions are ordered by conversation count.
3. Switch assistants without leaving the page. Confirm that the previous
results clear and the new assistant results load.
4. Search for suggestions and move between pages. Change the search or
page again before the first request finishes, and confirm that the
latest request controls the results and loading state.
5. Open a suggestion and review its source conversations.
6. Make the source conversation request fail. Confirm that the dialog
keeps the error visible and that Retry loads the sources.
7. Sign in as an agent. Confirm that you can read suggestions and source
conversations you can access, but cannot edit, approve, or dismiss
suggestions.
8. Sign in as an administrator. Edit and save a suggestion, approve one
suggestion, and dismiss another.
9. Confirm that the approved suggestion appears in the assistant FAQ
list.
10. Open the old pending FAQ URL and confirm that it redirects to FAQ
suggestions.

## What changed

1. Added the FAQ suggestion list, cards, search, pagination, and empty
state.
2. Added a review dialog with source conversation links, a clear error
message, and a Retry button.
3. Added edit, approve, and dismiss actions for administrators.
4. Removed the old pending FAQ status, count, page, and bulk approval
action.
5. Updated the Captain overview and FAQ banner to use open suggestion
counts and link to FAQ suggestions.
6. Made each FAQ page load data for the selected assistant and ignore
results from older requests.
7. Kept the old pending FAQ URL as a redirect so saved links continue to
work.

---------

Co-authored-by: Sony Mathew <sony@chatwoot.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
2026-07-24 12:53:09 +05:30
Sony Mathew
7786e38df9 feat: retry stalled Intercom imports after 15 minutes (#15050)
## 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
2026-07-24 12:30:36 +05:30
Aakash Bakhle
7948ea09ac feat(captain): add FAQ suggestion review API (3/4) (#14979)
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>
2026-07-24 12:12:13 +05:30
Aakash Bakhle
e10b236871 feat(captain): group conversation FAQ signals (2/4) (#14978)
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>
2026-07-24 11:33:47 +05:30
Sojan Jose
56e72eff8d feat: assign connected agent bot owner (#14875)
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.
2026-07-23 19:22:35 -07:00
Ajith KV
2a1dd481e9 test(playwright): add agent onboarding and inbox creation UI tests (#14707)
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>
2026-07-23 23:24:03 +05:30
Sivin Varghese
a98666030b chore: Calls page UI improvements (#15129) 2026-07-23 18:45:56 +05:30
Nicky Duijf
bae20ca83e feat(conversation): add label search to right-click context menu (#15084) 2026-07-23 15:10:46 +05:30
Sony Mathew
954e5844a8 Merge branch 'release/4.16.1' into develop 2026-07-23 13:58:16 +05:30
Sony Mathew
0efab5fb43 Bump version to 4.16.1 2026-07-23 13:57:15 +05:30
Muhsin Keloth
34ad78b122 fix(instagram): remove resolved restriction banners (#15136) 2026-07-23 12:56:21 +05:30
Shivam Mishra
ddb0535a93 perf: reuse resolved count for reopen rate (#15122)
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.
2026-07-22 22:03:25 +05:30
Sivin Varghese
42cbf7d3b9 fix: stray backslash after hard breaks before formatted list items (#15112) 2026-07-22 20:07:00 +05:30
Sony Mathew
887897ea98 fix: lock agent quota checks (#15029)
# 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>
2026-07-22 18:43:00 +05:30
Tanmay Deep Sharma
8aee518149 fix(integrations): restrict Linear/Notion/Shopify hook deletion to admins (#15126)
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>
2026-07-22 17:53:25 +05:30