1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-10 16:05:22 +02:00

Move sync orchestration to Sync class

This commit is contained in:
Zach Gollwitzer 2025-05-10 21:04:59 -04:00
parent 6ddb4d088e
commit 5432b3903d
6 changed files with 91 additions and 82 deletions

View file

@ -5,19 +5,23 @@ class Account::Syncer
@account = account @account = account
end end
def child_syncables
[]
end
def perform_sync(start_date: nil) def perform_sync(start_date: nil)
Rails.logger.info("Processing balances (#{account.linked? ? 'reverse' : 'forward'})") Rails.logger.info("Processing balances (#{account.linked? ? 'reverse' : 'forward'})")
sync_balances sync_balances
end end
def perform_post_sync(sync) def perform_post_sync
account.family.remove_syncing_notice! account.family.remove_syncing_notice!
account.accountable.post_sync(sync) # account.accountable.post_sync(sync)
unless sync.child? # unless sync.child?
account.family.auto_match_transfers! account.family.auto_match_transfers!
end # end
end end
private private

View file

@ -5,27 +5,21 @@ class Family::Syncer
@family = family @family = family
end end
def perform_sync(sync, start_date: nil) def child_syncables
family.plaid_items + family.accounts.manual
end
def perform_sync(start_date: nil)
# We don't rely on this value to guard the app, but keep it eventually consistent # We don't rely on this value to guard the app, but keep it eventually consistent
family.sync_trial_status! family.sync_trial_status!
Rails.logger.info("Syncing accounts for family #{family.id}")
family.accounts.manual.each do |account|
account.sync_later(start_date: start_date, parent_sync: sync)
end
Rails.logger.info("Syncing plaid items for family #{family.id}")
family.plaid_items.each do |plaid_item|
plaid_item.sync_later(start_date: start_date, parent_sync: sync)
end
Rails.logger.info("Applying rules for family #{family.id}") Rails.logger.info("Applying rules for family #{family.id}")
family.rules.each do |rule| family.rules.each do |rule|
rule.apply_later rule.apply_later
end end
end end
def perform_post_sync(sync) def perform_post_sync
family.auto_match_transfers! family.auto_match_transfers!
family.broadcast_refresh family.broadcast_refresh
end end

View file

@ -5,17 +5,16 @@ class PlaidItem::Syncer
@plaid_item = plaid_item @plaid_item = plaid_item
end end
def perform_sync(sync, start_date: nil) def child_syncables
plaid_item.accounts
end
def perform_sync(start_date: nil)
begin begin
Rails.logger.info("Fetching and loading Plaid data") Rails.logger.info("Fetching and loading Plaid data")
fetch_and_load_plaid_data fetch_and_load_plaid_data
plaid_item.update!(status: :good) if plaid_item.requires_update? plaid_item.update!(status: :good) if plaid_item.requires_update?
# Schedule account syncs
plaid_item.accounts.each do |account|
account.sync_later(start_date: start_date, parent_sync: sync)
end
Rails.logger.info("Plaid data fetched and loaded") Rails.logger.info("Plaid data fetched and loaded")
rescue Plaid::ApiError => e rescue Plaid::ApiError => e
handle_plaid_error(e) handle_plaid_error(e)
@ -23,7 +22,7 @@ class PlaidItem::Syncer
end end
end end
def perform_post_sync(sync) def perform_post_sync
plaid_item.auto_match_categories! plaid_item.auto_match_categories!
plaid_item.family.broadcast_refresh plaid_item.family.broadcast_refresh
end end

View file

@ -19,12 +19,17 @@ class Sync < ApplicationRecord
start! start!
begin begin
syncer.perform_sync(self, start_date: start_date) syncer.perform_sync(start_date: start_date)
# Schedule child syncables to sync later
syncer.child_syncables.each do |child_syncable|
child_syncable.sync_later(start_date: start_date, parent_sync: self)
end
unless has_pending_child_syncs? unless has_pending_child_syncs?
complete! complete!
Rails.logger.info("Sync completed, starting post-sync") Rails.logger.info("Sync completed, starting post-sync")
syncer.perform_post_sync(self) syncer.perform_post_sync
Rails.logger.info("Post-sync completed") Rails.logger.info("Post-sync completed")
end end
rescue StandardError => error rescue StandardError => error
@ -51,7 +56,7 @@ class Sync < ApplicationRecord
# If this sync is both a child and a parent, we need to notify the parent of completion # If this sync is both a child and a parent, we need to notify the parent of completion
notify_parent_of_completion! if has_parent? notify_parent_of_completion! if has_parent?
syncer.perform_post_sync(self) syncer.perform_post_sync
end end
end end
end end

View file

@ -11,14 +11,9 @@ class Family::SyncerTest < ActiveSupport::TestCase
manual_accounts_count = @family.accounts.manual.count manual_accounts_count = @family.accounts.manual.count
items_count = @family.plaid_items.count items_count = @family.plaid_items.count
Account.any_instance.expects(:sync_later) syncer = Family::Syncer.new(@family)
.with(start_date: nil, parent_sync: family_sync) syncer.perform_sync(start_date: family_sync.start_date)
.times(manual_accounts_count)
PlaidItem.any_instance.expects(:sync_later) assert_equal manual_accounts_count + items_count, syncer.child_syncables.count
.with(start_date: nil, parent_sync: family_sync)
.times(items_count)
Family::Syncer.new(@family).perform_sync(family_sync, start_date: family_sync.start_date)
end end
end end

View file

@ -1,74 +1,86 @@
require "test_helper" require "test_helper"
class SyncTest < ActiveSupport::TestCase class SyncTest < ActiveSupport::TestCase
setup do include ActiveJob::TestHelper
@sync = syncs(:account)
@sync.update(status: "pending")
end
test "runs successful sync" do test "runs successful sync" do
Account::Syncer.any_instance.expects(:perform_sync).with(@sync, start_date: @sync.start_date).once sync = Sync.create!(syncable: accounts(:depository), last_ran_at: 1.day.ago)
assert_equal "pending", @sync.status Account::Syncer.any_instance.expects(:perform_sync).with(start_date: sync.start_date).once
previously_ran_at = @sync.last_ran_at assert_equal "pending", sync.status
@sync.perform previously_ran_at = sync.last_ran_at
assert @sync.last_ran_at > previously_ran_at sync.perform
assert_equal "completed", @sync.status
assert sync.last_ran_at > previously_ran_at
assert_equal "completed", sync.status
end end
test "handles sync errors" do test "handles sync errors" do
Account::Syncer.any_instance.expects(:perform_sync).with(@sync, start_date: @sync.start_date).raises(StandardError.new("test sync error")) sync = Sync.create!(syncable: accounts(:depository), last_ran_at: 1.day.ago)
Account::Syncer.any_instance.expects(:perform_sync).with(start_date: sync.start_date).raises(StandardError.new("test sync error"))
assert_equal "pending", @sync.status assert_equal "pending", sync.status
previously_ran_at = @sync.last_ran_at previously_ran_at = sync.last_ran_at
@sync.perform sync.perform
assert @sync.last_ran_at > previously_ran_at assert sync.last_ran_at > previously_ran_at
assert_equal "failed", @sync.status assert_equal "failed", sync.status
assert_equal "test sync error", @sync.error assert_equal "test sync error", sync.error
end end
# Order is important here. Parent syncs must implement sync_data so that their own work test "can run nested syncs that alert the parent when complete" do
# is 100% complete *prior* to queueing up child syncs. # Clear out fixture syncs
test "runs sync with child syncs" do Sync.destroy_all
# These fixtures represent a Parent -> Child -> Grandchild sync hierarchy
# Family -> PlaidItem -> Account
family = families(:dylan_family) family = families(:dylan_family)
plaid_item = plaid_items(:one)
account = accounts(:connected)
parent = Sync.create!(syncable: family) sync = Sync.create!(syncable: family)
child1 = Sync.create!(syncable: family.accounts.first, parent: parent)
child2 = Sync.create!(syncable: family.accounts.second, parent: parent)
grandchild = Sync.create!(syncable: family.accounts.last, parent: child2)
Family::Syncer.any_instance.expects(:perform_sync).with(parent, start_date: parent.start_date).once Family::Syncer.any_instance.expects(:perform_sync).with(start_date: sync.start_date).once
Account::Syncer.any_instance.expects(:perform_sync).with(child1, start_date: parent.start_date).once Family::Syncer.any_instance.expects(:perform_post_sync).once
Account::Syncer.any_instance.expects(:perform_sync).with(child2, start_date: parent.start_date).once Family::Syncer.any_instance.expects(:child_syncables).returns([ plaid_item ])
Account::Syncer.any_instance.expects(:perform_sync).with(grandchild, start_date: parent.start_date).once
assert_equal "pending", parent.status PlaidItem::Syncer.any_instance.expects(:perform_sync).with(start_date: sync.start_date).once
assert_equal "pending", child1.status PlaidItem::Syncer.any_instance.expects(:perform_post_sync).once
assert_equal "pending", child2.status PlaidItem::Syncer.any_instance.expects(:child_syncables).returns([ account ])
assert_equal "pending", grandchild.status
parent.perform Account::Syncer.any_instance.expects(:perform_sync).with(start_date: sync.start_date).once
assert_equal "syncing", parent.reload.status Account::Syncer.any_instance.expects(:perform_post_sync).once
Account::Syncer.any_instance.expects(:child_syncables).returns([])
child1.perform sync.perform
assert_equal "completed", child1.reload.status
assert_equal "syncing", parent.reload.status
child2.perform assert_equal 1, family.syncs.count
assert_equal "syncing", child2.reload.status assert_equal "syncing", family.syncs.first.status
assert_equal "completed", child1.reload.status assert_equal 1, plaid_item.syncs.count
assert_equal "syncing", parent.reload.status assert_equal "pending", plaid_item.syncs.first.status
# Will complete the parent and grandparent syncs # We have to perform jobs 2x because the child sync will schedule the grandchild sync,
grandchild.perform # which then needs to be run.
assert_equal "completed", grandchild.reload.status perform_enqueued_jobs
assert_equal "completed", child1.reload.status
assert_equal "completed", child2.reload.status assert_equal 1, family.syncs.count
assert_equal "completed", parent.reload.status assert_equal "syncing", family.syncs.first.status
assert_equal 1, plaid_item.syncs.count
assert_equal "syncing", plaid_item.syncs.first.status
assert_equal 1, account.syncs.count
assert_equal "pending", account.syncs.first.status
perform_enqueued_jobs
assert_equal 1, family.syncs.count
assert_equal "completed", family.syncs.first.status
assert_equal 1, plaid_item.syncs.count
assert_equal "completed", plaid_item.syncs.first.status
assert_equal 1, account.syncs.count
assert_equal "completed", account.syncs.first.status
end end
end end