chore: improve plan-based feature handling with plan hierarchy (#11335)

- Refactor HandleStripeEventService to better manage features by plan
- Add constants for features available in each plan tier (Startup,
Business, Enterprise)
- Add channel_instagram to Startup plan features
- Improve downgrade handling to properly disable higher-tier features
- Clean up and optimize tests for maintainability
- Add comprehensive test coverage for plan upgrades and downgrades

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
Sojan Jose
2025-04-28 14:13:56 -07:00
committed by GitHub
parent ef6949e32d
commit c63b583f90
26 changed files with 742 additions and 144 deletions

View File

@@ -66,3 +66,5 @@ class SuperAdmin::AccountsController < SuperAdmin::ApplicationController
# rubocop:enable Rails/I18nLocaleTexts
end
end
SuperAdmin::AccountsController.prepend_mod_with('SuperAdmin::AccountsController')

View File

@@ -9,10 +9,17 @@ class AccountDashboard < Administrate::BaseDashboard
# on pages throughout the dashboard.
enterprise_attribute_types = if ChatwootApp.enterprise?
{
limits: Enterprise::AccountLimitsField,
all_features: Enterprise::AccountFeaturesField
attributes = {
limits: AccountLimitsField
}
# Only show manually managed features in Chatwoot Cloud deployment
attributes[:manually_managed_features] = ManuallyManagedFeaturesField if ChatwootApp.chatwoot_cloud?
# Add all_features last so it appears after manually_managed_features
attributes[:all_features] = AccountFeaturesField
attributes
else
{}
end
@@ -46,7 +53,14 @@ class AccountDashboard < Administrate::BaseDashboard
# SHOW_PAGE_ATTRIBUTES
# an array of attributes that will be displayed on the model's show page.
enterprise_show_page_attributes = ChatwootApp.enterprise? ? %i[custom_attributes limits all_features] : []
enterprise_show_page_attributes = if ChatwootApp.enterprise?
attrs = %i[custom_attributes limits]
attrs << :manually_managed_features if ChatwootApp.chatwoot_cloud?
attrs << :all_features
attrs
else
[]
end
SHOW_PAGE_ATTRIBUTES = (%i[
id
name
@@ -61,7 +75,14 @@ class AccountDashboard < Administrate::BaseDashboard
# FORM_ATTRIBUTES
# an array of attributes that will be displayed
# on the model's form (`new` and `edit`) pages.
enterprise_form_attributes = ChatwootApp.enterprise? ? %i[limits all_features] : []
enterprise_form_attributes = if ChatwootApp.enterprise?
attrs = %i[limits]
attrs << :manually_managed_features if ChatwootApp.chatwoot_cloud?
attrs << :all_features
attrs
else
[]
end
FORM_ATTRIBUTES = (%i[
name
locale
@@ -96,6 +117,11 @@ class AccountDashboard < Administrate::BaseDashboard
# to prevent an error from being raised (wrong number of arguments)
# Reference: https://github.com/thoughtbot/administrate/pull/2356/files#diff-4e220b661b88f9a19ac527c50d6f1577ef6ab7b0bed2bfdf048e22e6bfa74a05R204
def permitted_attributes(action)
super + [limits: {}]
attrs = super + [limits: {}]
# Add manually_managed_features to permitted attributes only for Chatwoot Cloud
attrs << { manually_managed_features: [] } if ChatwootApp.chatwoot_cloud?
attrs
end
end

View File

@@ -1,7 +0,0 @@
require 'administrate/field/base'
class Enterprise::AccountFeaturesField < Administrate::Field::Base
def to_s
data
end
end

View File

@@ -1,7 +0,0 @@
require 'administrate/field/base'
class Enterprise::AccountLimitsField < Administrate::Field::Base
def to_s
data.present? ? data.to_json : { agents: nil, inboxes: nil, captain_responses: nil, captain_documents: nil }.to_json
end
end

View File

@@ -15,7 +15,7 @@ module SuperAdmin::AccountFeaturesHelper
end
def self.filter_internal_features(features)
return features if GlobalConfig.get_value('DEPLOYMENT_ENV') == 'cloud'
return features if ChatwootApp.chatwoot_cloud?
internal_features = account_features.select { |f| f['chatwoot_internal'] }.pluck('name')
features.except(*internal_features)

View File

@@ -1,34 +0,0 @@
<div class="field-unit__label">
<%= f.label field.attribute %>
</div>
<div class="field-unit__field feature-container">
<% regular_features, premium_features = SuperAdmin::AccountFeaturesHelper.filtered_features(field.data).partition { |key_array, _val| !SuperAdmin::AccountFeaturesHelper.account_premium_features.include?(key_array.first) } %>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<% regular_features.each do |key_array, val| %>
<% feature_key, display_name = key_array %>
<div class="flex items-center justify-between p-3 bg-white rounded-lg shadow-sm outline outline-1 outline-n-container">
<span class="text-sm text-slate-700"><%= display_name %></span>
<span><%= check_box "enabled_features", "feature_#{feature_key}", { checked: val, class: "h-4 w-4 rounded border-slate-300 text-indigo-600 focus:ring-indigo-600" }, true, false %></span>
</div>
<% end %>
</div>
<hr class="my-8 boshadow-sm outline outline-1 outline-n-container">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<% premium_features.each do |key_array, val| %>
<% feature_key, display_name = key_array %>
<div class="flex items-center justify-between p-3 bg-white rounded-lg shadow-sm outline outline-1 outline-n-container">
<div class="flex items-center gap-2">
<span class="text-amber-500">
<svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M480 224l-186.828 7.487L401.688 64l-59.247-32L256 208 169.824 32l-59.496 32 108.5 167.487L32 224v64l185.537-10.066L113.65 448l55.969 32L256 304l86.381 176 55.949-32-103.867-170.066L480 288z" fill="currentColor"/></svg>
</span>
<span class="text-sm text-slate-700"><%= display_name %></span>
</div>
<% should_disable = ChatwootHub.pricing_plan == 'community' %>
<span><%= check_box "enabled_features", "feature_#{feature_key}", { checked: val, disabled: should_disable, class: "h-4 w-4 rounded border-slate-300 text-indigo-600 focus:ring-indigo-600" }, true, false %></span>
</div>
<% end %>
</div>
</div>

View File

@@ -1,34 +0,0 @@
<div class="w-full">
<% regular_features, premium_features = SuperAdmin::AccountFeaturesHelper.partition_features(field.data) %>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<% regular_features.each do |key_array, val| %>
<% feature_key, display_name = key_array %>
<div class="flex items-center justify-between p-3 bg-white rounded-md outline outline-n-container outline-1 shadow-sm">
<span class="text-sm text-n-slate-12"><%= display_name %></span>
<span class="<%= val.present? ? 'bg-green-400 text-white': 'bg-slate-50 text-slate-800' %> rounded-full p-1 inline-flex right-4 top-5">
<svg width="12" height="12"><use xlink:href="#icon-tick-line" /></svg>
</span>
</div>
<% end %>
</div>
<hr class="my-8 border-t border-n-weak">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<% premium_features.each do |key_array, val| %>
<% feature_key, display_name = key_array %>
<div class="flex items-center justify-between p-3 bg-white rounded-md outline outline-n-container outline-1 shadow-sm">
<div class="flex items-center gap-2">
<span class="bg-n-amber-3 text-n-amber-12 rounded-full p-1 inline-flex right-4 top-5">
<svg width="12" height="12"><use xlink:href="#icon-lock-line" /></svg>
</span>
<span class="text-sm text-n-slate-12"><%= display_name %></span>
</div>
<span class="<%= val.present? ? 'bg-green-400 text-white': 'bg-slate-50 text-slate-800' %> rounded-full p-1 inline-flex right-4 top-5">
<svg width="12" height="12"><use xlink:href="#icon-tick-line" /></svg>
</span>
</div>
<% end %>
</div>
</div>

View File

@@ -1,9 +0,0 @@
<div class="field-unit__label">
<%= f.label field.attribute %>
</div>
<div class="field-unit__field">
<% JSON.parse(field.to_s).each do |key,val| %>
<%= key %>: <%= number_field "account[limits]", key, value: val %> </br>
<% end %>
</div>

View File

@@ -1 +0,0 @@
<%= field.to_s %>

View File

@@ -1,3 +0,0 @@
<% JSON.parse(field.to_s).each do |k,v| %>
<%= k %>: <%= v %> </br>
<% end %>