class Settings::ProvidersController < ApplicationController layout "settings" guard_feature unless: -> { self_hosted? } before_action :ensure_admin, only: [ :show, :update ] def show @breadcrumbs = [ [ "Home", root_path ], [ "Bank Sync Providers", nil ] ] # Load all provider configurations Provider::Factory.ensure_adapters_loaded @provider_configurations = Provider::ConfigurationRegistry.all end def update # Build index of valid configurable fields with their metadata Provider::Factory.ensure_adapters_loaded valid_fields = {} Provider::ConfigurationRegistry.all.each do |config| config.fields.each do |field| valid_fields[field.setting_key.to_s] = field end end updated_fields = [] # This hash will store only the updates for dynamic (non-declared) fields dynamic_updates = {} # Perform all updates within a transaction for consistency Setting.transaction do provider_params.each do |param_key, param_value| # Only process keys that exist in the configuration registry field = valid_fields[param_key.to_s] next unless field # Clean the value and convert blank/empty strings to nil value = param_value.to_s.strip value = nil if value.empty? # For secret fields only, skip placeholder values to prevent accidental overwrite if field.secret && value == "********" next end key_str = field.setting_key.to_s # Check if the setting is a declared field in setting.rb # Use method_defined? to check if the setter actually exists on the singleton class, # not just respond_to? which returns true for dynamic fields due to respond_to_missing? if Setting.singleton_class.method_defined?("#{key_str}=") # If it's a declared field (e.g., openai_model), set it directly. # This is safe and uses the proper setter. Setting.public_send("#{key_str}=", value) else # If it's a dynamic field, add it to our batch hash # to avoid the Read-Modify-Write conflict. dynamic_updates[key_str] = value end updated_fields << param_key end # Now, if we have any dynamic updates, apply them all at once if dynamic_updates.any? # 1. READ the current hash once current_dynamic = Setting.dynamic_fields.dup # 2. MODIFY by merging changes # Treat nil values as deletions to keep the hash clean dynamic_updates.each do |key, value| if value.nil? current_dynamic.delete(key) else current_dynamic[key] = value end end # 3. WRITE the complete, merged hash back once Setting.dynamic_fields = current_dynamic end end if updated_fields.any? # Reload provider configurations if needed reload_provider_configs(updated_fields) redirect_to settings_providers_path, notice: "Provider settings updated successfully" else redirect_to settings_providers_path, notice: "No changes were made" end rescue => error Rails.logger.error("Failed to update provider settings: #{error.message}") flash.now[:alert] = "Failed to update provider settings: #{error.message}" # Set @provider_configurations so the view can render properly Provider::Factory.ensure_adapters_loaded @provider_configurations = Provider::ConfigurationRegistry.all render :show, status: :unprocessable_entity end private def provider_params # Dynamically permit all provider configuration fields Provider::Factory.ensure_adapters_loaded permitted_fields = [] Provider::ConfigurationRegistry.all.each do |config| config.fields.each do |field| permitted_fields << field.setting_key end end params.require(:setting).permit(*permitted_fields) end def ensure_admin redirect_to settings_providers_path, alert: "Not authorized" unless Current.user.admin? end # Reload provider configurations after settings update def reload_provider_configs(updated_fields) # Build a set of provider keys that had fields updated updated_provider_keys = Set.new # Look up the provider key directly from the configuration registry updated_fields.each do |field_key| Provider::ConfigurationRegistry.all.each do |config| field = config.fields.find { |f| f.setting_key.to_s == field_key.to_s } if field updated_provider_keys.add(field.provider_key) break end end end # Reload configuration for each updated provider updated_provider_keys.each do |provider_key| adapter_class = Provider::ConfigurationRegistry.get_adapter_class(provider_key) adapter_class&.reload_configuration end end end