mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-09 15:35:22 +02:00
feat: Holdings + Transactions
- Added initial syncing for holdings and transactions - Fixed accounts display not displaying anything for SimpleFIN connections - Centralized all migrations to one file
This commit is contained in:
parent
a5cf70f2df
commit
5f151ec66f
20 changed files with 339 additions and 81 deletions
|
@ -52,6 +52,7 @@ APP_DOMAIN=
|
|||
# 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
|
||||
SIMPLE_FIN_ACCESS_URL=
|
||||
SIMPLE_FIN_UPDATE_CRON="0 6 * * *"
|
||||
|
||||
# Disable enforcing SSL connections
|
||||
# DISABLE_SSL=true
|
||||
|
|
|
@ -5,6 +5,7 @@ class AccountsController < ApplicationController
|
|||
def index
|
||||
@manual_accounts = family.accounts.manual.alphabetically
|
||||
@plaid_items = family.plaid_items.ordered
|
||||
@simple_fin_connections = family.simple_fin_connections.ordered
|
||||
|
||||
render layout: "settings"
|
||||
end
|
||||
|
|
|
@ -52,7 +52,7 @@ module AccountableResource
|
|||
end
|
||||
|
||||
def destroy
|
||||
if @account.linked?
|
||||
if !@account.can_delete?
|
||||
redirect_to account_path(@account), alert: "Cannot delete a linked account"
|
||||
else
|
||||
@account.destroy_later
|
||||
|
|
|
@ -21,18 +21,11 @@ class SimpleFinController < ApplicationController
|
|||
Current.family.accounts.find_by(name: acc["name"])
|
||||
end
|
||||
|
||||
|
||||
##
|
||||
# Starts a sync across all SimpleFIN accounts
|
||||
def sync
|
||||
puts "Should sync"
|
||||
end
|
||||
|
||||
def create
|
||||
selected_ids = params[:selected_account_ids]
|
||||
if selected_ids.blank?
|
||||
Rails.logger.error "No accounts were selected."
|
||||
redirect_to new_simple_fin_connection_path(accountable_type: @accountable_type)
|
||||
redirect_to root_path, alert: t(".no_acc_selected")
|
||||
return
|
||||
end
|
||||
|
||||
|
@ -65,24 +58,20 @@ class SimpleFinController < ApplicationController
|
|||
|
||||
|
||||
# Create SimpleFinAccount and its associated Account
|
||||
SimpleFinAccount.find_or_create_from_simple_fin_data!(
|
||||
simple_fin_account = SimpleFinAccount.find_or_create_from_simple_fin_data!(
|
||||
acc_detail,
|
||||
simple_fin_connection
|
||||
)
|
||||
|
||||
# Optionally, trigger an initial sync for the new account if needed,
|
||||
# though find_or_create_from_simple_fin_data! already populates it.
|
||||
# simple_fin_account.sync_account_data!(acc_detail)
|
||||
|
||||
# The Account record is created via accepts_nested_attributes_for in SimpleFinAccount
|
||||
# or by the logic within find_or_create_from_simple_fin_data!
|
||||
# Trigger an account sync of our data
|
||||
simple_fin_account.sync_account_data!(acc_detail)
|
||||
end
|
||||
end
|
||||
|
||||
redirect_to root_path, notice: t(".accounts_created_success")
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "SimpleFIN: Failed to create accounts - #{e.message}"
|
||||
redirect_to new_simple_fin_connection_path
|
||||
redirect_to new_simple_fin_path
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -10,6 +10,7 @@ class FamilyResetJob < ApplicationJob
|
|||
family.tags.destroy_all
|
||||
family.merchants.destroy_all
|
||||
family.plaid_items.destroy_all
|
||||
family.simple_fin_connections.destroy_all
|
||||
family.imports.destroy_all
|
||||
family.budgets.destroy_all
|
||||
|
||||
|
|
|
@ -6,6 +6,14 @@ module Account::Linkable
|
|||
belongs_to :simple_fin_account, optional: true
|
||||
end
|
||||
|
||||
def can_delete?
|
||||
# SimpleFIN accounts can be removed and re-added
|
||||
if simple_fin_account_id.present?
|
||||
return true
|
||||
end
|
||||
!linked?
|
||||
end
|
||||
|
||||
# A "linked" account gets transaction and balance data from a third party like Plaid
|
||||
def linked?
|
||||
plaid_account_id.present? || simple_fin_account_id.present?
|
||||
|
|
|
@ -22,6 +22,7 @@ class Assistant::Function::GetAccounts < Assistant::Function
|
|||
type: account.accountable_type,
|
||||
start_date: account.start_date,
|
||||
is_plaid_linked: account.plaid_account_id.present?,
|
||||
is_simple_fin_linked: account.simple_fin_id.present?,
|
||||
is_active: account.is_active,
|
||||
historical_balances: historical_balances(account)
|
||||
}
|
||||
|
|
|
@ -15,6 +15,9 @@ class SimpleFinAccount < ApplicationRecord
|
|||
validates :external_id, presence: true, uniqueness: true
|
||||
validates :simple_fin_connection_id, presence: true
|
||||
|
||||
after_destroy :cleanup_connection_if_orphaned
|
||||
|
||||
|
||||
class << self
|
||||
def find_or_create_from_simple_fin_data!(sf_account_data, sfc)
|
||||
sfc.simple_fin_accounts.find_or_create_by!(external_id: sf_account_data["id"]) do |sfa|
|
||||
|
@ -35,66 +38,129 @@ class SimpleFinAccount < ApplicationRecord
|
|||
# sfa.sf_subtype = sf_account_data["name"]&.include?("Credit") ? "Credit Card" : accountable_klass.name
|
||||
end
|
||||
end
|
||||
def family
|
||||
simple_fin_connection&.family
|
||||
end
|
||||
end
|
||||
|
||||
# sf_account_data is a hash from Provider::SimpleFin#get_available_accounts
|
||||
def sync_account_data!(sf_account_data)
|
||||
# Ensure accountable_attributes has the ID for updates
|
||||
accountable_attributes = { id: account.accountable_id }
|
||||
# 'account' here refers to self.account (the associated Account instance)
|
||||
accountable_attributes = { id: self.account.accountable_id }
|
||||
|
||||
# Example: Update specific accountable types like PlaidAccount does
|
||||
# This will depend on the structure of sf_account_data and your Accountable models
|
||||
# case account.accountable_type
|
||||
# when "CreditCard"
|
||||
# accountable_attributes.merge!(
|
||||
# # minimum_payment: sf_account_data.dig("credit_details", "minimum_payment"),
|
||||
# # apr: sf_account_data.dig("credit_details", "apr")
|
||||
# )
|
||||
# when "Loan"
|
||||
# accountable_attributes.merge!(
|
||||
# # interest_rate: sf_account_data.dig("loan_details", "interest_rate")
|
||||
# )
|
||||
# end
|
||||
|
||||
update!(
|
||||
self.update!(
|
||||
current_balance: sf_account_data["balance"].to_d,
|
||||
available_balance: sf_account_data["available-balance"]&.to_d,
|
||||
currency: sf_account_data["currency"],
|
||||
# sf_type: derive_sf_type(sf_account_data), # Potentially update type/subtype
|
||||
# sf_subtype: derive_sf_subtype(sf_account_data),
|
||||
simple_fin_errors: sf_account_data["errors"] || [], # Assuming errors might come on account data
|
||||
# simple_fin_errors: sf_account_data["errors"] || [],
|
||||
account_attributes: {
|
||||
id: account.id,
|
||||
id: self.account.id,
|
||||
balance: sf_account_data["balance"].to_d,
|
||||
# cash_balance: derive_sf_cash_balance(sf_account_data), # If applicable
|
||||
last_synced_at: Time.current,
|
||||
accountable_attributes: accountable_attributes
|
||||
}
|
||||
)
|
||||
|
||||
# Sync transactions if present in the data
|
||||
if sf_account_data["transactions"].is_a?(Array)
|
||||
sync_transactions!(sf_account_data["transactions"])
|
||||
end
|
||||
|
||||
# Sync holdings if present in the data and it's an investment account
|
||||
if self.account&.investment? && sf_account_data["holdings"].is_a?(Array)
|
||||
sync_holdings!(sf_account_data["holdings"])
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Implement if SimpleFIN provides investment transactions/holdings
|
||||
# def sync_investments!(transactions:, holdings:, securities:)
|
||||
# # Similar to PlaidInvestmentSync.new(self).sync!(...)
|
||||
# end
|
||||
# sf_holdings_data is an array of holding hashes from SimpleFIN for this specific account
|
||||
def sync_holdings!(sf_holdings_data)
|
||||
# 'account' here refers to self.account
|
||||
return unless self.account.present? && self.account.investment? && sf_holdings_data.is_a?(Array)
|
||||
Rails.logger.info "SimpleFinAccount (#{self.account.id}): Entering sync_holdings! with #{sf_holdings_data.length} items."
|
||||
|
||||
# TODO: Implement if SimpleFIN provides transactions
|
||||
# def sync_transactions!(added:, modified:, removed:)
|
||||
# # Similar to PlaidAccount's sync_transactions!
|
||||
# end
|
||||
# Get existing SimpleFIN holding IDs for this account to detect deletions
|
||||
existing_provider_holding_ids = self.account.holdings.where.not(simple_fin_holding_id: nil).pluck(:simple_fin_holding_id)
|
||||
current_provider_holding_ids = sf_holdings_data.map { |h_data| h_data["id"] }
|
||||
|
||||
def family
|
||||
simple_fin_connection&.family
|
||||
# Delete holdings that are no longer present in SimpleFIN's data
|
||||
holdings_to_delete_ids = existing_provider_holding_ids - current_provider_holding_ids
|
||||
Rails.logger.info "SimpleFinAccount (#{self.account.id}): Will delete SF holding IDs: #{holdings_to_delete_ids}"
|
||||
self.account.holdings.where(simple_fin_holding_id: holdings_to_delete_ids).destroy_all
|
||||
|
||||
sf_holdings_data.each do |holding_data|
|
||||
# Find or create the Security based on the holding data
|
||||
security = find_or_create_security_from_holding_data(holding_data)
|
||||
next unless security # Skip if we can't determine a security
|
||||
|
||||
Rails.logger.info "SimpleFinAccount (#{self.account.id}): Processing SF holding ID #{holding_data['id']}"
|
||||
existing_holding = self.account.holdings.find_or_initialize_by(
|
||||
security: security,
|
||||
date: Date.current,
|
||||
currency: holding_data["currency"]
|
||||
)
|
||||
|
||||
existing_holding.qty = holding_data["shares"]&.to_d
|
||||
existing_holding.price = holding_data["purchase_price"]&.to_d
|
||||
existing_holding.amount = holding_data["market_value"]&.to_d
|
||||
# Cost basis is at holding level, not per share
|
||||
# existing_holding.cost_basis = holding_data["cost_basis"]&.to_d
|
||||
existing_holding.save!
|
||||
end
|
||||
end
|
||||
|
||||
# sf_transactions_data is an array of transaction hashes from SimpleFIN for this specific account
|
||||
def sync_transactions!(sf_transactions_data)
|
||||
# 'account' here refers to self.account
|
||||
return unless self.account.present? && sf_transactions_data.is_a?(Array)
|
||||
|
||||
sf_transactions_data.each do |transaction_data|
|
||||
entry = self.account.entries.find_or_initialize_by(simple_fin_transaction_id: transaction_data["id"])
|
||||
|
||||
entry.assign_attributes(
|
||||
name: transaction_data["description"],
|
||||
amount: transaction_data["amount"].to_d,
|
||||
currency: self.account.currency,
|
||||
date: Time.at(transaction_data["posted"].to_i).to_date,
|
||||
source: "simple_fin"
|
||||
)
|
||||
|
||||
entry.entryable ||= Transaction.new
|
||||
unless entry.entryable.is_a?(Transaction)
|
||||
entry.entryable = Transaction.new
|
||||
end
|
||||
|
||||
entry.entryable.simple_fin_category = transaction_data.dig("extra", "category") if entry.entryable.respond_to?(:simple_fin_category=)
|
||||
|
||||
if entry.changed? || entry.entryable.changed? # Check if entryable also changed
|
||||
entry.save!
|
||||
else
|
||||
Rails.logger.info "SimpleFinAccount (#{self.account.id}): Entry for SF transaction ID #{transaction_data['id']} not changed, not saving."
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Helper to find or create a Security record based on SimpleFIN holding data
|
||||
# SimpleFIN data is less detailed than Plaid securities, often just providing symbol and description.
|
||||
def find_or_create_security_from_holding_data(holding_data)
|
||||
symbol = holding_data["symbol"]&.upcase
|
||||
description = holding_data["description"]
|
||||
|
||||
# We need at least a symbol or description to create/find a security
|
||||
return nil unless symbol.present? || description.present?
|
||||
|
||||
# Try finding by ticker first, then by name (description) if no ticker
|
||||
Security.find_or_create_by!(ticker: symbol) do |sec|
|
||||
sec.name = description if description.present?
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Example helper, if needed
|
||||
# def derive_sf_cash_balance(sf_balances)
|
||||
# if account.investment?
|
||||
# sf_balances["available-balance"]&.to_d || 0
|
||||
# else
|
||||
# sf_balances["balance"]&.to_d
|
||||
# end
|
||||
# end
|
||||
def cleanup_connection_if_orphaned
|
||||
# Reload the connection to get the most up-to-date count of associated accounts
|
||||
connection = simple_fin_connection.reload
|
||||
connection.destroy_later if connection.simple_fin_accounts.empty?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
</div>
|
||||
</header>
|
||||
|
||||
<% if @manual_accounts.empty? && @plaid_items.empty? %>
|
||||
<% if @manual_accounts.empty? && @plaid_items.empty? && @simple_fin_connections.empty? %>
|
||||
<%= render "empty" %>
|
||||
<% else %>
|
||||
<div class="space-y-2">
|
||||
|
@ -31,6 +31,10 @@
|
|||
<%= render @plaid_items.sort_by(&:created_at) %>
|
||||
<% end %>
|
||||
|
||||
<% if @simple_fin_connections.any? %>
|
||||
<%= render @simple_fin_connections.sort_by(&:created_at) %>
|
||||
<% end %>
|
||||
|
||||
<% if @manual_accounts.any? %>
|
||||
<%= render "accounts/index/manual_accounts", accounts: @manual_accounts %>
|
||||
<% end %>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<% end %>
|
||||
|
||||
<% if simple_fin_avail %>
|
||||
<%= link_to new_simple_fin_connection_path(accountable_type: @accountable_type), class: "flex items-center gap-4 w-full text-center text-primary focus:outline-hidden focus:bg-surface border border-transparent focus:border focus:border-gray-200 px-2 hover:bg-surface rounded-lg p-2" do %>
|
||||
<%= link_to new_simple_fin_path(accountable_type: @accountable_type), class: "flex items-center gap-4 w-full text-center text-primary focus:outline-hidden focus:bg-surface border border-transparent focus:border focus:border-gray-200 px-2 hover:bg-surface rounded-lg p-2" do %>
|
||||
<span class="flex w-8 h-8 shrink-0 grow-0 items-center justify-center rounded-lg bg-alpha-black-50 shadow-[inset_0_0_0_1px_rgba(0,0,0,0.02)]">
|
||||
<%= image_tag "simple-fin-logo.svg", class: "w-6 h-6" %>
|
||||
</span>
|
||||
|
|
|
@ -34,9 +34,7 @@
|
|||
<% end %>
|
||||
<% elsif account.simple_fin_account_id.present? %>
|
||||
<%# SimpleFIN information %>
|
||||
<%= image_tag "simple-fin-logo.svg", class: "h-6 w-auto", title: "Connected via SimpleFIN" %>
|
||||
<%# TODO: Add manual sync %>
|
||||
|
||||
<%= image_tag "simple-fin-logo.svg", class: "h-6 w-auto", title: "Connected via SimpleFIN. This account will auto refresh." %>
|
||||
<% else %>
|
||||
<%= icon(
|
||||
"refresh-cw",
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
) %>
|
||||
<% end %>
|
||||
|
||||
<% unless account.linked? %>
|
||||
<% if account.can_delete? %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: "Delete account",
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
<div class="text-center text-gray-500 py-8">
|
||||
<p>No accounts found matching this type from SimpleFIN.</p>
|
||||
<p>Please ensure your SimpleFIN subscription is active.</p>
|
||||
<%= link_to "Try Again", new_simple_fin_connection_path(accountable_type: @accountable_type), class: "mt-4 inline-block text-primary hover:underline" %>
|
||||
<%= link_to "Try Again", new_simple_fin_path(accountable_type: @accountable_type), class: "mt-4 inline-block text-primary hover:underline" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= styled_form_with url: simple_fin_connections_path(accountable_type: @accountable_type), method: :post, data: { turbo: false } do |form| %>
|
||||
<%= styled_form_with url: simple_fin_index_path(accountable_type: @accountable_type), method: :post, data: { turbo: false } do |form| %>
|
||||
<%# Render each account option parsed from SimpleFIN %>
|
||||
<% @simple_fin_accounts.each_with_index do |account, index| %>
|
||||
<div class="mb-4">
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
<%# locals: (simple_fin_connection:) %>
|
||||
|
||||
<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="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<% if simple_fin_connection.logo.attached? %>
|
||||
<%= image_tag simple_fin_connection.logo, class: "h-8 w-8 rounded-full" %>
|
||||
<% else %>
|
||||
<div>
|
||||
<%= simple_fin_connection.institution_name&.first || "?" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<h3 >
|
||||
<%= simple_fin_connection.institution_name || "SimpleFIN Connection" %>
|
||||
</h3>
|
||||
</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>
|
||||
|
||||
<ul role="list" class="divide-y divide-gray-200">
|
||||
<% simple_fin_connection.accounts.includes(:accountable, :logo_attachment).active.alphabetically.each do |account| %>
|
||||
<%= render "accounts/account", account: account %>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
|
@ -6,5 +6,8 @@ Rails.application.configure do
|
|||
if ENV["SIMPLE_FIN_ACCESS_URL"].present?
|
||||
config.simple_fin = OpenStruct.new()
|
||||
config.simple_fin["ACCESS_URL"] = ENV["SIMPLE_FIN_ACCESS_URL"]
|
||||
config.simple_fin["UPDATE_CRON"] = ENV["SIMPLE_FIN_UPDATE_CRON"]
|
||||
# Fallback
|
||||
config.simple_fin["UPDATE_CRON"] = "0 6 * * *" if config.simple_fin["UPDATE_CRON"].nil?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,3 +6,4 @@ en:
|
|||
create:
|
||||
accounts_created_success: "Accounts Successfully Created"
|
||||
fetch_failed: "Failed to fetch accounts"
|
||||
no_acc_selected: "No accounts were selected to add"
|
||||
|
|
|
@ -201,14 +201,8 @@ Rails.application.routes.draw do
|
|||
end
|
||||
|
||||
# SimpleFIN routes
|
||||
namespace :simple_fin do
|
||||
resources :connections, only: [ :new, :create, :sync ], controller: "/simple_fin"
|
||||
end
|
||||
|
||||
# SimpleFIN routes (controller: SimpleFinController)
|
||||
get "simple_fin/select_accounts", to: "simple_fin#select_accounts", as: "select_simple_fin_accounts"
|
||||
# Defines POST /simple_fin/create_selected_accounts -> simple_fin#create_selected_accounts
|
||||
post "simple_fin/create_selected_accounts", to: "simple_fin#create_selected_accounts", as: "create_selected_simple_fin_accounts"
|
||||
resources :simple_fin, only: %i[create new] do
|
||||
end
|
||||
|
||||
|
||||
namespace :webhooks do
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
class SetupSimpleFinIntegration < ActiveRecord::Migration[7.2]
|
||||
class SimpleFinIntegration < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
create_table :simple_fin_connections do |t|
|
||||
t.references :family, null: false, foreign_key: true, type: :uuid
|
||||
t.string :name
|
||||
t.string :institution_id
|
||||
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_name
|
||||
t.string :institution_url
|
||||
t.string :institution_domain
|
||||
t.string :status, default: "good"
|
||||
t.string :status, default: "good" # e.g., good, requires_update
|
||||
t.datetime :last_synced_at
|
||||
t.boolean :scheduled_for_deletion, default: false
|
||||
t.string :api_versions_supported, array: true, default: []
|
||||
|
@ -23,11 +23,21 @@ class SetupSimpleFinIntegration < ActiveRecord::Migration[7.2]
|
|||
t.string :currency
|
||||
t.string :sf_type
|
||||
t.string :sf_subtype
|
||||
t.string :simple_fin_errors, array: true, default: []
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
add_index :simple_fin_accounts, [ :simple_fin_connection_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_column :entries, :simple_fin_transaction_id, :string
|
||||
add_index :entries, :simple_fin_transaction_id, unique: true, where: "simple_fin_transaction_id IS NOT NULL"
|
||||
add_column :entries, :source, :string
|
||||
add_column :transactions, :simple_fin_category, :string
|
||||
|
||||
add_column :holdings, :simple_fin_holding_id, :string
|
||||
add_index :holdings, :simple_fin_holding_id, unique: true, where: "simple_fin_holding_id IS NOT NULL"
|
||||
add_column :holdings, :source, :string
|
||||
end
|
||||
end
|
8
db/schema.rb
generated
8
db/schema.rb
generated
|
@ -202,8 +202,11 @@ ActiveRecord::Schema[7.2].define(version: 2025_05_09_134646) do
|
|||
t.boolean "excluded", default: false
|
||||
t.string "plaid_id"
|
||||
t.jsonb "locked_attributes", default: {}
|
||||
t.string "simple_fin_transaction_id"
|
||||
t.string "source"
|
||||
t.index ["account_id"], name: "index_entries_on_account_id"
|
||||
t.index ["import_id"], name: "index_entries_on_import_id"
|
||||
t.index ["simple_fin_transaction_id"], name: "index_entries_on_simple_fin_transaction_id", unique: true, where: "(simple_fin_transaction_id IS NOT NULL)"
|
||||
end
|
||||
|
||||
create_table "exchange_rates", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
|
@ -243,9 +246,12 @@ ActiveRecord::Schema[7.2].define(version: 2025_05_09_134646) do
|
|||
t.string "currency", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "simple_fin_holding_id"
|
||||
t.string "source"
|
||||
t.index ["account_id", "security_id", "date", "currency"], name: "idx_on_account_id_security_id_date_currency_5323e39f8b", unique: true
|
||||
t.index ["account_id"], name: "index_holdings_on_account_id"
|
||||
t.index ["security_id"], name: "index_holdings_on_security_id"
|
||||
t.index ["simple_fin_holding_id"], name: "index_holdings_on_simple_fin_holding_id", unique: true, where: "(simple_fin_holding_id IS NOT NULL)"
|
||||
end
|
||||
|
||||
create_table "impersonation_session_logs", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
|
@ -561,6 +567,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_05_09_134646) do
|
|||
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 "updated_at", null: false
|
||||
t.index ["simple_fin_connection_id", "external_id"], name: "index_sfa_on_sfc_id_and_external_id", unique: true
|
||||
|
@ -685,6 +692,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_05_09_134646) do
|
|||
t.jsonb "locked_attributes", default: {}
|
||||
t.string "plaid_category"
|
||||
t.string "plaid_category_detailed"
|
||||
t.string "simple_fin_category"
|
||||
t.index ["category_id"], name: "index_transactions_on_category_id"
|
||||
t.index ["merchant_id"], name: "index_transactions_on_merchant_id"
|
||||
end
|
||||
|
|
|
@ -17,7 +17,38 @@
|
|||
"balance": "-250.75",
|
||||
"available-balance": "5000.00",
|
||||
"balance-date": 1700000001,
|
||||
"transactions": [],
|
||||
"transactions": [
|
||||
{
|
||||
"id": "TRN-CHASE-CREDIT-001-A",
|
||||
"posted": 1699910000,
|
||||
"amount": "-25.50",
|
||||
"description": "Corner Cafe Lunch",
|
||||
"pending": false,
|
||||
"extra": {
|
||||
"category": "Food & Dining"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRN-CHASE-CREDIT-001-B",
|
||||
"posted": 1699810000,
|
||||
"amount": "-75.00",
|
||||
"description": "Online Shopping - Books",
|
||||
"pending": true,
|
||||
"extra": {
|
||||
"category": "Shopping"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRN-CHASE-CREDIT-001-C",
|
||||
"posted": 1699710000,
|
||||
"amount": "100.00",
|
||||
"description": "Payment Received - Thank You!",
|
||||
"pending": false,
|
||||
"extra": {
|
||||
"category": "Payments"
|
||||
}
|
||||
}
|
||||
],
|
||||
"holdings": []
|
||||
},
|
||||
{
|
||||
|
@ -34,7 +65,28 @@
|
|||
"balance": "-150320.90",
|
||||
"available-balance": "0.00",
|
||||
"balance-date": 1700000002,
|
||||
"transactions": [],
|
||||
"transactions": [
|
||||
{
|
||||
"id": "TRN-CHASE-LOAN-002-A",
|
||||
"posted": 1699510000,
|
||||
"amount": "-1200.00",
|
||||
"description": "Monthly Loan Payment",
|
||||
"pending": false,
|
||||
"extra": {
|
||||
"category": "Loan Payments"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRN-CHASE-LOAN-002-B",
|
||||
"posted": 1699410000,
|
||||
"amount": "-5.20",
|
||||
"description": "Interest Charge",
|
||||
"pending": false,
|
||||
"extra": {
|
||||
"category": "Fees & Charges"
|
||||
}
|
||||
}
|
||||
],
|
||||
"holdings": []
|
||||
},
|
||||
{
|
||||
|
@ -255,7 +307,28 @@
|
|||
"balance": "1234.56",
|
||||
"available-balance": "1200.00",
|
||||
"balance-date": 1700000007,
|
||||
"transactions": [],
|
||||
"transactions": [
|
||||
{
|
||||
"id": "TRN-WPCU-SAVINGS-007-A",
|
||||
"posted": 1699310000,
|
||||
"amount": "50.00",
|
||||
"description": "Transfer from Checking",
|
||||
"pending": false,
|
||||
"extra": {
|
||||
"category": "Transfers"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRN-WPCU-SAVINGS-007-B",
|
||||
"posted": 1699210000,
|
||||
"amount": "0.25",
|
||||
"description": "Monthly Interest Earned",
|
||||
"pending": false,
|
||||
"extra": {
|
||||
"category": "Interest Income"
|
||||
}
|
||||
}
|
||||
],
|
||||
"holdings": []
|
||||
},
|
||||
{
|
||||
|
@ -272,7 +345,38 @@
|
|||
"balance": "3456.78",
|
||||
"available-balance": "3456.78",
|
||||
"balance-date": 1700000008,
|
||||
"transactions": [],
|
||||
"transactions": [
|
||||
{
|
||||
"id": "TRN-WPCU-CHECKING-008-A",
|
||||
"posted": 1699110000,
|
||||
"amount": "-45.99",
|
||||
"description": "Grocery Store Run",
|
||||
"pending": false,
|
||||
"extra": {
|
||||
"category": "Groceries"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRN-WPCU-CHECKING-008-B",
|
||||
"posted": 1699010000,
|
||||
"amount": "-12.50",
|
||||
"description": "Coffee Shop Visit",
|
||||
"pending": true,
|
||||
"extra": {
|
||||
"category": "Food & Dining"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRN-WPCU-CHECKING-008-C",
|
||||
"posted": 1698910000,
|
||||
"amount": "1500.00",
|
||||
"description": "Direct Deposit - Paycheck",
|
||||
"pending": false,
|
||||
"extra": {
|
||||
"category": "Income"
|
||||
}
|
||||
}
|
||||
],
|
||||
"holdings": []
|
||||
},
|
||||
{
|
||||
|
@ -289,7 +393,18 @@
|
|||
"balance": "25000.00",
|
||||
"available-balance": "24900.00",
|
||||
"balance-date": 1700000009,
|
||||
"transactions": [],
|
||||
"transactions": [
|
||||
{
|
||||
"id": "TRN-WPCU-MMA-009-A",
|
||||
"posted": 1698810000,
|
||||
"amount": "200.00",
|
||||
"description": "Transfer to Savings",
|
||||
"pending": false,
|
||||
"extra": {
|
||||
"category": "Transfers"
|
||||
}
|
||||
}
|
||||
],
|
||||
"holdings": []
|
||||
},
|
||||
{
|
||||
|
@ -309,6 +424,34 @@
|
|||
"transactions": [],
|
||||
"holdings": []
|
||||
},
|
||||
{
|
||||
"org": {
|
||||
"domain": "www.discover.com",
|
||||
"name": "Discover Credit Card",
|
||||
"sfin-url": "https://beta-bridge.simplefin.org/simplefin",
|
||||
"url": "https://www.discover.com",
|
||||
"id": "www.discover.com"
|
||||
},
|
||||
"id": "ACT-RANDOM-DISCOVER-CREDIT-CARD",
|
||||
"name": "Discover Credit Card",
|
||||
"currency": "USD",
|
||||
"balance": "-10000",
|
||||
"available-balance": "0.00",
|
||||
"balance-date": 1746720778,
|
||||
"transactions": [
|
||||
{
|
||||
"id": "TRN-TOYOTA-LOAN-010-A",
|
||||
"posted": 1698710000,
|
||||
"amount": "-550.75",
|
||||
"description": "Car Payment - November",
|
||||
"pending": false,
|
||||
"extra": {
|
||||
"category": "Auto Payment"
|
||||
}
|
||||
}
|
||||
],
|
||||
"holdings": []
|
||||
},
|
||||
{
|
||||
"org": {
|
||||
"domain": "www.discover.com",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue