Files
sure/app/controllers/settings/preferences_controller.rb
Guillem Arias 7fdc205f25 feat(modules): v1 feature-module gating + Investments
Exploration spike following expert-panel synthesis. Codifies the 3-tier
gating model (instance / per-user / per-family) without adding a
framework. Investments is the first family-scoped module.

- Family#disabled_modules: string[] column, opt-out semantics. No
  per-module DB column. Existing families default to [] = enabled.
- Family::AVAILABLE_MODULES = %w[investments] (the registry).
- ModuleGateable concern (auto-included in ApplicationController):
  require_module! class macro + module_enabled? helper.
- Api::V1::BaseController#require_module!: 403 feature_disabled JSON,
  mirrors require_ai_enabled.
- NavigationHelper extracts mobile/desktop nav into a single source
  with module: key support; mobile shrinks via justify-around, never
  auto-fills empty slots.
- Settings → Preferences gains a family-scoped module toggle card.
- 4 HTML controllers (Investments, Holdings, Trades, Securities) +
  3 API controllers gated. Investment/Crypto account types hidden in
  the new-account modal when off.
- docs/feature-gating.md codifies the rule for future modules.

Background-job layer not wired (no Investments-specific scheduled job
to gate; flagged as TODO in docs). Run db:migrate before bin/rails
test. No PR yet — awaiting decisions in open-questions list.
2026-05-22 15:01:57 +02:00

45 lines
1.5 KiB
Ruby

class Settings::PreferencesController < ApplicationController
layout "settings"
def show
@user = Current.user
@family = Current.family
end
# Writes per-user boolean preferences stored in the JSONB `users.preferences`
# column, plus per-family module toggles in `families.disabled_modules`. The
# auto-submit pattern matches Settings::AppearancesController#update.
def update
@user = Current.user
user_params = params.permit(user: [ :preview_features_enabled ]).fetch(:user, {})
module_params = params.permit(family: { modules: {} }).dig(:family, :modules)
ActiveRecord::Base.transaction do
@user.lock!
updated_prefs = (@user.preferences || {}).deep_dup
if user_params.key?(:preview_features_enabled)
updated_prefs["preview_features_enabled"] =
ActiveModel::Type::Boolean.new.cast(user_params[:preview_features_enabled])
end
@user.update!(preferences: updated_prefs)
if module_params.present?
family = Current.family
family.lock!
disabled = Array(family.disabled_modules).map(&:to_s)
module_params.each do |name, enabled|
name = name.to_s
next unless Family::AVAILABLE_MODULES.include?(name)
if ActiveModel::Type::Boolean.new.cast(enabled)
disabled.delete(name)
else
disabled << name unless disabled.include?(name)
end
end
family.update!(disabled_modules: disabled)
end
end
redirect_to settings_preferences_path
end
end