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 @@
- <%= 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