Files
sure/app/models/concerns/syncable.rb
LPW c391ba2b23 Harden SimpleFIN sync: protect user data, fix stuck syncs, optimize API calls (#671)
* Implement entry protection flags for sync overwrites

- Added `user_modified` and `import_locked` flags to `entries` table to prevent provider sync from overwriting user-edited and imported data.
- Introduced backfill migration to mark existing entries based on conditions.
- Enhanced sync and processing logic to respect protection flags, track skipped entries, and log detailed stats.
- Updated UI to display skipped/protected entries and reasons in sync summaries.

* Localize error details summary text and adjust `sync_account_later` method placement

* Restored schema.rb

---------

Co-authored-by: luckyPipewrench <luckypipewrench@proton.me>
2026-01-16 12:34:06 +01:00

87 lines
2.1 KiB
Ruby

module Syncable
extend ActiveSupport::Concern
included do
has_many :syncs, as: :syncable, dependent: :destroy
end
def syncing?
syncs.visible.any?
end
# Schedules a sync for syncable. If there is an existing sync pending/syncing for this syncable,
# we do not create a new sync, and attempt to expand the sync window if needed.
#
# NOTE: Uses `visible` scope (syncs < 5 min old) instead of `incomplete` to prevent
# getting stuck on stale syncs after server/Sidekiq restarts. If a sync is older than
# 5 minutes, we assume its job was lost and create a new sync.
def sync_later(parent_sync: nil, window_start_date: nil, window_end_date: nil)
Sync.transaction do
with_lock do
sync = self.syncs.visible.first
if sync
Rails.logger.info("There is an existing recent sync, expanding window if needed (#{sync.id})")
sync.expand_window_if_needed(window_start_date, window_end_date)
# Update parent relationship if one is provided and sync doesn't already have a parent
if parent_sync && !sync.parent_id
sync.update!(parent: parent_sync)
end
else
sync = self.syncs.create!(
parent: parent_sync,
window_start_date: window_start_date,
window_end_date: window_end_date
)
SyncJob.perform_later(sync)
end
sync
end
end
end
def perform_sync(sync)
syncer.perform_sync(sync)
end
def perform_post_sync
syncer.perform_post_sync
end
def broadcast_sync_complete
sync_broadcaster.broadcast
end
def sync_error
latest_sync&.error || latest_sync&.children&.map(&:error)&.compact&.first
end
def last_synced_at
latest_completed_sync&.completed_at
end
def last_sync_created_at
latest_sync&.created_at
end
private
def latest_sync
syncs.ordered.first
end
def latest_completed_sync
syncs.completed.ordered.first
end
def syncer
self.class::Syncer.new(self)
end
def sync_broadcaster
self.class::SyncCompleteEvent.new(self)
end
end