feat: scale email rate limits by agent count for paid plans (#13966)

Paid plan accounts with many agents were hitting the flat daily email
rate limit cap.
This multiplies the plan base limit by the account's agent seat count,
so larger teams
get proportionally higher limits. Free/hacker plan keeps the flat limit
unchanged.

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

## How to test
1. Set up `ACCOUNT_EMAILS_PLAN_LIMITS` config with plan limits (e.g.,
`{"hacker": 10, "startups": 20, "business": 30, "enterprise": 40}`)
2. Create an account on a paid plan (e.g., startups) with multiple agent
seats
3. Verify `account.email_rate_limit` returns `base_limit × agent_seats`
(e.g., 20 × 5 = 100)
4. Create an account on the hacker plan — verify limit stays flat (10)
5. Set a per-account override via super admin `limits.emails` — verify
it takes priority over the multiplied limit

## What changed
- `plan_email_limit` now multiplies the base plan limit by agent seat
count for paid plans
- Added `free_plan?` helper to skip the multiplier for the default/free
plan
This commit is contained in:
Vishnu Narayanan
2026-06-16 18:56:30 +05:30
committed by GitHub
parent 41a3ab6dfa
commit cf6ee13665
@@ -75,6 +75,14 @@ module Enterprise::Account::PlanUsageAndLimits # rubocop:disable Metrics/ModuleL
end
def plan_email_limit
base_limit = plan_base_email_limit
return nil if base_limit.nil?
return base_limit if free_plan?
base_limit * [agent_limits.to_i, 1].max
end
def plan_base_email_limit
config = InstallationConfig.find_by(name: 'ACCOUNT_EMAILS_PLAN_LIMITS')&.value
return nil if config.blank? || plan_name.blank?
@@ -84,6 +92,11 @@ module Enterprise::Account::PlanUsageAndLimits # rubocop:disable Metrics/ModuleL
nil
end
def free_plan?
default_plan = InstallationConfig.find_by(name: 'CHATWOOT_CLOUD_PLANS')&.value&.first
default_plan.present? && plan_name&.downcase == default_plan['name']&.downcase
end
def default_captain_limits
max_limits = { documents: ChatwootApp.max_limit, responses: ChatwootApp.max_limit }.with_indifferent_access
zero_limits = { documents: 0, responses: 0 }.with_indifferent_access