diff --git a/app/controllers/concerns/auto_sync.rb b/app/controllers/concerns/auto_sync.rb index 970eec0a..4e375359 100644 --- a/app/controllers/concerns/auto_sync.rb +++ b/app/controllers/concerns/auto_sync.rb @@ -7,7 +7,6 @@ module AutoSync private def sync_family - Current.family.update!(last_synced_at: Time.current) Current.family.sync_later end diff --git a/app/models/account.rb b/app/models/account.rb index 14dd85c6..acd013a8 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -93,8 +93,6 @@ class Account < ApplicationRecord end def sync_data(sync, start_date: nil) - update!(last_synced_at: Time.current) - Rails.logger.info("Processing balances (#{linked? ? 'reverse' : 'forward'})") sync_balances end diff --git a/app/models/concerns/syncable.rb b/app/models/concerns/syncable.rb index 0ae45122..d804b992 100644 --- a/app/models/concerns/syncable.rb +++ b/app/models/concerns/syncable.rb @@ -27,7 +27,11 @@ module Syncable end def sync_error - latest_sync.error + latest_sync&.error + end + + def last_synced_at + latest_sync&.last_ran_at end private diff --git a/app/models/family.rb b/app/models/family.rb index 0136d540..bb88495a 100644 --- a/app/models/family.rb +++ b/app/models/family.rb @@ -67,8 +67,6 @@ class Family < ApplicationRecord end def sync_data(sync, start_date: nil) - update!(last_synced_at: Time.current) - # We don't rely on this value to guard the app, but keep it eventually consistent sync_trial_status! @@ -77,16 +75,6 @@ class Family < ApplicationRecord account.sync_later(start_date: start_date, parent_sync: sync) end - Rails.logger.info("Syncing plaid items for family #{id}") - plaid_items.each do |plaid_item| - plaid_item.sync_later(start_date: start_date, parent_sync: sync) - end - - Rails.logger.info("Syncing simple_fin items for family #{id}") - simple_fin_items.each do |simple_fin_item| - simple_fin_item.sync_later(start_date: start_date, parent_sync: sync) - end - Rails.logger.info("Applying rules for family #{id}") rules.each do |rule| rule.apply_later diff --git a/app/models/plaid_item.rb b/app/models/plaid_item.rb index a527dc3e..2226f12f 100644 --- a/app/models/plaid_item.rb +++ b/app/models/plaid_item.rb @@ -38,8 +38,6 @@ class PlaidItem < ApplicationRecord end def sync_data(sync, start_date: nil) - update!(last_synced_at: Time.current) - begin Rails.logger.info("Fetching and loading Plaid data") fetch_and_load_plaid_data(sync) diff --git a/app/models/sync.rb b/app/models/sync.rb index 22b61829..4d923f12 100644 --- a/app/models/sync.rb +++ b/app/models/sync.rb @@ -21,51 +21,17 @@ class Sync < ApplicationRecord begin syncable.sync_data(self, start_date: start_date) - unless has_pending_child_syncs? - complete! - Rails.logger.info("Sync completed, starting post-sync") - syncable.post_sync(self) - Rails.logger.info("Post-sync completed") - end - rescue StandardError => error - fail! error - raise error if Rails.env.development? - ensure - notify_parent_of_completion! if has_parent? - end - end - end - - def handle_child_completion_event - Sync.transaction do - # We need this to ensure 2 child syncs don't update the parent at the exact same time with different results - # and cause the sync to hang in "syncing" status indefinitely - self.lock! - - unless has_pending_child_syncs? complete! - - # 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? - + Rails.logger.info("Sync completed, starting post-sync") syncable.post_sync(self) + Rails.logger.info("Post-sync completed") + rescue StandardError => error + fail! error, report_error: true end end end private - def has_pending_child_syncs? - children.where(status: [ :pending, :syncing ]).any? - end - - def has_parent? - parent_id.present? - end - - def notify_parent_of_completion! - parent.handle_child_completion_event - end - def start! Rails.logger.info("Starting sync") update! status: :syncing @@ -76,12 +42,14 @@ class Sync < ApplicationRecord update! status: :completed, last_ran_at: Time.current end - def fail!(error) + def fail!(error, report_error: false) Rails.logger.error("Sync failed: #{error.message}") - Sentry.capture_exception(error) do |scope| - scope.set_context("sync", { id: id, syncable_type: syncable_type, syncable_id: syncable_id }) - scope.set_tags(sync_id: id) + if report_error + Sentry.capture_exception(error) do |scope| + scope.set_context("sync", { id: id, syncable_type: syncable_type, syncable_id: syncable_id }) + scope.set_tags(sync_id: id) + end end update!( diff --git a/app/views/accounts/index.html.erb b/app/views/accounts/index.html.erb index 788848c0..c105ef08 100644 --- a/app/views/accounts/index.html.erb +++ b/app/views/accounts/index.html.erb @@ -2,15 +2,16 @@

<%= t(".accounts") %>

- <%= render ButtonComponent.new( - text: "Sync all", - href: sync_all_accounts_path, - method: :post, - variant: "outline", - disabled: Current.family.syncing?, - icon: "refresh-cw", - class: "" - ) %> + <% if Rails.env.development? %> + <%= render ButtonComponent.new( + text: "Sync all", + href: sync_all_accounts_path, + method: :post, + variant: "outline", + disabled: Current.family.syncing?, + icon: "refresh-cw", + ) %> + <% end %> <%= render LinkComponent.new( text: "New account", diff --git a/app/views/accounts/show/_header.html.erb b/app/views/accounts/show/_header.html.erb index de7dd316..4d6bd38a 100644 --- a/app/views/accounts/show/_header.html.erb +++ b/app/views/accounts/show/_header.html.erb @@ -28,13 +28,12 @@ <% end %>
- <% if account.plaid_account_id.present? %> - <% if Rails.env.development? %> - <%= icon( + <% if Rails.env.development? %> + <%= icon( "refresh-cw", as_button: true, size: "sm", - href: sync_plaid_item_path(account.plaid_account.plaid_item), + href: account.linked? ? sync_plaid_item_path(account.plaid_account.plaid_item) : sync_account_path(account), disabled: account.syncing?, frame: :_top, title: "Refresh all SimpleFIN accounts" diff --git a/app/views/plaid_items/_plaid_item.html.erb b/app/views/plaid_items/_plaid_item.html.erb index 7c3dc8b2..61dea7dc 100644 --- a/app/views/plaid_items/_plaid_item.html.erb +++ b/app/views/plaid_items/_plaid_item.html.erb @@ -92,7 +92,7 @@
<% end %> - <% else %> + <% elsif Rails.env.development? %> <%= icon( "refresh-cw", as_button: true, diff --git a/db/migrate/20250509182903_dynamic_last_synced.rb b/db/migrate/20250509182903_dynamic_last_synced.rb new file mode 100644 index 00000000..6ade36eb --- /dev/null +++ b/db/migrate/20250509182903_dynamic_last_synced.rb @@ -0,0 +1,7 @@ +class DynamicLastSynced < ActiveRecord::Migration[7.2] + def change + remove_column :plaid_items, :last_synced_at, :datetime + remove_column :accounts, :last_synced_at, :datetime + remove_column :families, :last_synced_at, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index 2914efb7..b4f87232 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2025_05_09_134646) do +ActiveRecord::Schema[7.2].define(version: 2025_05_09_182903) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" @@ -34,7 +34,6 @@ ActiveRecord::Schema[7.2].define(version: 2025_05_09_134646) do t.uuid "import_id" t.uuid "plaid_account_id" t.boolean "scheduled_for_deletion", default: false - t.datetime "last_synced_at" t.decimal "cash_balance", precision: 19, scale: 4, default: "0.0" t.jsonb "locked_attributes", default: {} t.bigint "simple_fin_account_id" @@ -230,7 +229,6 @@ ActiveRecord::Schema[7.2].define(version: 2025_05_09_134646) do t.string "stripe_customer_id" t.string "date_format", default: "%m-%d-%Y" t.string "country", default: "US" - t.datetime "last_synced_at" t.string "timezone" t.boolean "data_enrichment_enabled", default: false t.boolean "early_access", default: false @@ -453,7 +451,6 @@ ActiveRecord::Schema[7.2].define(version: 2025_05_09_134646) do t.datetime "updated_at", null: false t.string "available_products", default: [], array: true t.string "billed_products", default: [], array: true - t.datetime "last_synced_at" t.string "plaid_region", default: "us", null: false t.string "institution_url" t.string "institution_id" diff --git a/test/fixtures/families.yml b/test/fixtures/families.yml index 9c6790b9..10d5bd18 100644 --- a/test/fixtures/families.yml +++ b/test/fixtures/families.yml @@ -1,8 +1,5 @@ empty: name: Family - last_synced_at: <%= Time.now %> dylan_family: name: The Dylan Family - last_synced_at: <%= Time.now %> - diff --git a/test/models/family_test.rb b/test/models/family_test.rb index 7223e64b..24876a77 100644 --- a/test/models/family_test.rb +++ b/test/models/family_test.rb @@ -20,10 +20,6 @@ class FamilyTest < ActiveSupport::TestCase .with(start_date: nil, parent_sync: family_sync) .times(manual_accounts_count) - PlaidItem.any_instance.expects(:sync_later) - .with(start_date: nil, parent_sync: family_sync) - .times(items_count) - @syncable.sync_data(family_sync, start_date: family_sync.start_date) end end diff --git a/test/models/sync_test.rb b/test/models/sync_test.rb index 6b246a1f..99019146 100644 --- a/test/models/sync_test.rb +++ b/test/models/sync_test.rb @@ -31,44 +31,4 @@ class SyncTest < ActiveSupport::TestCase assert_equal "failed", @sync.status assert_equal "test sync error", @sync.error end - - # Order is important here. Parent syncs must implement sync_data so that their own work - # is 100% complete *prior* to queueing up child syncs. - test "runs sync with child syncs" do - family = families(:dylan_family) - - parent = 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) - - parent.syncable.expects(:sync_data).returns([]).once - child1.syncable.expects(:sync_data).returns([]).once - child2.syncable.expects(:sync_data).returns([]).once - grandchild.syncable.expects(:sync_data).returns([]).once - - assert_equal "pending", parent.status - assert_equal "pending", child1.status - assert_equal "pending", child2.status - assert_equal "pending", grandchild.status - - parent.perform - assert_equal "syncing", parent.reload.status - - child1.perform - assert_equal "completed", child1.reload.status - assert_equal "syncing", parent.reload.status - - child2.perform - assert_equal "syncing", child2.reload.status - assert_equal "completed", child1.reload.status - assert_equal "syncing", parent.reload.status - - # Will complete the parent and grandparent syncs - grandchild.perform - assert_equal "completed", grandchild.reload.status - assert_equal "completed", child1.reload.status - assert_equal "completed", child2.reload.status - assert_equal "completed", parent.reload.status - end end