mirror of
https://github.com/we-promise/sure.git
synced 2026-04-07 14:31:25 +00:00
Include newer providers in automatic family sync (#934)
* Include newer providers in automatic family sync
Coinbase, CoinStats, Mercury, and SnapTrade all implement Syncable
and have Syncer classes but were not listed in child_syncables,
meaning their data only refreshed on manual sync button clicks.
* refactor(syncer): Open/Closed principle for provider sync
- Adding new providers requires modifying child_syncables (violates O/C)
- plaid_items missing .active scope (bug: syncs deleted items)
- snaptrade_items can exist without user registration → fails on sync
- Scattered knowledge about 'ready to sync' logic
1. **Registry pattern**: SYNCABLE_ITEM_ASSOCIATIONS constant lists all
provider associations that participate in family sync
2. **Encapsulated sync-readiness**: Each item model defines its own
`syncable` scope that knows when it's ready for auto-sync:
- Most providers: `syncable = active` (not scheduled for deletion)
- SnapTrade: `syncable = active + user_registered` (has API creds)
3. **Single loop**: child_syncables iterates the registry, calling
`.syncable` on each association
- Adding a provider = add to registry + define syncable scope
- Each model owns its 'ready to sync' business logic
- Fixes plaid_items bug (now uses .active via .syncable)
- Fixes snaptrade auto-sync failures (filters unregistered items)
- Easy to extend with new conditions per provider
- family/syncer.rb: Registry + dynamic collection
- *_item.rb (7 files): Add `scope :syncable, -> { active }`
- snaptrade_item.rb: Add syncable with user_registered filter
* Fix rubocop bracket spacing in SnaptradeItem syncable scope
This commit is contained in:
@@ -30,6 +30,7 @@ class CoinbaseItem < ApplicationRecord
|
|||||||
has_many :accounts, through: :coinbase_accounts
|
has_many :accounts, through: :coinbase_accounts
|
||||||
|
|
||||||
scope :active, -> { where(scheduled_for_deletion: false) }
|
scope :active, -> { where(scheduled_for_deletion: false) }
|
||||||
|
scope :syncable, -> { active }
|
||||||
scope :ordered, -> { order(created_at: :desc) }
|
scope :ordered, -> { order(created_at: :desc) }
|
||||||
scope :needs_update, -> { where(status: :requires_update) }
|
scope :needs_update, -> { where(status: :requires_update) }
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class CoinstatsItem < ApplicationRecord
|
|||||||
has_many :accounts, through: :coinstats_accounts
|
has_many :accounts, through: :coinstats_accounts
|
||||||
|
|
||||||
scope :active, -> { where(scheduled_for_deletion: false) }
|
scope :active, -> { where(scheduled_for_deletion: false) }
|
||||||
|
scope :syncable, -> { active }
|
||||||
scope :ordered, -> { order(created_at: :desc) }
|
scope :ordered, -> { order(created_at: :desc) }
|
||||||
scope :needs_update, -> { where(status: :requires_update) }
|
scope :needs_update, -> { where(status: :requires_update) }
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class EnableBankingItem < ApplicationRecord
|
|||||||
has_many :accounts, through: :enable_banking_accounts
|
has_many :accounts, through: :enable_banking_accounts
|
||||||
|
|
||||||
scope :active, -> { where(scheduled_for_deletion: false) }
|
scope :active, -> { where(scheduled_for_deletion: false) }
|
||||||
|
scope :syncable, -> { active }
|
||||||
scope :ordered, -> { order(created_at: :desc) }
|
scope :ordered, -> { order(created_at: :desc) }
|
||||||
scope :needs_update, -> { where(status: :requires_update) }
|
scope :needs_update, -> { where(status: :requires_update) }
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,25 @@
|
|||||||
class Family::Syncer
|
class Family::Syncer
|
||||||
attr_reader :family
|
attr_reader :family
|
||||||
|
|
||||||
|
# Registry of item association names that participate in family sync.
|
||||||
|
# Each model must:
|
||||||
|
# 1. Include Syncable
|
||||||
|
# 2. Define a `syncable` scope (items ready for auto-sync)
|
||||||
|
#
|
||||||
|
# To add a new provider: add its association name here.
|
||||||
|
# The model handles its own "ready to sync" logic via the syncable scope.
|
||||||
|
SYNCABLE_ITEM_ASSOCIATIONS = %i[
|
||||||
|
plaid_items
|
||||||
|
simplefin_items
|
||||||
|
lunchflow_items
|
||||||
|
enable_banking_items
|
||||||
|
indexa_capital_items
|
||||||
|
coinbase_items
|
||||||
|
coinstats_items
|
||||||
|
mercury_items
|
||||||
|
snaptrade_items
|
||||||
|
].freeze
|
||||||
|
|
||||||
def initialize(family)
|
def initialize(family)
|
||||||
@family = family
|
@family = family
|
||||||
end
|
end
|
||||||
@@ -25,7 +44,15 @@ class Family::Syncer
|
|||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
# Collects all syncable items from registered providers + manual accounts.
|
||||||
|
# Each provider model defines its own `syncable` scope that encapsulates
|
||||||
|
# the "ready to sync" business logic (active, configured, etc.)
|
||||||
def child_syncables
|
def child_syncables
|
||||||
family.plaid_items + family.simplefin_items.active + family.lunchflow_items.active + family.enable_banking_items.active + family.indexa_capital_items + family.accounts.manual
|
provider_items = SYNCABLE_ITEM_ASSOCIATIONS.flat_map do |association|
|
||||||
|
family.public_send(association).syncable
|
||||||
|
end
|
||||||
|
|
||||||
|
provider_items + family.accounts.manual
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class IndexaCapitalItem < ApplicationRecord
|
|||||||
has_many :accounts, through: :indexa_capital_accounts
|
has_many :accounts, through: :indexa_capital_accounts
|
||||||
|
|
||||||
scope :active, -> { where(scheduled_for_deletion: false) }
|
scope :active, -> { where(scheduled_for_deletion: false) }
|
||||||
|
scope :syncable, -> { active }
|
||||||
scope :ordered, -> { order(created_at: :desc) }
|
scope :ordered, -> { order(created_at: :desc) }
|
||||||
scope :needs_update, -> { where(status: :requires_update) }
|
scope :needs_update, -> { where(status: :requires_update) }
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class LunchflowItem < ApplicationRecord
|
|||||||
has_many :accounts, through: :lunchflow_accounts
|
has_many :accounts, through: :lunchflow_accounts
|
||||||
|
|
||||||
scope :active, -> { where(scheduled_for_deletion: false) }
|
scope :active, -> { where(scheduled_for_deletion: false) }
|
||||||
|
scope :syncable, -> { active }
|
||||||
scope :ordered, -> { order(created_at: :desc) }
|
scope :ordered, -> { order(created_at: :desc) }
|
||||||
scope :needs_update, -> { where(status: :requires_update) }
|
scope :needs_update, -> { where(status: :requires_update) }
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class MercuryItem < ApplicationRecord
|
|||||||
has_many :accounts, through: :mercury_accounts
|
has_many :accounts, through: :mercury_accounts
|
||||||
|
|
||||||
scope :active, -> { where(scheduled_for_deletion: false) }
|
scope :active, -> { where(scheduled_for_deletion: false) }
|
||||||
|
scope :syncable, -> { active }
|
||||||
scope :ordered, -> { order(created_at: :desc) }
|
scope :ordered, -> { order(created_at: :desc) }
|
||||||
scope :needs_update, -> { where(status: :requires_update) }
|
scope :needs_update, -> { where(status: :requires_update) }
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class PlaidItem < ApplicationRecord
|
|||||||
has_many :legacy_accounts, through: :plaid_accounts, source: :account
|
has_many :legacy_accounts, through: :plaid_accounts, source: :account
|
||||||
|
|
||||||
scope :active, -> { where(scheduled_for_deletion: false) }
|
scope :active, -> { where(scheduled_for_deletion: false) }
|
||||||
|
scope :syncable, -> { active }
|
||||||
scope :ordered, -> { order(created_at: :desc) }
|
scope :ordered, -> { order(created_at: :desc) }
|
||||||
scope :needs_update, -> { where(status: :requires_update) }
|
scope :needs_update, -> { where(status: :requires_update) }
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class SimplefinItem < ApplicationRecord
|
|||||||
has_many :legacy_accounts, through: :simplefin_accounts, source: :account
|
has_many :legacy_accounts, through: :simplefin_accounts, source: :account
|
||||||
|
|
||||||
scope :active, -> { where(scheduled_for_deletion: false) }
|
scope :active, -> { where(scheduled_for_deletion: false) }
|
||||||
|
scope :syncable, -> { active }
|
||||||
scope :ordered, -> { order(created_at: :desc) }
|
scope :ordered, -> { order(created_at: :desc) }
|
||||||
scope :needs_update, -> { where(status: :requires_update) }
|
scope :needs_update, -> { where(status: :requires_update) }
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ class SnaptradeItem < ApplicationRecord
|
|||||||
has_many :linked_accounts, through: :snaptrade_accounts
|
has_many :linked_accounts, through: :snaptrade_accounts
|
||||||
|
|
||||||
scope :active, -> { where(scheduled_for_deletion: false) }
|
scope :active, -> { where(scheduled_for_deletion: false) }
|
||||||
|
# Syncable = active + fully configured (user registered with SnapTrade API)
|
||||||
|
# Items without user registration will fail sync, so exclude them from auto-sync
|
||||||
|
scope :syncable, -> { active.where.not(snaptrade_user_id: [ nil, "" ]).where.not(snaptrade_user_secret: [ nil, "" ]) }
|
||||||
scope :ordered, -> { order(created_at: :desc) }
|
scope :ordered, -> { order(created_at: :desc) }
|
||||||
scope :needs_update, -> { where(status: :requires_update) }
|
scope :needs_update, -> { where(status: :requires_update) }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user