Files
sure/app/models/family/syncer.rb
David Gil 28d99a2b0d 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
2026-02-10 23:42:22 +01:00

59 lines
1.6 KiB
Ruby

class Family::Syncer
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)
@family = family
end
def perform_sync(sync)
# We don't rely on this value to guard the app, but keep it eventually consistent
family.sync_trial_status!
# Schedule child syncs
child_syncables.each do |syncable|
syncable.sync_later(parent_sync: sync, window_start_date: sync.window_start_date, window_end_date: sync.window_end_date)
end
end
def perform_post_sync
family.auto_match_transfers!
Rails.logger.info("Applying rules for family #{family.id}")
family.rules.where(active: true).each do |rule|
rule.apply_later
end
end
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
provider_items = SYNCABLE_ITEM_ASSOCIATIONS.flat_map do |association|
family.public_send(association).syncable
end
provider_items + family.accounts.manual
end
end