Skip to content

Conversation

@sokie
Copy link
Collaborator

@sokie sokie commented Oct 28, 2025

My idea is to have a module for providers to declare their configuration requirements.
It supports any number of fields and they will automatically show in UI.
Example below with Plaid and SimpleFIN

When you add a new provider (e.g., a Stripe adapter):

  class Provider::StripeAdapter < Provider::Base
    include Provider::Configurable

    configure do
      field :api_key,
            label: "API Key",
            required: true,
            secret: true,
            env_key: "STRIPE_API_KEY",
            description: "Your Stripe API key"
    end
  end

It will automatically appear in the UI with:

  • A section titled "Stripe"
  • Form fields for all configured fields
  • Proper secret masking
  • ENV variable override support
  • Auto-save on blur

Some assumptions:

  • Set values via ENV variables (for Docker/deployment)
  • Override them in the UI (for quick testing or changes)
  • Settings saved in the UI take precedence over ENV variables

Screenshots:

Screenshot 2025-10-28 at 20 40 56 Screenshot 2025-10-28 at 20 41 01

Summary by CodeRabbit

  • New Features

    • Admin-only "Providers" settings page and nav item to view and update bank-sync provider credentials with masked secrets, env-var hints, and configuration status.
    • Provider configuration support added for Plaid (US and EU), and SimpleFIN (optional setup token); env-var overrides and live reload of provider configs after save and at startup.
    • Dynamic storage for ad-hoc provider settings accessible via bracket-style access.
  • Tests

    • Scoped environment handling improved for provider registry tests.

@sokie sokie marked this pull request as draft October 28, 2025 20:11
@coderabbitai
Copy link

coderabbitai bot commented Oct 28, 2025

Walkthrough

Adds a provider configuration DSL and registry, dynamic Setting storage for runtime credentials, adapter autoloading and reload hooks, a new admin Settings::ProvidersController with routes and views to manage provider credentials, and adapter configuration updates for Plaid, Plaid EU, and SimpleFIN.

Changes

Cohort / File(s) Summary
Configuration DSL & Registry
app/models/provider/configurable.rb
New Provider::Configurable DSL: Configuration and ConfigField classes, value resolution (Setting → ENV → default), validation, and Provider::ConfigurationRegistry for registering and listing provider configurations and adapter classes.
Dynamic Settings storage
app/models/setting.rb
Adds dynamic_fields hash and bracket-style accessors/mutators ([], []=, key?, delete, dynamic_keys) to store and manage runtime/undeclared settings.
Provider Adapters
app/models/provider/plaid_adapter.rb, app/models/provider/plaid_eu_adapter.rb, app/models/provider/simplefin_adapter.rb
Adapters include Provider::Configurable. Plaid (US) and Plaid EU: new configurable fields (client_id, secret, environment) and reload_configuration to populate Rails.application.config.plaid / plaid_eu. SimpleFIN: optional secret setup_token field.
Adapter Factory & Loading
app/models/provider/factory.rb
AdapterNotFoundError, lazy adapter loading (ensure_adapters_loaded, adapter_files), registry management changes, registered?, clear_registry!, and adapter lookup refactor (find_adapter_class).
Admin Controller & Routing
app/controllers/settings/providers_controller.rb, config/routes.rb
New Settings::ProvidersController with show and update (admin-protected): dynamic strong-param building, trims values, ignores secret placeholders, persists via Setting[...] = ..., reloads affected adapter configurations; route resource :providers, only: %i[show update].
Admin UI Views
app/views/settings/providers/show.html.erb, app/views/settings/providers/_provider_form.html.erb, app/views/settings/_settings_nav.html.erb
New providers listing page and form partial (masks secrets, shows ENV overrides, descriptions) and added "Providers" nav item in admin settings.
Initializer
config/initializers/plaid.rb
Initialize config.plaid and config.plaid_eu to nil, add after_initialize hook to ensure adapters are loaded, and call adapter reload_configuration methods.
Tests
test/models/provider/registry_test.rb
Replace direct ENV stubbing with ClimateControl.modify for scoped ENV changes; assertions unchanged.

Sequence Diagram(s)

sequenceDiagram
    participant Admin as Admin User
    participant Browser as Browser/UI
    participant Controller as Settings::ProvidersController
    participant Registry as Provider::ConfigurationRegistry
    participant Factory as Provider::Factory
    participant Setting as Setting
    participant Adapter as Provider Adapter

    Admin->>Browser: GET /settings/providers
    Browser->>Controller: request
    Controller->>Factory: ensure_adapters_loaded()
    Factory->>Registry: trigger adapter registrations (autoload)
    Controller->>Registry: all() -> provider_configurations
    Controller->>Browser: render provider forms

    Admin->>Browser: PATCH /settings/providers (params)
    Browser->>Controller: submit params
    Controller->>Controller: sanitize & permit fields (trim, ignore "********")
    Controller->>Setting: Setting[key] = value (for changed keys)
    Controller->>Controller: collect updated fields
    Controller->>Factory: determine affected adapters
    Controller->>Adapter: call Adapter.reload_configuration for affected adapters
    Adapter->>Setting: read config values (Setting -> ENV -> default)
    Adapter->>Adapter: apply runtime config (e.g., Rails.application.config.plaid)
    Controller->>Browser: redirect with notice / or render 422 on error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Focus review on:
    • Provider::Configurable: setting_key generation, value resolution order, secret handling, and validation.
    • Setting dynamic storage: precedence between declared fields and dynamic_fields, persistence and deletion behavior.
    • Provider::Factory: adapter discovery (adapter_files), ensure_adapters_loaded behavior, and new error AdapterNotFoundError.
    • Settings::ProvidersController: dynamic strong params, secret-placeholder filtering, and reload_provider_configs side effects.
    • Plaid/ PlaidEu reload_configuration: correct initialization of Plaid client config and handling of missing credentials.

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • jjmata

Poem

🐰
I hopped through configs late at night,
stitched secrets, ENV, and settings tight.
Adapters wake with fresh new cheer,
Providers hum — the sync is near! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.38% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "Add support for dynamic config UI" directly relates to the main objective of the changeset, which introduces a Provider::Configurable module and automatic UI exposure of provider configuration fields. The title is concise at six words, avoids noise and vague terminology, and clearly indicates that UI support is being added for dynamic configuration. While the term "dynamic config" could theoretically refer to multiple contexts, it is sufficiently specific within the provider/settings domain and would enable a teammate scanning the commit history to understand the primary change involves adding configuration UI capabilities.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

sokie added 2 commits October 29, 2025 09:03
Added dynamic_fields hash field - Stores all undeclared settings
[] method - Checks declared fields first, then falls back to dynamic hash
[]= method - Updates declared fields normally, stores others in hash
No runtime field declaration - Fields are never dynamically created on the class
@sokie sokie marked this pull request as ready for review October 29, 2025 08:39
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (8)
app/models/provider/configurable.rb (1)

132-143: Consider validating field name uniqueness.

The field method doesn't prevent duplicate field names. While unlikely in practice (developers control the DSL), duplicate names would cause get_value to return only the first match.

Add validation if you want to catch configuration errors early:

 def field(name, label:, required: false, secret: false, env_key: nil, default: nil, description: nil)
+  if @fields.any? { |f| f.name == name }
+    raise ArgumentError, "Field '#{name}' is already defined for provider '#{@provider_key}'"
+  end
+
   @fields << ConfigField.new(
     name: name,
app/models/provider/plaid_adapter.rb (1)

43-57: Simplify redundant ENV fallback logic.

The config_value(:field).presence || ENV["KEY"] pattern is redundant because config_value already checks ENV via ConfigField.value. The explicit || ENV["..."] fallback will never be reached.

Apply this diff to simplify:

 def self.reload_configuration
-    client_id = config_value(:client_id).presence || ENV["PLAID_CLIENT_ID"]
-    secret = config_value(:secret).presence || ENV["PLAID_SECRET"]
-    environment = config_value(:environment).presence || ENV["PLAID_ENV"] || "sandbox"
+    client_id = config_value(:client_id)
+    secret = config_value(:secret)
+    environment = config_value(:environment) || "sandbox"
 
     if client_id.present? && secret.present?
app/models/provider/factory.rb (1)

47-58: Consider thread-safety and use require instead of require_dependency.

The eager loading pattern is necessary to trigger adapter registration side-effects, but has two minor issues:

  1. Deprecated API: require_dependency is deprecated in Rails 7+. Use require instead.
  2. Thread-safety: The @adapters_loaded check-then-act is not thread-safe, though unlikely to cause issues since loading typically happens at boot or on first access.

Apply this diff:

 def ensure_adapters_loaded
-      return if @adapters_loaded
+      return if defined?(@adapters_loaded) && @adapters_loaded
 
       # Require all adapter files to trigger registration
       Dir[Rails.root.join("app/models/provider/*_adapter.rb")].each do |file|
-        require_dependency file
+        require file
       end
 
       @adapters_loaded = true
     end

Alternatively, use a mutex for proper thread-safety:

def ensure_adapters_loaded
  @load_mutex ||= Mutex.new
  return if @adapters_loaded
  
  @load_mutex.synchronize do
    return if @adapters_loaded
    
    Dir[Rails.root.join("app/models/provider/*_adapter.rb")].each do |file|
      require file
    end
    
    @adapters_loaded = true
  end
end
app/views/settings/providers/_provider_form.html.erb (2)

42-72: Extract field rendering logic to a helper method.

The inline field rendering logic (lines 42-63) is complex and violates the guideline to "Keep heavy logic out of ERB views." Consider extracting this to a helper method for better testability and maintainability.

As per coding guidelines.

Add to app/helpers/settings/providers_helper.rb:

module Settings::ProvidersHelper
  def provider_field_display_value(field)
    env_value = ENV[field.env_key] if field.env_key
    setting_value = Setting[field.setting_key]
    current_value = setting_value.presence || env_value
    
    if field.secret && current_value.present?
      "********"
    else
      current_value
    end
  end
  
  def provider_field_type(field)
    field.secret ? "password" : "text"
  end
end

Then simplify the view:

<% configuration.fields.each do |field| %>
  <%= form.text_field field.setting_key,
                      label: field.label,
                      type: provider_field_type(field),
                      placeholder: field.default || "",
                      value: provider_field_display_value(field),
                      data: { "auto-submit-form-target": "auto" } %>
<% end %>

62-62: Remove unnecessary disabled = false assignment.

The disabled variable is always set to false and never changes. Either remove it or add a comment explaining why it's preserved for future use.

Apply this diff:

-          # Don't disable fields - allow overriding ENV variables
-          disabled = false
         %>
 
         <%= form.text_field field.setting_key,
                             type: input_type,
                             placeholder: field.default || "",
                             value: display_value,
-                            disabled: disabled,
                             data: { "auto-submit-form-target": "auto" } %>
app/models/provider/simplefin_adapter.rb (1)

9-23: Solid config DSL; consider one‑time token hygiene

The field is correctly marked secret with ENV override. Given setup_token is one‑time use, consider clearing it after first successful SimpleFIN setup to avoid storing stale secrets. If not in scope, add a note to the UI description.

app/controllers/settings/providers_controller.rb (1)

22-38: Update only when values change; centralize secret placeholder

Skip writing when unchanged and avoid magic string duplication for secret placeholders.

+  SECRET_PLACEHOLDER = "********".freeze
@@
-    provider_params.each do |param_key, param_value|
+    provider_params.each do |param_key, param_value|
       setting_key = param_key.to_sym
-      # Clean the value
-      value = param_value.to_s.strip
+      # Clean the value
+      value = param_value.to_s.strip
@@
-      if value == "********"
+      if value == SECRET_PLACEHOLDER
         next
       end
@@
-      Setting[setting_key] = value
-      updated_fields << param_key
+      current = Setting[setting_key]
+      next if current.to_s == value
+      Setting[setting_key] = value
+      updated_fields << param_key

Optionally expose SECRET_PLACEHOLDER to the form partial to keep UI/backend in sync.

app/models/setting.rb (1)

52-101: Bracket API is clean; add tests and note concurrency edge

  • Reads declared fields first, then dynamic hash — aligns with provider precedence logic. Good.
  • Add tests for:
    • precedence (declared > dynamic),
    • ENV fallback when value is blank,
    • delete semantics,
    • secret round‑trip masking in controller/view.
  • Minor: dynamic_fields writes are last‑write‑wins; if concurrent admin edits are possible, consider a lightweight merge/retry or per‑key storage later.

I can draft the test cases if helpful.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4fb0a38 and 6c7fead.

📒 Files selected for processing (11)
  • app/controllers/settings/providers_controller.rb (1 hunks)
  • app/models/provider/configurable.rb (1 hunks)
  • app/models/provider/factory.rb (1 hunks)
  • app/models/provider/plaid_adapter.rb (1 hunks)
  • app/models/provider/simplefin_adapter.rb (1 hunks)
  • app/models/setting.rb (2 hunks)
  • app/views/settings/_settings_nav.html.erb (1 hunks)
  • app/views/settings/providers/_provider_form.html.erb (1 hunks)
  • app/views/settings/providers/show.html.erb (1 hunks)
  • config/routes.rb (1 hunks)
  • test/models/provider/registry_test.rb (2 hunks)
🧰 Additional context used
📓 Path-based instructions (19)
config/**

📄 CodeRabbit inference engine (AGENTS.md)

Store application and environment configuration under config/

Files:

  • config/routes.rb
**/*.rb

📄 CodeRabbit inference engine (AGENTS.md)

Ruby style: 2-space indentation; snake_case for methods/variables; CamelCase for classes/modules

Files:

  • config/routes.rb
  • test/models/provider/registry_test.rb
  • app/controllers/settings/providers_controller.rb
  • app/models/provider/configurable.rb
  • app/models/provider/plaid_adapter.rb
  • app/models/provider/simplefin_adapter.rb
  • app/models/provider/factory.rb
  • app/models/setting.rb
**/*.{rb,js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{rb,js,jsx,ts,tsx}: Make changes atomic, testable, and explain their impact briefly in code suggestions.
Respect existing tests and add tests when changing critical logic.

Files:

  • config/routes.rb
  • test/models/provider/registry_test.rb
  • app/controllers/settings/providers_controller.rb
  • app/models/provider/configurable.rb
  • app/models/provider/plaid_adapter.rb
  • app/models/provider/simplefin_adapter.rb
  • app/models/provider/factory.rb
  • app/models/setting.rb
app/views/**/*.erb

📄 CodeRabbit inference engine (AGENTS.md)

app/views/**/*.erb: Keep heavy logic out of ERB views; prefer helpers/components instead
ERB templates are linted by erb-lint per .erb_lint.yml

Always use the icon helper (icon) for icons; never call lucide_icon directly

Files:

  • app/views/settings/providers/show.html.erb
  • app/views/settings/providers/_provider_form.html.erb
  • app/views/settings/_settings_nav.html.erb
app/views/**/*.html.*

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

app/views/**/*.html.*: Use partials only for simple, context-specific, mostly static HTML content.
Prefer semantic HTML; use Turbo Frames where possible instead of client-side solutions.
Use query params for state, not localStorage or sessionStorage.
Always perform server-side formatting for currencies, numbers, and dates.

Files:

  • app/views/settings/providers/show.html.erb
  • app/views/settings/providers/_provider_form.html.erb
  • app/views/settings/_settings_nav.html.erb
**/*.{html,erb,slim,js,jsx,ts,tsx,css,scss}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Always use functional design tokens (e.g., text-primary, bg-container) from the design system; do not use raw colors or ad-hoc classes.

Files:

  • app/views/settings/providers/show.html.erb
  • app/views/settings/providers/_provider_form.html.erb
  • app/views/settings/_settings_nav.html.erb
app/views/**/*.html.erb

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

app/views/**/*.html.erb: Prefer native HTML elements (e.g., ,

) over JS-based components
Leverage Turbo frames to break up pages instead of JS-driven client-side solutions
Prefer native client-side form validation when possible

Files:

  • app/views/settings/providers/show.html.erb
  • app/views/settings/providers/_provider_form.html.erb
  • app/views/settings/_settings_nav.html.erb
app/{models,controllers,views}/**/*.{rb,erb}

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

Avoid N+1 queries

Files:

  • app/views/settings/providers/show.html.erb
  • app/controllers/settings/providers_controller.rb
  • app/models/provider/configurable.rb
  • app/models/provider/plaid_adapter.rb
  • app/models/provider/simplefin_adapter.rb
  • app/models/provider/factory.rb
  • app/views/settings/providers/_provider_form.html.erb
  • app/models/setting.rb
  • app/views/settings/_settings_nav.html.erb
{app/javascript/controllers/**/*.{js,ts},app/views/**/*.erb,app/components/**/*.erb}

📄 CodeRabbit inference engine (.cursor/rules/stimulus_conventions.mdc)

Use declarative Stimulus actions in ERB (data-action) instead of imperative event listeners in controller lifecycle (e.g., avoid addEventListener in connect); controllers should just respond to actions

Files:

  • app/views/settings/providers/show.html.erb
  • app/views/settings/providers/_provider_form.html.erb
  • app/views/settings/_settings_nav.html.erb
{app/components/**/*.{js,ts,erb},app/views/**/*.erb}

📄 CodeRabbit inference engine (.cursor/rules/stimulus_conventions.mdc)

Component-scoped Stimulus controllers in app/components must be used only within their component views, not in app/views

Files:

  • app/views/settings/providers/show.html.erb
  • app/views/settings/providers/_provider_form.html.erb
  • app/views/settings/_settings_nav.html.erb
{app/views/**,app/helpers/**,app/javascript/controllers/**}

📄 CodeRabbit inference engine (.cursor/rules/ui-ux-design-guidelines.mdc)

{app/views/**,app/helpers/**,app/javascript/controllers/**}: Style UI using TailwindCSS v4.x with the custom design system defined in app/assets/tailwind/maybe-design-system.css
Always start by referencing app/assets/tailwind/maybe-design-system.css to understand available primitives, functional tokens, and component tokens before styling
Prefer functional tokens from the design system over raw Tailwind values (e.g., use text-primary, bg-container, border border-primary instead of text-white, bg-white, border-gray-200)

Files:

  • app/views/settings/providers/show.html.erb
  • app/views/settings/providers/_provider_form.html.erb
  • app/views/settings/_settings_nav.html.erb
{app/views/**,app/helpers/**}

📄 CodeRabbit inference engine (.cursor/rules/ui-ux-design-guidelines.mdc)

Always generate semantic HTML

Files:

  • app/views/settings/providers/show.html.erb
  • app/views/settings/providers/_provider_form.html.erb
  • app/views/settings/_settings_nav.html.erb
{app/views,app/components}/**/*.html.erb

📄 CodeRabbit inference engine (.cursor/rules/view_conventions.mdc)

{app/views,app/components}/**/*.html.erb: Keep domain logic out of ERB templates; compute values in component/controller code and reference them in the template
Integrate Stimulus declaratively in ERB: templates declare data-controller/data-action; controllers respond to those declarations
Pass data from Rails to Stimulus via data-*-value attributes, not inline JavaScript

Files:

  • app/views/settings/providers/show.html.erb
  • app/views/settings/providers/_provider_form.html.erb
  • app/views/settings/_settings_nav.html.erb
test/**/*_test.rb

📄 CodeRabbit inference engine (AGENTS.md)

Name Minitest files with *_test.rb and mirror the app/ structure under test/

test/**/*_test.rb: ALWAYS use Minitest + fixtures + Mocha for tests; NEVER RSpec or FactoryBot.
Use Mocha for mocking in tests when necessary.
Use VCR for external API tests.

test/**/*_test.rb: Always use Minitest for tests; do not use RSpec
Do not use factories (e.g., FactoryBot) in tests; rely on fixtures instead
For tests needing many records, use Rails helpers to construct data and inline the creation in the test
Only write tests for critical and important code paths
Avoid tests that merely verify framework/ActiveRecord behavior
Test boundaries correctly: for queries, assert returned values; for commands, assert collaborators are called with correct params
Never test the implementation details of one class in another class’s test suite
Use the mocha gem for stubs and mocks
Prefer OpenStruct for mock instances; use a mock class for complex cases
Only mock what’s necessary; don’t mock return values unless they are under test

Files:

  • test/models/provider/registry_test.rb
app/**/*.rb

📄 CodeRabbit inference engine (AGENTS.md)

Place Rails application Ruby code (models, controllers, services, jobs, mailers, components) under app/

Files:

  • app/controllers/settings/providers_controller.rb
  • app/models/provider/configurable.rb
  • app/models/provider/plaid_adapter.rb
  • app/models/provider/simplefin_adapter.rb
  • app/models/provider/factory.rb
  • app/models/setting.rb
app/models/**/*.rb

📄 CodeRabbit inference engine (.cursor/rules/project-design.mdc)

Domain models should not call Provider::Registry directly; use a Provided concern within the model’s namespace to select providers and expose convenience methods

Use ActiveRecord validations for forms and complex domain constraints.

app/models/**/*.rb: Place business logic in POROs and model classes under app/models
Models should answer questions about themselves (e.g., prefer account.balance_series over service objects)
Implement complex validations and business logic with ActiveRecord validations
Model-level validations may mirror DB constraints but are not strictly required

Files:

  • app/models/provider/configurable.rb
  • app/models/provider/plaid_adapter.rb
  • app/models/provider/simplefin_adapter.rb
  • app/models/provider/factory.rb
  • app/models/setting.rb
app/models/provider/**/*.rb

📄 CodeRabbit inference engine (.cursor/rules/project-design.mdc)

Concrete provider classes must be under the Provider:: namespace, inherit from Provider, wrap calls with with_provider_response, and raise on invalid/unavailable data

Files:

  • app/models/provider/configurable.rb
  • app/models/provider/plaid_adapter.rb
  • app/models/provider/simplefin_adapter.rb
  • app/models/provider/factory.rb
app/{helpers,models}/**/*.rb

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

Format currencies, numbers, and dates on the server side (Ruby) before sending to the client

Files:

  • app/models/provider/configurable.rb
  • app/models/provider/plaid_adapter.rb
  • app/models/provider/simplefin_adapter.rb
  • app/models/provider/factory.rb
  • app/models/setting.rb
app/views/**/_*.html.erb

📄 CodeRabbit inference engine (.cursor/rules/view_conventions.mdc)

Name partials with a leading underscore (e.g., _trend_change.html.erb, _form_errors.html.erb)

Files:

  • app/views/settings/providers/_provider_form.html.erb
  • app/views/settings/_settings_nav.html.erb
🧠 Learnings (1)
📚 Learning: 2025-09-16T13:15:56.406Z
Learnt from: CR
PR: we-promise/sure#0
File: .cursor/rules/project-design.mdc:0-0
Timestamp: 2025-09-16T13:15:56.406Z
Learning: Applies to app/models/**/*.rb : Domain models should not call Provider::Registry directly; use a Provided concern within the model’s namespace to select providers and expose convenience methods

Applied to files:

  • app/models/provider/factory.rb
🧬 Code graph analysis (7)
test/models/provider/registry_test.rb (1)
app/models/provider/registry.rb (4)
  • for_concept (11-13)
  • providers (85-87)
  • get_provider (15-19)
  • get_provider (89-95)
app/controllers/settings/providers_controller.rb (5)
app/models/provider/factory.rb (1)
  • ensure_adapters_loaded (49-58)
app/models/provider/configurable.rb (8)
  • all (223-225)
  • setting_key (183-185)
  • value (188-202)
  • field (132-143)
  • provider_key (68-70)
  • provider_key (92-94)
  • get_adapter_class (232-234)
  • reload_configuration (85-88)
app/models/current.rb (1)
  • user (8-10)
app/models/user.rb (1)
  • admin? (67-69)
app/models/provider/plaid_adapter.rb (1)
  • reload_configuration (44-57)
app/models/provider/configurable.rb (1)
app/models/provider/plaid_adapter.rb (1)
  • reload_configuration (44-57)
app/models/provider/plaid_adapter.rb (2)
app/models/provider/simplefin_adapter.rb (2)
  • include (1-77)
  • provider_name (25-27)
app/models/provider/configurable.rb (8)
  • register (214-217)
  • configure (55-59)
  • description (120-122)
  • field (132-143)
  • reload_configuration (85-88)
  • config_value (73-75)
  • config_value (100-102)
  • present? (205-207)
app/models/provider/simplefin_adapter.rb (2)
app/models/provider/plaid_adapter.rb (1)
  • include (1-95)
app/models/provider/configurable.rb (4)
  • register (214-217)
  • configure (55-59)
  • description (120-122)
  • field (132-143)
app/models/provider/factory.rb (1)
app/models/provider/configurable.rb (1)
  • registry (237-239)
app/models/setting.rb (1)
app/models/provider/configurable.rb (2)
  • field (132-143)
  • value (188-202)
🔇 Additional comments (9)
test/models/provider/registry_test.rb (1)

6-13: LGTM! Proper environment scoping.

The use of ClimateControl.modify correctly scopes the OPENAI_ACCESS_TOKEN environment variable for this test, preventing side effects on other tests.

app/models/provider/configurable.rb (1)

53-89: LGTM! Well-designed configuration API.

The class methods provide a clean DSL for provider configuration with sensible defaults and proper nil handling. The provider_key derivation works correctly for the documented naming convention.

app/views/settings/_settings_nav.html.erb (1)

31-31: LGTM! Consistent with existing navigation items.

The new "Providers" navigation item follows the established pattern and is correctly placed in the admin-only advanced section. Note that it uses a hardcoded label like some other items in this section (e.g., "LLM Usage", "SimpleFIN").

config/routes.rb (1)

79-79: LGTM! Correct use of singleton resource.

The singular resource :providers with show and update actions is appropriate for a single configuration page managing multiple providers.

app/views/settings/providers/show.html.erb (1)

1-15: LGTM! Clean and straightforward view.

The view correctly iterates over provider configurations and delegates rendering to the partial. The use of titleize for display names is appropriate.

app/models/provider/plaid_adapter.rb (1)

9-37: LGTM! Well-structured Plaid configuration.

The configuration block properly declares all required fields with appropriate metadata (labels, descriptions, env_keys). The use of secret: true for the secret key and the setup instructions are helpful.

app/views/settings/providers/_provider_form.html.erb (1)

33-39: LGTM! Auto-submit on blur provides good UX.

The form setup with auto-submit-form controller and blur trigger allows seamless saving of provider credentials without requiring a submit button.

app/models/provider/simplefin_adapter.rb (1)

4-4: Configurable inclusion looks good

Adapter now participates in the dynamic config system as intended. No issues.

app/models/setting.rb (1)

14-17: Dynamic fields fit the use‑case

Single hash for arbitrary provider settings keeps the schema stable and pairs well with the controller’s permit list. LGTM.

sokie added 2 commits October 29, 2025 09:55
- Also validate configurable values properly.
- Change Provider factory to use Rails autoloading (Zeitwerk)
The derive_adapter_name method relies on string manipulation ("PlaidAccount".sub(/Account$/, "") + "Adapter" → "PlaidAdapter"), but we already have explicit registration in place.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (3)
app/controllers/settings/providers_controller.rb (2)

69-71: Nil-safe admin check

Current.user can be nil; this will raise. Use safe navigation.

-redirect_to settings_providers_path, alert: "Not authorized" unless Current.user.admin?
+redirect_to settings_providers_path, alert: "Not authorized" unless Current.user&.admin?

73-94: Avoid NameError(Set) and simplify provider reload mapping

You use Set without requiring it; also the scan is O(N^2). Build a field->provider map once and uniq the keys; no Set needed. Ensure adapters are loaded before resolving.

-# 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
+def reload_provider_configs(updated_fields)
+  Provider::Factory.ensure_adapters_loaded
+  # Build mapping once: setting_key -> provider_key
+  field_to_provider = {}
+  Provider::ConfigurationRegistry.all.each do |config|
+    config.fields.each do |f|
+      field_to_provider[f.setting_key.to_s] = config.provider_key
+    end
+  end
+  updated_provider_keys = updated_fields.filter_map { |k| field_to_provider[k.to_s] }.uniq
+  updated_provider_keys.each do |provider_key|
+    Provider::ConfigurationRegistry.get_adapter_class(provider_key)&.reload_configuration
+  end
+end

Additionally (top of file), if you keep Set anywhere else:

+require "set"
app/models/provider/configurable.rb (1)

187-202: Empty-string precedence causes ENV fallback; align with intended semantics

Current logic uses present?, so a saved empty string won’t override ENV/default. Decide policy, then implement consistently:

Option A (recommended with controller change): treat blank UI values as “unset” (controller sets nil). Keep this method, no change required.

Option B (if you want “UI-saved even if blank overrides ENV”): check key existence, not value presence.

-      setting_value = Setting[setting_key]
-      return normalize_value(setting_value) if setting_value.present?
+      if Setting.respond_to?(:key?) ? Setting.key?(setting_key) : !Setting[setting_key].nil?
+        return normalize_value(Setting[setting_key])
+      end
@@
-      if env_key.present?
-        env_value = ENV[env_key]
-        return normalize_value(env_value) if env_value.present?
-      end
+      if env_key.present? && ENV.key?(env_key)
+        return normalize_value(ENV[env_key])
+      end

If migrating from prior behavior where “” may be stored, consider a one-off data cleanup to set such values to NULL.

🧹 Nitpick comments (3)
app/controllers/settings/providers_controller.rb (1)

48-52: Rescue is fine; consider surfacing validation errors

If field.validate! raised inside the transaction, return those messages instead of a generic error.

app/models/provider/factory.rb (1)

66-69: Also clear state consistently

clear_registry! should reset the same object or clear both registries if needed by tests. Optional.

 def clear_registry!
-  @registry = {}
+  @registry&.clear
+  @registry ||= {}
 end
app/models/provider/configurable.rb (1)

277-284: Thread-safety of registries (dev reloads)

Memoizing Hashes with ||= can race under multithreaded dev reload. Consider Concurrent::Map or a mutex. Low risk in prod.

-      def registry
-        @registry ||= {}
-      end
+      def registry
+        @registry ||= {}
+      end # consider Concurrent::Map if you observe races
@@
-      def adapter_registry
-        @adapter_registry ||= {}
-      end
+      def adapter_registry
+        @adapter_registry ||= {}
+      end # consider Concurrent::Map if you observe races
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6c7fead and fb540a4.

📒 Files selected for processing (3)
  • app/controllers/settings/providers_controller.rb (1 hunks)
  • app/models/provider/configurable.rb (1 hunks)
  • app/models/provider/factory.rb (3 hunks)
🧰 Additional context used
📓 Path-based instructions (7)
app/**/*.rb

📄 CodeRabbit inference engine (AGENTS.md)

Place Rails application Ruby code (models, controllers, services, jobs, mailers, components) under app/

Files:

  • app/controllers/settings/providers_controller.rb
  • app/models/provider/factory.rb
  • app/models/provider/configurable.rb
**/*.rb

📄 CodeRabbit inference engine (AGENTS.md)

Ruby style: 2-space indentation; snake_case for methods/variables; CamelCase for classes/modules

Files:

  • app/controllers/settings/providers_controller.rb
  • app/models/provider/factory.rb
  • app/models/provider/configurable.rb
**/*.{rb,js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{rb,js,jsx,ts,tsx}: Make changes atomic, testable, and explain their impact briefly in code suggestions.
Respect existing tests and add tests when changing critical logic.

Files:

  • app/controllers/settings/providers_controller.rb
  • app/models/provider/factory.rb
  • app/models/provider/configurable.rb
app/{models,controllers,views}/**/*.{rb,erb}

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

Avoid N+1 queries

Files:

  • app/controllers/settings/providers_controller.rb
  • app/models/provider/factory.rb
  • app/models/provider/configurable.rb
app/models/**/*.rb

📄 CodeRabbit inference engine (.cursor/rules/project-design.mdc)

Domain models should not call Provider::Registry directly; use a Provided concern within the model’s namespace to select providers and expose convenience methods

Use ActiveRecord validations for forms and complex domain constraints.

app/models/**/*.rb: Place business logic in POROs and model classes under app/models
Models should answer questions about themselves (e.g., prefer account.balance_series over service objects)
Implement complex validations and business logic with ActiveRecord validations
Model-level validations may mirror DB constraints but are not strictly required

Files:

  • app/models/provider/factory.rb
  • app/models/provider/configurable.rb
app/models/provider/**/*.rb

📄 CodeRabbit inference engine (.cursor/rules/project-design.mdc)

Concrete provider classes must be under the Provider:: namespace, inherit from Provider, wrap calls with with_provider_response, and raise on invalid/unavailable data

Files:

  • app/models/provider/factory.rb
  • app/models/provider/configurable.rb
app/{helpers,models}/**/*.rb

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

Format currencies, numbers, and dates on the server side (Ruby) before sending to the client

Files:

  • app/models/provider/factory.rb
  • app/models/provider/configurable.rb
🧠 Learnings (1)
📚 Learning: 2025-09-16T13:15:56.406Z
Learnt from: CR
PR: we-promise/sure#0
File: .cursor/rules/project-design.mdc:0-0
Timestamp: 2025-09-16T13:15:56.406Z
Learning: Applies to app/models/**/*.rb : Domain models should not call Provider::Registry directly; use a Provided concern within the model’s namespace to select providers and expose convenience methods

Applied to files:

  • app/models/provider/factory.rb
🧬 Code graph analysis (3)
app/controllers/settings/providers_controller.rb (5)
app/models/provider/factory.rb (1)
  • ensure_adapters_loaded (45-57)
app/models/provider/configurable.rb (8)
  • all (264-266)
  • setting_key (183-185)
  • value (188-202)
  • field (132-143)
  • provider_key (68-70)
  • provider_key (92-94)
  • get_adapter_class (273-275)
  • reload_configuration (85-88)
app/models/current.rb (1)
  • user (8-10)
app/models/user.rb (1)
  • admin? (67-69)
app/models/provider/plaid_adapter.rb (1)
  • reload_configuration (44-57)
app/models/provider/factory.rb (1)
app/models/provider/base.rb (1)
  • provider_type (36-38)
app/models/provider/configurable.rb (2)
app/models/provider/factory.rb (3)
  • register (1-116)
  • register (8-10)
  • registry (73-75)
app/models/provider/plaid_adapter.rb (1)
  • reload_configuration (44-57)
🪛 GitHub Check: ci / lint
app/models/provider/factory.rb

[failure] 116-116:
Layout/TrailingEmptyLines: Final newline missing.


[failure] 73-73:
Layout/IndentationWidth: Use 2 (not 0) spaces for indented_internal_methods indentation.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ci / test
🔇 Additional comments (1)
app/models/provider/configurable.rb (1)

153-156: verified: configured? behavior is correct; key? method available for Option B

Verification complete. The key? method is available in Setting class (line 81-84), checking both declared fields and dynamic_fields. The configured? method behavior is correct: blank UI values are normalized to nil via normalize_value() (line 227-231), which then fail the present? check, preventing required fields from being satisfied. This implementation matches the described UX behavior.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
app/models/provider/factory.rb (1)

73-99: Fix indentation and add final newline.

Static analysis has flagged two formatting issues that were already noted in previous review comments:

  1. Line 73: Methods under private need 2 additional spaces of indentation
  2. Line 101: File is missing a final newline

Apply this diff to fix the indentation:

-    private
-
-    def registry
-      @registry ||= {}
-    end
-
-    # Find adapter class, attempting to load all adapters if not registered
-    def find_adapter_class(provider_type)
-      # Return if already registered
-      return registry[provider_type] if registry[provider_type]
-
-      # Load all adapters to ensure they're registered
-      # This triggers their self-registration calls
-      ensure_adapters_loaded
-
-      # Check registry again after loading
-      registry[provider_type]
-    end
-
-    # Discover all adapter files in the provider directory
-    # Returns adapter class names (e.g., ["PlaidAdapter", "SimplefinAdapter"])
-    def adapter_files
-      return [] unless defined?(Rails)
-
-      pattern = Rails.root.join("app/models/provider/*_adapter.rb")
-      Dir[pattern].map do |file|
-        File.basename(file, ".rb").camelize
+    private
+
+      def registry
+        @registry ||= {}
+      end
+
+      # Find adapter class, attempting to load all adapters if not registered
+      def find_adapter_class(provider_type)
+        # Return if already registered
+        return registry[provider_type] if registry[provider_type]
+
+        # Load all adapters to ensure they're registered
+        # This triggers their self-registration calls
+        ensure_adapters_loaded
+
+        # Check registry again after loading
+        registry[provider_type]
+      end
+
+      # Discover all adapter files in the provider directory
+      # Returns adapter class names (e.g., ["PlaidAdapter", "SimplefinAdapter"])
+      def adapter_files
+        return [] unless defined?(Rails)
+
+        pattern = Rails.root.join("app/models/provider/*_adapter.rb")
+        Dir[pattern].map do |file|
+          File.basename(file, ".rb").camelize
+        end
       end
-    end
   end
 end
+
🧹 Nitpick comments (1)
app/models/provider/factory.rb (1)

43-57: Consider rescuing LoadError in addition to NameError.

The error handling currently only rescues NameError, which may miss other loading failures such as LoadError (missing dependencies) or SyntaxError. Consider broadening the rescue to catch more potential issues during adapter loading.

Apply this diff to improve error handling robustness:

         begin
           adapter_class_name.constantize
-        rescue NameError => e
+        rescue NameError, LoadError => e
           Rails.logger.warn("Failed to load adapter: #{adapter_class_name} - #{e.message}")
         end
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fb540a4 and bc0be96.

📒 Files selected for processing (1)
  • app/models/provider/factory.rb (3 hunks)
🧰 Additional context used
📓 Path-based instructions (7)
app/models/**/*.rb

📄 CodeRabbit inference engine (.cursor/rules/project-design.mdc)

Domain models should not call Provider::Registry directly; use a Provided concern within the model’s namespace to select providers and expose convenience methods

Use ActiveRecord validations for forms and complex domain constraints.

app/models/**/*.rb: Place business logic in POROs and model classes under app/models
Models should answer questions about themselves (e.g., prefer account.balance_series over service objects)
Implement complex validations and business logic with ActiveRecord validations
Model-level validations may mirror DB constraints but are not strictly required

Files:

  • app/models/provider/factory.rb
app/models/provider/**/*.rb

📄 CodeRabbit inference engine (.cursor/rules/project-design.mdc)

Concrete provider classes must be under the Provider:: namespace, inherit from Provider, wrap calls with with_provider_response, and raise on invalid/unavailable data

Files:

  • app/models/provider/factory.rb
app/**/*.rb

📄 CodeRabbit inference engine (AGENTS.md)

Place Rails application Ruby code (models, controllers, services, jobs, mailers, components) under app/

Files:

  • app/models/provider/factory.rb
**/*.rb

📄 CodeRabbit inference engine (AGENTS.md)

Ruby style: 2-space indentation; snake_case for methods/variables; CamelCase for classes/modules

Files:

  • app/models/provider/factory.rb
**/*.{rb,js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{rb,js,jsx,ts,tsx}: Make changes atomic, testable, and explain their impact briefly in code suggestions.
Respect existing tests and add tests when changing critical logic.

Files:

  • app/models/provider/factory.rb
app/{helpers,models}/**/*.rb

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

Format currencies, numbers, and dates on the server side (Ruby) before sending to the client

Files:

  • app/models/provider/factory.rb
app/{models,controllers,views}/**/*.{rb,erb}

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

Avoid N+1 queries

Files:

  • app/models/provider/factory.rb
🧠 Learnings (1)
📚 Learning: 2025-09-16T13:15:56.406Z
Learnt from: CR
PR: we-promise/sure#0
File: .cursor/rules/project-design.mdc:0-0
Timestamp: 2025-09-16T13:15:56.406Z
Learning: Applies to app/models/**/*.rb : Domain models should not call Provider::Registry directly; use a Provided concern within the model’s namespace to select providers and expose convenience methods

Applied to files:

  • app/models/provider/factory.rb
🧬 Code graph analysis (1)
app/models/provider/factory.rb (1)
app/models/provider/base.rb (1)
  • provider_type (36-38)
🪛 GitHub Check: ci / lint
app/models/provider/factory.rb

[failure] 101-101:
Layout/TrailingEmptyLines: Final newline missing.


[failure] 73-73:
Layout/IndentationWidth: Use 2 (not 0) spaces for indented_internal_methods indentation.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ci / test
🔇 Additional comments (4)
app/models/provider/factory.rb (4)

2-2: LGTM: Custom exception improves error semantics.

The new AdapterNotFoundError provides clearer error semantics than a generic ArgumentError and follows Ruby exception conventions.


20-22: LGTM: Adapter lookup now supports lazy loading.

The change from direct registry lookup to find_adapter_class enables dynamic adapter discovery, and the new exception type improves error clarity.


38-41: LGTM: Ensures adapters are loaded and returns sorted keys.

The changes ensure all adapters are discovered before returning registered types, and sorting provides a deterministic, user-friendly order.


59-69: LGTM: Registry helpers are well-implemented.

The registered? method correctly triggers adapter loading if needed, and clear_registry! provides a clean way to reset state for testing.

@jjmata jjmata self-requested a review October 29, 2025 12:02
- This looks like partial support atm
Copy link
Collaborator

@jjmata jjmata left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@jjmata jjmata merged commit 96713ee into we-promise:main Oct 29, 2025
5 of 6 checks passed
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
app/models/provider/plaid_adapter.rb (2)

55-68: Consider adding logging when credentials are missing.

When client_id or secret are missing, the configuration is silently set to nil. This could make troubleshooting difficult for users who expect Plaid integration to work but haven't configured credentials.

Apply this diff to add informative logging:

 def self.reload_configuration
   client_id = config_value(:client_id).presence || ENV["PLAID_CLIENT_ID"]
   secret = config_value(:secret).presence || ENV["PLAID_SECRET"]
   environment = config_value(:environment).presence || ENV["PLAID_ENV"] || "sandbox"

   if client_id.present? && secret.present?
     Rails.application.config.plaid = Plaid::Configuration.new
     Rails.application.config.plaid.server_index = Plaid::Configuration::Environment[environment]
     Rails.application.config.plaid.api_key["PLAID-CLIENT-ID"] = client_id
     Rails.application.config.plaid.api_key["PLAID-SECRET"] = secret
   else
+    Rails.logger.info("Plaid US credentials not configured - Plaid integration disabled")
     Rails.application.config.plaid = nil
   end
 end

55-68: Consider extracting shared reload logic to reduce duplication.

The reload_configuration method is duplicated almost identically in Provider::PlaidEuAdapter (lines 47-60). The only differences are ENV variable names and the config property being set.

Consider extracting shared logic into a private helper method in Provider::Configurable or a shared concern. This would ensure both adapters remain synchronized if the reload logic needs to change. However, given the simplicity and clarity of the current implementation, this refactor can be deferred.

app/models/provider/plaid_eu_adapter.rb (1)

47-60: Consider adding logging when credentials are missing.

Similar to Provider::PlaidAdapter, when credentials are missing, the configuration is silently set to nil, which could make troubleshooting difficult.

Apply this diff to add informative logging:

 def self.reload_configuration
   client_id = config_value(:client_id).presence || ENV["PLAID_EU_CLIENT_ID"]
   secret = config_value(:secret).presence || ENV["PLAID_EU_SECRET"]
   environment = config_value(:environment).presence || ENV["PLAID_EU_ENV"] || "sandbox"

   if client_id.present? && secret.present?
     Rails.application.config.plaid_eu = Plaid::Configuration.new
     Rails.application.config.plaid_eu.server_index = Plaid::Configuration::Environment[environment]
     Rails.application.config.plaid_eu.api_key["PLAID-CLIENT-ID"] = client_id
     Rails.application.config.plaid_eu.api_key["PLAID-SECRET"] = secret
   else
+    Rails.logger.info("Plaid EU credentials not configured - Plaid EU integration disabled")
     Rails.application.config.plaid_eu = nil
   end
 end
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aaa92aa and fc73b1d.

📒 Files selected for processing (3)
  • app/models/provider/plaid_adapter.rb (1 hunks)
  • app/models/provider/plaid_eu_adapter.rb (1 hunks)
  • config/initializers/plaid.rb (1 hunks)
🧰 Additional context used
📓 Path-based instructions (8)
config/**

📄 CodeRabbit inference engine (AGENTS.md)

Store application and environment configuration under config/

Files:

  • config/initializers/plaid.rb
**/*.rb

📄 CodeRabbit inference engine (AGENTS.md)

Ruby style: 2-space indentation; snake_case for methods/variables; CamelCase for classes/modules

Files:

  • config/initializers/plaid.rb
  • app/models/provider/plaid_eu_adapter.rb
  • app/models/provider/plaid_adapter.rb
**/*.{rb,js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{rb,js,jsx,ts,tsx}: Make changes atomic, testable, and explain their impact briefly in code suggestions.
Respect existing tests and add tests when changing critical logic.

Files:

  • config/initializers/plaid.rb
  • app/models/provider/plaid_eu_adapter.rb
  • app/models/provider/plaid_adapter.rb
app/models/**/*.rb

📄 CodeRabbit inference engine (.cursor/rules/project-design.mdc)

Domain models should not call Provider::Registry directly; use a Provided concern within the model’s namespace to select providers and expose convenience methods

Use ActiveRecord validations for forms and complex domain constraints.

app/models/**/*.rb: Place business logic in POROs and model classes under app/models
Models should answer questions about themselves (e.g., prefer account.balance_series over service objects)
Implement complex validations and business logic with ActiveRecord validations
Model-level validations may mirror DB constraints but are not strictly required

Files:

  • app/models/provider/plaid_eu_adapter.rb
  • app/models/provider/plaid_adapter.rb
app/models/provider/**/*.rb

📄 CodeRabbit inference engine (.cursor/rules/project-design.mdc)

Concrete provider classes must be under the Provider:: namespace, inherit from Provider, wrap calls with with_provider_response, and raise on invalid/unavailable data

Files:

  • app/models/provider/plaid_eu_adapter.rb
  • app/models/provider/plaid_adapter.rb
app/**/*.rb

📄 CodeRabbit inference engine (AGENTS.md)

Place Rails application Ruby code (models, controllers, services, jobs, mailers, components) under app/

Files:

  • app/models/provider/plaid_eu_adapter.rb
  • app/models/provider/plaid_adapter.rb
app/{helpers,models}/**/*.rb

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

Format currencies, numbers, and dates on the server side (Ruby) before sending to the client

Files:

  • app/models/provider/plaid_eu_adapter.rb
  • app/models/provider/plaid_adapter.rb
app/{models,controllers,views}/**/*.{rb,erb}

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

Avoid N+1 queries

Files:

  • app/models/provider/plaid_eu_adapter.rb
  • app/models/provider/plaid_adapter.rb
🧬 Code graph analysis (3)
config/initializers/plaid.rb (4)
config/application.rb (1)
  • config (10-44)
app/models/provider/factory.rb (1)
  • ensure_adapters_loaded (45-57)
app/models/provider/plaid_adapter.rb (1)
  • reload_configuration (55-68)
app/models/provider/plaid_eu_adapter.rb (1)
  • reload_configuration (47-60)
app/models/provider/plaid_eu_adapter.rb (2)
app/models/provider/plaid_adapter.rb (2)
  • include (12-106)
  • reload_configuration (55-68)
app/models/provider/configurable.rb (2)
  • configure (55-59)
  • present? (205-207)
app/models/provider/plaid_adapter.rb (2)
app/models/provider/plaid_eu_adapter.rb (2)
  • include (13-61)
  • reload_configuration (47-60)
app/models/provider/configurable.rb (8)
  • register (255-258)
  • configure (55-59)
  • description (120-122)
  • field (132-143)
  • reload_configuration (85-88)
  • config_value (73-75)
  • config_value (100-102)
  • present? (205-207)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ci / test
🔇 Additional comments (3)
app/models/provider/plaid_adapter.rb (1)

1-11: LGTM - Excellent documentation!

The comment block clearly explains the dual-purpose nature of this adapter (configuration manager vs. instance adapter) and the separation of concerns with PlaidEuAdapter. This will help future maintainers understand the design.

config/initializers/plaid.rb (1)

1-5: LGTM - Clean initialization pattern.

Explicitly initializing both Plaid configurations to nil ensures a clean slate before the adapters reload their configurations. This prevents any stale configuration from persisting.

app/models/provider/plaid_eu_adapter.rb (1)

1-13: LGTM - Clear architectural documentation!

The comment block effectively explains why this adapter exists as a configuration-only manager rather than being registered as a provider type. This design rationale will help future maintainers understand the separation between US and EU configurations.

Comment on lines +42 to +48
field :environment,
label: "Environment",
required: false,
env_key: "PLAID_ENV",
default: "sandbox",
description: "Plaid environment: sandbox, development, or production"
end
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Validate environment values to prevent runtime errors.

The environment field accepts any string, but Line 62 uses Plaid::Configuration::Environment[environment], which may raise an error if an invalid value is provided (not "sandbox", "development", or "production").

Consider adding validation to the field definition or handling invalid values gracefully in reload_configuration. Run this script to check how Plaid handles invalid environment values:


🏁 Script executed:

#!/bin/bash
# Search for Plaid::Configuration::Environment usage and error handling patterns
rg -nP --type=ruby -C3 'Plaid::Configuration::Environment\[' 

Length of output: 1824


🏁 Script executed:

#!/bin/bash
# Get full context of the environment field definition and reload_configuration method
rg -nP --type=ruby -B20 'field :environment' app/models/provider/plaid_adapter.rb | head -40

Length of output: 826


🏁 Script executed:

#!/bin/bash
# Check the full reload_configuration method for error handling
rg -nP --type=ruby -A10 'def reload_configuration' app/models/provider/plaid_adapter.rb

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if there are any validation patterns used elsewhere in provider adapters
rg -nP --type=ruby 'field.*validation:|options:' app/models/provider/ | head -20

Length of output: 231


🏁 Script executed:

#!/bin/bash
# Check Plaid adapter imports to understand the gem version being used
rg -nP --type=ruby 'require.*plaid|gem.*plaid' app/ config/

Length of output: 123


🏁 Script executed:

#!/bin/bash
# Search for any error handling around Plaid::Configuration::Environment usage
rg -nP --type=ruby -B3 -A5 'Plaid::Configuration::Environment\[environment\]' app/models/provider/plaid_adapter.rb

Length of output: 441


🏁 Script executed:

#!/bin/bash
# Check if there's a Gemfile to see Plaid version
cat Gemfile | grep -i plaid

Length of output: 11


🏁 Script executed:

#!/bin/bash
# Check for any tests that might show expected behavior
fd -e rb -p '**/*spec.rb' -o '**/*_test.rb' | xargs rg -l 'plaid.*environment|Environment\[' 2>/dev/null | head -5

Length of output: 287


🏁 Script executed:

#!/bin/bash
# Look for validation patterns in the Configurable concern
fd -e rb -p '**/provider/configurable.rb' -o '**/concerns/*configurable*.rb' | xargs cat 2>/dev/null | head -100

Length of output: 177


🏁 Script executed:

#!/bin/bash
# Search for 'options' key usage in field definitions that might indicate validation
rg -nP --type=ruby 'field.*options:|options:\s*\[' app/models/provider/

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check the PlaidAdapter class definition for any validation
rg -nP --type=ruby -A50 'class PlaidAdapter' app/models/provider/plaid_adapter.rb | head -60

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if Plaid::Configuration::Environment is documented or has error handling
rg -nP --type=ruby 'Environment.*=' app/models/provider/ -C2

Length of output: 0


🌐 Web query:

What does Plaid ruby gem Plaid::Configuration::Environment do with invalid values

💡 Result:

It raises a KeyError for invalid names (e.g. Plaid::Configuration::Environment["not_a_real_env"] will raise). [1]

Sources:

  • Plaid ruby README (shows Environment[...] usage). [1]

🌐 Web query:

Plaid ruby gem environment configuration options

💡 Result:

Short summary — how to configure environments for the Plaid Ruby gem

  • New (current client) style (Configuration / ApiClient):

    • configuration.server_index = Plaid::Configuration::Environment["sandbox"] (available values: "sandbox", "development" or "production").[1][2]
    • set API credentials and headers via configuration.api_key:
      • configuration.api_key["PLAID-CLIENT-ID"] = ENV["..."]
      • configuration.api_key["PLAID-SECRET"] = ENV["..."]
      • configuration.api_key["Plaid-Version"] = "YYYY-MM-DD" (optional/recommended to pin API version).[1][2]
    • build ApiClient and client:
      • api_client = Plaid::ApiClient.new(configuration)
      • client = Plaid::PlaidApi.new(api_client).[1][2]
  • Faraday / HTTP tuning:

    • adjust timeouts: api_client.connection.options[:timeout] = seconds
    • replace/add middleware by reinitializing connection with api_client.create_connection { |builder| builder.use ... }.[6]
  • Legacy/older client (Plaid::Client):

    • initialize with env: :tartan or :production (or full URL string); client = Plaid::Client.new(env: :sandbox/:production, client_id: ..., secret: ...).[0]

References

  • Plaid Ruby GitHub README / migration & examples. [1][4]
  • RubyDoc / Plaid gem usage (Configuration, server_index, Faraday tuning). [6][2]
  • Legacy Plaid::Client docs (env symbols and URL behavior). [0]

🏁 Script executed:

#!/bin/bash
# Get complete plaid_adapter.rb file to see full class structure
cat app/models/provider/plaid_adapter.rb

Length of output: 3323


🏁 Script executed:

#!/bin/bash
# Find where reload_configuration is defined (parent class or concern)
rg -nP --type=ruby 'def reload_configuration' app/models/provider/

Length of output: 67


🏁 Script executed:

#!/bin/bash
# Check Provider base class
fd -e rb 'provider.rb' app/models/provider/

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for any field definitions with validation examples
rg -nP --type=ruby 'field.*required.*true' app/models/provider/ -A2 | head -30

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check the Configurable concern to understand field validation options
cat app/models/provider/configurable.rb

Length of output: 8285


🏁 Script executed:

#!/bin/bash
# Search for any options parameter in other field definitions
rg -nP --type=ruby 'field :' app/models/provider/ -A3 | grep -E 'field|options|validation' | head -40

Length of output: 640


Add validation for environment field to prevent KeyError at runtime.

The environment field accepts any string, but line 62 uses Plaid::Configuration::Environment[environment], which raises KeyError if the value is not "sandbox", "development", or "production". Currently, the Configurable framework does not support enum validation, so add error handling or validation logic.

  • app/models/provider/plaid_adapter.rb:62 – Wrap with error handling (try/catch) or validate before assignment
  • app/models/provider/plaid_eu_adapter.rb:54 – Same issue; apply the same fix

Consider either:

  1. Add try/catch around Plaid::Configuration::Environment[environment] to raise a meaningful error, or
  2. Validate the environment value in reload_configuration before calling Plaid::Configuration::Environment[], or
  3. Implement enum validation support in Provider::Configurable::ConfigField to reject invalid values

Comment on lines +38 to +44
field :environment,
label: "Environment",
required: false,
env_key: "PLAID_EU_ENV",
default: "sandbox",
description: "Plaid environment: sandbox, development, or production"
end
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate environment values to prevent runtime errors.

Similar to Provider::PlaidAdapter, the environment field accepts any string, but Line 54 uses Plaid::Configuration::Environment[environment], which may raise an error if an invalid value is provided.

Consider adding validation to the field definition or handling invalid values gracefully in reload_configuration (see review comment on app/models/provider/plaid_adapter.rb lines 42-48 for the same issue).

🤖 Prompt for AI Agents
In app/models/provider/plaid_eu_adapter.rb around lines 38 to 44, the
environment field currently accepts any string but later usage
(Plaid::Configuration::Environment[environment]) can raise on invalid values;
add validation to the field to restrict values to the allowed set ("sandbox",
"development", "production") (e.g., inclusion validator or enum) and/or make
reload_configuration defensively handle unknown values by mapping invalid inputs
to a safe default (like "sandbox") and logging a warning before using
Plaid::Configuration::Environment to avoid runtime errors.

Comment on lines +7 to 15
# Load Plaid configuration from adapters after initialization
Rails.application.config.after_initialize do
# Ensure provider adapters are loaded
Provider::Factory.ensure_adapters_loaded

if ENV["PLAID_EU_CLIENT_ID"].present? && ENV["PLAID_EU_SECRET"].present?
config.plaid_eu = Plaid::Configuration.new
config.plaid_eu.server_index = Plaid::Configuration::Environment[ENV["PLAID_ENV"] || "sandbox"]
config.plaid_eu.api_key["PLAID-CLIENT-ID"] = ENV["PLAID_EU_CLIENT_ID"]
config.plaid_eu.api_key["PLAID-SECRET"] = ENV["PLAID_EU_SECRET"]
end
# Reload configurations from settings/ENV
Provider::PlaidAdapter.reload_configuration # US region
Provider::PlaidEuAdapter.reload_configuration # EU region
end
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add error handling to prevent initialization failures.

If reload_configuration raises an exception (e.g., invalid environment value, network issue, or missing adapter constant), the application will fail to start. This is especially risky in production where a configuration error could cause downtime.

Apply this diff to wrap the reload calls in error handling:

 # Load Plaid configuration from adapters after initialization
 Rails.application.config.after_initialize do
   # Ensure provider adapters are loaded
   Provider::Factory.ensure_adapters_loaded

   # Reload configurations from settings/ENV
-  Provider::PlaidAdapter.reload_configuration      # US region
-  Provider::PlaidEuAdapter.reload_configuration    # EU region
+  begin
+    Provider::PlaidAdapter.reload_configuration      # US region
+  rescue => e
+    Rails.logger.error("Failed to load Plaid US configuration: #{e.message}")
+  end
+
+  begin
+    Provider::PlaidEuAdapter.reload_configuration    # EU region
+  rescue => e
+    Rails.logger.error("Failed to load Plaid EU configuration: #{e.message}")
+  end
 end
🤖 Prompt for AI Agents
In config/initializers/plaid.rb around lines 7 to 15, the calls to
Provider::PlaidAdapter.reload_configuration and
Provider::PlaidEuAdapter.reload_configuration run without error handling and can
raise exceptions that abort app startup; wrap each reload call in begin/rescue
that catches StandardError (or specific expected exceptions), log the error with
context (including adapter name and exception message/backtrace) using
Rails.logger.error, and continue startup (do not re-raise) so a configuration
failure for one adapter does not crash the application; ensure any sensitive
data is not logged and return a safe default or skip the adapter when errors
occur.

@coderabbitai coderabbitai bot mentioned this pull request Oct 30, 2025
thuutin added a commit to thuutin/sure that referenced this pull request Nov 2, 2025
…from-upstream

* 'main' of https://github.com/we-promise/sure: (60 commits)
  Add delay after disabling AI Assistant
  Avoid auto-categorizing without user categories (we-promise#261)
  Localization for an accountable group and sidebar (we-promise#257)
  Recurring transactions (we-promise#271)
  Lunch flow improvements (we-promise#268)
  Update ai.md (we-promise#263)
  Fix "invisible" merchants (we-promise#262)
  Lunchflow integration (we-promise#259)
  Build up to 0.6.5-alpha.6
  Update AI model recommendations section
  Update AI assistant documentation with version caution
  Add support for dynamic config UI (we-promise#256)
  Feature/yahoo finance (we-promise#123)
  Update pages/dashboard locales (we-promise#255)
  Providers factory (we-promise#250)
  Add onboarding state selector for self-hosted signup (we-promise#251)
  Added a clickable drop down arrow that reveals list of instructions for Brand Fetch Client ID (we-promise#246)
  Added a clickable drop down arrow that reveals list of instructions for Twelve Data Secret Key (we-promise#247)
  Add runServices for db and redis in devcontainer
  Add PikaPods
  ...

# Conflicts:
#	.devcontainer/docker-compose.yml
#	app/controllers/concerns/self_hostable.rb
#	app/models/provider/twelve_data.rb
#	app/views/pages/redis_configuration_error.html.erb
#	config/cable.yml
#	config/database.yml
#	config/initializers/sidekiq.rb
alessiocappa pushed a commit to alessiocappa/sure that referenced this pull request Nov 16, 2025
* Add support for dynamic config UI

* Add support for section description

* Better dynamic class settings

Added dynamic_fields hash field - Stores all undeclared settings
[] method - Checks declared fields first, then falls back to dynamic hash
[]= method - Updates declared fields normally, stores others in hash
No runtime field declaration - Fields are never dynamically created on the class

* FIX proper lookup for provider keys

- Also validate configurable values properly.
- Change Provider factory to use Rails autoloading (Zeitwerk)

* Fix factory

The derive_adapter_name method relies on string manipulation ("PlaidAccount".sub(/Account$/, "") + "Adapter" → "PlaidAdapter"), but we already have explicit registration in place.

* Make updates atomic, field-aware, and handle blanks explicitly

* Small UX detail

* Add support for PlaidEU in UI also

- This looks like partial support atm
alessiocappa pushed a commit to alessiocappa/sure that referenced this pull request Nov 17, 2025
* Add support for dynamic config UI

* Add support for section description

* Better dynamic class settings

Added dynamic_fields hash field - Stores all undeclared settings
[] method - Checks declared fields first, then falls back to dynamic hash
[]= method - Updates declared fields normally, stores others in hash
No runtime field declaration - Fields are never dynamically created on the class

* FIX proper lookup for provider keys

- Also validate configurable values properly.
- Change Provider factory to use Rails autoloading (Zeitwerk)

* Fix factory

The derive_adapter_name method relies on string manipulation ("PlaidAccount".sub(/Account$/, "") + "Adapter" → "PlaidAdapter"), but we already have explicit registration in place.

* Make updates atomic, field-aware, and handle blanks explicitly

* Small UX detail

* Add support for PlaidEU in UI also

- This looks like partial support atm
alessiocappa pushed a commit to alessiocappa/sure that referenced this pull request Nov 17, 2025
* Add support for dynamic config UI

* Add support for section description

* Better dynamic class settings

Added dynamic_fields hash field - Stores all undeclared settings
[] method - Checks declared fields first, then falls back to dynamic hash
[]= method - Updates declared fields normally, stores others in hash
No runtime field declaration - Fields are never dynamically created on the class

* FIX proper lookup for provider keys

- Also validate configurable values properly.
- Change Provider factory to use Rails autoloading (Zeitwerk)

* Fix factory

The derive_adapter_name method relies on string manipulation ("PlaidAccount".sub(/Account$/, "") + "Adapter" → "PlaidAdapter"), but we already have explicit registration in place.

* Make updates atomic, field-aware, and handle blanks explicitly

* Small UX detail

* Add support for PlaidEU in UI also

- This looks like partial support atm
@coderabbitai coderabbitai bot mentioned this pull request Nov 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Plaid config to new "Bank Sync" settings UI

2 participants