## Linear ticket
https://linear.app/chatwoot/issue/CW-7187/voice-calls-followup-tasks
## Description
Improvements to the WhatsApp voice-calling experience plus a cheaper,
more accurate audio-transcription model.
- First-time callers now get a real name. An inbound WhatsApp call
creates the contact from the caller's WhatsApp profile name instead of
the bare phone number.
- Clear, consistent call attribution. Call bubbles show a unified
"Handled by {agent}"
- Cleaner call widget. The dismiss (✕) button is shown only for incoming
calls
- WhatsApp calling for manual inboxes. voice_calling_supported? now
covers any whatsapp_cloud inbox
- Transcription: whisper-1 → gpt-4o-mini-transcribe.
## Type of change
- [ ] New feature (non-breaking change which adds functionality)
## 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
# Pull Request Template
## Description
Better scheduling and queueing mechanics for document auto-sync
- add jitter plan wise for document sync
- move auto-sync documents to purgeable queue
## Type of change
Please delete options that are not relevant.
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.
locally tested and with specs
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Sony Mathew <sony@chatwoot.com>
Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
## Summary
Frontend for WhatsApp Cloud Calling: header / contact-panel call
buttons, ringing widget, accept/reject/hangup, mute, in-bubble audio
player + transcript, recording-on-hangup upload, mid-call reload
warning. WebRTC is browser-direct to Meta — no media server bridge.
## Closes
- https://linear.app/chatwoot/issue/PLA-150
## How to test
Requires backend support — the controller, services, model changes, and
routes ship in **#14334** (`feature/pla-150`). Merge / deploy that first
(or simultaneously); the FE alone won't function without those
endpoints.
Then on staging, for a WhatsApp Cloud + embedded-signup inbox with the
new \`Configuration → Enable voice calling\` toggle ON and webhook
registered:
1. **Outbound** — open a conversation, click the phone icon in the
conversation header (or contact panel), grant mic, your phone rings,
answer, audio both ways, hang up. Recording + transcript land in the
bubble within ~10s.
2. **Inbound** — call the business number from your phone. The
FloatingCallWidget appears bottom-right with caller name. Click accept,
audio both ways, hang up. Recording + transcript appear.
3. **Mute** — during an active WhatsApp call, click the mic icon next to
hangup. Speech stops reaching Meta until you click again.
4. **Mid-call reload guard** — try `Cmd-R` during an active call;
browser shows a confirm prompt.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
This PR makes SAML login independent of Rails session cookies
## Problem
The normal SAML login flow should be straightforward:
- User opens Chatwoot.
- Chatwoot creates `_chatwoot_session`.
- User starts SSO.
- Chatwoot redirects the browser to the SAML provider.
- The provider authenticates the user.
- The provider sends the browser back to Chatwoot's ACS URL.
- Chatwoot reads the SAML response, finds or creates the user, and logs
them in.
The fragile step is the ACS callback. Most SSO flows return to the app
through browser redirects where cookies usually pass through as
expected. **ADFS commonly returns the SAML response with a cross-site
POST**. With Chatwoot's session cookie using `SameSite=Lax`, browsers
may not send `_chatwoot_session` on that POST.
SAML validation itself does not need the old Rails session cookie. The
problem was our callback handoff after validation. DeviseTokenAuth
stores the verified OmniAuth payload in Rails session, then redirects to
a second callback route. If the browser does not preserve that session,
Chatwoot has already received a valid SAML response but can no longer
finish login.
## Solution
This PR removes the session-backed handoff for SAML only:
- The SAML callback completes login in the same request where OmniAuth
validates the SAML response.
- Chatwoot reads the verified auth payload directly from
`request.env['omniauth.auth']`.
- Account context and RelayState come from callback params or OmniAuth
env data, not Rails session.
- Other OmniAuth providers continue using the existing DeviseTokenAuth
flow.
- Mobile SAML still works when the IdP returns `RelayState=mobile`; the
callback redirects to the mobile deep link with the generated SSO token.
The previous SAML override used `303 See Other` to avoid replaying the
SAML POST into the second callback route. This change keeps that intent,
but removes the second callback route for SAML entirely.
## Screen recording
### SP Initiated
https://github.com/user-attachments/assets/b0735e93-3864-4cc3-b6fc-419fff4b549e
### IDP Initiated
https://github.com/user-attachments/assets/3ded0246-933c-4c85-9b7c-fa15fdc34883
## Testing
Manual validation:
- Complete a SAML login.
- In the browser network trace, find the IdP POST to
`/omniauth/saml/callback?account_id=<account-id>`.
- Confirm it redirects directly to `/app/login?...sso_auth_token=...`
for web login.
- For mobile, confirm `RelayState=mobile` redirects to the configured
mobile deep link.
- Confirm there is no intermediate `/auth/saml/callback` request.
Testing with mocksaml.com:
- Configure Chatwoot with a public `FRONTEND_URL`.
- Set the mocksaml ACS URL to:
```text
https://<chatwoot-host>/omniauth/saml/callback?account_id=<account-id>
```
- Set the mocksaml audience/SP entity ID to the value shown in Chatwoot
SAML settings, usually:
```text
https://<chatwoot-host>/saml/sp/<account-id>
```
- Use an email returned by mocksaml that exists in the SAML-enabled
account.
- Start login from Chatwoot's SSO login page.
- Confirm the callback redirects directly to the app login URL with an
SSO token.
---------
Co-authored-by: Sojan Jose <sojan@pepalo.com>
Adds the server-side flow that turns Meta WhatsApp Cloud Calling
webhooks into Chatwoot Calls, conversations, voice_call message bubbles,
and ActionCable broadcasts. Stacked on top of #14312 (PR-2 — provider
methods); intentionally does not include the HTTP controller, routes, or
frontend (those land in PR-4 and PR-9).
## Closes
- Part of the WhatsApp Cloud Calling rollout. Linear: TBD
## What changed
**Webhook routing**
- `app/jobs/webhooks/whatsapp_events_job.rb` — append
`prepend_mod_with('Webhooks::WhatsappEventsJob')` so EE can extend it
without forking.
- `enterprise/app/jobs/enterprise/webhooks/whatsapp_events_job.rb` (new)
— overlay that prepends `handle_message_events` to intercept `field:
'calls'` payloads (route to `Whatsapp::IncomingCallService`) and
`interactive.call_permission_reply` messages (route to
`Whatsapp::CallPermissionReplyService`); falls through with `super` for
regular messages.
**Services**
- `enterprise/app/services/whatsapp/incoming_call_service.rb` (new) —
gated on `provider_config['calling_enabled']`; processes `connect`
(creates inbound call via `Voice::InboundCallBuilder` or transitions an
existing outbound call to `in_progress`) and `terminate` events; updates
conversation `additional_attributes` and broadcasts
`voice_call.incoming`/`voice_call.outbound_connected`/`voice_call.ended`.
- `enterprise/app/services/whatsapp/call_permission_reply_service.rb`
(new) — handles WhatsApp interactive `call_permission_reply` replies;
clears the conversation's `call_permission_requested_at` flag and
broadcasts `voice_call.permission_granted` so the agent UI can re-enable
the call button.
**Builder/model adjustments**
- `enterprise/app/services/voice/inbound_call_builder.rb` —
provider-agnostic; accepts `provider:` and `extra_meta:` kwargs, drops
`account:` (now derived from `inbox.account` to keep the param count
under rubocop's ceiling without disabling cops), uses digits-only
`source_id` for WhatsApp ContactInbox (validation requires
`^\d{1,15}\z`), skips Twilio-only `conference_sid` for non-Twilio
providers.
- `enterprise/app/services/voice/call_message_builder.rb` — adds
`create!`/`update_status!` API and `CALL_TO_VOICE_STATUS` map; uses
direct `Message.create!` (bypasses `Messages::MessageBuilder`'s
incoming-on-non-Api-inbox guard, which would otherwise reject the system
bubble); content is `'WhatsApp Call'` for WhatsApp and `'Voice Call'`
for Twilio. Backwards-compatible `perform!` retained for the existing
Twilio call sites.
- `enterprise/app/models/call.rb` — adds `default_ice_servers` (driven
by `VOICE_CALL_STUN_URLS` env), `direction_label` alias for the
`inbound`/`outbound` strings the FE expects, and
`ringing?`/`in_progress?`/`terminal?` predicates used throughout the
pipeline.
**Outgoing-channel guard**
- `app/services/base/send_on_channel_service.rb` — extends
`invalid_message?` to skip messages with `content_type == 'voice_call'`.
Without this, agent-initiated outbound calls (PR-4) would deliver
\"WhatsApp Call\" as a text message to the contact every time.
**Twilio call-site update**
- `enterprise/app/controllers/twilio/voice_controller.rb` — drops the
now-redundant `account: current_account` kwarg from the
`Voice::InboundCallBuilder.perform!` call.
**Tests**
- New: `spec/enterprise/services/whatsapp/incoming_call_service_spec.rb`
(5 examples — calling-disabled, inbound connect, outbound connect,
terminate completed, terminate no-answer, unknown event).
- New:
`spec/enterprise/services/whatsapp/call_permission_reply_service_spec.rb`
(3 examples — accept, reject, calling-disabled).
- Updated: `spec/enterprise/services/voice/inbound_call_builder_spec.rb`
and `spec/enterprise/controllers/twilio/voice_controller_spec.rb` to
drop the `account:` kwarg from call expectations.
## How to test
In `rails console` against an account with a WhatsApp inbox where
`provider_config['calling_enabled']` is true:
```ruby
inbox = Inbox.find(<id>)
params = { calls: [{ id: 'wacid_test', from: '15550001111', event: 'connect',
session: { sdp: 'v=0...', sdp_type: 'offer' } }] }
Whatsapp::IncomingCallService.new(inbox: inbox, params: params).perform
# => Conversation + Call (status: 'ringing', provider: 'whatsapp') + voice_call message bubble
# => ActionCable broadcasts `voice_call.incoming` to the assignee or account-wide
# Then terminate it:
Whatsapp::IncomingCallService.new(inbox: inbox,
params: { calls: [{ id: 'wacid_test', event: 'terminate', duration: 0, terminate_reason: 'no_answer' }] }
).perform
# => Call status flips to 'no_answer', message bubble updates, `voice_call.ended` broadcast fires
```
End-to-end browser flow (Meta → cable → UI) requires the controller from
PR-4 and the frontend from PR-9.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When an inbound voice call ends, the conversation bubble now (1) renders
an inline audio player as soon as Twilio finishes the recording and (2)
shows the call duration alongside "Call ended" so the agent gets the
at-a-glance summary without opening the recording.
Fixes
https://linear.app/chatwoot/issue/PLA-118/feat-recordings-on-calls-should-be-attached-on-the-conversation
and
https://linear.app/chatwoot/issue/PLA-119/duration-of-the-call-is-not-visible-on-the-chat-bubble
## How to test
1. Set up a Twilio voice inbox and trigger an inbound call.
2. Answer the call from an agent, talk for a few seconds, then hang up.
3. As soon as the call ends, the bubble should read **"Call ended —
0:NN"** (where NN is the call duration in seconds).
4. Wait a few seconds for Twilio to finish processing the recording
(usually <30s after hangup).
5. The same bubble should now show an inline audio player below the
duration. Press play; the recording should be audible.
6. Refresh the page — both the duration and the player should still be
there.
7. End a second call on the same conversation — its bubble should get
its own duration + player, independent of the first.
---------
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
### Description
Inbound voice calls now route ownership cleanly: the call widget is
hidden from agents who aren't the conversation assignee, the first agent
to pick up becomes the assignee, and any later join attempt by another
agent is rejected with a clear "<agent> is already handling the call."
alert.
Closes
https://linear.app/chatwoot/issue/PLA-98/inbound-voice-calls-assignment-aware-visibility-auto-assignment-on
### How to test
1. As Agent A and Agent B, open the dashboard for the same voice inbox
in two browsers.
2. Place an inbound call to the inbox with the conversation
**unassigned** — both agents should see the call widget.
3. Have Agent A click **Join**. Agent A's widget transitions to the
active call; Agent B's widget disappears (conversation is now assigned
to Agent A).
4. While the call is in progress, attempt to join from a third agent
(e.g., via the bubble in the conversation timeline) — the join is
rejected with the toast `Agent A is already handling the call.`
5. Resolve the conversation, then place a second call to a conversation
that is already manually assigned to Agent A — only Agent A sees the
widget; nobody else does.
6. Race test: trigger two near-simultaneous join attempts (two agents
click Join within a few hundred ms of each other) — exactly one wins;
the other gets the conflict alert.
---------
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Twilio voice now uses first-class `Call` records as the source of truth
for call state, instead of storing it on
`conversation.additional_attributes` and `conversation.identifier`. Each
call gets its own record, its own `voice_call` bubble matched by
`call_sid`, and its own conference name keyed off `Call.id`. Multiple
calls on the same conversation (for `lock_to_single_conversation`
inboxes) now work correctly, and the conversation card stays in sync
with the real latest message.
Fixes https://linear.app/chatwoot/issue/PLA-121/lock-to-single-thread
---------
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
# Pull Request Template
## Description
- Wires up Controllers to auto-sync job
- adds plan based sync schedule
- a scheduler that runs every hour to check syncable documents
- guards the whole feature behind feature flag by reclaiming
`twilio_content_templates`
- Adds a global and account level cap on how many documents to enqueue
to prevent sudden burst at first run
- some refactor to simplify code
- specs
Fixes # (issue)
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.
specs and locally
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
Voice calling is now a capability on the existing TwilioSms rather than
a separate Voice model. A single Twilio phone number handles both SMS
and voice calls through one inbox.
Fixes
https://linear.app/chatwoot/issue/CW-6683/add-voice-calling-as-a-capability-on-twilio-sms-channel
and https://linear.app/chatwoot/issue/PLA-120/add-the-support-for-sms
**What changed**
- Replaced Channel::Voice with voice_enabled flag on Channel::TwilioSms
- Added voice_enabled, twiml_app_sid, api_key_secret columns to
channel_twilio_sms table
- Dropped channel_voice table (no production data)
- All voice logic lives in Enterprise layer via
prepend_mod_with('Channel::TwilioSms')
- Added Voice settings tab on Twilio SMS inbox settings to
enable/disable voice
- Validates Twilio number voice capability before provisioning
- Teardown service cleans up TwiML app and credentials when voice is
disabled
- Frontend voice detection uses isVoiceCallEnabled() /
getVoiceCallProvider() helpers — extensible to future providers
- Gated by channel_voice feature flag
**How to test**
1. Enable feature flag:
Account.find(<id>).enable_features('channel_voice')
2. Create voice inbox: Inboxes → Voice tile → enter Twilio credentials →
verify incoming/outgoing calls and SMS work
3. Enable voice on existing SMS inbox: Inboxes → select Twilio SMS inbox
→ Voice tab → toggle on → provide API key credentials → verify calls
work
4. Disable voice: Voice tab → toggle off → verify TwiML app is deleted,
credentials cleared, SMS still works
5. Re-enable voice: Toggle on again → must provide api_key_secret again
→ new TwiML app provisioned
---------
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Adds the ability to translate help center articles to other languages using Captain's LLM infrastructure. Translated articles are created as drafts linked to the source article.
Fixes
https://linear.app/chatwoot/issue/CW-6901/translate-article-to-another-language
**How to test**
1. Navigate to Help Center → Articles for a portal with multiple locales
2. Click the three-dot menu on any article → "Translate"
3. Select a target language and category → click Translate
4. Switch to the target locale — the translated article appears as a
draft
5. Try translating the same article again — a warning shows the existing
translation with a link to open it in a new tab
6. Click "Overwrite and translate" to replace the existing translation
https://github.com/user-attachments/assets/1d2e991b-f0ac-403a-bcc1-2181b5731ea4
## Account branding enrichment during signup
This PR does the following
### Replace Firecrawl with Context.dev
Switches the enterprise brand lookup from Firecrawl to Context.dev for
better data quality, built-in caching, and automatic filtering of
free/disposable email providers. The service interface changes from URL
to email input to match Context.dev's email endpoint. OSS still falls
back to basic HTML scraping with a normalized output shape across both
paths.
The enterprise path intentionally does not fall back to HTML scraping on
failure — speed matters more than completeness. We want the user on the
editable onboarding form fast, and a slow fallback scrape is worse than
letting them fill it in.
Requires `CONTEXT_DEV_API_KEY` in Super Admin → App Config. Without it,
falls back to OSS HTML scraping.
### Add job to enrich account details
After account creation, `Account::BrandingEnrichmentJob` looks up the
signup email and pre-fills the account name, colors, logos, social
links, and industry into `custom_attributes['brand_info']`.
The job signals completion via a short-lived Redis key (30s TTL) + an
ActionCable broadcast (`account.enrichment_completed`). The Redis key
lets the frontend distinguish "still running" from "finished with no
results."
## Summary
When a Super Admin creates a new account via the Administrate dashboard,
the `manually_managed_features` field (a virtual attribute stored in
`internal_attributes` JSON) is passed to `Account.new(...)`, raising
`ActiveModel::UnknownAttributeError`. The existing `update` action
already strips this param — this fix adds the same handling to `create`.
Closes -> https://linear.app/chatwoot/issue/INF-66
Related Sentry ->
https://chatwoot-p3.sentry.io/issues/7168237533/?project=6382945&referrer=Linear
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How to reproduce
1. Log in as Super Admin
2. Navigate to Accounts → New
3. Fill in the form (with or without manually managed features selected)
4. Submit → `ActiveModel::UnknownAttributeError: unknown attribute
'manually_managed_features' for Account`
## What changed
- Added a `create` override in
`Enterprise::SuperAdmin::AccountsController` that strips
`manually_managed_features` from params before calling `super`, then
persists them via `InternalAttributesService` after the account is
saved.
# Pull Request Template
## Description
Adds custom tool support to v1
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.
<img width="1816" height="958" alt="CleanShot 2026-03-24 at 11 37 33@2x"
src="https://github.com/user-attachments/assets/2777a953-8b65-4a2d-88ec-39f395b3fb47"
/>
<img width="378" height="488" alt="CleanShot 2026-03-24 at 11 38 18@2x"
src="https://github.com/user-attachments/assets/f6973c99-efd0-40e4-90fe-4472a2f63cea"
/>
<img width="1884" height="1452" alt="CleanShot 2026-03-24 at 11 38
32@2x"
src="https://github.com/user-attachments/assets/9fba4fc4-0c33-46da-888a-52ec6bad6130"
/>
## Checklist:
- [x] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
# Pull Request Template
## Description
Relocate controller from enterprise/ to app/ and add
Api::V1::Accounts::Captain::TasksController.prepend_mod_with for EE
overrides.
Fixes: Ai assist giving 404 on CE
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.
before:
<img width="482" height="130" alt="image"
src="https://github.com/user-attachments/assets/f51dc28a-ac54-45c4-9015-6f956fdf5057"
/>
after:
<img width="458" height="182" alt="image"
src="https://github.com/user-attachments/assets/eb86a679-5482-4157-9f4e-f3e9953d8649"
/>
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
# Pull Request Template
## Description
Captain v1 does not have access to contact attributes. Added a toggle to
let user choose if they want contact information available to Captain.
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.
Specs and locally
<img width="1924" height="740" alt="CleanShot 2026-03-19 at 18 48 19@2x"
src="https://github.com/user-attachments/assets/353cfeaa-cd58-40eb-89e7-d660a1dc1185"
/>
![Uploading CleanShot 2026-03-19 at 18.53.26@2x.png…]()
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
SAML sign-in now only links an existing user when that user already
belongs to the account that initiated SSO. New users can still be
created for SAML-enabled accounts, and invited members can continue to
sign in through their IdP, but SAML will no longer auto-attach an
unrelated existing user record during login.
**What changed**
- Added an account-membership check before SAML reuses an existing user
by email.
- Kept first-time SAML user creation unchanged for valid new users.
- Added builder and request specs covering the allowed and rejected
login paths.
# Pull Request Template
## Description
Playground now uses v2. It was only wired to use v1. Traces get `source:
playground` on langfuse when playground has been used.
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.
locally and specs
<img width="1806" height="1276" alt="image"
src="https://github.com/user-attachments/assets/41ef4eb3-52b1-4b8e-9a4f-e8510c90cb39"
/>
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
## How to reproduce
In Chatwoot Cloud, mark an account for deletion from account settings
while the account has an active Stripe subscription. Before this change,
deletion marking did not explicitly mark subscriptions to stop renewing
at period end.
## What changed
This PR adds `Enterprise::Billing::CancelCloudSubscriptionsService` and
calls it from the delete action path in
`Enterprise::Api::V1::AccountsController`. The service lists only active
Stripe subscriptions for the customer and sets `cancel_at_period_end:
true` when needed. The account deletion schedule remains unchanged
(existing static 7-day behavior), and Stripe deleted-event fallback
behavior remains unchanged.
## How this was tested
Added and updated specs:
-
`spec/enterprise/services/enterprise/billing/cancel_cloud_subscriptions_service_spec.rb`
-
`spec/enterprise/controllers/enterprise/api/v1/accounts_controller_spec.rb`
Executed:
- `bundle exec rspec
spec/enterprise/services/enterprise/billing/cancel_cloud_subscriptions_service_spec.rb`
- `bundle exec rspec
spec/enterprise/controllers/enterprise/api/v1/accounts_controller_spec.rb:363`
We are expanding Chatwoot’s automation capabilities by
introducing **Conversation Workflows**, a dedicated section in settings
where teams can configure rules that govern how conversations are closed
and what information agents must fill before resolving. This feature
helps teams enforce data consistency, collect structured resolution
information, and ensure downstream reporting is accurate.
Instead of having auto‑resolution buried inside Account Settings, we
introduced a new sidebar item:
- Auto‑resolve conversations (existing behaviour)
- Required attributes on resolution (new)
This groups all conversation‑closing logic into a single place.
#### Required Attributes on Resolve
Admins can now pick which custom conversation attributes must be filled
before an agent can resolve a conversation.
**How it works**
- Admin selects one or more attributes from the list of existing
conversation level custom attributes.
- These selected attributes become mandatory during resolution.
- List all the attributes configured via Required Attributes (Text,
Number, Link, Date, List, Checkbox)
- When an agent clicks Resolve Conversation:
If attributes already have values → the conversation resolves normally.
If attributes are missing → a modal appears prompting the agent to fill
them.
<img width="1554" height="1282" alt="CleanShot 2025-12-10 at 11 42
23@2x"
src="https://github.com/user-attachments/assets/4cd5d6e1-abe8-4999-accd-d4a08913b373"
/>
#### Custom Attributes Integration
On the Custom Attributes page, we will surfaced indicators showing how
each attribute is being used.
Each attribute will show badges such as:
- Resolution → used in the required‑on‑resolve workflow
- Pre‑chat form → already existing
<img width="2390" height="1822" alt="CleanShot 2025-12-10 at 11 43
42@2x"
src="https://github.com/user-attachments/assets/b92a6eb7-7f6c-40e6-bf23-6a5310f2d9c5"
/>
#### Admin Flow
- Navigate to Settings → Conversation Workflows.
- Under Required attributes on resolve, click Add Required Attribute.
- Pick from the dropdown list of conversation attributes.
- Save changes.
Agents will now be prompted automatically whenever they resolve.
<img width="2434" height="872" alt="CleanShot 2025-12-10 at 11 44 42@2x"
src="https://github.com/user-attachments/assets/632fc0e5-767c-4a1c-8cf4-ffe3d058d319"
/>
#### NOTES
- The Required Attributes on Resolve modal should only appear when
values are missing.
- Required attributes must block the resolution action until satisfied.
- Bulk‑resolve actions should follow the same rules — any conversation
missing attributes cannot be bulk‑resolved, rest will be resolved, show
a notification that the resolution cannot be done.
- API resolution does not respect the attributes.
---------
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
CSAT scores are helpful, but on their own they rarely tell the full
story. A drop in rating can come from delayed timelines, unclear
expectations, or simple misunderstandings, even when the issue itself
was handled correctly.
Review Notes for CSAT let admins/report manager roles add internal-only
context next to each CSAT response. This makes it easier to interpret
scores properly and focus on patterns and root causes, not just numbers.
<img width="2170" height="1680" alt="image"
src="https://github.com/user-attachments/assets/56df7fab-d0a7-4a94-95b9-e4c459ad33d5"
/>
### Why this matters
* Capture the real context behind individual CSAT ratings
* Clarify whether a low score points to a genuine service issue or a
process gap
* Spot recurring themes across conversations and teams
* Make CSAT reviews more useful for leadership reviews and
retrospectives
### How Review Notes work
**View CSAT responses**
Open the CSAT report to see overall metrics, rating distribution, and
individual responses.
**Add a Review Note**
For any CSAT entry, managers can add a Review Note directly below the
customer’s feedback.
**Document internal insights**
Use Review Notes to capture things like:
* Why a score was lower or higher than expected
* Patterns you are seeing across similar cases
* Observations around communication, timelines, or customer expectations
Review Notes are visible only to administrators and people with report
access only. We may expand visibility to agents in the future based on
feedback. However, customers never see them.
Each note clearly shows who added it and when, making it easy to review
context and changes over time.
## Description
Adds the ability to sort companies by the number of contacts they have
(contacts_count) in ascending or descending order. This is part of the
Chatwoot 5.0 release requirements for the companies feature.
The implementation uses a scope-based approach consistent with other
sorting implementations in the codebase (e.g., contacts sorting by
last_activity_at).
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## Available Sorting Options
After this change, the Companies API supports the following sorting
options:
| Sort Field | Type | Ascending | Descending |
|------------|------|-----------|------------|
| `name` | string | `?sort=name` | `?sort=-name` |
| `domain` | string | `?sort=domain` | `?sort=-domain` |
| `created_at` | datetime | `?sort=created_at` | `?sort=-created_at` |
| `contacts_count` | integer (scope) | `?sort=contacts_count` |
`?sort=-contacts_count` |
**Note:** Prefix with `-` for descending order. Companies with NULL
contacts_count will appear last (NULLS LAST).
## CURL Examples
**Sort by contacts count (ascending):**
```bash
curl -X GET 'https://app.chatwoot.com/api/v1/accounts/{account_id}/companies?sort=contacts_count' \
-H 'api_access_token: YOUR_API_TOKEN'
```
**Sort by contacts count (descending):**
```bash
curl -X GET 'https://app.chatwoot.com/api/v1/accounts/{account_id}/companies?sort=-contacts_count' \
-H 'api_access_token: YOUR_API_TOKEN'
```
**Sort by name (ascending):**
```bash
curl -X GET 'https://app.chatwoot.com/api/v1/accounts/{account_id}/companies?sort=name' \
-H 'api_access_token: YOUR_API_TOKEN'
```
**Sort by created_at (descending):**
```bash
curl -X GET 'https://app.chatwoot.com/api/v1/accounts/{account_id}/companies?sort=-created_at' \
-H 'api_access_token: YOUR_API_TOKEN'
```
**With pagination:**
```bash
curl -X GET 'https://app.chatwoot.com/api/v1/accounts/{account_id}/companies?sort=-contacts_count&page=2' \
-H 'api_access_token: YOUR_API_TOKEN'
```
## How Has This Been Tested?
- Added RSpec tests for both ascending and descending sort
- All 24 existing specs pass
- Manually tested the sorting functionality with test data
**Test configuration:**
- Ruby 3.4.4
- Rails 7.1.5.2
- PostgreSQL (test database)
**To reproduce:**
1. Run `bundle exec rspec
spec/enterprise/controllers/api/v1/accounts/companies_controller_spec.rb`
2. All tests should pass (24 examples, 0 failures)
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
## Technical Details
**Backend changes:**
- Controller: Added `sort_on :contacts_count` with scope-based sorting
- Model: Added `order_on_contacts_count` scope using
`Arel::Nodes::SqlLiteral` and `sanitize_sql_for_order` with `NULLS LAST`
for consistent NULL handling
- Specs: Added 2 new tests for ascending/descending sort validation
**Files changed:**
- `enterprise/app/controllers/api/v1/accounts/companies_controller.rb`
- `enterprise/app/models/company.rb`
-
`spec/enterprise/controllers/api/v1/accounts/companies_controller_spec.rb`
**Note:** This PR only includes the backend implementation. Frontend
changes (sort menu UI + i18n) will follow in a separate commit.
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
Migrates our analytics integration on Cloud from PostHog to Amplitude.
This change updates the core AnalyticsHelper class to use the Amplitude
SDK while maintaining the same tracking interface. Rest of all existing
analytics calls throughout the codebase continue to work without
modification.
**Changes:**
- Replace PostHog analytics with Amplitude SDK
- Rename ANALYTICS_TOKEN to CLOUD_ANALYTICS_TOKEN for clarity
- Fix bug in page() method signature that was causing malformed payloads
This PR fixes the HTTP 500 timeout errors occurring when deleting SLA
policies that have large volumes of historical data.
The fix moves the deletion workflow to asynchronous background
processing using the existing `DeleteObjectJob`.
By offloading heavy cascaded deletions (applied SLAs, SLA events,
conversation nullifications) from the request cycle, the API can now
return immediately while the cleanup continues in the background
avoiding the `Rack::Timeout::RequestTimeoutException`. This ensures that
SLA policies can be deleted reliably, regardless of data size.
### Problem
Deleting an SLA policy via `DELETE
/api/v1/accounts/{account_id}/sla_policies/{id}` fails consistently with
`Rack::Timeout::RequestTimeoutException (15s)` for policies with large
amounts of related data.
Because the current implementation performs all dependent deletions
**synchronously**, Rails processes:
- `has_many :applied_slas, dependent: :destroy` (thousands)
- Each `AppliedSla#destroy` → triggers destruction of many `SlaEvent`
records
- `has_many :conversations, dependent: :nullify` (thousands)
This processing far exceeds the Rack timeout window and consistently
triggers HTTP 500 errors for users.
### Solution
This PR applies the same pattern used successfully in Inbox deletion.
**Move deletion to async background jobs**
- Uses `DeleteObjectJob` for centralized, reliable cleanup.
- Allows the DELETE API call to respond immediately.
**Chunk large datasets**
- Records are processed in **batches of 5,000** to reduce DB load and
avoid job timeouts.
## Description
- Replaces Stripe Checkout session flow with direct card charging for AI
credit top-ups
- Adds a two-step confirmation modal (select package → confirm purchase)
for better UX
- Creates Stripe invoice directly and charges the customer's default
payment method immediately
## Type of change
- [ ] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
- Using the specs
- UI manual test cases
<img width="945" height="580" alt="image"
src="https://github.com/user-attachments/assets/52bdad46-cd0e-4927-b13f-54c6b6353bcc"
/>
<img width="945" height="580" alt="image"
src="https://github.com/user-attachments/assets/231bc7e9-41ac-440d-a93d-cba45a4d3e3e"
/>
## Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
This PR adds LLM instrumentation on langfuse for ai-editor feature
## Type of change
New feature (non-breaking change which adds functionality)
Needs langfuse account and env vars to be set
## How Has This Been Tested?
I configured personal langfuse credentials and instrumented the app,
traces can be seen in langfuse.
each conversation is one session.
<img width="1683" height="714" alt="image"
src="https://github.com/user-attachments/assets/3fcba1c9-63cf-44b9-a355-fd6608691559"
/>
<img width="1446" height="172" alt="image"
src="https://github.com/user-attachments/assets/dfa6e98f-4741-4e04-9a9e-078d1f01e97b"
/>
## Checklist:
- [x ] My code follows the style guidelines of this project
- [ x] I have performed a self-review of my code
- [ x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: aakashb95 <aakash@chatwoot.com>
Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
## Description
Adds API endpoint to list companies with pagination, search, and
sorting.
Fixes
https://linear.app/chatwoot/issue/CW-5930/add-backend-routes-to-get-companies-result
Parent issue:
https://linear.app/chatwoot/issue/CW-5928/add-companies-tab-to-dashboard
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
Added comprehensive specs to
`spec/enterprise/controllers/api/v1/accounts/companies_controller_spec.rb`:
- Pagination (25 per page, multiple pages)
- Search by name and domain (case-insensitive)
- Counter cache for contacts_count
- Account scoping
- Authorization
To reproduce:
```bash
bundle exec rspec spec/enterprise/controllers/api/v1/accounts/companies_controller_spec.rb
bundle exec rubocop enterprise/app/controllers/api/v1/accounts/companies_controller.rb
```
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
# Pull Request Template
## Description
* add Company model with validations for name, domain, description and
avatar
* Add database migration fo
* Implement endpoints for company CRUD operations
* Add optional company relationship for contacts
* Add test for models, controllers, factories and policies
* Add authorization policies restricting delete to admins
* support JSON API responses
Please include a summary of the change and issue(s) fixed. Also, mention
relevant motivation, context, and any dependencies that this change
requires.
Fixes #(cw-5650)
## Type of change
Please delete options that are not relevant.
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update
## How Has This Been Tested?
Tests are implemented using `RSpec`
```
$ bundle exec rails db:migrate
$ bundle exec rspec spec/models/company_spec.rb spec/controllers/api/v1/accounts/companies_controller_spec.rb
```
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
## Description
This fixes an oversight from my previous PR #12120 where I forgot to add
the config key to the enterprise config controller, leading to the
configuration not being showed in the super admin panel.
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
I believe this is the correct code change but haven't ran a full test
due to hardware constraints. It would be ideal if you could perform a
quick check on this.
## 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
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules