mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-09 15:35:22 +02:00
feat(errors): Updating error display to better handle SimpleFIN errors
- Also updating models to follow the plaid style a bit more
This commit is contained in:
parent
ce98a9f134
commit
1137f1f57b
20 changed files with 350 additions and 165 deletions
|
@ -52,7 +52,6 @@ APP_DOMAIN=
|
||||||
# Allows configuration of SimpleFIN for account linking: https://www.simplefin.org/
|
# Allows configuration of SimpleFIN for account linking: https://www.simplefin.org/
|
||||||
# You'll want to follow the steps here for getting an AccessURL https://beta-bridge.simplefin.org/info/developers
|
# You'll want to follow the steps here for getting an AccessURL https://beta-bridge.simplefin.org/info/developers
|
||||||
SIMPLE_FIN_ACCESS_URL=
|
SIMPLE_FIN_ACCESS_URL=
|
||||||
SIMPLE_FIN_UPDATE_CRON="0 6 * * *"
|
|
||||||
|
|
||||||
# Disable enforcing SSL connections
|
# Disable enforcing SSL connections
|
||||||
# DISABLE_SSL=true
|
# DISABLE_SSL=true
|
||||||
|
|
|
@ -5,7 +5,7 @@ class AccountsController < ApplicationController
|
||||||
def index
|
def index
|
||||||
@manual_accounts = family.accounts.manual.alphabetically
|
@manual_accounts = family.accounts.manual.alphabetically
|
||||||
@plaid_items = family.plaid_items.ordered
|
@plaid_items = family.plaid_items.ordered
|
||||||
@simple_fin_connections = family.simple_fin_connections.ordered
|
@simple_fin_items = family.simple_fin_items.ordered
|
||||||
|
|
||||||
render layout: "settings"
|
render layout: "settings"
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
class SimpleFinController < ApplicationController
|
class SimpleFinController < ApplicationController
|
||||||
before_action :set_accountable_type
|
before_action :set_accountable_type
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :set_simple_fin_provider
|
before_action :set_simple_fin_provider, only: %i[create new]
|
||||||
before_action :require_simple_fin_provider
|
before_action :require_simple_fin_provider, only: %i[create new]
|
||||||
|
|
||||||
def new
|
def new
|
||||||
@simple_fin_accounts = @simple_fin_provider.get_available_accounts(@accountable_type)
|
@simple_fin_accounts = @simple_fin_provider.get_available_accounts(@accountable_type)
|
||||||
|
@ -21,6 +21,20 @@ class SimpleFinController < ApplicationController
|
||||||
Current.family.accounts.find_by(name: acc["name"])
|
Current.family.accounts.find_by(name: acc["name"])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Requests all accounts to be re-synced
|
||||||
|
def sync
|
||||||
|
@simple_fin_item = Current.family.simple_fin_items.find(params[:id])
|
||||||
|
unless @simple_fin_item.syncing?
|
||||||
|
@simple_fin_item.sync_later
|
||||||
|
end
|
||||||
|
|
||||||
|
respond_to do |format|
|
||||||
|
format.html { redirect_back_or_to accounts_path }
|
||||||
|
format.json { head :ok }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
selected_ids = params[:selected_account_ids]
|
selected_ids = params[:selected_account_ids]
|
||||||
if selected_ids.blank?
|
if selected_ids.blank?
|
||||||
|
@ -39,17 +53,17 @@ class SimpleFinController < ApplicationController
|
||||||
first_sf_account = sf_accounts_for_institution.first # Use data from the first account for connection details
|
first_sf_account = sf_accounts_for_institution.first # Use data from the first account for connection details
|
||||||
org_details = first_sf_account["org"]
|
org_details = first_sf_account["org"]
|
||||||
|
|
||||||
# Find or Create the SimpleFinConnection for this institution
|
# Find or Create the SimpleFinItem for this institution
|
||||||
simple_fin_connection = Current.family.simple_fin_connections.find_or_create_by!(institution_id: institution_id) do |sfc|
|
simple_fin_item = Current.family.simple_fin_items.find_or_create_by!(institution_id: institution_id) do |sfc|
|
||||||
sfc.name = org_details["name"] || "SimpleFIN Connection"
|
|
||||||
sfc.institution_name = org_details["name"]
|
sfc.institution_name = org_details["name"]
|
||||||
sfc.institution_url = org_details["url"]
|
sfc.institution_url = org_details["url"]
|
||||||
sfc.institution_domain = org_details["domain"]
|
sfc.institution_domain = org_details["domain"]
|
||||||
sfc.last_synced_at = Time.current # Mark as synced upon creation
|
# TODO: Fix
|
||||||
|
sfc.last_sync_count_reset_at = Time.current # Mark as synced upon creation
|
||||||
end
|
end
|
||||||
|
|
||||||
sf_accounts_for_institution.each do |acc_detail|
|
sf_accounts_for_institution.each do |acc_detail|
|
||||||
next if simple_fin_connection.simple_fin_accounts.exists?(external_id: acc_detail["id"])
|
next if simple_fin_item.simple_fin_accounts.exists?(external_id: acc_detail["id"])
|
||||||
next if account_exists(acc_detail)
|
next if account_exists(acc_detail)
|
||||||
|
|
||||||
# Get sub type for this account from params
|
# Get sub type for this account from params
|
||||||
|
@ -60,7 +74,7 @@ class SimpleFinController < ApplicationController
|
||||||
# Create SimpleFinAccount and its associated Account
|
# Create SimpleFinAccount and its associated Account
|
||||||
simple_fin_account = SimpleFinAccount.find_or_create_from_simple_fin_data!(
|
simple_fin_account = SimpleFinAccount.find_or_create_from_simple_fin_data!(
|
||||||
acc_detail,
|
acc_detail,
|
||||||
simple_fin_connection
|
simple_fin_item
|
||||||
)
|
)
|
||||||
|
|
||||||
# Trigger an account sync of our data
|
# Trigger an account sync of our data
|
||||||
|
@ -75,7 +89,6 @@ class SimpleFinController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_accountable_type
|
def set_accountable_type
|
||||||
@accountable_type = params[:accountable_type]
|
@accountable_type = params[:accountable_type]
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,7 +10,7 @@ class FamilyResetJob < ApplicationJob
|
||||||
family.tags.destroy_all
|
family.tags.destroy_all
|
||||||
family.merchants.destroy_all
|
family.merchants.destroy_all
|
||||||
family.plaid_items.destroy_all
|
family.plaid_items.destroy_all
|
||||||
family.simple_fin_connections.destroy_all
|
family.simple_fin_items.destroy_all
|
||||||
family.imports.destroy_all
|
family.imports.destroy_all
|
||||||
family.budgets.destroy_all
|
family.budgets.destroy_all
|
||||||
|
|
||||||
|
|
|
@ -68,7 +68,7 @@ class Account < ApplicationRecord
|
||||||
url_string = if plaid_account.present?
|
url_string = if plaid_account.present?
|
||||||
plaid_account.plaid_item&.institution_url
|
plaid_account.plaid_item&.institution_url
|
||||||
elsif simple_fin_account.present?
|
elsif simple_fin_account.present?
|
||||||
simple_fin_account.simple_fin_connection&.institution_domain
|
simple_fin_account.simple_fin_item&.institution_domain
|
||||||
end
|
end
|
||||||
|
|
||||||
return nil unless url_string.present?
|
return nil unless url_string.present?
|
||||||
|
|
|
@ -16,7 +16,7 @@ class Family < ApplicationRecord
|
||||||
has_many :users, dependent: :destroy
|
has_many :users, dependent: :destroy
|
||||||
has_many :accounts, dependent: :destroy
|
has_many :accounts, dependent: :destroy
|
||||||
has_many :plaid_items, dependent: :destroy
|
has_many :plaid_items, dependent: :destroy
|
||||||
has_many :simple_fin_connections, dependent: :destroy
|
has_many :simple_fin_items, dependent: :destroy
|
||||||
has_many :invitations, dependent: :destroy
|
has_many :invitations, dependent: :destroy
|
||||||
|
|
||||||
has_many :imports, dependent: :destroy
|
has_many :imports, dependent: :destroy
|
||||||
|
@ -82,6 +82,11 @@ class Family < ApplicationRecord
|
||||||
plaid_item.sync_later(start_date: start_date, parent_sync: sync)
|
plaid_item.sync_later(start_date: start_date, parent_sync: sync)
|
||||||
end
|
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}")
|
Rails.logger.info("Applying rules for family #{id}")
|
||||||
rules.each do |rule|
|
rules.each do |rule|
|
||||||
rule.apply_later
|
rule.apply_later
|
||||||
|
@ -103,8 +108,9 @@ class Family < ApplicationRecord
|
||||||
Sync.where(
|
Sync.where(
|
||||||
"(syncable_type = 'Family' AND syncable_id = ?) OR
|
"(syncable_type = 'Family' AND syncable_id = ?) OR
|
||||||
(syncable_type = 'Account' AND syncable_id IN (SELECT id FROM accounts WHERE family_id = ? AND plaid_account_id IS NULL)) OR
|
(syncable_type = 'Account' AND syncable_id IN (SELECT id FROM accounts WHERE family_id = ? AND plaid_account_id IS NULL)) OR
|
||||||
(syncable_type = 'PlaidItem' AND syncable_id IN (SELECT id FROM plaid_items WHERE family_id = ?))",
|
(syncable_type = 'PlaidItem' AND syncable_id IN (SELECT id FROM plaid_items WHERE family_id = ?)) OR
|
||||||
id, id, id
|
(syncable_type = 'SimpleFinItem' AND syncable_id IN (SELECT id FROM simple_fin_items WHERE family_id = ?))",
|
||||||
|
id, id, id, id
|
||||||
).where(status: [ "pending", "syncing" ], created_at: 10.minutes.ago..).exists?
|
).where(status: [ "pending", "syncing" ], created_at: 10.minutes.ago..).exists?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -92,14 +92,17 @@ class Provider::SimpleFin
|
||||||
# TODO: Remove JSON Reading for real requests. Disabled currently due to preventing rate limits.
|
# TODO: Remove JSON Reading for real requests. Disabled currently due to preventing rate limits.
|
||||||
json_file_path = Rails.root.join("sample.simple.fin.json")
|
json_file_path = Rails.root.join("sample.simple.fin.json")
|
||||||
accounts = []
|
accounts = []
|
||||||
|
error_messages = []
|
||||||
if File.exist?(json_file_path)
|
if File.exist?(json_file_path)
|
||||||
file_content = File.read(json_file_path)
|
file_content = File.read(json_file_path)
|
||||||
parsed_json = JSON.parse(file_content)
|
parsed_json = JSON.parse(file_content)
|
||||||
accounts = parsed_json["accounts"] || []
|
accounts = parsed_json["accounts"] || []
|
||||||
|
error_messages = parsed_json["errors"] || []
|
||||||
else
|
else
|
||||||
Rails.logger.warn "SimpleFIN: Sample JSON file not found at #{json_file_path}. Returning empty accounts."
|
Rails.logger.warn "SimpleFIN: Sample JSON file not found at #{json_file_path}. Returning empty accounts."
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
# The only way we can really determine types right now is by some properties. Try and set their types
|
# The only way we can really determine types right now is by some properties. Try and set their types
|
||||||
accounts.each do |account|
|
accounts.each do |account|
|
||||||
# Accounts can be considered Investment accounts if they have any holdings associated to them
|
# Accounts can be considered Investment accounts if they have any holdings associated to them
|
||||||
|
@ -112,10 +115,22 @@ class Provider::SimpleFin
|
||||||
else
|
else
|
||||||
account["type"] = "Depository" # Default for positive balance
|
account["type"] = "Depository" # Default for positive balance
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Set error messages if related
|
||||||
|
account["org"]["institution_errors"] = []
|
||||||
|
error_messages.each do |error|
|
||||||
|
if error.include? account["org"]["name"]
|
||||||
|
account["org"]["institution_errors"].push(error)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Update accounts to only include relevant accounts to the typ
|
if accountable_type == nil
|
||||||
accounts.filter { |acc| acc["type"] == accountable_type }
|
accounts
|
||||||
|
else
|
||||||
|
# Update accounts to only include relevant accounts to the type
|
||||||
|
accounts.filter { |acc| acc["type"] == accountable_type }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns if this is a supported API of SimpleFIN by the access url in the config.
|
# Returns if this is a supported API of SimpleFIN by the access url in the config.
|
||||||
|
|
|
@ -7,13 +7,13 @@ class SimpleFinAccount < ApplicationRecord
|
||||||
"Other" => OtherAsset
|
"Other" => OtherAsset
|
||||||
}
|
}
|
||||||
|
|
||||||
belongs_to :simple_fin_connection
|
belongs_to :simple_fin_item
|
||||||
has_one :account, dependent: :destroy, foreign_key: :simple_fin_account_id, inverse_of: :simple_fin_account
|
has_one :account, dependent: :destroy, foreign_key: :simple_fin_account_id, inverse_of: :simple_fin_account
|
||||||
|
|
||||||
accepts_nested_attributes_for :account
|
accepts_nested_attributes_for :account
|
||||||
|
|
||||||
validates :external_id, presence: true, uniqueness: true
|
validates :external_id, presence: true, uniqueness: true
|
||||||
validates :simple_fin_connection_id, presence: true
|
validates :simple_fin_item_id, presence: true
|
||||||
|
|
||||||
after_destroy :cleanup_connection_if_orphaned
|
after_destroy :cleanup_connection_if_orphaned
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ class SimpleFinAccount < ApplicationRecord
|
||||||
|
|
||||||
|
|
||||||
def family
|
def family
|
||||||
simple_fin_connection&.family
|
simple_fin_item&.family
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -70,6 +70,8 @@ class SimpleFinAccount < ApplicationRecord
|
||||||
# 'account' here refers to self.account (the associated Account instance)
|
# 'account' here refers to self.account (the associated Account instance)
|
||||||
accountable_attributes = { id: self.account.accountable_id }
|
accountable_attributes = { id: self.account.accountable_id }
|
||||||
|
|
||||||
|
puts "SFA #{sf_account_data}"
|
||||||
|
|
||||||
self.update!(
|
self.update!(
|
||||||
current_balance: sf_account_data["balance"].to_d,
|
current_balance: sf_account_data["balance"].to_d,
|
||||||
available_balance: sf_account_data["available-balance"]&.to_d,
|
available_balance: sf_account_data["available-balance"]&.to_d,
|
||||||
|
@ -83,6 +85,12 @@ class SimpleFinAccount < ApplicationRecord
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.simple_fin_item.update!(
|
||||||
|
institution_errors: sf_account_data["org"]["institution_errors"],
|
||||||
|
status: sf_account_data["org"]["institution_errors"].empty? ? :good : :requires_update
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
# Sync transactions if present in the data
|
# Sync transactions if present in the data
|
||||||
if sf_account_data["transactions"].is_a?(Array)
|
if sf_account_data["transactions"].is_a?(Array)
|
||||||
sync_transactions!(sf_account_data["transactions"])
|
sync_transactions!(sf_account_data["transactions"])
|
||||||
|
@ -181,7 +189,7 @@ class SimpleFinAccount < ApplicationRecord
|
||||||
|
|
||||||
def cleanup_connection_if_orphaned
|
def cleanup_connection_if_orphaned
|
||||||
# Reload the connection to get the most up-to-date count of associated accounts
|
# Reload the connection to get the most up-to-date count of associated accounts
|
||||||
connection = simple_fin_connection.reload
|
connection = simple_fin_item.reload
|
||||||
connection.destroy_later if connection.simple_fin_accounts.empty?
|
connection.destroy_later if connection.simple_fin_accounts.empty?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,87 +0,0 @@
|
||||||
class SimpleFinConnection < ApplicationRecord
|
|
||||||
include Syncable
|
|
||||||
|
|
||||||
enum :status, { good: "good", requires_update: "requires_update" }, default: :good
|
|
||||||
|
|
||||||
validates :name, presence: true
|
|
||||||
validates :family_id, presence: true
|
|
||||||
|
|
||||||
belongs_to :family
|
|
||||||
has_one_attached :logo
|
|
||||||
|
|
||||||
has_many :simple_fin_accounts, dependent: :destroy
|
|
||||||
has_many :accounts, through: :simple_fin_accounts
|
|
||||||
|
|
||||||
scope :active, -> { where(scheduled_for_deletion: false) }
|
|
||||||
scope :ordered, -> { order(created_at: :desc) }
|
|
||||||
scope :needs_update, -> { where(status: :requires_update) }
|
|
||||||
|
|
||||||
class << self
|
|
||||||
# `provided_access_url` is the full URL from SimpleFIN (https://user:pass@beta-bridge.simplefin.org/simplefin)
|
|
||||||
# `connection_name` can be user-provided or derived.
|
|
||||||
def create_and_sync_from_access_url(provided_access_url, connection_name, family_obj)
|
|
||||||
# Basic validation of the URL format
|
|
||||||
uri = URI.parse(provided_access_url)
|
|
||||||
raise ArgumentError, "Invalid SimpleFIN Access URL: Missing credentials" unless uri.user && uri.password
|
|
||||||
raise ArgumentError, "Invalid SimpleFIN Access URL: Must be HTTPS" unless uri.scheme == "https"
|
|
||||||
|
|
||||||
# Create the connection object first
|
|
||||||
connection = family_obj.simple_fin_connections.create!(
|
|
||||||
name: connection_name,
|
|
||||||
access_url: provided_access_url,
|
|
||||||
status: :good # Assume good initially
|
|
||||||
)
|
|
||||||
|
|
||||||
# Perform an initial sync to populate institution details and accounts
|
|
||||||
connection.sync_later
|
|
||||||
connection
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def sync_data(sync, start_date: nil)
|
|
||||||
update!(last_synced_at: Time.current)
|
|
||||||
Rails.logger.info("SimpleFinConnection: Starting sync for connection ID #{id}")
|
|
||||||
|
|
||||||
begin
|
|
||||||
# Fetch initial info if not present (like institution details)
|
|
||||||
if institution_id.blank? || api_versions_supported.blank?
|
|
||||||
info_data = provider.get_api_versions_and_org_details_from_accounts
|
|
||||||
update!(
|
|
||||||
institution_id: info_data[:org_id],
|
|
||||||
institution_name: info_data[:org_name],
|
|
||||||
institution_url: info_data[:org_url],
|
|
||||||
institution_domain: info_data[:org_domain],
|
|
||||||
api_versions_supported: info_data[:versions]
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
sf_accounts_data = provider.get_available_accounts(nil) # Pass nil to get all types
|
|
||||||
|
|
||||||
sf_accounts_data.each do |sf_account_data|
|
|
||||||
accountable_klass_name = Provider::SimpleFin::ACCOUNTABLE_TYPE_MAPPING.find { |key, _val| sf_account_data["type"]&.downcase == key.downcase }&.last
|
|
||||||
accountable_klass_name ||= (sf_account_data["balance"].to_d >= 0 ? Depository : CreditCard) # Basic fallback
|
|
||||||
accountable_klass = accountable_klass_name
|
|
||||||
|
|
||||||
sfa = simple_fin_accounts.find_or_create_from_simple_fin_data!(sf_account_data, self, accountable_klass)
|
|
||||||
sfa.sync_account_data!(sf_account_data)
|
|
||||||
end
|
|
||||||
|
|
||||||
update!(status: :good) if requires_update?
|
|
||||||
Rails.logger.info("SimpleFinConnection: Sync completed for connection ID #{id}")
|
|
||||||
|
|
||||||
rescue StandardError => e
|
|
||||||
Rails.logger.error("SimpleFinConnection: Sync failed for connection ID #{id}: #{e.message}")
|
|
||||||
update!(status: :requires_update)
|
|
||||||
raise e
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def provider
|
|
||||||
@provider ||= Provider::SimpleFin.new()
|
|
||||||
end
|
|
||||||
|
|
||||||
def destroy_later
|
|
||||||
update!(scheduled_for_deletion: true)
|
|
||||||
DestroyJob.perform_later(self)
|
|
||||||
end
|
|
||||||
end
|
|
178
app/models/simple_fin_item.rb
Normal file
178
app/models/simple_fin_item.rb
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
class SimpleFinItem < ApplicationRecord
|
||||||
|
include Syncable
|
||||||
|
|
||||||
|
enum :status, { good: "good", requires_update: "requires_update" }, default: :good
|
||||||
|
|
||||||
|
validates :institution_name, presence: true
|
||||||
|
validates :family_id, presence: true
|
||||||
|
|
||||||
|
belongs_to :family
|
||||||
|
has_one_attached :logo
|
||||||
|
|
||||||
|
has_many :simple_fin_accounts, dependent: :destroy
|
||||||
|
has_many :accounts, through: :simple_fin_accounts
|
||||||
|
|
||||||
|
scope :active, -> { where(scheduled_for_deletion: false) }
|
||||||
|
scope :ordered, -> { order(created_at: :desc) }
|
||||||
|
scope :needs_update, -> { where(status: :requires_update) }
|
||||||
|
|
||||||
|
class << self
|
||||||
|
# # `provided_access_url` is the full URL from SimpleFIN (https://user:pass@beta-bridge.simplefin.org/simplefin)
|
||||||
|
# # `connection_name` can be user-provided or derived.
|
||||||
|
# def create_and_sync_from_access_url(provided_access_url, connection_name, family_obj)
|
||||||
|
# # Basic validation of the URL format
|
||||||
|
# uri = URI.parse(provided_access_url)
|
||||||
|
# raise ArgumentError, "Invalid SimpleFIN Access URL: Missing credentials" unless uri.user && uri.password
|
||||||
|
# raise ArgumentError, "Invalid SimpleFIN Access URL: Must be HTTPS" unless uri.scheme == "https"
|
||||||
|
|
||||||
|
# # Create the connection object first
|
||||||
|
# connection = family_obj.simple_fin_connections.create!(
|
||||||
|
# name: connection_name,
|
||||||
|
# access_url: provided_access_url,
|
||||||
|
# status: :good # Assume good initially
|
||||||
|
# )
|
||||||
|
|
||||||
|
# # Perform an initial sync to populate institution details and accounts
|
||||||
|
# connection.sync_later
|
||||||
|
# connection
|
||||||
|
# end
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# 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)
|
||||||
|
# TODO: Rate limit
|
||||||
|
# now = Time.current
|
||||||
|
# # Rate Limiting Check
|
||||||
|
# # We use a transaction here to ensure that checking the count, resetting it if needed,
|
||||||
|
# # and incrementing it are atomic.
|
||||||
|
# ActiveRecord::Base.transaction do
|
||||||
|
# # Reload self to ensure we have the latest values from the DB,
|
||||||
|
# # especially if this method could be called concurrently for the same item.
|
||||||
|
# self.reload
|
||||||
|
|
||||||
|
# if self.last_sync_count_reset_at.nil? || self.last_sync_count_reset_at.to_date < now.to_date
|
||||||
|
# # If it's a new day (or first sync ever for rate limiting), reset the count and the reset timestamp.
|
||||||
|
# self.update_columns(syncs_today_count: 0, last_sync_count_reset_at: now)
|
||||||
|
# self.reload # Reload again to get the just-updated values for the check below.
|
||||||
|
# end
|
||||||
|
|
||||||
|
# if self.syncs_today_count >= 24
|
||||||
|
# msg = "SimpleFinItem ID #{self.id}: Sync limit of 24 per day reached. Count: #{self.syncs_today_count}."
|
||||||
|
# Rails.logger.warn(msg)
|
||||||
|
# sync.fail!(StandardError.new(msg)) # Record failure in the Sync object
|
||||||
|
# raise StandardError, msg # Raise to stop execution and ensure SyncJob handles it as a failure
|
||||||
|
# end
|
||||||
|
|
||||||
|
# # If not rate-limited, increment the count for this sync attempt.
|
||||||
|
# self.increment!(:syncs_today_count)
|
||||||
|
# end
|
||||||
|
|
||||||
|
|
||||||
|
# unless access_url.present?
|
||||||
|
# # This is a configuration error for the connection itself.
|
||||||
|
# msg = "SimpleFinConnection: Sync cannot proceed for connection ID #{id}: Missing access_url."
|
||||||
|
# Rails.logger.error(msg)
|
||||||
|
# update!(status: :requires_update) # Mark connection as needing attention
|
||||||
|
# # Raise an error to ensure the SyncJob records this failure.
|
||||||
|
# # Sync#perform will catch this and call sync.fail!
|
||||||
|
# raise StandardError, msg
|
||||||
|
# end
|
||||||
|
|
||||||
|
# TODO: Populate this
|
||||||
|
# update!(last_synced_at: Time.current, status: :requires_update)
|
||||||
|
|
||||||
|
Rails.logger.info("SimpleFinConnection: Starting sync for connection ID #{id}")
|
||||||
|
|
||||||
|
begin
|
||||||
|
# Fetch all accounts for this specific connection from SimpleFIN.
|
||||||
|
sf_accounts_data = provider.get_available_accounts(nil)
|
||||||
|
|
||||||
|
|
||||||
|
# Keep track of external IDs reported by the provider in this sync.
|
||||||
|
# This can be used later to identify accounts that might have been removed on the SimpleFIN side.
|
||||||
|
current_provider_external_ids = []
|
||||||
|
|
||||||
|
sf_accounts_data.each do |sf_account_data|
|
||||||
|
current_provider_external_ids << sf_account_data["id"]
|
||||||
|
|
||||||
|
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
|
||||||
|
# Sync the detailed data for this account (e.g., balance, and potentially transactions).
|
||||||
|
# This method is expected to be on the SimpleFinAccount model.
|
||||||
|
sfa.sync_account_data!(sf_account_data)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Optional: You could add logic here to handle accounts that exist in your DB for this
|
||||||
|
# SimpleFinConnection but were NOT reported by the provider in `sf_accounts_data`.
|
||||||
|
# These could be marked as closed, archived, etc. For example:
|
||||||
|
# simple_fin_accounts.where.not(external_id: current_provider_external_ids).find_each(&:archive!)
|
||||||
|
|
||||||
|
update!(status: :good) # Mark connection as successfully synced.
|
||||||
|
Rails.logger.info("SimpleFinConnection: Sync completed for connection ID #{id}")
|
||||||
|
|
||||||
|
# rescue Provider::SimpleFin::AuthenticationError => e # Catch specific auth errors if your provider defines them.
|
||||||
|
# Rails.logger.error("SimpleFinConnection: Authentication failed for connection ID #{id}: #{e.message}")
|
||||||
|
# update!(status: :requires_update) # Mark the connection so the user knows to update credentials.
|
||||||
|
# raise e # Re-raise so Sync#perform can record the failure.
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger.error("SimpleFinConnection: Sync failed for connection ID #{id}: #{e.message}")
|
||||||
|
update!(status: :requires_update)
|
||||||
|
raise e # Re-raise so Sync#perform can record the failure.
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# def sync_data(sync, start_date: nil)
|
||||||
|
# update!(last_synced_at: Time.current)
|
||||||
|
# Rails.logger.info("SimpleFinConnection: Starting sync for connection ID #{id}")
|
||||||
|
|
||||||
|
# # begin
|
||||||
|
# # # Fetch initial info if not present (like institution details)
|
||||||
|
# # if institution_id.blank? || api_versions_supported.blank?
|
||||||
|
# # info_data = provider.get_api_versions_and_org_details_from_accounts
|
||||||
|
# # update!(
|
||||||
|
# # institution_id: info_data[:org_id],
|
||||||
|
# # institution_name: info_data[:org_name],
|
||||||
|
# # institution_url: info_data[:org_url],
|
||||||
|
# # institution_domain: info_data[:org_domain],
|
||||||
|
# # api_versions_supported: info_data[:versions]
|
||||||
|
# # )
|
||||||
|
# # end
|
||||||
|
|
||||||
|
# # sf_accounts_data = provider.get_available_accounts(nil) # Pass nil to get all types
|
||||||
|
|
||||||
|
# # sf_accounts_data.each do |sf_account_data|
|
||||||
|
# # accountable_klass_name = Provider::SimpleFin::ACCOUNTABLE_TYPE_MAPPING.find { |key, _val| sf_account_data["type"]&.downcase == key.downcase }&.last
|
||||||
|
# # accountable_klass_name ||= (sf_account_data["balance"].to_d >= 0 ? Depository : CreditCard) # Basic fallback
|
||||||
|
# # accountable_klass = accountable_klass_name
|
||||||
|
|
||||||
|
# # sfa = simple_fin_accounts.find_or_create_from_simple_fin_data!(sf_account_data, self, accountable_klass)
|
||||||
|
# # sfa.sync_account_data!(sf_account_data)
|
||||||
|
# # end
|
||||||
|
|
||||||
|
# update!(status: :good) if requires_update?
|
||||||
|
# Rails.logger.info("SimpleFinConnection: Sync completed for connection ID #{id}")
|
||||||
|
|
||||||
|
# # rescue StandardError => e
|
||||||
|
# # Rails.logger.error("SimpleFinConnection: Sync failed for connection ID #{id}: #{e.message}")
|
||||||
|
# # update!(status: :requires_update)
|
||||||
|
# # raise e
|
||||||
|
# # end
|
||||||
|
# end
|
||||||
|
|
||||||
|
def provider
|
||||||
|
@provider ||= Provider::Registry.get_provider(:simple_fin)
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy_later
|
||||||
|
update!(scheduled_for_deletion: true)
|
||||||
|
DestroyJob.perform_later(self)
|
||||||
|
end
|
||||||
|
end
|
|
@ -30,6 +30,8 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-8">
|
<div class="flex items-center gap-8">
|
||||||
|
<%= render "accounts/account_error", account: account, given_title: "Account has an error", link_to_path: account_path(account, return_to: return_to) %>
|
||||||
|
|
||||||
<p class="text-sm font-medium <%= account.is_active ? "text-primary" : "text-subdued" %>">
|
<p class="text-sm font-medium <%= account.is_active ? "text-primary" : "text-subdued" %>">
|
||||||
<%= format_money account.balance_money %>
|
<%= format_money account.balance_money %>
|
||||||
</p>
|
</p>
|
||||||
|
|
34
app/views/accounts/_account_error.html.erb
Normal file
34
app/views/accounts/_account_error.html.erb
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<%# locals: (account:, link_to_path: nil, given_title: nil, target: nil) %>
|
||||||
|
|
||||||
|
<%# Flag indicators of account issues %>
|
||||||
|
<% if account.simple_fin_account.simple_fin_item.status == "requires_update" %>
|
||||||
|
<%= link_to link_to_path, target: target do %>
|
||||||
|
<%= icon(
|
||||||
|
"alert-triangle",
|
||||||
|
id: "account-issue-icon",
|
||||||
|
as_button: true,
|
||||||
|
size: "sm",
|
||||||
|
rel: "noopener noreferrer",
|
||||||
|
disabled: account.syncing?,
|
||||||
|
title: given_title
|
||||||
|
) %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%# Force some styling that keeps getting overwritten %>
|
||||||
|
<style>
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.2); }
|
||||||
|
100% { transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
#account-issue-icon * {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
#account-issue-icon {
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
|
@ -15,21 +15,30 @@
|
||||||
|
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<% account_group.accounts.each do |account| %>
|
<% account_group.accounts.each do |account| %>
|
||||||
<%= link_to account_path(account),
|
|
||||||
class: class_names(
|
|
||||||
"block flex items-center gap-2 px-3 py-2 rounded-lg",
|
|
||||||
page_active?(account_path(account)) ? "bg-container" : "hover:bg-surface-hover"
|
|
||||||
),
|
|
||||||
data: { sidebar_tabs_target: "account", action: "click->sidebar-tabs#select" },
|
|
||||||
title: account.name do %>
|
|
||||||
<%= render "accounts/logo", account: account, size: "sm", color: account_group.color %>
|
|
||||||
|
|
||||||
<div class="min-w-0 grow">
|
<div style="width: 100%;"
|
||||||
<%= tag.p account.name, class: "text-sm text-primary font-medium mb-0.5 truncate" %>
|
class="<%= class_names(
|
||||||
<%= tag.p account.short_subtype_label, class: "text-sm text-secondary truncate" %>
|
"block flex items-center gap-2 px-3 py-2 rounded-lg",
|
||||||
</div>
|
page_active?(account_path(account)) ? "bg-container" : "hover:bg-surface-hover"
|
||||||
|
) %>"
|
||||||
|
data-sidebar-tabs-target="account">
|
||||||
|
|
||||||
<div class="ml-auto text-right grow h-10">
|
<%= link_to account_path(account),
|
||||||
|
style: "overflow: hidden;",
|
||||||
|
class: "flex items-center gap-2 grow",
|
||||||
|
title: account.name,
|
||||||
|
data: { action: "click->sidebar-tabs#select" } do %>
|
||||||
|
<%= render "accounts/logo", account: account, size: "sm", color: account_group.color %>
|
||||||
|
|
||||||
|
<div style="max-width: 80%;">
|
||||||
|
<%= 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", link_to_path: account_path(account) %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="ml-auto text-right h-10 shrink-0">
|
||||||
<%= tag.p format_money(account.balance_money), class: "text-sm font-medium text-primary whitespace-nowrap" %>
|
<%= tag.p format_money(account.balance_money), class: "text-sm font-medium text-primary whitespace-nowrap" %>
|
||||||
|
|
||||||
<%= turbo_frame_tag dom_id(account, :sparkline), src: sparkline_account_path(account), loading: "lazy" do %>
|
<%= turbo_frame_tag dom_id(account, :sparkline), src: sparkline_account_path(account), loading: "lazy" do %>
|
||||||
|
@ -38,7 +47,7 @@
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<% if @manual_accounts.empty? && @plaid_items.empty? && @simple_fin_connections.empty? %>
|
<% if @manual_accounts.empty? && @plaid_items.empty? && @simple_fin_items.empty? %>
|
||||||
<%= render "empty" %>
|
<%= render "empty" %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
|
@ -31,8 +31,8 @@
|
||||||
<%= render @plaid_items.sort_by(&:created_at) %>
|
<%= render @plaid_items.sort_by(&:created_at) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<% if @simple_fin_connections.any? %>
|
<% if @simple_fin_items.any? %>
|
||||||
<%= render @simple_fin_connections.sort_by(&:created_at) %>
|
<%= render @simple_fin_items.sort_by(&:created_at) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<% if @manual_accounts.any? %>
|
<% if @manual_accounts.any? %>
|
||||||
|
|
|
@ -14,21 +14,19 @@
|
||||||
<%= tag.p format_money(account.balance_money), class: "text-primary text-3xl font-medium truncate" %>
|
<%= tag.p format_money(account.balance_money), class: "text-primary text-3xl font-medium truncate" %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= form_with url: request.path, method: :get, data: { controller: "auto-submit-form" } do |form| %>
|
<%= styled_form_with url: request.path, method: :get, data: { controller: "auto-submit-form" } do |form| %>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2 w-[25rem]">
|
||||||
<% if chart_view.present? %>
|
<% if chart_view.present? %>
|
||||||
<%= form.select :chart_view,
|
<%= form.select :chart_view,
|
||||||
[["Total value", "balance"], ["Holdings", "holdings_balance"], ["Cash", "cash_balance"]],
|
[["Total value", "balance"], ["Holdings", "holdings_balance"], ["Cash", "cash_balance"]],
|
||||||
{ selected: chart_view },
|
{ selected: chart_view },
|
||||||
class: "border border-secondary rounded-lg text-sm pr-7 cursor-pointer text-primary focus:outline-hidden focus:ring-0",
|
|
||||||
data: { "auto-submit-form-target": "auto" } %>
|
data: { "auto-submit-form-target": "auto" } %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= form.select :period,
|
<%= form.select :period,
|
||||||
Period.as_options,
|
Period.as_options,
|
||||||
{ selected: period.key },
|
{ selected: period.key },
|
||||||
data: { "auto-submit-form-target": "auto" },
|
data: { "auto-submit-form-target": "auto" } %>
|
||||||
class: "bg-container border border-secondary rounded-lg px-3 py-2 text-sm pr-7 cursor-pointer text-primary focus:outline-hidden focus:ring-0" %>
|
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -20,6 +20,13 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
|
||||||
|
<%# Show error warnings %>
|
||||||
|
<% if account.simple_fin_account_id.present? %>
|
||||||
|
<div style="color: red; text-align: center;">
|
||||||
|
<%= account.simple_fin_account.simple_fin_item.institution_errors.join(",") %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<div class="flex items-center gap-1 ml-auto">
|
<div class="flex items-center gap-1 ml-auto">
|
||||||
<% if account.plaid_account_id.present? %>
|
<% if account.plaid_account_id.present? %>
|
||||||
<% if Rails.env.development? %>
|
<% if Rails.env.development? %>
|
||||||
|
@ -33,8 +40,17 @@
|
||||||
) %>
|
) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% elsif account.simple_fin_account_id.present? %>
|
<% elsif account.simple_fin_account_id.present? %>
|
||||||
<%# SimpleFIN information %>
|
|
||||||
<%= image_tag "simple-fin-logo.svg", class: "h-6 w-auto", title: "Connected via SimpleFIN. This account will auto refresh." %>
|
<%= image_tag "simple-fin-logo.svg", class: "h-6 w-auto", title: "Connected via SimpleFIN. This account will auto refresh." %>
|
||||||
|
<%= render "accounts/account_error", account: account, link_to_path: "https://beta-bridge.simplefin.org/my-account", given_title: "This account has issues in SimpleFIN bridge. Update it and then re-sync.", target: :_blank %>
|
||||||
|
<%# Re sync button. Will sync all SimpleFIN accounts %>
|
||||||
|
<%= icon(
|
||||||
|
"refresh-cw",
|
||||||
|
as_button: true,
|
||||||
|
size: "sm",
|
||||||
|
href: sync_simple_fin_path(account.simple_fin_account.simple_fin_item),
|
||||||
|
disabled: account.syncing?,
|
||||||
|
frame: :_top
|
||||||
|
) %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= icon(
|
<%= icon(
|
||||||
"refresh-cw",
|
"refresh-cw",
|
||||||
|
@ -49,4 +65,4 @@
|
||||||
<%= render "accounts/show/menu", account: account %>
|
<%= render "accounts/show/menu", account: account %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
|
@ -1,29 +1,25 @@
|
||||||
<%# locals: (simple_fin_connection:) %>
|
<%# locals: (simple_fin_item:) %>
|
||||||
|
|
||||||
<div class="group bg-container p-4 shadow-border-xs rounded-xl">
|
<div class="group bg-container p-4 shadow-border-xs rounded-xl">
|
||||||
<div class="px-4 py-3 sm:px-6 border-b border-gray-200">
|
<div class="px-4 py-3 sm:px-6 border-b border-gray-200">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<% if simple_fin_connection.logo.attached? %>
|
<% if simple_fin_item.logo.attached? %>
|
||||||
<%= image_tag simple_fin_connection.logo, class: "h-8 w-8 rounded-full" %>
|
<%= image_tag simple_fin_item.logo, class: "h-8 w-8 rounded-full" %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div>
|
<div>
|
||||||
<%= simple_fin_connection.institution_name&.first || "?" %>
|
<%= simple_fin_item.institution_name&.first || "?" %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<h3 >
|
<h3 >
|
||||||
<%= simple_fin_connection.institution_name || "SimpleFIN Connection" %>
|
<%= simple_fin_item.institution_name || "SimpleFIN Connection" %>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-4 flex-shrink-0">
|
|
||||||
<%# Add a sync button for the entire connection if needed %>
|
|
||||||
<%# Example: render ButtonComponent.new(href: sync_simple_fin_connection_path(simple_fin_connection), method: :post, text: "Sync", icon: "refresh-cw", variant: "outline", size: "sm") %>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul role="list" class="divide-y divide-gray-200">
|
<ul role="list" class="divide-y divide-gray-200">
|
||||||
<% simple_fin_connection.accounts.includes(:accountable, :logo_attachment).active.alphabetically.each do |account| %>
|
<% simple_fin_item.accounts.includes(:accountable, :logo_attachment).active.alphabetically.each do |account| %>
|
||||||
<%= render "accounts/account", account: account %>
|
<%= render "accounts/account", account: account %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -202,6 +202,9 @@ Rails.application.routes.draw do
|
||||||
|
|
||||||
# SimpleFIN routes
|
# SimpleFIN routes
|
||||||
resources :simple_fin, only: %i[create new] do
|
resources :simple_fin, only: %i[create new] do
|
||||||
|
member do
|
||||||
|
post :sync
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,33 +1,31 @@
|
||||||
class SimpleFinIntegration < ActiveRecord::Migration[7.2]
|
class SimpleFinIntegration < ActiveRecord::Migration[7.2]
|
||||||
def change
|
def change
|
||||||
create_table :simple_fin_connections do |t|
|
create_table :simple_fin_items, id: :uuid do |t|
|
||||||
t.references :family, null: false, foreign_key: true, type: :uuid
|
t.references :family, null: false, foreign_key: true, type: :uuid
|
||||||
t.string :name # e.g., "Chase via SimpleFIN" or user-defined
|
|
||||||
t.string :institution_id # From SimpleFIN org.id (e.g., "www.chase.com")
|
t.string :institution_id # From SimpleFIN org.id (e.g., "www.chase.com")
|
||||||
t.string :institution_name
|
t.string :institution_name
|
||||||
t.string :institution_url
|
t.string :institution_url
|
||||||
t.string :institution_domain
|
t.string :institution_domain
|
||||||
t.string :status, default: "good" # e.g., good, requires_update
|
t.string :status, default: "good" # e.g., good, requires_update
|
||||||
t.datetime :last_synced_at
|
t.string :institution_errors, array: true, default: []
|
||||||
t.boolean :scheduled_for_deletion, default: false
|
t.boolean :scheduled_for_deletion, default: false
|
||||||
t.string :api_versions_supported, array: true, default: []
|
# Columns for rate limiting
|
||||||
|
t.integer :syncs_today_count, default: 0, null: false
|
||||||
|
t.datetime :last_sync_count_reset_at
|
||||||
|
|
||||||
t.timestamps
|
t.timestamps
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table :simple_fin_accounts do |t|
|
create_table :simple_fin_accounts do |t|
|
||||||
t.references :simple_fin_connection, null: false, foreign_key: true
|
t.references :simple_fin_item, null: false, foreign_key: true, type: :uuid
|
||||||
t.string :external_id, null: false
|
t.string :external_id, null: false
|
||||||
t.decimal :current_balance, precision: 19, scale: 4
|
t.decimal :current_balance, precision: 19, scale: 4
|
||||||
t.decimal :available_balance, precision: 19, scale: 4
|
t.decimal :available_balance, precision: 19, scale: 4
|
||||||
t.string :currency
|
t.string :currency
|
||||||
t.string :sf_type
|
|
||||||
t.string :sf_subtype
|
|
||||||
t.string :simple_fin_errors, array: true, default: []
|
|
||||||
|
|
||||||
t.timestamps
|
t.timestamps
|
||||||
end
|
end
|
||||||
add_index :simple_fin_accounts, [ :simple_fin_connection_id, :external_id ], unique: true, name: 'index_sfa_on_sfc_id_and_external_id'
|
add_index :simple_fin_accounts, [ :simple_fin_item_id, :external_id ], unique: true, name: 'index_sfa_on_sfc_id_and_external_id'
|
||||||
|
|
||||||
add_reference :accounts, :simple_fin_account, foreign_key: true, null: true, index: true
|
add_reference :accounts, :simple_fin_account, foreign_key: true, null: true, index: true
|
||||||
|
|
||||||
|
|
23
db/schema.rb
generated
23
db/schema.rb
generated
|
@ -560,34 +560,31 @@ ActiveRecord::Schema[7.2].define(version: 2025_05_09_134646) do
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "simple_fin_accounts", force: :cascade do |t|
|
create_table "simple_fin_accounts", force: :cascade do |t|
|
||||||
t.bigint "simple_fin_connection_id", null: false
|
t.uuid "simple_fin_item_id", null: false
|
||||||
t.string "external_id", null: false
|
t.string "external_id", null: false
|
||||||
t.decimal "current_balance", precision: 19, scale: 4
|
t.decimal "current_balance", precision: 19, scale: 4
|
||||||
t.decimal "available_balance", precision: 19, scale: 4
|
t.decimal "available_balance", precision: 19, scale: 4
|
||||||
t.string "currency"
|
t.string "currency"
|
||||||
t.string "sf_type"
|
|
||||||
t.string "sf_subtype"
|
|
||||||
t.string "simple_fin_errors", default: [], array: true
|
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.index ["simple_fin_connection_id", "external_id"], name: "index_sfa_on_sfc_id_and_external_id", unique: true
|
t.index ["simple_fin_item_id", "external_id"], name: "index_sfa_on_sfc_id_and_external_id", unique: true
|
||||||
t.index ["simple_fin_connection_id"], name: "index_simple_fin_accounts_on_simple_fin_connection_id"
|
t.index ["simple_fin_item_id"], name: "index_simple_fin_accounts_on_simple_fin_item_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "simple_fin_connections", force: :cascade do |t|
|
create_table "simple_fin_items", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||||
t.uuid "family_id", null: false
|
t.uuid "family_id", null: false
|
||||||
t.string "name"
|
|
||||||
t.string "institution_id"
|
t.string "institution_id"
|
||||||
t.string "institution_name"
|
t.string "institution_name"
|
||||||
t.string "institution_url"
|
t.string "institution_url"
|
||||||
t.string "institution_domain"
|
t.string "institution_domain"
|
||||||
t.string "status", default: "good"
|
t.string "status", default: "good"
|
||||||
t.datetime "last_synced_at"
|
t.string "institution_errors", default: [], array: true
|
||||||
t.boolean "scheduled_for_deletion", default: false
|
t.boolean "scheduled_for_deletion", default: false
|
||||||
t.string "api_versions_supported", default: [], array: true
|
t.integer "syncs_today_count", default: 0, null: false
|
||||||
|
t.datetime "last_sync_count_reset_at"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.index ["family_id"], name: "index_simple_fin_connections_on_family_id"
|
t.index ["family_id"], name: "index_simple_fin_items_on_family_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "stock_exchanges", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
create_table "stock_exchanges", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||||
|
@ -794,8 +791,8 @@ ActiveRecord::Schema[7.2].define(version: 2025_05_09_134646) do
|
||||||
add_foreign_key "security_prices", "securities"
|
add_foreign_key "security_prices", "securities"
|
||||||
add_foreign_key "sessions", "impersonation_sessions", column: "active_impersonator_session_id"
|
add_foreign_key "sessions", "impersonation_sessions", column: "active_impersonator_session_id"
|
||||||
add_foreign_key "sessions", "users"
|
add_foreign_key "sessions", "users"
|
||||||
add_foreign_key "simple_fin_accounts", "simple_fin_connections"
|
add_foreign_key "simple_fin_accounts", "simple_fin_items"
|
||||||
add_foreign_key "simple_fin_connections", "families"
|
add_foreign_key "simple_fin_items", "families"
|
||||||
add_foreign_key "subscriptions", "families"
|
add_foreign_key "subscriptions", "families"
|
||||||
add_foreign_key "syncs", "syncs", column: "parent_id"
|
add_foreign_key "syncs", "syncs", column: "parent_id"
|
||||||
add_foreign_key "taggings", "tags"
|
add_foreign_key "taggings", "tags"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue