This keeps Chatwoot-side attribution persistence intentionally small and Enterprise-only. The website owns attribution capture, normalization, and source classification. Chatwoot Cloud only reads the already-shaped first-party attribution cookies during the current web signup account creation path and stores the decoded payload in internal account metadata. Self-hosted installs remain unchanged in this repo. ## What changed - Added Enterprise-only attribution persistence to the current Cloud web signup account creation path. - Stores attribution only when `ChatwootApp.chatwoot_cloud?` is true. - Reads the existing first-touch and last-touch attribution cookies. - Saves only the documented scalar attribution fields under account internal metadata. - Preserves raw attribution values and leaves escaping to display boundaries. - Bounds stored attribution values to the website field-size limit. - Keeps OSS controller code unchanged. - Keeps signup attribution request coverage in Enterprise specs. - Avoids backend attribution derivation, referrer parsing, or fallback classification. - Skips authenticated add-workspace flows so additional workspaces are not counted as signup attribution. - Does not hook unused account creation paths or OmniAuth account creation. ## How to test - `bundle exec rubocop spec/controllers/api/v1/accounts_controller_spec.rb spec/enterprise/controllers/api/v1/accounts_controller_spec.rb enterprise/app/controllers/enterprise/api/v1/accounts_settings.rb enterprise/app/services/internal/accounts/marketing_attribution_service.rb spec/enterprise/services/internal/accounts/marketing_attribution_service_spec.rb` - `bundle exec rspec spec/controllers/api/v1/accounts_controller_spec.rb spec/enterprise/controllers/api/v1/accounts_controller_spec.rb spec/enterprise/services/internal/accounts/marketing_attribution_service_spec.rb` - On a cloud-like setup, create an account through the current web signup path with attribution cookies and confirm account internal metadata is populated. - On a non-cloud setup or authenticated add-workspace flow, confirm account-create behavior is unchanged and no attribution is stored.
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]
|
|
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
|