1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-07-19 05:09:38 +02:00

Basic account onboarding (#1328)

* Basic account onboarding

* Cleanup
This commit is contained in:
Zach Gollwitzer 2024-10-18 17:18:54 -04:00 committed by GitHub
parent e8e100e1d8
commit 263d65ea7e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 146 additions and 73 deletions

View file

@ -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

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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

View file

@ -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:)

View file

@ -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") %>

View file

@ -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>

View file

@ -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 %>
<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 %>

View 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>

View file

@ -1,15 +1,26 @@
<%# locals: (account:, selected_tab:) %>
<div class="flex gap-2 text-sm text-gray-900 font-medium mb-4">
<% 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"]) %>
<%= render "accounts/accountables/tab", account: account, key: "transactions", is_selected: selected_tab == "transactions" %>
</div>
<div class="min-h-[800px]">
<% 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>
</div>
<% end %>

View file

@ -1 +1 @@
<%= render "accounts/accountables/default_tabs", account: account %>
<%= render "accounts/accountables/default_tabs", account: account, selected_tab: selected_tab %>

View file

@ -1 +1 @@
<%= render "accounts/accountables/default_tabs", account: account %>
<%= render "accounts/accountables/default_tabs", account: account, selected_tab: selected_tab %>

View file

@ -1,13 +1,18 @@
<%# 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">
<% 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]">
<% if account.mode == "transactions" %>
<% case selected_tab %>
<% when nil, "holdings" %>
<%= turbo_frame_tag dom_id(account, :holdings), src: account_holdings_path(account) do %>
@ -22,7 +27,8 @@
<%= render "account/entries/loading" %>
<% end %>
<% end %>
</div>
<% else %>
<% else %>
<%= render "accounts/accountables/valuations", account: account %>
<% end %>
</div>
<% end %>

View file

@ -1,15 +1,25 @@
<%# locals: (account:, selected_tab:) %>
<div class="flex gap-2 text-sm text-gray-900 font-medium mb-4">
<% 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" %>
</div>
<% end %>
</div>
<div class="min-h-[800px]">
<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>
</div>
<% end %>

View file

@ -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 %>

View file

@ -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>

View file

@ -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

View 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
View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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