1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-08 06:55:21 +02:00

fixes: Merge request fixes

- Changing to the new syncable structure
- Fixing missing account error display
- Shuffling some code around
This commit is contained in:
Cameron Roudebush 2025-05-18 09:40:59 -04:00
parent 3f11e357c3
commit 7e05681144
8 changed files with 122 additions and 62 deletions

View file

@ -70,7 +70,7 @@ class Account < ApplicationRecord
# Since Plaid Items sync as a "group", if the item is syncing, even if the account
# sync hasn't yet started (i.e. we're still fetching the Plaid data), show it as syncing in UI.
if linked?
plaid_account&.plaid_item&.syncing? || self_syncing
plaid_account&.plaid_item&.syncing? || simple_fin_account&.simple_fin_item&.syncing? || self_syncing
else
self_syncing
end

View file

@ -1,5 +1,5 @@
class Family < ApplicationRecord
include PlaidConnectable, Syncable, AutoTransferMatchable, Subscribeable
include PlaidConnectable, SimpleFinConnectable, Syncable, AutoTransferMatchable, Subscribeable
DATE_FORMATS = [
[ "MM-DD-YYYY", "%m-%d-%Y" ],
@ -38,6 +38,7 @@ class Family < ApplicationRecord
# If any accounts or plaid items are syncing, the family is also syncing, even if a formal "Family Sync" is not running.
def syncing?
Sync.joins("LEFT JOIN plaid_items ON plaid_items.id = syncs.syncable_id AND syncs.syncable_type = 'PlaidItem'")
.joins("LEFT JOIN simple_fin_items ON simple_fin_items.id = syncs.syncable_id AND syncs.syncable_type = 'SimpleFinItem'")
.joins("LEFT JOIN accounts ON accounts.id = syncs.syncable_id AND syncs.syncable_type = 'Account'")
.where("syncs.syncable_id = ? OR accounts.family_id = ? OR plaid_items.family_id = ?", id, id, id)
.visible

View file

@ -0,0 +1,15 @@
module Family::SimpleFinConnectable
extend ActiveSupport::Concern
included do
has_many :simple_fin_items, dependent: :destroy
end
def get_simple_fin_available(accountable_type: nil)
provider = Provider::Registry.get_provider(:simple_fin)
provider.is_available(id, accountable_type)
end
private
end

View file

@ -16,45 +16,6 @@ class SimpleFinItem < ApplicationRecord
scope :ordered, -> { order(created_at: :desc) }
scope :needs_update, -> { where(status: :requires_update) }
##
# Syncs the simple_fin_item given and all other simple_fin accounts available (reduces calls to the API)
def sync_data(sync, start_date: nil)
Rails.logger.info("SimpleFINItem: Starting sync for all SimpleFIN accounts")
begin
# Fetch all accounts for this specific connection from SimpleFIN.
sf_accounts_data = provider.get_available_accounts(nil)
# Iterate over every account and attempt to apply transactions where possible
sf_accounts_data.each do |sf_account_data|
begin
# Find or create the SimpleFinAccount record.
sfa = SimpleFinAccount.find_by(external_id: sf_account_data["id"])
rescue StandardError
# Ignore because it could be non existent accounts from the central sync
end
if sfa != nil
begin
# Sync the detailed data for this account
sfa.sync_account_data!(sf_account_data)
rescue StandardError => e
Rails.logger.error("SimpleFINItem: Sync failed for account #{sf_account_data["id"]}: #{e.message}")
sfa.simple_fin_item.update(id: sf_account_data["id"], status: :requires_update) # We had problems so make sure this account knows
end
end
end
Rails.logger.info("SimpleFINItem: Sync completed for all accounts")
rescue Provider::SimpleFin::RateLimitExceededError =>e
Rails.logger.error("SimpleFINItem: Sync failed: #{e.message}")
raise StandardError, "SimpleFIN Rate Limit: #{e.message}" # Re-raise as a generic StandardError
rescue StandardError => e
Rails.logger.error("SimpleFINItem: Sync failed: #{e.message}")
raise e # Re-raise so Sync#perform can record the failure.
end
end
def provider
@provider ||= Provider::Registry.get_provider(:simple_fin)
end
@ -63,4 +24,42 @@ class SimpleFinItem < ApplicationRecord
update!(scheduled_for_deletion: true)
DestroyJob.perform_later(self)
end
def syncing?
Sync.joins("LEFT JOIN accounts a ON a.id = syncs.syncable_id AND syncs.syncable_type = 'Account'")
.joins("LEFT JOIN simple_fin_accounts sfa ON sfa.id = a.simple_fin_account_id")
.where("syncs.syncable_id = ? OR sfa.simple_fin_item_id = ?", id, id)
.visible
.exists?
end
def auto_match_categories!
if family.categories.none?
family.categories.bootstrap!
end
alias_matcher = build_category_alias_matcher(family.categories)
accounts.each do |account|
matchable_transactions = account.transactions
.where(category_id: nil)
.where.not(simple_fin_category: nil)
.enrichable(:category_id)
matchable_transactions.each do |transaction|
category = alias_matcher.match(transaction.simple_fin_category)
if category.present?
SimpleFinItem.transaction do
transaction.log_enrichment!(
attribute_name: "category_id",
attribute_value: category.id,
source: "simple_fin"
)
transaction.set_category!(category)
end
end
end
end
end
end

View file

@ -0,0 +1,51 @@
class SimpleFinItem::Syncer
attr_reader :simple_fin_item
def initialize(simple_fin_item)
@simple_fin_item = simple_fin_item
@provider ||= Provider::Registry.get_provider(:simple_fin)
end
def perform_sync(sync)
Rails.logger.info("Starting sync for all SimpleFIN accounts")
begin
# Fetch all accounts for this specific connection from SimpleFIN.
sf_accounts_data = @provider.get_available_accounts(nil)
# Iterate over every account and attempt to apply transactions where possible
sf_accounts_data.each do |sf_account_data|
begin
# Find or create the SimpleFinAccount record.
sfa = SimpleFinAccount.find_by(external_id: sf_account_data["id"])
rescue StandardError
# Ignore because it could be non existent accounts from the central sync
end
if sfa != nil
begin
# Sync the detailed data for this account
sfa.sync_account_data!(sf_account_data)
rescue StandardError => e
Rails.logger.error("Sync failed for account #{sf_account_data["id"]}: #{e.message}")
sfa.simple_fin_item.update(id: sf_account_data["id"], status: :requires_update) # We had problems so make sure this account knows
end
end
end
Rails.logger.info("Sync completed for all accounts")
rescue Provider::SimpleFin::RateLimitExceededError =>e
Rails.logger.error("Sync failed: #{e.message}")
raise StandardError, "SimpleFIN Rate Limit: #{e.message}" # Re-raise as a generic StandardError
rescue StandardError => e
Rails.logger.error("Sync failed: #{e.message}")
raise e # Re-raise so Sync#perform can record the failure.
end
end
def perform_post_sync
simple_fin_item.auto_match_categories!
end
private
end

View file

@ -2,6 +2,17 @@
<%# Flag indicators of account issues %>
<% if account.simple_fin_account&.simple_fin_item&.status == "requires_update" %>
<% if link_to_path == nil %>
<%= icon(
"alert-triangle",
id: "account-issue-icon",
as_button: true,
size: "sm",
rel: "noopener noreferrer",
disabled: account.syncing?,
title: given_title
) %>
<% elsif %>
<%= link_to link_to_path, target: target do %>
<%= icon(
"alert-triangle",
@ -13,6 +24,7 @@
title: given_title
) %>
<% end %>
<% end %>
<% end %>
<%# Force some styling that keeps getting overwritten %>

View file

@ -37,6 +37,9 @@
<%= tag.p account.name, class: "text-sm text-primary font-medium mb-0.5 truncate" %>
<%= tag.p account.short_subtype_label, class: "text-sm text-secondary truncate" %>
</div>
<%# Render account error warnings %>
<%= render "accounts/account_error", account: account, given_title: "Account has an error" %>
<% if account.syncing? %>
<div class="ml-auto text-right grow h-10">

21
db/schema.rb generated
View file

@ -592,27 +592,6 @@ ActiveRecord::Schema[7.2].define(version: 2025_05_17_134646) do
t.index ["date"], name: "index_sfrl_on_date", unique: true
end
create_table "stock_exchanges", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.string "name", null: false
t.string "acronym"
t.string "mic", null: false
t.string "country", null: false
t.string "country_code", null: false
t.string "city"
t.string "website"
t.string "timezone_name"
t.string "timezone_abbr"
t.string "timezone_abbr_dst"
t.string "currency_code"
t.string "currency_symbol"
t.string "currency_name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["country"], name: "index_stock_exchanges_on_country"
t.index ["country_code"], name: "index_stock_exchanges_on_country_code"
t.index ["currency_code"], name: "index_stock_exchanges_on_currency_code"
end
create_table "subscriptions", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.uuid "family_id", null: false
t.string "status", null: false