Files
sure/app/controllers/api/v1/securities_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

62 lines
1.7 KiB
Ruby

# frozen_string_literal: true
class Api::V1::SecuritiesController < Api::V1::BaseController
include Pagy::Backend
include Api::V1::SecurityResourceFiltering
before_action -> { require_module!(:investments) }
before_action :ensure_read_scope
before_action :set_security, only: :show
def index
securities_query = apply_filters(securities_scope).order(:ticker, :exchange_operating_mic, :name)
@per_page = safe_per_page_param
@pagy, @securities = pagy(
securities_query,
page: safe_page_param,
limit: @per_page
)
render :index
rescue Api::V1::SecurityResourceFiltering::InvalidFilterError => e
render_validation_error(e.message)
end
def show
render :show
end
private
def set_security
raise ActiveRecord::RecordNotFound, "Security not found" unless valid_uuid?(params[:id])
@security = securities_scope.find(params[:id])
end
def ensure_read_scope
authorize_scope!(:read)
end
def securities_scope
Security
.where(id: scoped_security_ids)
end
def apply_filters(query)
query = query.where("LOWER(securities.ticker) = ?", params[:ticker].to_s.strip.downcase) if params[:ticker].present?
query = query.where(exchange_operating_mic: params[:exchange_operating_mic].to_s.strip.upcase) if params[:exchange_operating_mic].present?
if params[:kind].present?
invalid_filter!("kind must be one of: #{Security::KINDS.join(', ')}") unless Security::KINDS.include?(params[:kind])
query = query.where(kind: params[:kind])
end
if params.key?(:offline)
offline = parse_boolean_filter_param(:offline)
query = query.where(offline: offline)
end
query
end
end