Providers factory (#250)

* Implement providers factory

* Multiple providers sync support

- Proper Multi-Provider Syncing: When you click sync on an account with multiple providers (e.g., both Plaid and SimpleFin), all provider items are synced
- Better API: The existing account.providers method already returns all providers, and account.provider returns the first one for backward compatibility
- Correct Holdings Deletion Logic: Holdings can only be deleted if ALL providers allow it, preventing accidental deletions that would be recreated on next sync
TODO: validate this is the way we want to go? We would need to check holdings belong to which account, and then check provider allows deletion. More complex
- Database Constraints: The existing validations ensure an account can have at most one provider of each type (one PlaidAccount, one SimplefinAccount, etc.)

* Add generic provider_import_adapter

* Finish unified import strategy

* Update app/models/plaid_account.rb

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: soky srm <sokysrm@gmail.com>

* Update app/models/provider/factory.rb

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: soky srm <sokysrm@gmail.com>

* Fix account linked by plaid_id instead of external_id

* Parse numerics to BigDecimal

Parse numerics to BigDecimal before computing amount; guard nils.

Avoid String * String and float drift; also normalize date.

* Fix incorrect usage of assert_raises.

* Fix linter

* Fix processor test.

* Update current_balance_manager.rb

* Test fixes

* Fix plaid linked account test

* Add support for holding per account_provider

* Fix proper account access

Also fix account deletion for simpefin too

* FIX match tests for consistency

* Some more factory updates

* Fix account schema for multipe providers

  Can do:
  - Account #1 → PlaidAccount + SimplefinAccount (multiple different providers)
  - Account #2 → PlaidAccount only
  - Account #3 → SimplefinAccount only

  Cannot do:
  - Account #1 → PlaidAccount + PlaidAccount (duplicate provider type)
  - PlaidAccount #123 → Account #1 + Account #2 (provider linked to multiple accounts)

* Fix account setup

- An account CAN have multiple providers (the schema shows account_providers with unique index on [account_id, provider_type])
  - Each provider should maintain its own separate entries
  - We should NOT update one provider's entry when another provider syncs

* Fix linter and guard migration

* FIX linter issues.

* Fixes

- Remove duplicated index
- Pass account_provider_id
- Guard holdings call to avoid NoMethodError

* Update schema and provider import fix

* Plaid doesn't allow holdings deletion

* Use ClimateControl for proper env setup

* No need for this in .git

---------

Signed-off-by: soky srm <sokysrm@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
This commit is contained in:
soky srm
2025-10-28 19:32:27 +01:00
committed by GitHub
parent 72e7d7736b
commit 4fb0a3856e
67 changed files with 2338 additions and 315 deletions

View File

@@ -0,0 +1,66 @@
class Provider::Factory
class << self
# Register a provider adapter
# @param provider_type [String] The provider account class name (e.g., "PlaidAccount")
# @param adapter_class [Class] The adapter class (e.g., Provider::PlaidAdapter)
def register(provider_type, adapter_class)
registry[provider_type] = adapter_class
end
# Creates an adapter for a given provider account
# @param provider_account [PlaidAccount, SimplefinAccount] The provider-specific account
# @param account [Account] Optional account reference
# @return [Provider::Base] An adapter instance
def create_adapter(provider_account, account: nil)
return nil if provider_account.nil?
provider_type = provider_account.class.name
adapter_class = registry[provider_type]
# If not registered, try to load the adapter
if adapter_class.nil?
ensure_adapters_loaded
adapter_class = registry[provider_type]
end
raise ArgumentError, "Unknown provider type: #{provider_type}. Did you forget to register it?" unless adapter_class
adapter_class.new(provider_account, account: account)
end
# Creates an adapter from an AccountProvider record
# @param account_provider [AccountProvider] The account provider record
# @return [Provider::Base] An adapter instance
def from_account_provider(account_provider)
return nil if account_provider.nil?
create_adapter(account_provider.provider, account: account_provider.account)
end
# Get list of registered provider types
# @return [Array<String>] List of registered provider type names
def registered_provider_types
ensure_adapters_loaded
registry.keys
end
private
def registry
@registry ||= {}
end
# Ensures all provider adapters are loaded
# This is needed for Rails autoloading in development/test environments
def ensure_adapters_loaded
return if @adapters_loaded
# Require all adapter files to trigger registration
Dir[Rails.root.join("app/models/provider/*_adapter.rb")].each do |file|
require_dependency file
end
@adapters_loaded = true
end
end
end