This introduces a new `api_and_webhooks` account feature flag that will
control access to the token-authenticated API and account webhooks. The
flag is part of the Startup plan features, so paid plans — including
trials of paid plans — get it through the billing reconcile, while
accounts on the default (Hacker) plan don't, with
`manually_managed_features` available as a per-account override. The
flag defaults to enabled, and nothing enforces it yet, so this PR is
behavior-neutral — enforcement lands in a follow-up.
## What changed
- Added `api_and_webhooks` to `features.yml` (first flag on the
`feature_flags_ext_1` column, default enabled).
- Added the flag to `STARTUP_PLAN_FEATURES` in
`Enterprise::Billing::ReconcilePlanFeaturesService`, so all paid tiers
get it and the default plan loses it on reconcile.
- Added the flag to the manually manageable features list so it can be
granted per account via Super Admin.
```rb
# Enables the api_and_webhooks feature for all existing accounts and marks it
# as manually managed so cloud billing reconciles never strip it.
#
# NOT committed to source control — run manually on production.
#
# Usage:
# bundle exec rails runner enable_api_and_webhooks.rb
# ACCOUNT_ID=123 bundle exec rails runner enable_api_and_webhooks.rb
#
# Idempotent: accounts already grandfathered are skipped; safe to re-run.
probe = Internal::Accounts::InternalAttributesService.new(Account.new)
abort 'api_and_webhooks is not in valid_feature_list — deploy the feature flag PR first.' unless probe.valid_feature_list.include?('api_and_webhooks')
account_id = ENV.fetch('ACCOUNT_ID', nil)
accounts = account_id.present? ? Account.where(id: account_id) : Account.all
abort "Account with ID #{account_id} not found" if account_id.present? && accounts.empty?
total = accounts.count
puts "Grandfathering api_and_webhooks for #{total} account(s)..."
puts "Started at: #{Time.current}"
updated = 0
skipped = 0
errored = 0
accounts.find_each(batch_size: 500) do |account|
service = Internal::Accounts::InternalAttributesService.new(account)
features = service.manually_managed_features
if features.include?('api_and_webhooks') && account.feature_enabled?('api_and_webhooks')
skipped += 1
else
service.manually_managed_features = features + ['api_and_webhooks'] unless features.include?('api_and_webhooks')
account.enable_features!('api_and_webhooks')
updated += 1
end
processed = updated + skipped + errored
puts "Processed #{processed}/#{total}..." if (processed % 1000).zero?
rescue StandardError => e
errored += 1
puts "Account #{account.id}: FAILED - #{e.message}"
end
puts "Done! Updated: #{updated}, Skipped: #{skipped}, Errored: #{errored}, Total: #{total}"
```
69 lines
2.0 KiB
Ruby
69 lines
2.0 KiB
Ruby
class Internal::Accounts::InternalAttributesService
|
|
attr_reader :account
|
|
|
|
# List of keys that can be managed through this service
|
|
# TODO: Add account_notes field in future
|
|
# This field can be used to store notes about account on Chatwoot cloud
|
|
VALID_KEYS = %w[manually_managed_features marketing_attribution].freeze
|
|
|
|
def initialize(account)
|
|
@account = account
|
|
end
|
|
|
|
# Get a value from internal_attributes
|
|
def get(key)
|
|
validate_key!(key)
|
|
account.internal_attributes[key]
|
|
end
|
|
|
|
# Set a value in internal_attributes
|
|
def set(key, value)
|
|
validate_key!(key)
|
|
|
|
# Create a new hash to avoid modifying the original
|
|
new_attrs = account.internal_attributes.dup || {}
|
|
new_attrs[key] = value
|
|
|
|
# Update the account
|
|
account.internal_attributes = new_attrs
|
|
account.save
|
|
end
|
|
|
|
# Get manually managed features
|
|
def manually_managed_features
|
|
get('manually_managed_features') || []
|
|
end
|
|
|
|
# Set manually managed features
|
|
def manually_managed_features=(features)
|
|
features = [] if features.nil?
|
|
features = [features] unless features.is_a?(Array)
|
|
|
|
# Clean up the array: remove empty strings, whitespace, and validate against valid features
|
|
valid_features = valid_feature_list
|
|
features = features.compact
|
|
.map(&:strip)
|
|
.reject(&:empty?)
|
|
.select { |f| valid_features.include?(f) }
|
|
.uniq
|
|
|
|
set('manually_managed_features', features)
|
|
end
|
|
|
|
# Get list of valid features that can be manually managed
|
|
def valid_feature_list
|
|
Enterprise::Billing::ReconcilePlanFeaturesService::BUSINESS_PLAN_FEATURES +
|
|
Enterprise::Billing::ReconcilePlanFeaturesService::ENTERPRISE_PLAN_FEATURES +
|
|
%w[inbound_emails api_and_webhooks]
|
|
end
|
|
|
|
# Account notes functionality removed for now
|
|
# Will be re-implemented when UI is ready
|
|
|
|
private
|
|
|
|
def validate_key!(key)
|
|
raise ArgumentError, "Invalid internal attribute key: #{key}" unless VALID_KEYS.include?(key)
|
|
end
|
|
end
|