Files
chatwoot/enterprise/app/services/internal/accounts/internal_attributes_service.rb
Sojan JoseandGitHub dfd656a7d9 feat(account): persist signup attribution (#14761)
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.
2026-06-19 11:01:12 -07:00

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