mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-18 20:59:39 +02:00
parent
e8e100e1d8
commit
263d65ea7e
24 changed files with 146 additions and 73 deletions
|
@ -79,6 +79,6 @@ class AccountsController < ApplicationController
|
|||
end
|
||||
|
||||
def account_params
|
||||
params.require(:account).permit(:name, :accountable_type, :balance, :start_date, :start_balance, :currency, :subtype, :is_active, :institution_id)
|
||||
params.require(:account).permit(:name, :accountable_type, :mode, :balance, :start_date, :start_balance, :currency, :subtype, :is_active, :institution_id)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -27,7 +27,7 @@ class CreditCardsController < ApplicationController
|
|||
def account_params
|
||||
params.require(:account)
|
||||
.permit(
|
||||
:name, :balance, :institution_id, :start_date, :start_balance, :currency, :accountable_type,
|
||||
:name, :balance, :institution_id, :mode, :start_date, :start_balance, :currency, :accountable_type,
|
||||
accountable_attributes: [
|
||||
:id,
|
||||
:available_credit,
|
||||
|
|
|
@ -27,7 +27,7 @@ class LoansController < ApplicationController
|
|||
def account_params
|
||||
params.require(:account)
|
||||
.permit(
|
||||
:name, :balance, :institution_id, :start_date, :start_balance, :currency, :accountable_type,
|
||||
:name, :balance, :institution_id, :start_date, :mode, :start_balance, :currency, :accountable_type,
|
||||
accountable_attributes: [
|
||||
:id,
|
||||
:rate_type,
|
||||
|
|
|
@ -27,7 +27,7 @@ class PropertiesController < ApplicationController
|
|||
def account_params
|
||||
params.require(:account)
|
||||
.permit(
|
||||
:name, :balance, :institution_id, :start_date, :start_balance, :currency, :accountable_type,
|
||||
:name, :balance, :institution_id, :start_date, :mode, :start_balance, :currency, :accountable_type,
|
||||
accountable_attributes: [
|
||||
:id,
|
||||
:year_built,
|
||||
|
|
|
@ -27,7 +27,7 @@ class VehiclesController < ApplicationController
|
|||
def account_params
|
||||
params.require(:account)
|
||||
.permit(
|
||||
:name, :balance, :institution_id, :start_date, :start_balance, :currency, :accountable_type,
|
||||
:name, :balance, :institution_id, :start_date, :mode, :start_balance, :currency, :accountable_type,
|
||||
accountable_attributes: [
|
||||
:id,
|
||||
:make,
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
class Account < ApplicationRecord
|
||||
VALUE_MODES = %w[balance transactions]
|
||||
|
||||
include Syncable, Monetizable, Issuable
|
||||
|
||||
validates :name, :balance, :currency, presence: true
|
||||
validates :mode, inclusion: { in: VALUE_MODES }, allow_nil: true
|
||||
|
||||
belongs_to :family
|
||||
belongs_to :institution, optional: true
|
||||
|
|
|
@ -23,11 +23,11 @@ class Account::Balance::Calculator
|
|||
attr_reader :account, :sync_start_date
|
||||
|
||||
def find_start_balance_for_partial_sync
|
||||
account.balances.find_by(currency: account.currency, date: sync_start_date - 1.day).balance
|
||||
account.balances.find_by(currency: account.currency, date: sync_start_date - 1.day)&.balance
|
||||
end
|
||||
|
||||
def find_start_balance_for_full_sync(cached_entries)
|
||||
account.balance + net_entry_flows(cached_entries)
|
||||
account.balance + net_entry_flows(cached_entries.select { |e| e.account_transaction? })
|
||||
end
|
||||
|
||||
def calculate_balance_for_date(date, entries:, prior_balance:)
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
<%= styled_form_with model: account, url: url, scope: :account, class: "flex flex-col gap-4 justify-between grow", data: { turbo: false } do |f| %>
|
||||
<div class="grow space-y-2">
|
||||
<% unless account.new_record? %>
|
||||
<%= f.select :mode, Account::VALUE_MODES.map { |mode| [mode.titleize, mode] }, { label: t(".mode"), prompt: t(".mode_prompt") } %>
|
||||
<% end %>
|
||||
|
||||
<%= f.select :accountable_type, Accountable::TYPES.map { |type| [type.titleize, type] }, { label: t(".accountable_type"), prompt: t(".type_prompt") }, required: true, autofocus: true %>
|
||||
<%= f.text_field :name, placeholder: t(".name_placeholder"), required: "required", label: t(".name_label") %>
|
||||
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
<div class="flex items-center gap-2 rounded-xl justify-between shadow-xs bg-white p-4 border border-alpha-black-25">
|
||||
<p class="text-lg font-medium text-gray-900">Setup your new account</p>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<%= link_to "Track balances only", new_account_valuation_path(@account), class: "btn btn--ghost", data: { turbo_frame: dom_id(@account.entries.account_valuations.new) } %>
|
||||
<%= link_to "Add your first transaction", new_transaction_path(account_id: @account.id), class: "btn btn--primary", data: { turbo_frame: :modal } %>
|
||||
</div>
|
||||
</div>
|
|
@ -1,7 +1,13 @@
|
|||
<%# locals: (account:) %>
|
||||
<%# locals: (account:, selected_tab:) %>
|
||||
|
||||
<% if account.transactions.any? %>
|
||||
<%= render "accounts/accountables/transactions", account: account %>
|
||||
<% if account.mode.nil? %>
|
||||
<%= render "accounts/accountables/value_onboarding", account: account %>
|
||||
<% else %>
|
||||
<%= render "accounts/accountables/valuations", account: account %>
|
||||
<div class="min-h-[800px]">
|
||||
<% if account.mode == "transactions" %>
|
||||
<%= render "accounts/accountables/transactions", account: account %>
|
||||
<% else %>
|
||||
<%= render "accounts/accountables/valuations", account: account %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
16
app/views/accounts/accountables/_value_onboarding.html.erb
Normal file
16
app/views/accounts/accountables/_value_onboarding.html.erb
Normal file
|
@ -0,0 +1,16 @@
|
|||
<%# locals: (account:) %>
|
||||
|
||||
<div data-test-id="value-onboarding" class="py-12 flex flex-col justify-center items-center bg-white rounded-lg border border-alpha-black-25 shadow-xs">
|
||||
<h3 class="font-medium text-lg mb-2">How would you like to track value for this account?</h3>
|
||||
<p class="text-sm text-gray-500 mb-8">We will use this to determine what data to show for this account.</p>
|
||||
<div class="flex items-center gap-4">
|
||||
<%= button_to account_path(account, { account: { mode: "balance" } }), method: :put, class: "btn btn--outline", data: { controller: "tooltip", turbo: false } do %>
|
||||
<%= render partial: "shared/text_tooltip", locals: { tooltip_text: "Choose this if you only need to track the historical value of this account over time and do not plan on importing any transactions." } %>
|
||||
<span>Balance only</span>
|
||||
<% end %>
|
||||
<%= button_to account_path(account, { account: { mode: "transactions" } }), method: :put, class: "btn btn--primary", data: { controller: "tooltip", turbo: false } do %>
|
||||
<%= render partial: "shared/text_tooltip", locals: { tooltip_text: "Choose this if you plan on importing transactions into this account for budgeting and other analytics." } %>
|
||||
<span>Transactions</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
|
@ -1,15 +1,26 @@
|
|||
<%# locals: (account:, selected_tab:) %>
|
||||
|
||||
<div class="flex gap-2 text-sm text-gray-900 font-medium mb-4">
|
||||
<%= render "accounts/accountables/tab", account: account, key: "overview", is_selected: selected_tab.in?([nil, "overview"]) %>
|
||||
<%= render "accounts/accountables/tab", account: account, key: "transactions", is_selected: selected_tab == "transactions" %>
|
||||
</div>
|
||||
<% if account.mode.nil? %>
|
||||
<%= render "accounts/accountables/value_onboarding", account: account %>
|
||||
<% else %>
|
||||
<div class="flex gap-2 text-sm text-gray-900 font-medium mb-4">
|
||||
<%= render "accounts/accountables/tab", account: account, key: "overview", is_selected: selected_tab.in?([nil, "overview"]) %>
|
||||
|
||||
<div class="min-h-[800px]">
|
||||
<% case selected_tab %>
|
||||
<% when nil, "overview" %>
|
||||
<%= render "accounts/accountables/credit_card/overview", account: account %>
|
||||
<% when "transactions" %>
|
||||
<%= render "accounts/accountables/transactions", account: account %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if account.mode == "transactions" %>
|
||||
<%= render "accounts/accountables/tab", account: account, key: "transactions", is_selected: selected_tab == "transactions" %>
|
||||
<% else %>
|
||||
<%= render "accounts/accountables/tab", account: account, key: "value", is_selected: selected_tab == "value" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="min-h-[800px]">
|
||||
<% case selected_tab %>
|
||||
<% when nil, "overview" %>
|
||||
<%= render "accounts/accountables/credit_card/overview", account: account %>
|
||||
<% when "transactions" %>
|
||||
<%= render "accounts/accountables/transactions", account: account %>
|
||||
<% when "value" %>
|
||||
<%= render "accounts/accountables/valuations", account: account %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
@ -1 +1 @@
|
|||
<%= render "accounts/accountables/default_tabs", account: account %>
|
||||
<%= render "accounts/accountables/default_tabs", account: account, selected_tab: selected_tab %>
|
||||
|
|
|
@ -1 +1 @@
|
|||
<%= render "accounts/accountables/default_tabs", account: account %>
|
||||
<%= render "accounts/accountables/default_tabs", account: account, selected_tab: selected_tab %>
|
||||
|
|
|
@ -1,28 +1,34 @@
|
|||
<%# locals: (account:, selected_tab:) %>
|
||||
|
||||
<% if account.entries.account_trades.any? || account.entries.account_transactions.any? %>
|
||||
<% if account.mode.nil? %>
|
||||
<%= render "accounts/accountables/value_onboarding", account: account %>
|
||||
<% else %>
|
||||
<div class="flex gap-2 text-sm text-gray-900 font-medium mb-4">
|
||||
<%= render "accounts/accountables/tab", account: account, key: "holdings", is_selected: selected_tab.in?([nil, "holdings"]) %>
|
||||
<%= render "accounts/accountables/tab", account: account, key: "cash", is_selected: selected_tab == "cash" %>
|
||||
<%= render "accounts/accountables/tab", account: account, key: "transactions", is_selected: selected_tab == "transactions" %>
|
||||
<% if account.mode == "transactions" %>
|
||||
<%= render "accounts/accountables/tab", account: account, key: "holdings", is_selected: selected_tab.in?([nil, "holdings"]) %>
|
||||
<%= render "accounts/accountables/tab", account: account, key: "cash", is_selected: selected_tab == "cash" %>
|
||||
<%= render "accounts/accountables/tab", account: account, key: "transactions", is_selected: selected_tab == "transactions" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="min-h-[800px]">
|
||||
<% case selected_tab %>
|
||||
<% when nil, "holdings" %>
|
||||
<%= turbo_frame_tag dom_id(account, :holdings), src: account_holdings_path(account) do %>
|
||||
<%= render "account/entries/loading" %>
|
||||
<% end %>
|
||||
<% when "cash" %>
|
||||
<%= turbo_frame_tag dom_id(account, :cash), src: account_cashes_path(account) do %>
|
||||
<%= render "account/entries/loading" %>
|
||||
<% end %>
|
||||
<% when "transactions" %>
|
||||
<%= turbo_frame_tag dom_id(account, :trades), src: account_trades_path(account) do %>
|
||||
<%= render "account/entries/loading" %>
|
||||
<% if account.mode == "transactions" %>
|
||||
<% case selected_tab %>
|
||||
<% when nil, "holdings" %>
|
||||
<%= turbo_frame_tag dom_id(account, :holdings), src: account_holdings_path(account) do %>
|
||||
<%= render "account/entries/loading" %>
|
||||
<% end %>
|
||||
<% when "cash" %>
|
||||
<%= turbo_frame_tag dom_id(account, :cash), src: account_cashes_path(account) do %>
|
||||
<%= render "account/entries/loading" %>
|
||||
<% end %>
|
||||
<% when "transactions" %>
|
||||
<%= turbo_frame_tag dom_id(account, :trades), src: account_trades_path(account) do %>
|
||||
<%= render "account/entries/loading" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= render "accounts/accountables/valuations", account: account %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= render "accounts/accountables/valuations", account: account %>
|
||||
<% end %>
|
||||
|
|
|
@ -1,15 +1,25 @@
|
|||
<%# locals: (account:, selected_tab:) %>
|
||||
|
||||
<div class="flex gap-2 text-sm text-gray-900 font-medium mb-4">
|
||||
<%= render "accounts/accountables/tab", account: account, key: "overview", is_selected: selected_tab.in?([nil, "overview"]) %>
|
||||
<%= render "accounts/accountables/tab", account: account, key: "value", is_selected: selected_tab == "value" %>
|
||||
</div>
|
||||
<% if account.mode.nil? %>
|
||||
<%= render "accounts/accountables/value_onboarding", account: account %>
|
||||
<% else %>
|
||||
<div class="flex gap-2 text-sm text-gray-900 font-medium mb-4">
|
||||
<%= render "accounts/accountables/tab", account: account, key: "overview", is_selected: selected_tab.in?([nil, "overview"]) %>
|
||||
<% if account.mode == "transactions" %>
|
||||
<%= render "accounts/accountables/tab", account: account, key: "transactions", is_selected: selected_tab == "transactions" %>
|
||||
<% else %>
|
||||
<%= render "accounts/accountables/tab", account: account, key: "value", is_selected: selected_tab == "value" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="min-h-[800px]">
|
||||
<% case selected_tab %>
|
||||
<% when nil, "overview" %>
|
||||
<%= render "accounts/accountables/loan/overview", account: account %>
|
||||
<% when "value" %>
|
||||
<%= render "accounts/accountables/valuations", account: account %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="min-h-[800px]">
|
||||
<% case selected_tab %>
|
||||
<% when nil, "overview" %>
|
||||
<%= render "accounts/accountables/loan/overview", account: account %>
|
||||
<% when "transactions" %>
|
||||
<%= render "accounts/accountables/transactions", account: account %>
|
||||
<% when "value" %>
|
||||
<%= render "accounts/accountables/valuations", account: account %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
@ -16,10 +16,6 @@
|
|||
</div>
|
||||
</header>
|
||||
|
||||
<% if @account.entries.empty? && @account.depository? %>
|
||||
<%= render "accounts/new_account_setup_bar", account: @account %>
|
||||
<% end %>
|
||||
|
||||
<% if @account.highest_priority_issue %>
|
||||
<%= render partial: "issues/issue", locals: { issue: @account.highest_priority_issue } %>
|
||||
<% end %>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<div role="tooltip" data-tooltip-target="tooltip" class="tooltip bg-gray-700 text-sm px-1.5 py-1 rounded-md">
|
||||
<div class="text-white font-normal">
|
||||
<div class="text-white font-normal max-w-[200px]">
|
||||
<%= tooltip_text %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -19,6 +19,8 @@ en:
|
|||
ungrouped: "(none)"
|
||||
balance: Today's balance
|
||||
accountable_type: Account type
|
||||
mode: Value tracking mode
|
||||
mode_prompt: Select a mode
|
||||
type_prompt: Select a type
|
||||
header:
|
||||
accounts: Accounts
|
||||
|
|
5
db/migrate/20241018201653_add_account_mode.rb
Normal file
5
db/migrate/20241018201653_add_account_mode.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
class AddAccountMode < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
add_column :accounts, :mode, :string
|
||||
end
|
||||
end
|
6
db/schema.rb
generated
6
db/schema.rb
generated
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.2].define(version: 2024_10_17_204250) do
|
||||
ActiveRecord::Schema[7.2].define(version: 2024_10_18_201653) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pgcrypto"
|
||||
enable_extension "plpgsql"
|
||||
|
@ -19,7 +19,6 @@ ActiveRecord::Schema[7.2].define(version: 2024_10_17_204250) do
|
|||
# Note that some types may not work with other database engines. Be careful if changing database.
|
||||
create_enum "account_status", ["ok", "syncing", "error"]
|
||||
create_enum "import_status", ["pending", "importing", "complete", "failed"]
|
||||
create_enum "user_role", ["admin", "member", "super_admin"]
|
||||
|
||||
create_table "account_balances", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
t.uuid "account_id", null: false
|
||||
|
@ -122,6 +121,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_10_17_204250) do
|
|||
t.uuid "institution_id"
|
||||
t.virtual "classification", type: :string, as: "\nCASE\n WHEN ((accountable_type)::text = ANY (ARRAY[('Loan'::character varying)::text, ('CreditCard'::character varying)::text, ('OtherLiability'::character varying)::text])) THEN 'liability'::text\n ELSE 'asset'::text\nEND", stored: true
|
||||
t.uuid "import_id"
|
||||
t.string "mode"
|
||||
t.index ["accountable_id", "accountable_type"], name: "index_accounts_on_accountable_id_and_accountable_type"
|
||||
t.index ["accountable_type"], name: "index_accounts_on_accountable_type"
|
||||
t.index ["family_id", "accountable_type"], name: "index_accounts_on_family_id_and_accountable_type"
|
||||
|
@ -535,7 +535,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_10_17_204250) do
|
|||
t.datetime "updated_at", null: false
|
||||
t.string "last_prompted_upgrade_commit_sha"
|
||||
t.string "last_alerted_upgrade_commit_sha"
|
||||
t.enum "role", default: "member", null: false, enum_type: "user_role"
|
||||
t.string "role", default: "member", null: false
|
||||
t.boolean "active", default: true, null: false
|
||||
t.index ["email"], name: "index_users_on_email", unique: true
|
||||
t.index ["family_id"], name: "index_users_on_family_id"
|
||||
|
|
8
test/fixtures/accounts.yml
vendored
8
test/fixtures/accounts.yml
vendored
|
@ -5,6 +5,7 @@ other_asset:
|
|||
currency: USD
|
||||
accountable_type: OtherAsset
|
||||
accountable: one
|
||||
mode: balance
|
||||
|
||||
other_liability:
|
||||
family: dylan_family
|
||||
|
@ -13,6 +14,7 @@ other_liability:
|
|||
currency: USD
|
||||
accountable_type: OtherLiability
|
||||
accountable: one
|
||||
mode: balance
|
||||
|
||||
depository:
|
||||
family: dylan_family
|
||||
|
@ -22,6 +24,7 @@ depository:
|
|||
accountable_type: Depository
|
||||
accountable: one
|
||||
institution: chase
|
||||
mode: transactions
|
||||
|
||||
credit_card:
|
||||
family: dylan_family
|
||||
|
@ -31,6 +34,7 @@ credit_card:
|
|||
accountable_type: CreditCard
|
||||
accountable: one
|
||||
institution: chase
|
||||
mode: transactions
|
||||
|
||||
investment:
|
||||
family: dylan_family
|
||||
|
@ -39,6 +43,7 @@ investment:
|
|||
currency: USD
|
||||
accountable_type: Investment
|
||||
accountable: one
|
||||
mode: transactions
|
||||
|
||||
loan:
|
||||
family: dylan_family
|
||||
|
@ -47,6 +52,7 @@ loan:
|
|||
currency: USD
|
||||
accountable_type: Loan
|
||||
accountable: one
|
||||
mode: transactions
|
||||
|
||||
property:
|
||||
family: dylan_family
|
||||
|
@ -55,6 +61,7 @@ property:
|
|||
currency: USD
|
||||
accountable_type: Property
|
||||
accountable: one
|
||||
mode: transactions
|
||||
|
||||
vehicle:
|
||||
family: dylan_family
|
||||
|
@ -63,3 +70,4 @@ vehicle:
|
|||
currency: USD
|
||||
accountable_type: Vehicle
|
||||
accountable: one
|
||||
mode: transactions
|
||||
|
|
|
@ -35,7 +35,7 @@ class Account::Balance::SyncerTest < ActiveSupport::TestCase
|
|||
assert_equal [ 19600, 19500, 19500, 20000, 20000, 20000 ], @account.balances.chronological.map(&:balance)
|
||||
end
|
||||
|
||||
test "syncs account with valuations and transactions" do
|
||||
test "syncs account with valuations and transactions when valuation starts" do
|
||||
create_valuation(account: @account, date: 5.days.ago.to_date, amount: 20000)
|
||||
create_transaction(account: @account, date: 3.days.ago.to_date, amount: -500)
|
||||
create_transaction(account: @account, date: 2.days.ago.to_date, amount: 100)
|
||||
|
@ -47,6 +47,17 @@ class Account::Balance::SyncerTest < ActiveSupport::TestCase
|
|||
assert_equal [ 20000, 20000, 20500, 20400, 25000, 25000 ], @account.balances.chronological.map(&:balance)
|
||||
end
|
||||
|
||||
test "syncs account with valuations and transactions when transaction starts" do
|
||||
new_account = families(:empty).accounts.create!(name: "Test Account", balance: 1000, currency: "USD", accountable: Depository.new)
|
||||
create_transaction(account: new_account, date: 2.days.ago.to_date, amount: 250)
|
||||
create_valuation(account: new_account, date: Date.current, amount: 1000)
|
||||
|
||||
run_sync_for(new_account)
|
||||
|
||||
assert_equal 1000, new_account.balance
|
||||
assert_equal [ 1250, 1000, 1000, 1000 ], new_account.balances.chronological.map(&:balance)
|
||||
end
|
||||
|
||||
test "syncs account with transactions in multiple currencies" do
|
||||
ExchangeRate.create! date: 1.day.ago.to_date, from_currency: "EUR", to_currency: "USD", rate: 1.2
|
||||
|
||||
|
|
|
@ -96,7 +96,10 @@ class AccountsTest < ApplicationSystemTestCase
|
|||
visit accounts_url
|
||||
assert_text account_name
|
||||
|
||||
visit account_url(Account.order(:created_at).last)
|
||||
created_account = Account.order(:created_at).last
|
||||
created_account.update!(mode: "transactions")
|
||||
|
||||
visit account_url(created_account)
|
||||
|
||||
within "header" do
|
||||
find('button[data-menu-target="button"]').click
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue