1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-07-22 14:49:38 +02:00

Transaction transfers, payments, and matching (#883)

* Add transfer model and clean up family snapshot fixtures

* Ignore transfers in income and expense snapshots

* Add transfer validations

* Implement basic transfer matching UI

* Fix merge conflicts

* Add missing translations

* Tweak selection states for transfer types

* Add missing i18n translation
This commit is contained in:
Zach Gollwitzer 2024-06-19 06:52:08 -04:00 committed by GitHub
parent b462bc8f8c
commit ca39b26070
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
57 changed files with 991 additions and 427 deletions

View file

@ -7,6 +7,10 @@
details > summary::-webkit-details-marker { details > summary::-webkit-details-marker {
@apply hidden; @apply hidden;
} }
details > summary {
@apply list-none;
}
} }
@layer components { @layer components {

View file

@ -9,9 +9,9 @@ class TransactionsController < ApplicationController
@pagy, @transactions = pagy(result, items: 50) @pagy, @transactions = pagy(result, items: 50)
@totals = { @totals = {
count: result.count, count: result.select { |t| t.currency == Current.family.currency }.count,
income: result.inflows.sum(&:amount_money).abs, income: result.income_total(Current.family.currency).abs,
expense: result.outflows.sum(&:amount_money).abs expense: result.expense_total(Current.family.currency)
} }
end end
@ -54,7 +54,7 @@ class TransactionsController < ApplicationController
def bulk_delete def bulk_delete
destroyed = Current.family.transactions.destroy_by(id: bulk_delete_params[:transaction_ids]) destroyed = Current.family.transactions.destroy_by(id: bulk_delete_params[:transaction_ids])
redirect_to transactions_url, notice: t(".success", count: destroyed.count) redirect_back_or_to transactions_url, notice: t(".success", count: destroyed.count)
end end
def bulk_edit def bulk_edit
@ -63,13 +63,31 @@ class TransactionsController < ApplicationController
def bulk_update def bulk_update
transactions = Current.family.transactions.where(id: bulk_update_params[:transaction_ids]) transactions = Current.family.transactions.where(id: bulk_update_params[:transaction_ids])
if transactions.update_all(bulk_update_params.except(:transaction_ids).to_h.compact_blank!) if transactions.update_all(bulk_update_params.except(:transaction_ids).to_h.compact_blank!)
redirect_to transactions_url, notice: t(".success", count: transactions.count) redirect_back_or_to transactions_url, notice: t(".success", count: transactions.count)
else else
flash.now[:error] = t(".failure") flash.now[:error] = t(".failure")
render :index, status: :unprocessable_entity render :index, status: :unprocessable_entity
end end
end end
def mark_transfers
Current.family
.transactions
.where(id: bulk_update_params[:transaction_ids])
.mark_transfers!
redirect_back_or_to transactions_url, notice: t(".success")
end
def unmark_transfers
Current.family
.transactions
.where(id: bulk_update_params[:transaction_ids])
.update_all marked_as_transfer: false
redirect_back_or_to transactions_url, notice: t(".success")
end
private private
def set_transaction def set_transaction

View file

@ -0,0 +1,41 @@
class TransfersController < ApplicationController
layout "with_sidebar"
before_action :set_transfer, only: :destroy
def new
@transfer = Transfer.new
end
def create
from_account = Current.family.accounts.find(transfer_params[:from_account_id])
to_account = Current.family.accounts.find(transfer_params[:to_account_id])
@transfer = Transfer.build_from_accounts from_account, to_account, \
date: transfer_params[:date],
amount: transfer_params[:amount].to_d,
currency: transfer_params[:currency],
name: transfer_params[:name]
if @transfer.save
redirect_to transactions_path, notice: t(".success")
else
render :new, status: :unprocessable_entity
end
end
def destroy
@transfer.destroy_and_remove_marks!
redirect_back_or_to transactions_url, notice: t(".success")
end
private
def set_transfer
@transfer = Transfer.find(params[:id])
end
def transfer_params
params.require(:transfer).permit(:from_account_id, :to_account_id, :amount, :currency, :date, :name)
end
end

View file

@ -25,7 +25,7 @@ class ApplicationFormBuilder < ActionView::Helpers::FormBuilder
# See `Monetizable` concern, which adds a _money suffix to the attribute name # See `Monetizable` concern, which adds a _money suffix to the attribute name
# For a monetized field, the setter will always be the attribute name without the _money suffix # For a monetized field, the setter will always be the attribute name without the _money suffix
def money_field(method, options = {}) def money_field(method, options = {})
money = @object.send(method) money = @object && @object.respond_to?(method) ? @object.send(method) : nil
raise ArgumentError, "The value of #{method} is not a Money object" unless money.is_a?(Money) || money.nil? raise ArgumentError, "The value of #{method} is not a Money object" unless money.is_a?(Money) || money.nil?
money_amount_method = method.to_s.chomp("_money").to_sym money_amount_method = method.to_s.chomp("_money").to_sym

View file

@ -17,4 +17,27 @@ module TransactionsHelper
content: content content: content
} }
end end
def unconfirmed_transfer?(transaction)
transaction.marked_as_transfer && transaction.transfer.nil?
end
def group_transactions_by_date(transactions)
grouped_by_date = {}
transactions.each do |transaction|
if transaction.transfer
transfer_date = transaction.transfer.inflow_transaction.date
grouped_by_date[transfer_date] ||= { transactions: [], transfers: [] }
unless grouped_by_date[transfer_date][:transfers].include?(transaction.transfer)
grouped_by_date[transfer_date][:transfers] << transaction.transfer
end
else
grouped_by_date[transaction.date] ||= { transactions: [], transfers: [] }
grouped_by_date[transaction.date][:transactions] << transaction
end
end
grouped_by_date
end
end end

View file

@ -0,0 +1,2 @@
module TransfersHelper
end

View file

@ -113,7 +113,7 @@ export default class extends Controller {
#updateGroups() { #updateGroups() {
this.groupTargets.forEach(group => { this.groupTargets.forEach(group => {
const rows = this.rowTargets.filter(row => group.contains(row)) const rows = this.rowTargets.filter(row => group.contains(row))
const groupSelected = rows.every(row => this.selectedIdsValue.includes(row.dataset.id)) const groupSelected = rows.length > 0 && rows.every(row => this.selectedIdsValue.includes(row.dataset.id))
group.querySelector("input[type='checkbox']").checked = groupSelected group.querySelector("input[type='checkbox']").checked = groupSelected
}) })
} }

View file

@ -42,6 +42,7 @@ class Family < ApplicationRecord
) )
.where("transactions.date >= ?", period.date_range.begin) .where("transactions.date >= ?", period.date_range.begin)
.where("transactions.date <= ?", period.date_range.end) .where("transactions.date <= ?", period.date_range.end)
.where("transactions.marked_as_transfer = ?", false)
.group("id") .group("id")
.to_a .to_a

View file

@ -4,6 +4,7 @@ class Transaction < ApplicationRecord
monetize :amount monetize :amount
belongs_to :account belongs_to :account
belongs_to :transfer, optional: true
belongs_to :category, optional: true belongs_to :category, optional: true
belongs_to :merchant, optional: true belongs_to :merchant, optional: true
has_many :taggings, as: :taggable, dependent: :destroy has_many :taggings, as: :taggable, dependent: :destroy
@ -42,6 +43,10 @@ class Transaction < ApplicationRecord
amount > 0 amount > 0
end end
def transfer?
marked_as_transfer
end
def sync_account_later def sync_account_later
if destroyed? if destroyed?
sync_start_date = previous_transaction_date sync_start_date = previous_transaction_date
@ -53,6 +58,21 @@ class Transaction < ApplicationRecord
end end
class << self class << self
def income_total(currency = "USD")
inflows.reject(&:transfer?).select { |t| t.currency == currency }.sum(&:amount_money)
end
def expense_total(currency = "USD")
outflows.reject(&:transfer?).select { |t| t.currency == currency }.sum(&:amount_money)
end
def mark_transfers!
update_all marked_as_transfer: true
# Attempt to "auto match" and save a transfer if 2 transactions selected
Transfer.new(transactions: all).save if all.count == 2
end
def daily_totals(transactions, period: Period.last_30_days, currency: Current.family.currency) def daily_totals(transactions, period: Period.last_30_days, currency: Current.family.currency)
# Sum spending and income for each day in the period with the given currency # Sum spending and income for each day in the period with the given currency
select( select(
@ -60,7 +80,7 @@ class Transaction < ApplicationRecord
"COALESCE(SUM(converted_amount) FILTER (WHERE converted_amount > 0), 0) AS spending", "COALESCE(SUM(converted_amount) FILTER (WHERE converted_amount > 0), 0) AS spending",
"COALESCE(SUM(-converted_amount) FILTER (WHERE converted_amount < 0), 0) AS income" "COALESCE(SUM(-converted_amount) FILTER (WHERE converted_amount < 0), 0) AS income"
) )
.from(transactions.with_converted_amount(currency), :t) .from(transactions.with_converted_amount(currency).where(marked_as_transfer: false), :t)
.joins(sanitize_sql([ "RIGHT JOIN generate_series(?, ?, interval '1 day') AS gs(date) ON t.date = gs.date", period.date_range.first, period.date_range.last ])) .joins(sanitize_sql([ "RIGHT JOIN generate_series(?, ?, interval '1 day') AS gs(date) ON t.date = gs.date", period.date_range.first, period.date_range.last ]))
.group("gs.date") .group("gs.date")
end end
@ -83,7 +103,7 @@ class Transaction < ApplicationRecord
end end
def search(params) def search(params)
query = all query = all.includes(:transfer)
query = query.by_name(params[:search]) if params[:search].present? query = query.by_name(params[:search]) if params[:search].present?
query = query.with_categories(params[:categories]) if params[:categories].present? query = query.with_categories(params[:categories]) if params[:categories].present?
query = query.with_accounts(params[:accounts]) if params[:accounts].present? query = query.with_accounts(params[:accounts]) if params[:accounts].present?
@ -96,7 +116,6 @@ class Transaction < ApplicationRecord
end end
private private
def previous_transaction_date def previous_transaction_date
self.account self.account
.transactions .transactions

57
app/models/transfer.rb Normal file
View file

@ -0,0 +1,57 @@
class Transfer < ApplicationRecord
has_many :transactions, dependent: :nullify
validate :transaction_count, :from_different_accounts, :net_zero_flows, :all_transactions_marked
def inflow_transaction
transactions.find { |t| t.inflow? }
end
def outflow_transaction
transactions.find { |t| t.outflow? }
end
def destroy_and_remove_marks!
transaction do
transactions.each do |t|
t.update! marked_as_transfer: false
end
destroy!
end
end
class << self
def build_from_accounts(from_account, to_account, date:, amount:, currency:, name:)
outflow = from_account.transactions.build(amount: amount.abs, currency: currency, date: date, name: name, marked_as_transfer: true)
inflow = to_account.transactions.build(amount: -amount.abs, currency: currency, date: date, name: name, marked_as_transfer: true)
new transactions: [ outflow, inflow ]
end
end
private
def transaction_count
unless transactions.size == 2
errors.add :transactions, "must have exactly 2 transactions"
end
end
def from_different_accounts
accounts = transactions.map(&:account_id).uniq
errors.add :transactions, "must be from different accounts" if accounts.size < transactions.size
end
def net_zero_flows
unless transactions.sum(&:amount).zero?
errors.add :transactions, "must have an inflow and outflow that net to zero"
end
end
def all_transactions_marked
unless transactions.all?(&:marked_as_transfer)
errors.add :transactions, "must be marked as transfer"
end
end
end

View file

@ -4,7 +4,14 @@
</div> </div>
<div class="col-span-3"> <div class="col-span-3">
<% if transaction.marked_as_transfer %>
<div class="flex items-center gap-1 text-gray-500 pl-5">
<%= lucide_icon "arrow-right-left", class: "w-4 h-4 text-gray-500" %>
<p>Transfer</p>
</div>
<% else %>
<%= render "transactions/categories/badge", category: transaction.category %> <%= render "transactions/categories/badge", category: transaction.category %>
<% end %>
</div> </div>
<%= link_to transaction.account.name, <%= link_to transaction.account.name,

View file

@ -1,17 +1,26 @@
<%# locals: (date:, transactions:) %> <%# locals: (date:, group:) %>
<div class="bg-gray-25 rounded-xl p-1 w-full" data-bulk-select-target="group"> <div class="bg-gray-25 rounded-xl p-1 w-full" data-bulk-select-target="group">
<div class="py-2 px-4 flex items-center justify-between font-medium text-xs text-gray-500"> <div class="py-2 px-4 flex items-center justify-between font-medium text-xs text-gray-500">
<div class="flex pl-0.5 items-center gap-4"> <div class="flex pl-0.5 items-center gap-4">
<%= check_box_tag "#{date}_transactions_selection", <%= check_box_tag "#{date}_transactions_selection",
class: "maybe-checkbox maybe-checkbox--light", class: ["maybe-checkbox maybe-checkbox--light", "hidden": group[:transactions].count == 0],
id: "selection_transaction_#{date}", id: "selection_transaction_#{date}",
data: { action: "bulk-select#toggleGroupSelection" } %> data: { action: "bulk-select#toggleGroupSelection" } %>
<%= tag.span "#{date.strftime('%b %d, %Y')} · #{transactions.size}" %>
<%= tag.span "#{date.strftime('%b %d, %Y')} · #{group[:transactions].size + (group[:transfers].size * 2)}" %>
</div> </div>
<div>
<% transactions_by_currency = group[:transactions].group_by(&:currency) %>
<% transactions_by_currency.each_with_index do |(_currency, transactions), idx| %>
<%= tag.span format_money(-transactions.sum(&:amount_money)) %> <%= tag.span format_money(-transactions.sum(&:amount_money)) %>
<%= tag.span "|", class: "mx-2" if idx < transactions_by_currency.count - 1 %>
<% end %>
</div>
</div> </div>
<div class="bg-white shadow-xs rounded-md border border-alpha-black-25 divide-y divide-alpha-black-50"> <div class="bg-white shadow-xs rounded-md border border-alpha-black-25 divide-y divide-alpha-black-50">
<%= render transactions %> <%= render group[:transactions] %>
<%= render group[:transfers] %>
</div> </div>
</div> </div>

View file

@ -1,9 +1,12 @@
<%= form_with model: @transaction, data: { turbo: false } do |f| %> <%= form_with model: @transaction, data: { turbo_frame: "_top" } do |f| %>
<section> <section>
<fieldset class="bg-gray-50 rounded-lg p-1 grid grid-flow-col justify-stretch gap-x-2"> <fieldset class="bg-gray-50 rounded-lg p-1 grid grid-flow-col justify-stretch gap-x-2">
<%= radio_tab_tag form: f, name: :nature, value: :expense, label: t(".expense"), icon: "minus-circle", checked: true %> <%= radio_tab_tag form: f, name: :nature, value: :expense, label: t(".expense"), icon: "minus-circle", checked: params[:nature] == "expense" || params[:nature].nil? %>
<%= radio_tab_tag form: f, name: :nature, value: :income, label: t(".income"), icon: "plus-circle" %> <%= radio_tab_tag form: f, name: :nature, value: :income, label: t(".income"), icon: "plus-circle", checked: params[:nature] == "income" %>
<%= radio_tab_tag form: f, name: :nature, value: :transfer, label: t(".transfer"), icon: "arrow-right-left", disabled: true %> <%= link_to new_transfer_path, data: { turbo_frame: :modal }, class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-gray-400 group-has-[:checked]:bg-white group-has-[:checked]:text-gray-800 group-has-[:checked]:shadow-sm" do %>
<%= lucide_icon "arrow-right-left", class: "w-5 h-5" %>
<%= tag.span t(".transfer") %>
<% end %>
</fieldset> </fieldset>
</section> </section>
@ -11,7 +14,7 @@
<%= f.text_field :name, label: t(".description"), placeholder: t(".description_placeholder"), required: true %> <%= f.text_field :name, label: t(".description"), placeholder: t(".description_placeholder"), required: true %>
<%= f.collection_select :account_id, Current.family.accounts.alphabetically, :id, :name, { prompt: t(".account_prompt"), label: t(".account") }, required: true %> <%= f.collection_select :account_id, Current.family.accounts.alphabetically, :id, :name, { prompt: t(".account_prompt"), label: t(".account") }, required: true %>
<%= f.money_field :amount_money, label: t(".amount"), required: true %> <%= f.money_field :amount_money, label: t(".amount"), required: true %>
<%= f.collection_select :category_id, Current.family.transaction_categories.alphabetically, :id, :name, { prompt: t(".category_prompt"), label: t(".category") }, required: true %> <%= f.collection_select :category_id, Current.family.transaction_categories.alphabetically, :id, :name, { prompt: t(".category_prompt"), label: t(".category") } %>
<%= f.date_field :date, label: t(".date"), required: true, max: Date.today %> <%= f.date_field :date, label: t(".date"), required: true, max: Date.today %>
</section> </section>

View file

@ -9,7 +9,7 @@
<% else %> <% else %>
<%= link_to transaction.name, <%= link_to transaction.name,
transaction_path(transaction), transaction_path(transaction),
data: { turbo_frame: "drawer" }, data: { turbo_frame: "drawer", turbo_prefetch: false },
class: "hover:underline hover:text-gray-800" %> class: "hover:underline hover:text-gray-800" %>
<% end %> <% end %>
</div> </div>

View file

@ -8,6 +8,21 @@
<div class="flex items-center gap-1 text-gray-500"> <div class="flex items-center gap-1 text-gray-500">
<%= turbo_frame_tag "bulk_transaction_edit_drawer" %> <%= turbo_frame_tag "bulk_transaction_edit_drawer" %>
<%= form_with url: mark_transfers_transactions_path,
builder: ActionView::Helpers::FormBuilder,
scope: "bulk_update",
data: {
turbo_confirm: {
title: t(".mark_transfers"),
body: t(".mark_transfers_message"),
accept: t(".mark_transfers_confirm"),
}
} do |f| %>
<button type="button" data-bulk-select-scope-param="bulk_update" data-action="bulk-select#submitBulkRequest" class="p-1.5 group hover:bg-gray-700 flex items-center justify-center rounded-md" title="Mark as transfer">
<%= lucide_icon "arrow-right-left", class: "w-5 group-hover:text-white" %>
</button>
<% end %>
<%= link_to bulk_edit_transactions_path, <%= link_to bulk_edit_transactions_path,
class: "p-1.5 group hover:bg-gray-700 flex items-center justify-center rounded-md", class: "p-1.5 group hover:bg-gray-700 flex items-center justify-center rounded-md",
title: "Edit", title: "Edit",

View file

@ -1,14 +1,32 @@
<%= turbo_frame_tag dom_id(transaction), class: "grid grid-cols-12 items-center text-gray-900 py-4 text-sm font-medium px-4" do %> <%= turbo_frame_tag dom_id(transaction), class: "grid grid-cols-12 items-center text-gray-900 text-sm font-medium p-4" do %>
<div class="col-span-4 flex items-center gap-4"> <div class="pr-10 flex items-center gap-4 <%= unconfirmed_transfer?(transaction) ? "col-span-10" : "col-span-4" %>">
<%= check_box_tag dom_id(transaction, "selection"), <%= check_box_tag dom_id(transaction, "selection"),
class: "maybe-checkbox maybe-checkbox--light", class: "maybe-checkbox maybe-checkbox--light",
data: { id: transaction.id, "bulk-select-target": "row", action: "bulk-select#toggleRowSelection" } %> data: { id: transaction.id, "bulk-select-target": "row", action: "bulk-select#toggleRowSelection" } %>
<div class="max-w-full pr-10"> <div class="max-w-full">
<%= render "transactions/name", transaction: transaction %> <%= render "transactions/name", transaction: transaction %>
</div> </div>
<% if unconfirmed_transfer?(transaction) %>
<%= form_with url: unmark_transfers_transactions_path, builder: ActionView::Helpers::FormBuilder, class: "flex items-center", data: {
turbo_confirm: {
title: t(".remove_transfer"),
body: t(".remove_transfer_body"),
accept: t(".remove_transfer_confirm"),
},
turbo_frame: "_top"
} do |f| %>
<%= f.hidden_field "bulk_update[transaction_ids][]", value: transaction.id %>
<%= f.button class: "flex items-center justify-center group", title: "Remove transfer" do %>
<%= lucide_icon "arrow-left-right", class: "group-hover:hidden text-gray-500 w-4 h-4" %>
<%= lucide_icon "unlink", class: "hidden group-hover:inline-block text-gray-900 w-4 h-4" %>
<% end %>
<% end %>
<% end %>
</div> </div>
<% unless unconfirmed_transfer?(transaction) %>
<div class="col-span-3"> <div class="col-span-3">
<%= render "transactions/categories/menu", transaction: transaction %> <%= render "transactions/categories/menu", transaction: transaction %>
</div> </div>
@ -17,6 +35,7 @@
account_path(transaction.account), account_path(transaction.account),
data: { turbo_frame: "_top" }, data: { turbo_frame: "_top" },
class: ["col-span-3 hover:underline"] %> class: ["col-span-3 hover:underline"] %>
<% end %>
<div class="col-span-2 ml-auto"> <div class="col-span-2 ml-auto">
<%= render "transactions/amount", transaction: transaction %> <%= render "transactions/amount", transaction: transaction %>

View file

@ -25,8 +25,8 @@
<p class="col-span-2 justify-self-end">amount</p> <p class="col-span-2 justify-self-end">amount</p>
</div> </div>
<div class="space-y-6"> <div class="space-y-6">
<% @transactions.group_by(&:date).each do |date, transactions| %> <% group_transactions_by_date(@transactions).each do |date, group| %>
<%= render partial: "date_group", locals: { date:, transactions: } %> <%= render partial: "date_group", locals: { date:, group: } %>
<% end %> <% end %>
</div> </div>
<% else %> <% else %>

View file

@ -1,11 +1,17 @@
<%= drawer do %> <%= drawer do %>
<div> <div>
<header class="mb-4 space-y-1"> <header class="mb-4 space-y-1">
<div class="flex items-center gap-4">
<h3 class="font-medium"> <h3 class="font-medium">
<span class="text-2xl"><%= format_money @transaction.amount_money %></span> <span class="text-2xl"><%= format_money -@transaction.amount_money %></span>
<span class="text-lg text-gray-500"><%= @transaction.currency %></span> <span class="text-lg text-gray-500"><%= @transaction.currency %></span>
</h3> </h3>
<% if @transaction.marked_as_transfer %>
<%= lucide_icon "arrow-left-right", class: "text-gray-500 mt-1 w-5 h-5" %>
<% end %>
</div>
<span class="text-sm text-gray-500"><%= @transaction.date.strftime("%A %d %B") %></span> <span class="text-sm text-gray-500"><%= @transaction.date.strftime("%A %d %B") %></span>
</header> </header>
@ -19,28 +25,20 @@
<div class="pb-6"> <div class="pb-6">
<%= form_with model: @transaction, html: { data: { controller: "auto-submit-form" } } do |f| %> <%= form_with model: @transaction, html: { data: { controller: "auto-submit-form" } } do |f| %>
<div class="space-y-2"> <div class="space-y-2">
<%= f.text_field :name, label: t(".name_label"), "data-auto-submit-form-target": "auto" %>
<%= f.date_field :date, label: t(".date_label"), max: Date.today, "data-auto-submit-form-target": "auto" %> <%= f.date_field :date, label: t(".date_label"), max: Date.today, "data-auto-submit-form-target": "auto" %>
<% unless @transaction.marked_as_transfer %>
<%= f.collection_select :category_id, Current.family.transaction_categories.alphabetically, :id, :name, { prompt: t(".category_placeholder"), label: t(".category_label"), class: "text-gray-400" }, "data-auto-submit-form-target": "auto" %> <%= f.collection_select :category_id, Current.family.transaction_categories.alphabetically, :id, :name, { prompt: t(".category_placeholder"), label: t(".category_label"), class: "text-gray-400" }, "data-auto-submit-form-target": "auto" %>
<%= f.collection_select :merchant_id, Current.family.transaction_merchants.alphabetically, :id, :name, { prompt: t(".merchant_placeholder"), label: t(".merchant_label"), class: "text-gray-400" }, "data-auto-submit-form-target": "auto" %> <%= f.collection_select :merchant_id, Current.family.transaction_merchants.alphabetically, :id, :name, { prompt: t(".merchant_placeholder"), label: t(".merchant_label"), class: "text-gray-400" }, "data-auto-submit-form-target": "auto" %>
<% end %>
<%= f.collection_select :account_id, Current.family.accounts.alphabetically, :id, :name, { prompt: t(".account_placeholder"), label: t(".account_label"), class: "text-gray-500" }, { class: "form-field__input cursor-not-allowed text-gray-400", disabled: "disabled" } %> <%= f.collection_select :account_id, Current.family.accounts.alphabetically, :id, :name, { prompt: t(".account_placeholder"), label: t(".account_label"), class: "text-gray-500" }, { class: "form-field__input cursor-not-allowed text-gray-400", disabled: "disabled" } %>
</div> </div>
<% end %> <% end %>
</div> </div>
</details> </details>
<details class="group space-y-2" open>
<summary class="flex list-none items-center justify-between rounded-xl px-3 py-2 text-xs font-medium uppercase text-gray-500 bg-gray-25 focus-visible:outline-none">
<h4><%= t(".description") %></h4>
<%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-gray-500 w-5" %>
</summary>
<div class="pb-6">
<%= form_with model: @transaction, html: { data: { controller: "auto-submit-form" } } do |f| %>
<%= f.text_field :name, label: t(".name_label"), "data-auto-submit-form-target": "auto" %>
<% end %>
</div>
</details>
<details class="group space-y-2" open> <details class="group space-y-2" open>
<summary class="flex list-none items-center justify-between rounded-xl px-3 py-2 text-xs font-medium uppercase text-gray-500 bg-gray-25 focus-visible:outline-none"> <summary class="flex list-none items-center justify-between rounded-xl px-3 py-2 text-xs font-medium uppercase text-gray-500 bg-gray-25 focus-visible:outline-none">
<h4><%= t(".additional") %></h4> <h4><%= t(".additional") %></h4>
@ -70,8 +68,8 @@
<div class="pb-6"> <div class="pb-6">
<%= form_with model: @transaction, html: { class: "p-3", data: { controller: "auto-submit-form" } } do |f| %> <%= form_with model: @transaction, html: { class: "p-3 space-y-3", data: { controller: "auto-submit-form" } } do |f| %>
<div class="flex cursor-pointer items-center justify-between"> <div class="flex cursor-pointer items-center gap-2 justify-between">
<div class="text-sm space-y-1"> <div class="text-sm space-y-1">
<h4 class="text-gray-900"><%= t(".exclude_title") %></h4> <h4 class="text-gray-900"><%= t(".exclude_title") %></h4>
<p class="text-gray-500"><%= t(".exclude_subtitle") %></p> <p class="text-gray-500"><%= t(".exclude_subtitle") %></p>
@ -84,6 +82,7 @@
</div> </div>
<% end %> <% end %>
<% unless @transaction.transfer? %>
<div class="flex items-center justify-between gap-2 p-3"> <div class="flex items-center justify-between gap-2 p-3">
<div class="text-sm space-y-1"> <div class="text-sm space-y-1">
<h4 class="text-gray-900"><%= t(".delete_title") %></h4> <h4 class="text-gray-900"><%= t(".delete_title") %></h4>
@ -96,6 +95,7 @@
class: "rounded-lg px-3 py-2 text-red-500 text-sm font-medium border border-alpha-black-200", class: "rounded-lg px-3 py-2 text-red-500 text-sm font-medium border border-alpha-black-200",
data: { turbo_confirm: true, turbo_frame: "_top" } %> data: { turbo_confirm: true, turbo_frame: "_top" } %>
</div> </div>
<% end %>
</div> </div>
</details> </details>
</div> </div>

View file

@ -0,0 +1,32 @@
<%= form_with model: transfer do |f| %>
<section>
<fieldset class="bg-gray-50 rounded-lg p-1 grid grid-flow-col justify-stretch gap-x-2">
<%= link_to new_transaction_path(nature: "expense"), data: { turbo_frame: :modal }, class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-gray-400" do %>
<%= lucide_icon "minus-circle", class: "w-5 h-5" %>
<%= tag.span t(".expense") %>
<% end %>
<%= link_to new_transaction_path(nature: "income"), data: { turbo_frame: :modal }, class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-gray-400" do %>
<%= lucide_icon "plus-circle", class: "w-5 h-5" %>
<%= tag.span t(".income") %>
<% end %>
<%= tag.div class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-gray-400 bg-white text-gray-800 shadow-sm" do %>
<%= lucide_icon "arrow-right-left", class: "w-5 h-5" %>
<%= tag.span t(".transfer") %>
<% end %>
</fieldset>
</section>
<section class="space-y-2">
<%= f.text_field :name, value: transfer.transactions.first&.name, label: t(".description"), placeholder: t(".description_placeholder"), required: true %>
<%= f.collection_select :from_account_id, Current.family.accounts.alphabetically, :id, :name, { prompt: t(".select_account"), label: t(".from") }, required: true %>
<%= f.collection_select :to_account_id, Current.family.accounts.alphabetically, :id, :name, { prompt: t(".select_account"), label: t(".to") }, required: true %>
<%= f.money_field :amount_money, label: t(".amount"), required: true %>
<%= f.date_field :date, value: transfer.transactions.first&.date, label: t(".date"), required: true, max: Date.current %>
</section>
<section>
<%= f.submit t(".submit") %>
</section>
<% end %>

View file

@ -0,0 +1,37 @@
<%= turbo_frame_tag dom_id(transfer), class: "block" do %>
<details class="group flex items-center text-gray-900 p-4 text-sm font-medium">
<summary class="flex items-center justify-between">
<div class="flex items-center gap-4">
<%= button_to transfer_path(transfer),
method: :delete,
class: "flex items-center group/transfer",
data: {
turbo_frame: "_top",
turbo_confirm: {
title: t(".remove_title"),
body: t(".remove_body"),
confirm: t(".remove_confirm")
}
} do %>
<%= lucide_icon "arrow-left-right", class: "group-hover/transfer:hidden w-5 h-5 text-gray-500" %>
<%= lucide_icon "unlink", class: "group-hover/transfer:inline-block hidden w-5 h-5 text-gray-500" %>
<% end %>
<div class="max-w-full pr-10 select-none">
<%= tag.p t(".transfer_name", from_account: transfer.outflow_transaction&.account&.name, to_account: transfer.inflow_transaction&.account&.name) %>
</div>
</div>
<%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-gray-500 w-5" %>
</summary>
<div class="pt-2 divide-y divide-alpha-black-200">
<% transfer.transactions.each do |transaction| %>
<div class="py-3 flex items-center justify-between">
<%= render "transactions/name", transaction: transaction %>
<%= render "transactions/amount", transaction: transaction %>
</div>
<% end %>
</div>
</details>
<% end %>

View file

@ -0,0 +1,17 @@
<%= modal do %>
<article class="mx-auto p-4 space-y-4 w-screen max-w-xl">
<header class="flex justify-between">
<%= tag.h2 t(".title"), class: "font-medium text-xl" %>
<%= lucide_icon "x", class: "w-5 h-5 text-gray-500", data: { action: "click->modal#close" } %>
</header>
<% if @transfer.errors.present? %>
<div class="text-red-600 flex items-center gap-2">
<%= lucide_icon "circle-alert", class: "w-5 h-5" %>
<p class="text-sm"><%= @transfer.errors.full_messages.to_sentence %></p>
</div>
<% end %>
<%= render "form", transfer: @transfer %>
</article>
<% end %>

View file

@ -89,6 +89,8 @@ en:
import: Import import: Import
index: index:
transaction: transaction transaction: transaction
mark_transfers:
success: Marked as transfer
merchants: merchants:
create: create:
success: New merchant created successfully success: New merchant created successfully
@ -116,6 +118,11 @@ en:
title: New merchant title: New merchant
update: update:
success: Merchant updated successfully success: Merchant updated successfully
selection_bar:
mark_transfers: Mark as transfers?
mark_transfers_confirm: Mark as transfers
mark_transfers_message: By marking transactions as transfers, they will no longer
be included in income or spending calculations.
show: show:
account_label: Account account_label: Account
account_placeholder: Select an account account_placeholder: Select an account
@ -127,7 +134,6 @@ en:
delete_subtitle: This permanently deletes the transaction, affects your historical delete_subtitle: This permanently deletes the transaction, affects your historical
balances, and cannot be undone. balances, and cannot be undone.
delete_title: Delete transaction delete_title: Delete transaction
description: Description
exclude_subtitle: This excludes the transaction from any in-app features or exclude_subtitle: This excludes the transaction from any in-app features or
analytics. analytics.
exclude_title: Exclude transaction exclude_title: Exclude transaction
@ -139,5 +145,11 @@ en:
overview: Overview overview: Overview
settings: Settings settings: Settings
tags_label: Select one or more tags tags_label: Select one or more tags
transaction:
remove_transfer: Remove transfer
remove_transfer_body: This will remove the transfer from this transaction
remove_transfer_confirm: Confirm
unmark_transfers:
success: Transfer removed
update: update:
success: Transaction updated successfully success: Transaction updated successfully

View file

@ -0,0 +1,27 @@
---
en:
transfers:
create:
success: Transfer created
destroy:
success: Transfer removed
form:
amount: Amount
date: Date
description: Description
description_placeholder: Transfer from Checking to Savings
expense: Expense
from: From
income: Income
select_account: Select account
submit: Create transfer
to: To
transfer: Transfer
new:
title: New transfer
transfer:
remove_body: This will NOT delete the underlying transactions. It will just
remove the transfer.
remove_confirm: Confirm
remove_title: Remove transfer?
transfer_name: Transfer from %{from_account} to %{to_account}

View file

@ -46,6 +46,8 @@ Rails.application.routes.draw do
post "bulk_delete" post "bulk_delete"
get "bulk_edit" get "bulk_edit"
post "bulk_update" post "bulk_update"
post "mark_transfers"
post "unmark_transfers"
scope module: :transactions, as: :transaction do scope module: :transactions, as: :transaction do
resources :rows, only: %i[ show update ] resources :rows, only: %i[ show update ]
@ -63,6 +65,8 @@ Rails.application.routes.draw do
end end
end end
resources :transfers, only: %i[ new create destroy ]
resources :accounts, shallow: true do resources :accounts, shallow: true do
get :summary, on: :collection get :summary, on: :collection
get :list, on: :collection get :list, on: :collection

View file

@ -0,0 +1,7 @@
class CreateTransfers < ActiveRecord::Migration[7.2]
def change
create_table :transfers, id: :uuid do |t|
t.timestamps
end
end
end

View file

@ -0,0 +1,8 @@
class AddTransferFieldsToTransaction < ActiveRecord::Migration[7.2]
def change
change_table :transactions do |t|
t.references :transfer, foreign_key: true, type: :uuid
t.boolean :marked_as_transfer, default: false, null: false
end
end
end

13
db/schema.rb generated
View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.2].define(version: 2024_06_12_164944) do ActiveRecord::Schema[7.2].define(version: 2024_06_14_121110) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto" enable_extension "pgcrypto"
enable_extension "plpgsql" enable_extension "plpgsql"
@ -87,7 +87,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_06_12_164944) do
t.uuid "accountable_id" t.uuid "accountable_id"
t.decimal "balance", precision: 19, scale: 4, default: "0.0" t.decimal "balance", precision: 19, scale: 4, default: "0.0"
t.string "currency", default: "USD" t.string "currency", default: "USD"
t.virtual "classification", type: :string, as: "\nCASE\n WHEN ((accountable_type)::text = ANY (ARRAY[('Account::Loan'::character varying)::text, ('Account::Credit'::character varying)::text, ('Account::OtherLiability'::character varying)::text])) THEN 'liability'::text\n ELSE 'asset'::text\nEND", stored: true t.virtual "classification", type: :string, as: "\nCASE\n WHEN ((accountable_type)::text = ANY ((ARRAY['Account::Loan'::character varying, 'Account::Credit'::character varying, 'Account::OtherLiability'::character varying])::text[])) THEN 'liability'::text\n ELSE 'asset'::text\nEND", stored: true
t.boolean "is_active", default: true, null: false t.boolean "is_active", default: true, null: false
t.enum "status", default: "ok", null: false, enum_type: "account_status" t.enum "status", default: "ok", null: false, enum_type: "account_status"
t.jsonb "sync_warnings", default: [], null: false t.jsonb "sync_warnings", default: [], null: false
@ -310,9 +310,17 @@ ActiveRecord::Schema[7.2].define(version: 2024_06_12_164944) do
t.boolean "excluded", default: false t.boolean "excluded", default: false
t.text "notes" t.text "notes"
t.uuid "merchant_id" t.uuid "merchant_id"
t.uuid "transfer_id"
t.boolean "marked_as_transfer", default: false, null: false
t.index ["account_id"], name: "index_transactions_on_account_id" t.index ["account_id"], name: "index_transactions_on_account_id"
t.index ["category_id"], name: "index_transactions_on_category_id" t.index ["category_id"], name: "index_transactions_on_category_id"
t.index ["merchant_id"], name: "index_transactions_on_merchant_id" t.index ["merchant_id"], name: "index_transactions_on_merchant_id"
t.index ["transfer_id"], name: "index_transactions_on_transfer_id"
end
create_table "transfers", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end end
create_table "users", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| create_table "users", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
@ -357,6 +365,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_06_12_164944) do
add_foreign_key "transactions", "accounts", on_delete: :cascade add_foreign_key "transactions", "accounts", on_delete: :cascade
add_foreign_key "transactions", "transaction_categories", column: "category_id", on_delete: :nullify add_foreign_key "transactions", "transaction_categories", column: "category_id", on_delete: :nullify
add_foreign_key "transactions", "transaction_merchants", column: "merchant_id" add_foreign_key "transactions", "transaction_merchants", column: "merchant_id"
add_foreign_key "transactions", "transfers"
add_foreign_key "users", "families" add_foreign_key "users", "families"
add_foreign_key "valuations", "accounts", on_delete: :cascade add_foreign_key "valuations", "accounts", on_delete: :cascade
end end

View file

@ -4,7 +4,7 @@ class TransactionsControllerTest < ActionDispatch::IntegrationTest
setup do setup do
sign_in @user = users(:family_admin) sign_in @user = users(:family_admin)
@transaction = transactions(:checking_one) @transaction = transactions(:checking_one)
@recent_transactions = @user.family.transactions.ordered.limit(20).to_a @recent_transactions = @user.family.transactions.ordered.where(transfer_id: nil).limit(20).to_a
end end
test "should get paginated index with most recent transactions first" do test "should get paginated index with most recent transactions first" do
@ -18,7 +18,7 @@ class TransactionsControllerTest < ActionDispatch::IntegrationTest
test "transaction count represents filtered total" do test "transaction count represents filtered total" do
get transactions_url get transactions_url
assert_dom "#total-transactions", count: 1, text: @user.family.transactions.count.to_s assert_dom "#total-transactions", count: 1, text: @user.family.transactions.select { |t| t.currency == "USD" }.count.to_s
new_transaction = @user.family.accounts.first.transactions.create! \ new_transaction = @user.family.accounts.first.transactions.create! \
name: "Transaction to search for", name: "Transaction to search for",
@ -42,7 +42,7 @@ class TransactionsControllerTest < ActionDispatch::IntegrationTest
end end
test "loads last page when page is out of range" do test "loads last page when page is out of range" do
user_oldest_transaction = @user.family.transactions.ordered.last user_oldest_transaction = @user.family.transactions.ordered.reject(&:transfer?).last
get transactions_url(page: 9999999999) get transactions_url(page: 9999999999)
assert_response :success assert_response :success

View file

@ -0,0 +1,33 @@
require "test_helper"
class TransfersControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in users(:family_admin)
end
test "should get new" do
get new_transfer_url
assert_response :success
end
test "can create transfers" do
assert_difference "Transfer.count", 1 do
post transfers_url, params: {
transfer: {
from_account_id: accounts(:checking).id,
to_account_id: accounts(:savings).id,
date: Date.current,
amount: 100,
currency: "USD",
name: "Test Transfer"
}
}
end
end
test "can destroy transfer" do
assert_difference -> { Transfer.count } => -1, -> { Transaction.count } => 0 do
delete transfer_url(transfers(:credit_card_payment))
end
end
end

View file

@ -1,2 +1 @@
one: credit_one: { }
id: "123e4567-e89b-12d3-a456-426614174003"

View file

@ -1,11 +1 @@
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html one: { }
# This model initially had no columns defined. If you add columns to the
# model remove the "{}" from the fixture names and add the columns immediately
# below each fixture, per the syntax in the comments below
#
# one: {}
# column: value
#
# two: {}
# column: value

View file

@ -1,8 +1,4 @@
checking: depository_checking: { }
id: "123e4567-e89b-12d3-a456-426614174000" depository_savings: { }
savings: depository_eur_checking: { }
id: "123e4567-e89b-12d3-a456-426614174001" depository_multi_currency: { }
eur_checking:
id: "123e4567-e89b-12d3-a456-426614174004"
multi_currency:
id: "123e4567-e89b-12d3-a456-426614174005"

View file

@ -1,32 +0,0 @@
date_offset,collectable,checking,savings_with_valuation_overrides,credit_card,eur_checking_eur,eur_checking_usd,multi_currency
-30,400,4000,21250,1040,11850,12947.31,10721.26
-29,400,3985,21750,940,12050,13182.7,10921.26
-28,400,3985,21750,940,12050,13194.75,10921.26
-27,400,3985,21750,940,12050,13132.09,10921.26
-26,400,3985,21750,940,12050,13083.89,10921.26
-25,400,3985,21000,940,12050,13081.48,10921.26
-24,400,3985,21000,940,12050,13062.2,10921.26
-23,400,3985,21000,940,12050,13022.435,10921.26
-22,400,5060,21000,940,12050,13060.995,10921.26
-21,400,5060,21000,940,12050,13068.225,10921.26
-20,400,5060,21000,940,12050,13079.07,10921.26
-19,400,5060,21000,940,11950,12932.29,10813.04
-18,400,5060,19000,940,11950,12934.68,10813.04
-17,400,5060,19000,940,11950,12927.51,10813.04
-16,400,5060,19000,940,11950,12916.755,10813.04
-15,400,5040,19000,960,11950,12882.1,10813.04
-14,400,5040,19000,960,11950,12879.71,10813.04
-13,400,5040,19000,960,11950,12873.735,10813.04
-12,700,5010,19500,990,11950,12821.155,10813.04
-11,700,5010,19500,990,11950,12797.255,10813.04
-10,700,5010,19500,990,11950,12873.735,10813.04
-9,700,5010,19500,990,12000,12939.6,10863.04
-8,700,5010,19500,990,12000,12933.6,10863.04
-7,700,5010,19500,990,12000,12928.8,10863.04
-6,700,5010,19500,990,12000,12906,10863.04
-5,700,5000,19700,1000,12000,12891.6,10863.04
-4,550,5000,19700,1000,12000,12945.6,10000
-3,550,5000,20500,1000,12000,13046.4,10000
-2,550,5000,20500,1000,12000,12982.8,10000
-1,550,5000,20500,1000,12000,13014,10000
0,550,5000,20500,1000,12000,13000.8,10000
1 date_offset collectable checking savings_with_valuation_overrides credit_card eur_checking_eur eur_checking_usd multi_currency
2 -30 400 4000 21250 1040 11850 12947.31 10721.26
3 -29 400 3985 21750 940 12050 13182.7 10921.26
4 -28 400 3985 21750 940 12050 13194.75 10921.26
5 -27 400 3985 21750 940 12050 13132.09 10921.26
6 -26 400 3985 21750 940 12050 13083.89 10921.26
7 -25 400 3985 21000 940 12050 13081.48 10921.26
8 -24 400 3985 21000 940 12050 13062.2 10921.26
9 -23 400 3985 21000 940 12050 13022.435 10921.26
10 -22 400 5060 21000 940 12050 13060.995 10921.26
11 -21 400 5060 21000 940 12050 13068.225 10921.26
12 -20 400 5060 21000 940 12050 13079.07 10921.26
13 -19 400 5060 21000 940 11950 12932.29 10813.04
14 -18 400 5060 19000 940 11950 12934.68 10813.04
15 -17 400 5060 19000 940 11950 12927.51 10813.04
16 -16 400 5060 19000 940 11950 12916.755 10813.04
17 -15 400 5040 19000 960 11950 12882.1 10813.04
18 -14 400 5040 19000 960 11950 12879.71 10813.04
19 -13 400 5040 19000 960 11950 12873.735 10813.04
20 -12 700 5010 19500 990 11950 12821.155 10813.04
21 -11 700 5010 19500 990 11950 12797.255 10813.04
22 -10 700 5010 19500 990 11950 12873.735 10813.04
23 -9 700 5010 19500 990 12000 12939.6 10863.04
24 -8 700 5010 19500 990 12000 12933.6 10863.04
25 -7 700 5010 19500 990 12000 12928.8 10863.04
26 -6 700 5010 19500 990 12000 12906 10863.04
27 -5 700 5000 19700 1000 12000 12891.6 10863.04
28 -4 550 5000 19700 1000 12000 12945.6 10000
29 -3 550 5000 20500 1000 12000 13046.4 10000
30 -2 550 5000 20500 1000 12000 12982.8 10000
31 -1 550 5000 20500 1000 12000 13014 10000
32 0 550 5000 20500 1000 12000 13000.8 10000

View file

@ -0,0 +1 @@
investment_brokerage: { }

View file

@ -0,0 +1 @@
loan_mortgage: { }

View file

@ -1,2 +1,3 @@
one: other_asset_collectable: { }
id: "123e4567-e89b-12d3-a456-426614174002"

View file

@ -0,0 +1 @@
other_asset_iou: { }

View file

@ -0,0 +1 @@
property_house: { }

View file

@ -0,0 +1 @@
vehicle_honda_accord: { }

View file

@ -1,13 +0,0 @@
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
# one:
# account: generic
# date: 2024-02-12
# balance: 9.99
# currency: MyString
# two:
# account: generic
# date: 2024-02-13
# balance: 9.99
# currency: MyString

View file

@ -1,36 +1,39 @@
# Account with only valuations
collectable: collectable:
family: dylan_family family: dylan_family
name: Collectable Account name: Collectable Account
balance: 550 balance: 550
accountable_type: Account::OtherAsset accountable_type: Account::OtherAsset
accountable_id: "123e4567-e89b-12d3-a456-426614174002" accountable: other_asset_collectable
iou:
family: dylan_family
name: IOU (personal debt to friend)
balance: 200
accountable_type: Account::OtherLiability
accountable: other_liability_iou
# Account with only transactions
checking: checking:
family: dylan_family family: dylan_family
name: Checking Account name: Checking Account
balance: 5000 balance: 5000
accountable_type: Account::Depository accountable_type: Account::Depository
accountable_id: "123e4567-e89b-12d3-a456-426614174000" accountable: depository_checking
institution: chase institution: chase
# Account with both transactions and valuations savings:
savings_with_valuation_overrides:
family: dylan_family family: dylan_family
name: Savings account with valuation overrides name: Savings account with valuation overrides
balance: 20000 balance: 19700
accountable_type: Account::Depository accountable_type: Account::Depository
accountable_id: "123e4567-e89b-12d3-a456-426614174001" accountable: depository_savings
institution: chase institution: chase
# Liability account
credit_card: credit_card:
family: dylan_family family: dylan_family
name: Credit Card name: Credit Card
balance: 1000 balance: 1000
accountable_type: Account::Credit accountable_type: Account::Credit
accountable_id: "123e4567-e89b-12d3-a456-426614174003" accountable: credit_one
institution: chase institution: chase
eur_checking: eur_checking:
@ -39,7 +42,7 @@ eur_checking:
currency: EUR currency: EUR
balance: 12000 balance: 12000
accountable_type: Account::Depository accountable_type: Account::Depository
accountable_id: "123e4567-e89b-12d3-a456-426614174004" accountable: depository_eur_checking
institution: revolut institution: revolut
# Multi-currency account (e.g. Wise, Revolut, etc.) # Multi-currency account (e.g. Wise, Revolut, etc.)
@ -47,7 +50,39 @@ multi_currency:
family: dylan_family family: dylan_family
name: Multi Currency Account name: Multi Currency Account
currency: USD # multi-currency accounts still have a "primary" currency currency: USD # multi-currency accounts still have a "primary" currency
balance: 10000 balance: 9467
accountable_type: Account::Depository accountable_type: Account::Depository
accountable_id: "123e4567-e89b-12d3-a456-426614174005" accountable: depository_multi_currency
institution: revolut institution: revolut
brokerage:
family: dylan_family
name: Robinhood Brokerage Account
currency: USD
balance: 10000
accountable_type: Account::Investment
accountable: investment_brokerage
mortgage_loan:
family: dylan_family
name: Mortgage Loan
currency: USD
balance: 500000
accountable_type: Account::Loan
accountable: loan_mortgage
house:
family: dylan_family
name: 123 Maybe Court
currency: USD
balance: 550000
accountable_type: Account::Property
accountable: property_house
car:
family: dylan_family
name: Honda Accord
currency: USD
balance: 18000
accountable_type: Account::Vehicle
accountable: vehicle_honda_accord

View file

@ -1,308 +1,381 @@
day_31_ago_eur_to_usd:
base_currency: EUR
converted_currency: USD
rate: 1.0986
date: <%= 31.days.ago.to_date %>
day_30_ago_eur_to_usd: day_30_ago_eur_to_usd:
base_currency: EUR base_currency: EUR
converted_currency: USD converted_currency: USD
rate: 1.0926 rate: 1.0926
date: <%= 30.days.ago.to_date %> date: <%= 30.days.ago.to_date %>
day_29_ago_eur_to_usd: day_29_ago_eur_to_usd:
base_currency: EUR base_currency: EUR
converted_currency: USD converted_currency: USD
rate: 1.094 rate: 1.094
date: <%= 29.days.ago.to_date %> date: <%= 29.days.ago.to_date %>
day_28_ago_eur_to_usd: day_28_ago_eur_to_usd:
base_currency: EUR base_currency: EUR
converted_currency: USD converted_currency: USD
rate: 1.095 rate: 1.095
date: <%= 28.days.ago.to_date %> date: <%= 28.days.ago.to_date %>
day_27_ago_eur_to_usd: day_27_ago_eur_to_usd:
base_currency: EUR base_currency: EUR
converted_currency: USD converted_currency: USD
rate: 1.0898 rate: 1.0898
date: <%= 27.days.ago.to_date %> date: <%= 27.days.ago.to_date %>
day_26_ago_eur_to_usd: day_26_ago_eur_to_usd:
base_currency: EUR base_currency: EUR
converted_currency: USD converted_currency: USD
rate: 1.0858 rate: 1.0858
date: <%= 26.days.ago.to_date %> date: <%= 26.days.ago.to_date %>
day_25_ago_eur_to_usd: day_25_ago_eur_to_usd:
base_currency: EUR base_currency: EUR
converted_currency: USD converted_currency: USD
rate: 1.0856 rate: 1.0856
date: <%= 25.days.ago.to_date %> date: <%= 25.days.ago.to_date %>
day_24_ago_eur_to_usd: day_24_ago_eur_to_usd:
base_currency: EUR base_currency: EUR
converted_currency: USD converted_currency: USD
rate: 1.084 rate: 1.084
date: <%= 24.days.ago.to_date %> date: <%= 24.days.ago.to_date %>
day_23_ago_eur_to_usd: day_23_ago_eur_to_usd:
base_currency: EUR base_currency: EUR
converted_currency: USD converted_currency: USD
rate: 1.0807 rate: 1.0807
date: <%= 23.days.ago.to_date %> date: <%= 23.days.ago.to_date %>
day_22_ago_eur_to_usd: day_22_ago_eur_to_usd:
base_currency: EUR base_currency: EUR
converted_currency: USD converted_currency: USD
rate: 1.0839 rate: 1.0839
date: <%= 22.days.ago.to_date %> date: <%= 22.days.ago.to_date %>
day_21_ago_eur_to_usd: day_21_ago_eur_to_usd:
base_currency: EUR base_currency: EUR
converted_currency: USD converted_currency: USD
rate: 1.0845 rate: 1.0845
date: <%= 21.days.ago.to_date %> date: <%= 21.days.ago.to_date %>
day_20_ago_eur_to_usd: day_20_ago_eur_to_usd:
base_currency: EUR base_currency: EUR
converted_currency: USD converted_currency: USD
rate: 1.0854 rate: 1.0854
date: <%= 20.days.ago.to_date %> date: <%= 20.days.ago.to_date %>
day_19_ago_eur_to_usd: day_19_ago_eur_to_usd:
base_currency: EUR base_currency: EUR
converted_currency: USD converted_currency: USD
rate: 1.0822 rate: 1.0822
date: <%= 19.days.ago.to_date %> date: <%= 19.days.ago.to_date %>
day_18_ago_eur_to_usd: day_18_ago_eur_to_usd:
base_currency: EUR base_currency: EUR
converted_currency: USD converted_currency: USD
rate: 1.0824 rate: 1.0824
date: <%= 18.days.ago.to_date %> date: <%= 18.days.ago.to_date %>
day_17_ago_eur_to_usd: day_17_ago_eur_to_usd:
base_currency: EUR base_currency: EUR
converted_currency: USD converted_currency: USD
rate: 1.0818 rate: 1.0818
date: <%= 17.days.ago.to_date %> date: <%= 17.days.ago.to_date %>
day_16_ago_eur_to_usd: day_16_ago_eur_to_usd:
base_currency: EUR base_currency: EUR
converted_currency: USD converted_currency: USD
rate: 1.0809 rate: 1.0809
date: <%= 16.days.ago.to_date %> date: <%= 16.days.ago.to_date %>
day_15_ago_eur_to_usd: day_15_ago_eur_to_usd:
base_currency: EUR base_currency: EUR
converted_currency: USD converted_currency: USD
rate: 1.078 rate: 1.078
date: <%= 15.days.ago.to_date %> date: <%= 15.days.ago.to_date %>
day_14_ago_eur_to_usd: day_14_ago_eur_to_usd:
base_currency: EUR base_currency: EUR
converted_currency: USD converted_currency: USD
rate: 1.0778 rate: 1.0778
date: <%= 14.days.ago.to_date %> date: <%= 14.days.ago.to_date %>
day_13_ago_eur_to_usd: day_13_ago_eur_to_usd:
base_currency: EUR base_currency: EUR
converted_currency: USD converted_currency: USD
rate: 1.0773 rate: 1.0773
date: <%= 13.days.ago.to_date %> date: <%= 13.days.ago.to_date %>
day_12_ago_eur_to_usd: day_12_ago_eur_to_usd:
base_currency: EUR base_currency: EUR
converted_currency: USD converted_currency: USD
rate: 1.0729 rate: 1.0729
date: <%= 12.days.ago.to_date %> date: <%= 12.days.ago.to_date %>
day_11_ago_eur_to_usd: day_11_ago_eur_to_usd:
base_currency: EUR base_currency: EUR
converted_currency: USD converted_currency: USD
rate: 1.0709 rate: 1.0709
date: <%= 11.days.ago.to_date %> date: <%= 11.days.ago.to_date %>
day_10_ago_eur_to_usd: day_10_ago_eur_to_usd:
base_currency: EUR base_currency: EUR
converted_currency: USD converted_currency: USD
rate: 1.0773 rate: 1.0773
date: <%= 10.days.ago.to_date %> date: <%= 10.days.ago.to_date %>
day_9_ago_eur_to_usd: day_9_ago_eur_to_usd:
base_currency: EUR base_currency: EUR
converted_currency: USD converted_currency: USD
rate: 1.0783 rate: 1.0783
date: <%= 9.days.ago.to_date %> date: <%= 9.days.ago.to_date %>
day_8_ago_eur_to_usd: day_8_ago_eur_to_usd:
base_currency: EUR base_currency: EUR
converted_currency: USD converted_currency: USD
rate: 1.0778 rate: 1.0778
date: <%= 8.days.ago.to_date %> date: <%= 8.days.ago.to_date %>
day_7_ago_eur_to_usd: day_7_ago_eur_to_usd:
base_currency: EUR base_currency: EUR
converted_currency: USD converted_currency: USD
rate: 1.0774 rate: 1.0774
date: <%= 7.days.ago.to_date %> date: <%= 7.days.ago.to_date %>
day_6_ago_eur_to_usd: day_6_ago_eur_to_usd:
base_currency: EUR base_currency: EUR
converted_currency: USD converted_currency: USD
rate: 1.0755 rate: 1.0755
date: <%= 6.days.ago.to_date %> date: <%= 6.days.ago.to_date %>
day_5_ago_eur_to_usd: day_5_ago_eur_to_usd:
base_currency: EUR base_currency: EUR
converted_currency: USD converted_currency: USD
rate: 1.0743 rate: 1.0743
date: <%= 5.days.ago.to_date %> date: <%= 5.days.ago.to_date %>
day_4_ago_eur_to_usd: day_4_ago_eur_to_usd:
base_currency: EUR base_currency: EUR
converted_currency: USD converted_currency: USD
rate: 1.0788 rate: 1.0788
date: <%= 4.days.ago.to_date %> date: <%= 4.days.ago.to_date %>
day_3_ago_eur_to_usd: day_3_ago_eur_to_usd:
base_currency: EUR base_currency: EUR
converted_currency: USD converted_currency: USD
rate: 1.0872 rate: 1.0872
date: <%= 3.days.ago.to_date %> date: <%= 3.days.ago.to_date %>
day_2_ago_eur_to_usd: day_2_ago_eur_to_usd:
base_currency: EUR base_currency: EUR
converted_currency: USD converted_currency: USD
rate: 1.0819 rate: 1.0819
date: <%= 2.days.ago.to_date %> date: <%= 2.days.ago.to_date %>
day_1_ago_eur_to_usd: day_1_ago_eur_to_usd:
base_currency: EUR base_currency: EUR
converted_currency: USD converted_currency: USD
rate: 1.0845 rate: 1.0845
date: <%= 1.days.ago.to_date %> date: <%= 1.days.ago.to_date %>
today_eur_to_usd: today_eur_to_usd:
base_currency: EUR base_currency: EUR
converted_currency: USD converted_currency: USD
rate: 1.0834 rate: 1.0834
date: <%= Date.current %> date: <%= Date.current %>
day_31_ago_usd_to_eur:
base_currency: USD
converted_currency: EUR
rate: 0.9279
date: <%= 31.days.ago.to_date %>
day_30_ago_usd_to_eur: day_30_ago_usd_to_eur:
base_currency: USD base_currency: USD
converted_currency: EUR converted_currency: EUR
rate: 0.9179 rate: 0.9179
date: <%= 30.days.ago.to_date %> date: <%= 30.days.ago.to_date %>
day_29_ago_usd_to_eur: day_29_ago_usd_to_eur:
base_currency: USD base_currency: USD
converted_currency: EUR converted_currency: EUR
rate: 0.9154 rate: 0.9154
date: <%= 29.days.ago.to_date %> date: <%= 29.days.ago.to_date %>
day_28_ago_usd_to_eur: day_28_ago_usd_to_eur:
base_currency: USD base_currency: USD
converted_currency: EUR converted_currency: EUR
rate: 0.9107 rate: 0.9107
date: <%= 28.days.ago.to_date %> date: <%= 28.days.ago.to_date %>
day_27_ago_usd_to_eur: day_27_ago_usd_to_eur:
base_currency: USD base_currency: USD
converted_currency: EUR converted_currency: EUR
rate: 0.9139 rate: 0.9139
date: <%= 27.days.ago.to_date %> date: <%= 27.days.ago.to_date %>
day_26_ago_usd_to_eur: day_26_ago_usd_to_eur:
base_currency: USD base_currency: USD
converted_currency: EUR converted_currency: EUR
rate: 0.9082 rate: 0.9082
date: <%= 26.days.ago.to_date %> date: <%= 26.days.ago.to_date %>
day_25_ago_usd_to_eur: day_25_ago_usd_to_eur:
base_currency: USD base_currency: USD
converted_currency: EUR converted_currency: EUR
rate: 0.9077 rate: 0.9077
date: <%= 25.days.ago.to_date %> date: <%= 25.days.ago.to_date %>
day_24_ago_usd_to_eur: day_24_ago_usd_to_eur:
base_currency: USD base_currency: USD
converted_currency: EUR converted_currency: EUR
rate: 0.9054 rate: 0.9054
date: <%= 24.days.ago.to_date %> date: <%= 24.days.ago.to_date %>
day_23_ago_usd_to_eur: day_23_ago_usd_to_eur:
base_currency: USD base_currency: USD
converted_currency: EUR converted_currency: EUR
rate: 0.9004 rate: 0.9004
date: <%= 23.days.ago.to_date %> date: <%= 23.days.ago.to_date %>
day_22_ago_usd_to_eur: day_22_ago_usd_to_eur:
base_currency: USD base_currency: USD
converted_currency: EUR converted_currency: EUR
rate: 0.9040 rate: 0.9040
date: <%= 22.days.ago.to_date %> date: <%= 22.days.ago.to_date %>
day_21_ago_usd_to_eur: day_21_ago_usd_to_eur:
base_currency: USD base_currency: USD
converted_currency: EUR converted_currency: EUR
rate: 0.9060 rate: 0.9060
date: <%= 21.days.ago.to_date %> date: <%= 21.days.ago.to_date %>
day_20_ago_usd_to_eur: day_20_ago_usd_to_eur:
base_currency: USD base_currency: USD
converted_currency: EUR converted_currency: EUR
rate: 0.9052 rate: 0.9052
date: <%= 20.days.ago.to_date %> date: <%= 20.days.ago.to_date %>
day_19_ago_usd_to_eur: day_19_ago_usd_to_eur:
base_currency: USD base_currency: USD
converted_currency: EUR converted_currency: EUR
rate: 0.9139 rate: 0.9139
date: <%= 19.days.ago.to_date %> date: <%= 19.days.ago.to_date %>
day_18_ago_usd_to_eur: day_18_ago_usd_to_eur:
base_currency: USD base_currency: USD
converted_currency: EUR converted_currency: EUR
rate: 0.9155 rate: 0.9155
date: <%= 18.days.ago.to_date %> date: <%= 18.days.ago.to_date %>
day_17_ago_usd_to_eur: day_17_ago_usd_to_eur:
base_currency: USD base_currency: USD
converted_currency: EUR converted_currency: EUR
rate: 0.9135 rate: 0.9135
date: <%= 17.days.ago.to_date %> date: <%= 17.days.ago.to_date %>
day_16_ago_usd_to_eur: day_16_ago_usd_to_eur:
base_currency: USD base_currency: USD
converted_currency: EUR converted_currency: EUR
rate: 0.9141 rate: 0.9141
date: <%= 16.days.ago.to_date %> date: <%= 16.days.ago.to_date %>
day_15_ago_usd_to_eur: day_15_ago_usd_to_eur:
base_currency: USD base_currency: USD
converted_currency: EUR converted_currency: EUR
rate: 0.9131 rate: 0.9131
date: <%= 15.days.ago.to_date %> date: <%= 15.days.ago.to_date %>
day_14_ago_usd_to_eur: day_14_ago_usd_to_eur:
base_currency: USD base_currency: USD
converted_currency: EUR converted_currency: EUR
rate: 0.9147 rate: 0.9147
date: <%= 14.days.ago.to_date %> date: <%= 14.days.ago.to_date %>
day_13_ago_usd_to_eur: day_13_ago_usd_to_eur:
base_currency: USD base_currency: USD
converted_currency: EUR converted_currency: EUR
rate: 0.9112 rate: 0.9112
date: <%= 13.days.ago.to_date %> date: <%= 13.days.ago.to_date %>
day_12_ago_usd_to_eur: day_12_ago_usd_to_eur:
base_currency: USD base_currency: USD
converted_currency: EUR converted_currency: EUR
rate: 0.9115 rate: 0.9115
date: <%= 12.days.ago.to_date %> date: <%= 12.days.ago.to_date %>
day_11_ago_usd_to_eur: day_11_ago_usd_to_eur:
base_currency: USD base_currency: USD
converted_currency: EUR converted_currency: EUR
rate: 0.9132 rate: 0.9132
date: <%= 11.days.ago.to_date %> date: <%= 11.days.ago.to_date %>
day_10_ago_usd_to_eur: day_10_ago_usd_to_eur:
base_currency: USD base_currency: USD
converted_currency: EUR converted_currency: EUR
rate: 0.9130 rate: 0.9130
date: <%= 10.days.ago.to_date %> date: <%= 10.days.ago.to_date %>
day_9_ago_usd_to_eur: day_9_ago_usd_to_eur:
base_currency: USD base_currency: USD
converted_currency: EUR converted_currency: EUR
rate: 0.9192 rate: 0.9192
date: <%= 9.days.ago.to_date %> date: <%= 9.days.ago.to_date %>
day_8_ago_usd_to_eur: day_8_ago_usd_to_eur:
base_currency: USD base_currency: USD
converted_currency: EUR converted_currency: EUR
rate: 0.9188 rate: 0.9188
date: <%= 8.days.ago.to_date %> date: <%= 8.days.ago.to_date %>
day_7_ago_usd_to_eur: day_7_ago_usd_to_eur:
base_currency: USD base_currency: USD
converted_currency: EUR converted_currency: EUR
rate: 0.9194 rate: 0.9194
date: <%= 7.days.ago.to_date %> date: <%= 7.days.ago.to_date %>
day_6_ago_usd_to_eur: day_6_ago_usd_to_eur:
base_currency: USD base_currency: USD
converted_currency: EUR converted_currency: EUR
rate: 0.9177 rate: 0.9177
date: <%= 6.days.ago.to_date %> date: <%= 6.days.ago.to_date %>
day_5_ago_usd_to_eur: day_5_ago_usd_to_eur:
base_currency: USD base_currency: USD
converted_currency: EUR converted_currency: EUR
rate: 0.9187 rate: 0.9187
date: <%= 5.days.ago.to_date %> date: <%= 5.days.ago.to_date %>
day_4_ago_usd_to_eur: day_4_ago_usd_to_eur:
base_currency: USD base_currency: USD
converted_currency: EUR converted_currency: EUR
rate: 0.9213 rate: 0.9213
date: <%= 4.days.ago.to_date %> date: <%= 4.days.ago.to_date %>
day_3_ago_usd_to_eur: day_3_ago_usd_to_eur:
base_currency: USD base_currency: USD
converted_currency: EUR converted_currency: EUR
rate: 0.9186 rate: 0.9186
date: <%= 3.days.ago.to_date %> date: <%= 3.days.ago.to_date %>
day_2_ago_usd_to_eur: day_2_ago_usd_to_eur:
base_currency: USD base_currency: USD
converted_currency: EUR converted_currency: EUR
rate: 0.9218 rate: 0.9218
date: <%= 2.days.ago.to_date %> date: <%= 2.days.ago.to_date %>
day_1_ago_usd_to_eur: day_1_ago_usd_to_eur:
base_currency: USD base_currency: USD
converted_currency: EUR converted_currency: EUR
rate: 0.9213 rate: 0.9213
date: <%= 1.days.ago.to_date %> date: <%= 1.days.ago.to_date %>
today_usd_to_eur: today_usd_to_eur:
base_currency: USD base_currency: USD
converted_currency: EUR converted_currency: EUR

View file

@ -1,32 +0,0 @@
date_offset,net_worth,assets,liabilities,depositories,investments,loans,credits,properties,vehicles,other_assets,other_liabilities,spending,income,rolling_spend,rolling_income,savings_rate
-30,48278.57,49318.57,1040.00,48918.57,0.00,0.00,1040.00,0.00,0.00,400.00,0.00,0,0,0,0,0
-29,49298.96,50238.96,940.00,49838.96,0.00,0.00,940.00,0.00,0.00,400.00,0.00,15,1018.8,15,1018.8,0.9852767962
-28,49311.01,50251.01,940.00,49851.01,0.00,0.00,940.00,0.00,0.00,400.00,0.00,0,0,15,1018.8,0.9852767962
-27,49248.35,50188.35,940.00,49788.35,0.00,0.00,940.00,0.00,0.00,400.00,0.00,0,0,15,1018.8,0.9852767962
-26,49200.15,50140.15,940.00,49740.15,0.00,0.00,940.00,0.00,0.00,400.00,0.00,0,0,15,1018.8,0.9852767962
-25,48447.74,49387.74,940.00,48987.74,0.00,0.00,940.00,0.00,0.00,400.00,0.00,0,0,15,1018.8,0.9852767962
-24,48428.46,49368.46,940.00,48968.46,0.00,0.00,940.00,0.00,0.00,400.00,0.00,0,0,15,1018.8,0.9852767962
-23,48388.70,49328.70,940.00,48928.70,0.00,0.00,940.00,0.00,0.00,400.00,0.00,0,0,15,1018.8,0.9852767962
-22,49502.26,50442.26,940.00,50042.26,0.00,0.00,940.00,0.00,0.00,400.00,0.00,0,1075,15,2093.8,0.992835992
-21,49509.49,50449.49,940.00,50049.49,0.00,0.00,940.00,0.00,0.00,400.00,0.00,0,0,15,2093.8,0.992835992
-20,49520.33,50460.33,940.00,50060.33,0.00,0.00,940.00,0.00,0.00,400.00,0.00,0,0,15,2093.8,0.992835992
-19,49265.33,50205.33,940.00,49805.33,0.00,0.00,940.00,0.00,0.00,400.00,0.00,216.44,0,231.44,2093.8,0.8894641322
-18,47267.72,48207.72,940.00,47807.72,0.00,0.00,940.00,0.00,0.00,400.00,0.00,2000,0,2231.44,2093.8,-0.06573693763
-17,47260.55,48200.55,940.00,47800.55,0.00,0.00,940.00,0.00,0.00,400.00,0.00,0,0,2231.44,2093.8,-0.06573693763
-16,47249.80,48189.80,940.00,47789.80,0.00,0.00,940.00,0.00,0.00,400.00,0.00,0,0,2231.44,2093.8,-0.06573693763
-15,47175.14,48135.14,960.00,47735.14,0.00,0.00,960.00,0.00,0.00,400.00,0.00,40,0,2271.44,2093.8,-0.08484095902
-14,47172.75,48132.75,960.00,47732.75,0.00,0.00,960.00,0.00,0.00,400.00,0.00,0,0,2271.44,2093.8,-0.08484095902
-13,47166.78,48126.78,960.00,47726.78,0.00,0.00,960.00,0.00,0.00,400.00,0.00,0,0,2271.44,2093.8,-0.08484095902
-12,47854.20,48844.20,990.00,48144.20,0.00,0.00,990.00,0.00,0.00,700.00,0.00,60,50,2331.44,2143.8,-0.08752682153
-11,47830.30,48820.30,990.00,48120.30,0.00,0.00,990.00,0.00,0.00,700.00,0.00,0,0,2331.44,2143.8,-0.08752682153
-10,47906.78,48896.78,990.00,48196.78,0.00,0.00,990.00,0.00,0.00,700.00,0.00,0,0,2331.44,2143.8,-0.08752682153
-9,48022.64,49012.64,990.00,48312.64,0.00,0.00,990.00,0.00,0.00,700.00,0.00,0,103.915,2331.44,2247.715,-0.03724893948
-8,48016.64,49006.64,990.00,48306.64,0.00,0.00,990.00,0.00,0.00,700.00,0.00,0,0,2331.44,2247.715,-0.03724893948
-7,48011.84,49001.84,990.00,48301.84,0.00,0.00,990.00,0.00,0.00,700.00,0.00,0,0,2331.44,2247.715,-0.03724893948
-6,47989.04,48979.04,990.00,48279.04,0.00,0.00,990.00,0.00,0.00,700.00,0.00,0,0,2331.44,2247.715,-0.03724893948
-5,48154.64,49154.64,1000.00,48454.64,0.00,0.00,1000.00,0.00,0.00,700.00,0.00,20,200,2351.44,2447.715,0.03933260204
-4,47195.60,48195.60,1000.00,47645.60,0.00,0.00,1000.00,0.00,0.00,550.00,0.00,863.04,0,3214.48,2447.715,-0.3132574667
-3,48096.40,49096.40,1000.00,48546.40,0.00,0.00,1000.00,0.00,0.00,550.00,0.00,0,0,3214.48,2447.715,-0.3132574667
-2,48032.80,49032.80,1000.00,48482.80,0.00,0.00,1000.00,0.00,0.00,550.00,0.00,0,0,3214.48,2447.715,-0.3132574667
-1,48064.00,49064.00,1000.00,48514.00,0.00,0.00,1000.00,0.00,0.00,550.00,0.00,0,0,3214.48,2447.715,-0.3132574667
0,48050.80,49050.80,1000.00,48514.00,0.00,0.00,1000.00,0.00,0.00,550.00,0.00,0,0,3214.48,2447.715,-0.3132574667
1 date_offset net_worth assets liabilities depositories investments loans credits properties vehicles other_assets other_liabilities spending income rolling_spend rolling_income savings_rate
2 -30 48278.57 49318.57 1040.00 48918.57 0.00 0.00 1040.00 0.00 0.00 400.00 0.00 0 0 0 0 0
3 -29 49298.96 50238.96 940.00 49838.96 0.00 0.00 940.00 0.00 0.00 400.00 0.00 15 1018.8 15 1018.8 0.9852767962
4 -28 49311.01 50251.01 940.00 49851.01 0.00 0.00 940.00 0.00 0.00 400.00 0.00 0 0 15 1018.8 0.9852767962
5 -27 49248.35 50188.35 940.00 49788.35 0.00 0.00 940.00 0.00 0.00 400.00 0.00 0 0 15 1018.8 0.9852767962
6 -26 49200.15 50140.15 940.00 49740.15 0.00 0.00 940.00 0.00 0.00 400.00 0.00 0 0 15 1018.8 0.9852767962
7 -25 48447.74 49387.74 940.00 48987.74 0.00 0.00 940.00 0.00 0.00 400.00 0.00 0 0 15 1018.8 0.9852767962
8 -24 48428.46 49368.46 940.00 48968.46 0.00 0.00 940.00 0.00 0.00 400.00 0.00 0 0 15 1018.8 0.9852767962
9 -23 48388.70 49328.70 940.00 48928.70 0.00 0.00 940.00 0.00 0.00 400.00 0.00 0 0 15 1018.8 0.9852767962
10 -22 49502.26 50442.26 940.00 50042.26 0.00 0.00 940.00 0.00 0.00 400.00 0.00 0 1075 15 2093.8 0.992835992
11 -21 49509.49 50449.49 940.00 50049.49 0.00 0.00 940.00 0.00 0.00 400.00 0.00 0 0 15 2093.8 0.992835992
12 -20 49520.33 50460.33 940.00 50060.33 0.00 0.00 940.00 0.00 0.00 400.00 0.00 0 0 15 2093.8 0.992835992
13 -19 49265.33 50205.33 940.00 49805.33 0.00 0.00 940.00 0.00 0.00 400.00 0.00 216.44 0 231.44 2093.8 0.8894641322
14 -18 47267.72 48207.72 940.00 47807.72 0.00 0.00 940.00 0.00 0.00 400.00 0.00 2000 0 2231.44 2093.8 -0.06573693763
15 -17 47260.55 48200.55 940.00 47800.55 0.00 0.00 940.00 0.00 0.00 400.00 0.00 0 0 2231.44 2093.8 -0.06573693763
16 -16 47249.80 48189.80 940.00 47789.80 0.00 0.00 940.00 0.00 0.00 400.00 0.00 0 0 2231.44 2093.8 -0.06573693763
17 -15 47175.14 48135.14 960.00 47735.14 0.00 0.00 960.00 0.00 0.00 400.00 0.00 40 0 2271.44 2093.8 -0.08484095902
18 -14 47172.75 48132.75 960.00 47732.75 0.00 0.00 960.00 0.00 0.00 400.00 0.00 0 0 2271.44 2093.8 -0.08484095902
19 -13 47166.78 48126.78 960.00 47726.78 0.00 0.00 960.00 0.00 0.00 400.00 0.00 0 0 2271.44 2093.8 -0.08484095902
20 -12 47854.20 48844.20 990.00 48144.20 0.00 0.00 990.00 0.00 0.00 700.00 0.00 60 50 2331.44 2143.8 -0.08752682153
21 -11 47830.30 48820.30 990.00 48120.30 0.00 0.00 990.00 0.00 0.00 700.00 0.00 0 0 2331.44 2143.8 -0.08752682153
22 -10 47906.78 48896.78 990.00 48196.78 0.00 0.00 990.00 0.00 0.00 700.00 0.00 0 0 2331.44 2143.8 -0.08752682153
23 -9 48022.64 49012.64 990.00 48312.64 0.00 0.00 990.00 0.00 0.00 700.00 0.00 0 103.915 2331.44 2247.715 -0.03724893948
24 -8 48016.64 49006.64 990.00 48306.64 0.00 0.00 990.00 0.00 0.00 700.00 0.00 0 0 2331.44 2247.715 -0.03724893948
25 -7 48011.84 49001.84 990.00 48301.84 0.00 0.00 990.00 0.00 0.00 700.00 0.00 0 0 2331.44 2247.715 -0.03724893948
26 -6 47989.04 48979.04 990.00 48279.04 0.00 0.00 990.00 0.00 0.00 700.00 0.00 0 0 2331.44 2247.715 -0.03724893948
27 -5 48154.64 49154.64 1000.00 48454.64 0.00 0.00 1000.00 0.00 0.00 700.00 0.00 20 200 2351.44 2447.715 0.03933260204
28 -4 47195.60 48195.60 1000.00 47645.60 0.00 0.00 1000.00 0.00 0.00 550.00 0.00 863.04 0 3214.48 2447.715 -0.3132574667
29 -3 48096.40 49096.40 1000.00 48546.40 0.00 0.00 1000.00 0.00 0.00 550.00 0.00 0 0 3214.48 2447.715 -0.3132574667
30 -2 48032.80 49032.80 1000.00 48482.80 0.00 0.00 1000.00 0.00 0.00 550.00 0.00 0 0 3214.48 2447.715 -0.3132574667
31 -1 48064.00 49064.00 1000.00 48514.00 0.00 0.00 1000.00 0.00 0.00 550.00 0.00 0 0 3214.48 2447.715 -0.3132574667
32 0 48050.80 49050.80 1000.00 48514.00 0.00 0.00 1000.00 0.00 0.00 550.00 0.00 0 0 3214.48 2447.715 -0.3132574667

View file

View file

@ -0,0 +1,33 @@
date_offset,collectable,iou,checking,credit_card,savings,eur_checking_eur,eur_checking_usd,multi_currency,brokerage,mortgage_loan,house,car,net_worth,assets,liabilities,depositories,investments,loans,credits,properties,vehicles,other_assets,other_liabilities,spending,income,rolling_spend,rolling_income,savings_rate
31,400.00,200.00,4950.00,1040.00,20700.00,11850.00,13018.41,10200.00,10000.00,500000.00,550000.00,18000.00,126028.41,627268.41,501240.00,48868.41,10000.00,500000.00,1040.00,550000.00,18000.00,400.00,200.00,0.00,0.00,0.00,0.00,0.0000
30,400.00,200.00,4100.00,940.00,20950.00,12050.00,13165.83,10200.00,10000.00,500000.00,550000.00,18000.00,125675.83,626815.83,501140.00,48415.83,10000.00,500000.00,940.00,550000.00,18000.00,400.00,200.00,0.00,218.52,0.00,218.52,1.0000
29,400.00,200.00,3985.00,940.00,21450.00,12050.00,13182.70,10400.00,10000.00,500000.00,550000.00,18000.00,126277.70,627417.70,501140.00,49017.70,10000.00,500000.00,940.00,550000.00,18000.00,400.00,200.00,15.00,700.00,15.00,918.52,0.9837
28,400.00,200.00,3985.00,940.00,21450.00,12050.00,13194.75,10400.00,10000.00,500000.00,550000.00,18000.00,126289.75,627429.75,501140.00,49029.75,10000.00,500000.00,940.00,550000.00,18000.00,400.00,200.00,0.00,0.00,15.00,918.52,0.9837
27,400.00,200.00,3985.00,940.00,21450.00,12050.00,13132.09,10400.00,10000.00,500000.00,550000.00,18000.00,126227.09,627367.09,501140.00,48967.09,10000.00,500000.00,940.00,550000.00,18000.00,400.00,200.00,0.00,0.00,15.00,918.52,0.9837
26,400.00,200.00,3985.00,940.00,21450.00,12050.00,13083.89,10400.00,10000.00,500000.00,550000.00,18000.00,126178.89,627318.89,501140.00,48918.89,10000.00,500000.00,940.00,550000.00,18000.00,400.00,200.00,0.00,0.00,15.00,918.52,0.9837
25,400.00,200.00,3985.00,940.00,21000.00,12050.00,13081.48,10400.00,10000.00,500000.00,550000.00,18000.00,125726.48,626866.48,501140.00,48466.48,10000.00,500000.00,940.00,550000.00,18000.00,400.00,200.00,0.00,0.00,15.00,918.52,0.9837
24,400.00,200.00,3985.00,940.00,21000.00,12050.00,13062.20,10400.00,10000.00,500000.00,550000.00,18000.00,125707.20,626847.20,501140.00,48447.20,10000.00,500000.00,940.00,550000.00,18000.00,400.00,200.00,0.00,0.00,15.00,918.52,0.9837
23,400.00,200.00,3985.00,940.00,21000.00,12050.00,13022.44,10400.00,10000.00,500000.00,550000.00,18000.00,125667.44,626807.44,501140.00,48407.44,10000.00,500000.00,940.00,550000.00,18000.00,400.00,200.00,0.00,0.00,15.00,918.52,0.9837
22,400.00,200.00,5060.00,940.00,21000.00,12050.00,13061.00,10400.00,10000.00,500000.00,550000.00,18000.00,126781.00,627921.00,501140.00,49521.00,10000.00,500000.00,940.00,550000.00,18000.00,400.00,200.00,0.00,1075.00,15.00,1993.52,0.9925
21,400.00,200.00,5060.00,940.00,21000.00,12050.00,13068.23,10400.00,10000.00,500000.00,550000.00,18000.00,126788.23,627928.23,501140.00,49528.23,10000.00,500000.00,940.00,550000.00,18000.00,400.00,200.00,0.00,0.00,15.00,1993.52,0.9925
20,400.00,200.00,5060.00,940.00,21000.00,12050.00,13079.07,10400.00,10000.00,500000.00,550000.00,18000.00,126799.07,627939.07,501140.00,49539.07,10000.00,500000.00,940.00,550000.00,18000.00,400.00,200.00,0.00,0.00,15.00,1993.52,0.9925
19,400.00,200.00,5060.00,940.00,21000.00,11950.00,12932.29,10280.04,10000.00,500000.00,550000.00,18000.00,126532.33,627672.33,501140.00,49272.33,10000.00,500000.00,940.00,550000.00,18000.00,400.00,200.00,228.18,0.00,243.18,1993.52,0.8780
18,400.00,200.00,5060.00,940.00,19000.00,11950.00,12934.68,10280.04,10000.00,500000.00,550000.00,18000.00,124534.72,625674.72,501140.00,47274.72,10000.00,500000.00,940.00,550000.00,18000.00,400.00,200.00,2000.00,0.00,2243.18,1993.52,-0.1252
17,400.00,200.00,5060.00,940.00,19000.00,11950.00,12927.51,10280.04,10000.00,500000.00,550000.00,18000.00,124527.55,625667.55,501140.00,47267.55,10000.00,500000.00,940.00,550000.00,18000.00,400.00,200.00,0.00,0.00,2243.18,1993.52,-0.1252
16,400.00,200.00,5060.00,940.00,19000.00,11950.00,12916.76,10280.04,10000.00,500000.00,550000.00,18000.00,124516.79,625656.79,501140.00,47256.79,10000.00,500000.00,940.00,550000.00,18000.00,400.00,200.00,0.00,0.00,2243.18,1993.52,-0.1252
15,400.00,200.00,5040.00,960.00,19000.00,11950.00,12882.10,10280.04,10000.00,500000.00,550000.00,18000.00,124442.14,625602.14,501160.00,47202.14,10000.00,500000.00,960.00,550000.00,18000.00,400.00,200.00,40.00,0.00,2283.18,1993.52,-0.1453
14,400.00,200.00,5040.00,960.00,19000.00,11950.00,12879.71,10280.04,10000.00,500000.00,550000.00,18000.00,124439.75,625599.75,501160.00,47199.75,10000.00,500000.00,960.00,550000.00,18000.00,400.00,200.00,0.00,0.00,2283.18,1993.52,-0.1453
13,400.00,200.00,5040.00,960.00,19000.00,11950.00,12873.74,10280.04,10000.00,500000.00,550000.00,18000.00,124433.77,625593.77,501160.00,47193.77,10000.00,500000.00,960.00,550000.00,18000.00,400.00,200.00,0.00,0.00,2283.18,1993.52,-0.1453
12,700.00,200.00,5010.00,990.00,19500.00,11950.00,12821.16,10280.04,10000.00,500000.00,550000.00,18000.00,125121.19,626311.19,501190.00,47611.19,10000.00,500000.00,990.00,550000.00,18000.00,700.00,200.00,60.00,50.00,2343.18,2043.52,-0.1466
11,700.00,200.00,5010.00,990.00,19500.00,11950.00,12797.26,10280.04,10000.00,500000.00,550000.00,18000.00,125097.29,626287.29,501190.00,47587.29,10000.00,500000.00,990.00,550000.00,18000.00,700.00,200.00,0.00,0.00,2343.18,2043.52,-0.1466
10,700.00,200.00,5010.00,990.00,19500.00,11950.00,12873.74,10280.04,10000.00,500000.00,550000.00,18000.00,125173.77,626363.77,501190.00,47663.77,10000.00,500000.00,990.00,550000.00,18000.00,700.00,200.00,0.00,0.00,2343.18,2043.52,-0.1466
9,700.00,200.00,5010.00,990.00,19500.00,12000.00,12939.60,10330.04,10000.00,500000.00,550000.00,18000.00,125289.64,626479.64,501190.00,47779.64,10000.00,500000.00,990.00,550000.00,18000.00,700.00,200.00,0.00,103.92,2343.18,2147.44,-0.0912
8,700.00,200.00,5010.00,990.00,19500.00,12000.00,12933.60,10330.04,10000.00,500000.00,550000.00,18000.00,125283.64,626473.64,501190.00,47773.64,10000.00,500000.00,990.00,550000.00,18000.00,700.00,200.00,0.00,0.00,2343.18,2147.44,-0.0912
7,700.00,200.00,5010.00,990.00,19500.00,12000.00,12928.80,10330.04,10000.00,500000.00,550000.00,18000.00,125278.84,626468.84,501190.00,47768.84,10000.00,500000.00,990.00,550000.00,18000.00,700.00,200.00,0.00,0.00,2343.18,2147.44,-0.0912
6,700.00,200.00,5010.00,990.00,19500.00,12000.00,12906.00,10330.04,10000.00,500000.00,550000.00,18000.00,125256.04,626446.04,501190.00,47746.04,10000.00,500000.00,990.00,550000.00,18000.00,700.00,200.00,0.00,0.00,2343.18,2147.44,-0.0912
5,700.00,200.00,5000.00,1000.00,19700.00,12000.00,12891.60,10330.04,10000.00,500000.00,550000.00,18000.00,125421.64,626621.64,501200.00,47921.64,10000.00,500000.00,1000.00,550000.00,18000.00,700.00,200.00,20.00,200.00,2363.18,2347.44,-0.0067
4,550.00,200.00,5000.00,1000.00,19700.00,12000.00,12945.60,9467.00,10000.00,500000.00,550000.00,18000.00,124462.60,625662.60,501200.00,47112.60,10000.00,500000.00,1000.00,550000.00,18000.00,550.00,200.00,863.04,0.00,3226.22,2347.44,-0.3744
3,550.00,200.00,5000.00,1000.00,19700.00,12000.00,13046.40,9467.00,10000.00,500000.00,550000.00,18000.00,124563.40,625763.40,501200.00,47213.40,10000.00,500000.00,1000.00,550000.00,18000.00,550.00,200.00,0.00,0.00,3226.22,2347.44,-0.3744
2,550.00,200.00,5000.00,1000.00,19700.00,12000.00,12982.80,9467.00,10000.00,500000.00,550000.00,18000.00,124499.80,625699.80,501200.00,47149.80,10000.00,500000.00,1000.00,550000.00,18000.00,550.00,200.00,0.00,0.00,3226.22,2347.44,-0.3744
1,550.00,200.00,5000.00,1000.00,19700.00,12000.00,13014.00,9467.00,10000.00,500000.00,550000.00,18000.00,124531.00,625731.00,501200.00,47181.00,10000.00,500000.00,1000.00,550000.00,18000.00,550.00,200.00,0.00,0.00,3226.22,2347.44,-0.3744
0,550.00,200.00,5000.00,1000.00,19700.00,12000.00,13000.80,9467.00,10000.00,500000.00,550000.00,18000.00,124517.80,625717.80,501200.00,47167.80,10000.00,500000.00,1000.00,550000.00,18000.00,550.00,200.00,0.00,0.00,3226.22,2347.44,-0.3744
1 date_offset collectable iou checking credit_card savings eur_checking_eur eur_checking_usd multi_currency brokerage mortgage_loan house car net_worth assets liabilities depositories investments loans credits properties vehicles other_assets other_liabilities spending income rolling_spend rolling_income savings_rate
2 31 400.00 200.00 4950.00 1040.00 20700.00 11850.00 13018.41 10200.00 10000.00 500000.00 550000.00 18000.00 126028.41 627268.41 501240.00 48868.41 10000.00 500000.00 1040.00 550000.00 18000.00 400.00 200.00 0.00 0.00 0.00 0.00 0.0000
3 30 400.00 200.00 4100.00 940.00 20950.00 12050.00 13165.83 10200.00 10000.00 500000.00 550000.00 18000.00 125675.83 626815.83 501140.00 48415.83 10000.00 500000.00 940.00 550000.00 18000.00 400.00 200.00 0.00 218.52 0.00 218.52 1.0000
4 29 400.00 200.00 3985.00 940.00 21450.00 12050.00 13182.70 10400.00 10000.00 500000.00 550000.00 18000.00 126277.70 627417.70 501140.00 49017.70 10000.00 500000.00 940.00 550000.00 18000.00 400.00 200.00 15.00 700.00 15.00 918.52 0.9837
5 28 400.00 200.00 3985.00 940.00 21450.00 12050.00 13194.75 10400.00 10000.00 500000.00 550000.00 18000.00 126289.75 627429.75 501140.00 49029.75 10000.00 500000.00 940.00 550000.00 18000.00 400.00 200.00 0.00 0.00 15.00 918.52 0.9837
6 27 400.00 200.00 3985.00 940.00 21450.00 12050.00 13132.09 10400.00 10000.00 500000.00 550000.00 18000.00 126227.09 627367.09 501140.00 48967.09 10000.00 500000.00 940.00 550000.00 18000.00 400.00 200.00 0.00 0.00 15.00 918.52 0.9837
7 26 400.00 200.00 3985.00 940.00 21450.00 12050.00 13083.89 10400.00 10000.00 500000.00 550000.00 18000.00 126178.89 627318.89 501140.00 48918.89 10000.00 500000.00 940.00 550000.00 18000.00 400.00 200.00 0.00 0.00 15.00 918.52 0.9837
8 25 400.00 200.00 3985.00 940.00 21000.00 12050.00 13081.48 10400.00 10000.00 500000.00 550000.00 18000.00 125726.48 626866.48 501140.00 48466.48 10000.00 500000.00 940.00 550000.00 18000.00 400.00 200.00 0.00 0.00 15.00 918.52 0.9837
9 24 400.00 200.00 3985.00 940.00 21000.00 12050.00 13062.20 10400.00 10000.00 500000.00 550000.00 18000.00 125707.20 626847.20 501140.00 48447.20 10000.00 500000.00 940.00 550000.00 18000.00 400.00 200.00 0.00 0.00 15.00 918.52 0.9837
10 23 400.00 200.00 3985.00 940.00 21000.00 12050.00 13022.44 10400.00 10000.00 500000.00 550000.00 18000.00 125667.44 626807.44 501140.00 48407.44 10000.00 500000.00 940.00 550000.00 18000.00 400.00 200.00 0.00 0.00 15.00 918.52 0.9837
11 22 400.00 200.00 5060.00 940.00 21000.00 12050.00 13061.00 10400.00 10000.00 500000.00 550000.00 18000.00 126781.00 627921.00 501140.00 49521.00 10000.00 500000.00 940.00 550000.00 18000.00 400.00 200.00 0.00 1075.00 15.00 1993.52 0.9925
12 21 400.00 200.00 5060.00 940.00 21000.00 12050.00 13068.23 10400.00 10000.00 500000.00 550000.00 18000.00 126788.23 627928.23 501140.00 49528.23 10000.00 500000.00 940.00 550000.00 18000.00 400.00 200.00 0.00 0.00 15.00 1993.52 0.9925
13 20 400.00 200.00 5060.00 940.00 21000.00 12050.00 13079.07 10400.00 10000.00 500000.00 550000.00 18000.00 126799.07 627939.07 501140.00 49539.07 10000.00 500000.00 940.00 550000.00 18000.00 400.00 200.00 0.00 0.00 15.00 1993.52 0.9925
14 19 400.00 200.00 5060.00 940.00 21000.00 11950.00 12932.29 10280.04 10000.00 500000.00 550000.00 18000.00 126532.33 627672.33 501140.00 49272.33 10000.00 500000.00 940.00 550000.00 18000.00 400.00 200.00 228.18 0.00 243.18 1993.52 0.8780
15 18 400.00 200.00 5060.00 940.00 19000.00 11950.00 12934.68 10280.04 10000.00 500000.00 550000.00 18000.00 124534.72 625674.72 501140.00 47274.72 10000.00 500000.00 940.00 550000.00 18000.00 400.00 200.00 2000.00 0.00 2243.18 1993.52 -0.1252
16 17 400.00 200.00 5060.00 940.00 19000.00 11950.00 12927.51 10280.04 10000.00 500000.00 550000.00 18000.00 124527.55 625667.55 501140.00 47267.55 10000.00 500000.00 940.00 550000.00 18000.00 400.00 200.00 0.00 0.00 2243.18 1993.52 -0.1252
17 16 400.00 200.00 5060.00 940.00 19000.00 11950.00 12916.76 10280.04 10000.00 500000.00 550000.00 18000.00 124516.79 625656.79 501140.00 47256.79 10000.00 500000.00 940.00 550000.00 18000.00 400.00 200.00 0.00 0.00 2243.18 1993.52 -0.1252
18 15 400.00 200.00 5040.00 960.00 19000.00 11950.00 12882.10 10280.04 10000.00 500000.00 550000.00 18000.00 124442.14 625602.14 501160.00 47202.14 10000.00 500000.00 960.00 550000.00 18000.00 400.00 200.00 40.00 0.00 2283.18 1993.52 -0.1453
19 14 400.00 200.00 5040.00 960.00 19000.00 11950.00 12879.71 10280.04 10000.00 500000.00 550000.00 18000.00 124439.75 625599.75 501160.00 47199.75 10000.00 500000.00 960.00 550000.00 18000.00 400.00 200.00 0.00 0.00 2283.18 1993.52 -0.1453
20 13 400.00 200.00 5040.00 960.00 19000.00 11950.00 12873.74 10280.04 10000.00 500000.00 550000.00 18000.00 124433.77 625593.77 501160.00 47193.77 10000.00 500000.00 960.00 550000.00 18000.00 400.00 200.00 0.00 0.00 2283.18 1993.52 -0.1453
21 12 700.00 200.00 5010.00 990.00 19500.00 11950.00 12821.16 10280.04 10000.00 500000.00 550000.00 18000.00 125121.19 626311.19 501190.00 47611.19 10000.00 500000.00 990.00 550000.00 18000.00 700.00 200.00 60.00 50.00 2343.18 2043.52 -0.1466
22 11 700.00 200.00 5010.00 990.00 19500.00 11950.00 12797.26 10280.04 10000.00 500000.00 550000.00 18000.00 125097.29 626287.29 501190.00 47587.29 10000.00 500000.00 990.00 550000.00 18000.00 700.00 200.00 0.00 0.00 2343.18 2043.52 -0.1466
23 10 700.00 200.00 5010.00 990.00 19500.00 11950.00 12873.74 10280.04 10000.00 500000.00 550000.00 18000.00 125173.77 626363.77 501190.00 47663.77 10000.00 500000.00 990.00 550000.00 18000.00 700.00 200.00 0.00 0.00 2343.18 2043.52 -0.1466
24 9 700.00 200.00 5010.00 990.00 19500.00 12000.00 12939.60 10330.04 10000.00 500000.00 550000.00 18000.00 125289.64 626479.64 501190.00 47779.64 10000.00 500000.00 990.00 550000.00 18000.00 700.00 200.00 0.00 103.92 2343.18 2147.44 -0.0912
25 8 700.00 200.00 5010.00 990.00 19500.00 12000.00 12933.60 10330.04 10000.00 500000.00 550000.00 18000.00 125283.64 626473.64 501190.00 47773.64 10000.00 500000.00 990.00 550000.00 18000.00 700.00 200.00 0.00 0.00 2343.18 2147.44 -0.0912
26 7 700.00 200.00 5010.00 990.00 19500.00 12000.00 12928.80 10330.04 10000.00 500000.00 550000.00 18000.00 125278.84 626468.84 501190.00 47768.84 10000.00 500000.00 990.00 550000.00 18000.00 700.00 200.00 0.00 0.00 2343.18 2147.44 -0.0912
27 6 700.00 200.00 5010.00 990.00 19500.00 12000.00 12906.00 10330.04 10000.00 500000.00 550000.00 18000.00 125256.04 626446.04 501190.00 47746.04 10000.00 500000.00 990.00 550000.00 18000.00 700.00 200.00 0.00 0.00 2343.18 2147.44 -0.0912
28 5 700.00 200.00 5000.00 1000.00 19700.00 12000.00 12891.60 10330.04 10000.00 500000.00 550000.00 18000.00 125421.64 626621.64 501200.00 47921.64 10000.00 500000.00 1000.00 550000.00 18000.00 700.00 200.00 20.00 200.00 2363.18 2347.44 -0.0067
29 4 550.00 200.00 5000.00 1000.00 19700.00 12000.00 12945.60 9467.00 10000.00 500000.00 550000.00 18000.00 124462.60 625662.60 501200.00 47112.60 10000.00 500000.00 1000.00 550000.00 18000.00 550.00 200.00 863.04 0.00 3226.22 2347.44 -0.3744
30 3 550.00 200.00 5000.00 1000.00 19700.00 12000.00 13046.40 9467.00 10000.00 500000.00 550000.00 18000.00 124563.40 625763.40 501200.00 47213.40 10000.00 500000.00 1000.00 550000.00 18000.00 550.00 200.00 0.00 0.00 3226.22 2347.44 -0.3744
31 2 550.00 200.00 5000.00 1000.00 19700.00 12000.00 12982.80 9467.00 10000.00 500000.00 550000.00 18000.00 124499.80 625699.80 501200.00 47149.80 10000.00 500000.00 1000.00 550000.00 18000.00 550.00 200.00 0.00 0.00 3226.22 2347.44 -0.3744
32 1 550.00 200.00 5000.00 1000.00 19700.00 12000.00 13014.00 9467.00 10000.00 500000.00 550000.00 18000.00 124531.00 625731.00 501200.00 47181.00 10000.00 500000.00 1000.00 550000.00 18000.00 550.00 200.00 0.00 0.00 3226.22 2347.44 -0.3744
33 0 550.00 200.00 5000.00 1000.00 19700.00 12000.00 13000.80 9467.00 10000.00 500000.00 550000.00 18000.00 124517.80 625717.80 501200.00 47167.80 10000.00 500000.00 1000.00 550000.00 18000.00 550.00 200.00 0.00 0.00 3226.22 2347.44 -0.3744

View file

@ -39,12 +39,46 @@ checking_five:
currency: USD currency: USD
merchant: netflix merchant: netflix
# Savings account that has these transactions and valuation overrides checking_six_payment:
name: Payment to Credit Card
date: <%= 29.days.ago.to_date %>
amount: 100
account: checking
currency: USD
marked_as_transfer: true
transfer: credit_card_payment
checking_seven_transfer:
name: Transfer to Savings
date: <%= 30.days.ago.to_date %>
amount: 250
account: checking
currency: USD
marked_as_transfer: true
transfer: savings_transfer
checking_eight_external_payment:
name: Transfer TO external CC account (owned by user but not known to app)
date: <%= 30.days.ago.to_date %>
amount: 800
account: checking
currency: USD
marked_as_transfer: true
checking_nine_external_transfer:
name: Transfer FROM external investing account (owned by user but not known to app)
date: <%= 30.days.ago.to_date %>
amount: -200
account: checking
currency: USD
marked_as_transfer: true
# Savings account that has transactions and valuation overrides
savings_one: savings_one:
name: Interest Received name: Interest Received
date: <%= 5.days.ago.to_date %> date: <%= 5.days.ago.to_date %>
amount: -200 amount: -200
account: savings_with_valuation_overrides account: savings
category: income category: income
currency: USD currency: USD
@ -52,7 +86,7 @@ savings_two:
name: Check Deposit name: Check Deposit
date: <%= 12.days.ago.to_date %> date: <%= 12.days.ago.to_date %>
amount: -50 amount: -50
account: savings_with_valuation_overrides account: savings
category: income category: income
currency: USD currency: USD
@ -60,17 +94,26 @@ savings_three:
name: Withdrawal name: Withdrawal
date: <%= 18.days.ago.to_date %> date: <%= 18.days.ago.to_date %>
amount: 2000 amount: 2000
account: savings_with_valuation_overrides account: savings
currency: USD currency: USD
savings_four: savings_four:
name: Check Deposit name: Check Deposit
date: <%= 29.days.ago.to_date %> date: <%= 29.days.ago.to_date %>
amount: -500 amount: -500
account: savings_with_valuation_overrides account: savings
category: income category: income
currency: USD currency: USD
savings_five_transfer:
name: Received Transfer from Checking Account
date: <%= 30.days.ago.to_date %>
amount: -250
account: savings
currency: USD
marked_as_transfer: true
transfer: savings_transfer
# Credit card account transactions # Credit card account transactions
credit_card_one: credit_card_one:
name: Starbucks name: Starbucks
@ -96,12 +139,14 @@ credit_card_three:
currency: USD currency: USD
merchant: amazon merchant: amazon
credit_card_four: credit_card_four_payment:
name: CC Payment name: Received CC Payment from Checking Account
date: <%= 29.days.ago.to_date %> date: <%= 30.days.ago.to_date %>
amount: -100 amount: -100
account: credit_card account: credit_card
currency: USD currency: USD
marked_as_transfer: true
transfer: credit_card_payment
# eur_checking transactions # eur_checking transactions
eur_checking_one: eur_checking_one:
@ -120,7 +165,7 @@ eur_checking_two:
eur_checking_three: eur_checking_three:
name: Check name: Check
date: <%= 29.days.ago.to_date %> date: <%= 30.days.ago.to_date %>
amount: -200 amount: -200
currency: EUR currency: EUR
account: eur_checking account: eur_checking
@ -143,7 +188,7 @@ multi_currency_two:
multi_currency_three: multi_currency_three:
name: Outflow 2 name: Outflow 2
date: <%= 19.days.ago.to_date %> date: <%= 19.days.ago.to_date %>
amount: 100 amount: 110.85
currency: EUR currency: EUR
account: multi_currency account: multi_currency

2
test/fixtures/transfers.yml vendored Normal file
View file

@ -0,0 +1,2 @@
credit_card_payment: { }
savings_transfer: { }

View file

@ -11,21 +11,45 @@ collectable_two:
collectable_three: collectable_three:
value: 400 value: 400
date: <%= 30.days.ago.to_date %> date: <%= 31.days.ago.to_date %>
account: collectable account: collectable
# For checking account that has valuations and transactions iou_one:
savings_one: value: 200
value: 20500 date: <%= 31.days.ago.to_date %>
date: <%= 3.days.ago.to_date %> account: iou
account: savings_with_valuation_overrides
savings_two: multi_currency_one:
value: 10200
date: <%= 31.days.ago.to_date %>
account: multi_currency
savings_one:
value: 19500 value: 19500
date: <%= 12.days.ago.to_date %> date: <%= 12.days.ago.to_date %>
account: savings_with_valuation_overrides account: savings
savings_three: savings_two:
value: 21000 value: 21000
date: <%= 25.days.ago.to_date %> date: <%= 25.days.ago.to_date %>
account: savings_with_valuation_overrides account: savings
brokerage_one:
value: 10000
date: <%= 31.days.ago.to_date %>
account: brokerage
mortgage_loan_one:
value: 500000
date: <%= 31.days.ago.to_date %>
account: mortgage_loan
house_one:
value: 550000
date: <%= 31.days.ago.to_date %>
account: house
car_one:
value: 18000
date: <%= 31.days.ago.to_date %>
account: car

View file

@ -2,111 +2,82 @@ require "test_helper"
require "csv" require "csv"
class Account::Balance::CalculatorTest < ActiveSupport::TestCase class Account::Balance::CalculatorTest < ActiveSupport::TestCase
# See: https://docs.google.com/spreadsheets/d/18LN5N-VLq4b49Mq1fNwF7_eBiHSQB46qQduRtdAEN98/edit?usp=sharing include FamilySnapshotTestHelper
setup do
@expected_balances = CSV.read("test/fixtures/account/expected_balances.csv", headers: true).map do |row|
{
"date" => (Date.current + row["date_offset"].to_i.days).to_date,
"collectable" => row["collectable"],
"checking" => row["checking"],
"savings_with_valuation_overrides" => row["savings_with_valuation_overrides"],
"credit_card" => row["credit_card"],
"multi_currency" => row["multi_currency"],
# Balances should be calculated for all currencies of an account test "syncs other asset balances" do
"eur_checking_eur" => row["eur_checking_eur"], expected_balances = get_expected_balances_for(:collectable)
"eur_checking_usd" => row["eur_checking_usd"] assert_account_balances calculated_balances_for(:collectable), expected_balances
} end
test "syncs other liability balances" do
expected_balances = get_expected_balances_for(:iou)
assert_account_balances calculated_balances_for(:iou), expected_balances
end
test "syncs credit balances" do
expected_balances = get_expected_balances_for :credit_card
assert_account_balances calculated_balances_for(:credit_card), expected_balances
end
test "syncs checking account balances" do
expected_balances = get_expected_balances_for(:checking)
assert_account_balances calculated_balances_for(:checking), expected_balances
end
test "syncs foreign checking account balances" do
# Foreign accounts will generate balances for all currencies
expected_usd_balances = get_expected_balances_for(:eur_checking_usd)
expected_eur_balances = get_expected_balances_for(:eur_checking_eur)
calculated_balances = calculated_balances_for(:eur_checking)
calculated_usd_balances = calculated_balances.select { |b| b[:currency] == "USD" }
calculated_eur_balances = calculated_balances.select { |b| b[:currency] == "EUR" }
assert_account_balances calculated_usd_balances, expected_usd_balances
assert_account_balances calculated_eur_balances, expected_eur_balances
end
test "syncs multi-currency checking account balances" do
expected_balances = get_expected_balances_for(:multi_currency)
assert_account_balances calculated_balances_for(:multi_currency), expected_balances
end
test "syncs savings accounts balances" do
expected_balances = get_expected_balances_for(:savings)
assert_account_balances calculated_balances_for(:savings), expected_balances
end
test "syncs investment account balances" do
expected_balances = get_expected_balances_for(:brokerage)
assert_account_balances calculated_balances_for(:brokerage), expected_balances
end
test "syncs loan account balances" do
expected_balances = get_expected_balances_for(:mortgage_loan)
assert_account_balances calculated_balances_for(:mortgage_loan), expected_balances
end
test "syncs property account balances" do
expected_balances = get_expected_balances_for(:house)
assert_account_balances calculated_balances_for(:house), expected_balances
end
test "syncs vehicle account balances" do
expected_balances = get_expected_balances_for(:car)
assert_account_balances calculated_balances_for(:car), expected_balances
end
private
def assert_account_balances(actual_balances, expected_balances)
assert_equal expected_balances.count, actual_balances.count
actual_balances.each do |ab|
expected_balance = expected_balances.find { |eb| eb[:date] == ab[:date] }
assert_in_delta expected_balance[:balance], ab[:balance], 0.01, "Balance incorrect on date: #{ab[:date]}"
end end
end end
test "syncs account with only valuations" do def calculated_balances_for(account_key)
account = accounts(:collectable) Account::Balance::Calculator.new(accounts(account_key)).calculate.daily_balances
calculator = Account::Balance::Calculator.new(account)
calculator.calculate
expected = @expected_balances.map { |row| row["collectable"].to_d }
actual = calculator.daily_balances.map { |b| b[:balance] }
assert_equal expected, actual
end
test "syncs account with only transactions" do
account = accounts(:checking)
calculator = Account::Balance::Calculator.new(account)
calculator.calculate
expected = @expected_balances.map { |row| row["checking"].to_d }
actual = calculator.daily_balances.map { |b| b[:balance] }
assert_equal expected, actual
end
test "syncs account with both valuations and transactions" do
account = accounts(:savings_with_valuation_overrides)
calculator = Account::Balance::Calculator.new(account)
calculator.calculate
expected = @expected_balances.map { |row| row["savings_with_valuation_overrides"].to_d }
actual = calculator.daily_balances.map { |b| b[:balance] }
assert_equal expected, actual
end
test "syncs liability account" do
account = accounts(:credit_card)
calculator = Account::Balance::Calculator.new(account)
calculator.calculate
expected = @expected_balances.map { |row| row["credit_card"].to_d }
actual = calculator.daily_balances.map { |b| b[:balance] }
assert_equal expected, actual
end
test "syncs foreign currency account" do
account = accounts(:eur_checking)
calculator = Account::Balance::Calculator.new(account)
calculator.calculate
# Calculator should calculate balances in both account and family currency
expected_eur_balances = @expected_balances.map { |row| row["eur_checking_eur"].to_d }
expected_usd_balances = @expected_balances.map { |row| row["eur_checking_usd"].to_d }
actual_eur_balances = calculator.daily_balances.select { |b| b[:currency] == "EUR" }.sort_by { |b| b[:date] }.map { |b| b[:balance] }
actual_usd_balances = calculator.daily_balances.select { |b| b[:currency] == "USD" }.sort_by { |b| b[:date] }.map { |b| b[:balance] }
assert_equal expected_eur_balances, actual_eur_balances
assert_equal expected_usd_balances, actual_usd_balances
end
test "syncs multi currency account" do
account = accounts(:multi_currency)
calculator = Account::Balance::Calculator.new(account)
calculator.calculate
expected_balances = @expected_balances.map { |row| row["multi_currency"].to_d }
actual_balances = calculator.daily_balances.map { |b| b[:balance] }
assert_equal expected_balances, actual_balances
end
test "syncs with overridden start date" do
account = accounts(:multi_currency)
account.sync
calc_start_date = 10.days.ago.to_date
calculator = Account::Balance::Calculator.new(account, { calc_start_date: })
calculator.calculate
expected_balances = @expected_balances.filter { |row| row["date"] >= calc_start_date }.map { |row| row["multi_currency"].to_d }
actual_balances = calculator.daily_balances.map { |b| b[:balance] }
assert_equal expected_balances, actual_balances
end end
end end

View file

@ -4,7 +4,7 @@ class Account::SyncableTest < ActiveSupport::TestCase
include ActiveJob::TestHelper include ActiveJob::TestHelper
setup do setup do
@account = accounts(:savings_with_valuation_overrides) @account = accounts(:savings)
end end
test "triggers sync job" do test "triggers sync job" do
@ -14,27 +14,27 @@ class Account::SyncableTest < ActiveSupport::TestCase
end end
test "account has no balances until synced" do test "account has no balances until synced" do
account = accounts(:savings_with_valuation_overrides) account = accounts(:savings)
assert_equal 0, account.balances.count assert_equal 0, account.balances.count
end end
test "account has balances after syncing" do test "account has balances after syncing" do
account = accounts(:savings_with_valuation_overrides) account = accounts(:savings)
account.sync account.sync
assert_equal 31, account.balances.count assert_equal 32, account.balances.count
end end
test "partial sync with missing historical balances performs a full sync" do test "partial sync with missing historical balances performs a full sync" do
account = accounts(:savings_with_valuation_overrides) account = accounts(:savings)
account.sync 10.days.ago.to_date account.sync 10.days.ago.to_date
assert_equal 31, account.balances.count assert_equal 32, account.balances.count
end end
test "balances are updated after syncing" do test "balances are updated after syncing" do
account = accounts(:savings_with_valuation_overrides) account = accounts(:savings)
balance_date = 10.days.ago balance_date = 10.days.ago
account.balances.create!(date: balance_date, balance: 1000) account.balances.create!(date: balance_date, balance: 1000)
account.sync account.sync
@ -43,7 +43,7 @@ class Account::SyncableTest < ActiveSupport::TestCase
end end
test "balances before sync start date are not updated after syncing" do test "balances before sync start date are not updated after syncing" do
account = accounts(:savings_with_valuation_overrides) account = accounts(:savings)
balance_date = 10.days.ago balance_date = 10.days.ago
account.balances.create!(date: balance_date, balance: 1000) account.balances.create!(date: balance_date, balance: 1000)
account.sync 5.days.ago.to_date account.sync 5.days.ago.to_date
@ -52,7 +52,7 @@ class Account::SyncableTest < ActiveSupport::TestCase
end end
test "balances after sync start date are updated after syncing" do test "balances after sync start date are updated after syncing" do
account = accounts(:savings_with_valuation_overrides) account = accounts(:savings)
balance_date = 10.days.ago balance_date = 10.days.ago
account.balances.create!(date: balance_date, balance: 1000) account.balances.create!(date: balance_date, balance: 1000)
account.sync 20.days.ago.to_date account.sync 20.days.ago.to_date
@ -61,7 +61,7 @@ class Account::SyncableTest < ActiveSupport::TestCase
end end
test "balance on the sync date is updated after syncing" do test "balance on the sync date is updated after syncing" do
account = accounts(:savings_with_valuation_overrides) account = accounts(:savings)
balance_date = 5.days.ago balance_date = 5.days.ago
account.balances.create!(date: balance_date, balance: 1000) account.balances.create!(date: balance_date, balance: 1000)
account.sync balance_date.to_date account.sync balance_date.to_date
@ -73,13 +73,13 @@ class Account::SyncableTest < ActiveSupport::TestCase
account = accounts(:eur_checking) account = accounts(:eur_checking)
account.sync account.sync
assert_equal 62, account.balances.count assert_equal 64, account.balances.count
assert_equal 31, account.balances.where(currency: "EUR").count assert_equal 32, account.balances.where(currency: "EUR").count
assert_equal 31, account.balances.where(currency: "USD").count assert_equal 32, account.balances.where(currency: "USD").count
end end
test "stale balances are purged after syncing" do test "stale balances are purged after syncing" do
account = accounts(:savings_with_valuation_overrides) account = accounts(:savings)
# Create old, stale balances that should be purged (since they are before account start date) # Create old, stale balances that should be purged (since they are before account start date)
account.balances.create!(date: 1.year.ago, balance: 1000) account.balances.create!(date: 1.year.ago, balance: 1000)
@ -88,14 +88,6 @@ class Account::SyncableTest < ActiveSupport::TestCase
account.sync account.sync
assert_equal 31, account.balances.count assert_equal 32, account.balances.count
end
test "account balance is updated after sync" do
account = accounts(:savings_with_valuation_overrides)
assert_changes -> { account.balance }, to: 20500 do
account.sync
end
end end
end end

View file

@ -5,16 +5,6 @@ class AccountTest < ActiveSupport::TestCase
def setup def setup
@account = accounts(:checking) @account = accounts(:checking)
@family = families(:dylan_family) @family = families(:dylan_family)
@snapshots = CSV.read("test/fixtures/family/expected_snapshots.csv", headers: true).map do |row|
{
"date" => (Date.current + row["date_offset"].to_i.days).to_date,
"assets" => row["assets"],
"liabilities" => row["liabilities"],
"Account::Depository" => row["depositories"],
"Account::Credit" => row["credits"],
"Account::OtherAsset" => row["other_assets"]
}
end
end end
test "new account should be valid" do test "new account should be valid" do
@ -47,26 +37,23 @@ class AccountTest < ActiveSupport::TestCase
test "syncs regular account" do test "syncs regular account" do
@account.sync @account.sync
assert_equal "ok", @account.status assert_equal "ok", @account.status
assert_equal 31, @account.balances.count assert_equal 32, @account.balances.count
end end
test "syncs foreign currency account" do test "syncs foreign currency account" do
account = accounts(:eur_checking) account = accounts(:eur_checking)
account.sync account.sync
assert_equal "ok", account.status assert_equal "ok", account.status
assert_equal 31, account.balances.where(currency: "USD").count assert_equal 32, account.balances.where(currency: "USD").count
assert_equal 31, account.balances.where(currency: "EUR").count assert_equal 32, account.balances.where(currency: "EUR").count
end end
test "groups accounts by type" do test "groups accounts by type" do
@family.accounts.each do |account| @family.accounts.each do |account|
account.sync account.sync
end end
result = @family.accounts.by_group(period: Period.all) result = @family.accounts.by_group(period: Period.all)
expected_assets = @snapshots.last["assets"].to_d
expected_liabilities = @snapshots.last["liabilities"].to_d
assets = result[:assets] assets = result[:assets]
liabilities = result[:liabilities] liabilities = result[:liabilities]
@ -84,14 +71,14 @@ class AccountTest < ActiveSupport::TestCase
other_liabilities = liabilities.children.find { |group| group.name == "Account::OtherLiability" } other_liabilities = liabilities.children.find { |group| group.name == "Account::OtherLiability" }
assert_equal 4, depositories.children.count assert_equal 4, depositories.children.count
assert_equal 0, properties.children.count assert_equal 1, properties.children.count
assert_equal 0, vehicles.children.count assert_equal 1, vehicles.children.count
assert_equal 0, investments.children.count assert_equal 1, investments.children.count
assert_equal 1, other_assets.children.count assert_equal 1, other_assets.children.count
assert_equal 1, credits.children.count assert_equal 1, credits.children.count
assert_equal 0, loans.children.count assert_equal 1, loans.children.count
assert_equal 0, other_liabilities.children.count assert_equal 1, other_liabilities.children.count
end end
test "generates series with last balance equal to current account balance" do test "generates series with last balance equal to current account balance" do

View file

@ -2,26 +2,13 @@ require "test_helper"
require "csv" require "csv"
class FamilyTest < ActiveSupport::TestCase class FamilyTest < ActiveSupport::TestCase
include FamilySnapshotTestHelper
def setup def setup
@family = families(:dylan_family) @family = families(:dylan_family)
@family.accounts.each do |account| @family.accounts.each do |account|
account.sync account.sync
end end
# See this Google Sheet for calculations and expected results for dylan_family:
# https://docs.google.com/spreadsheets/d/18LN5N-VLq4b49Mq1fNwF7_eBiHSQB46qQduRtdAEN98/edit?usp=sharing
@expected_snapshots = CSV.read("test/fixtures/family/expected_snapshots.csv", headers: true).map do |row|
{
"date" => (Date.current + row["date_offset"].to_i.days).to_date,
"net_worth" => row["net_worth"],
"assets" => row["assets"],
"liabilities" => row["liabilities"],
"rolling_spend" => row["rolling_spend"],
"rolling_income" => row["rolling_income"],
"savings_rate" => row["savings_rate"]
}
end
end end
test "should have many users" do test "should have many users" do
@ -58,57 +45,62 @@ class FamilyTest < ActiveSupport::TestCase
end end
test "should calculate total assets" do test "should calculate total assets" do
expected = @expected_snapshots.last["assets"].to_d expected = get_today_snapshot_value_for :assets
assert_equal Money.new(expected), @family.assets assert_in_delta expected, @family.assets.amount, 0.01
end end
test "should calculate total liabilities" do test "should calculate total liabilities" do
expected = @expected_snapshots.last["liabilities"].to_d expected = get_today_snapshot_value_for :liabilities
assert_equal Money.new(expected), @family.liabilities assert_in_delta expected, @family.liabilities.amount, 0.01
end end
test "should calculate net worth" do test "should calculate net worth" do
expected = @expected_snapshots.last["net_worth"].to_d expected = get_today_snapshot_value_for :net_worth
assert_equal Money.new(expected), @family.net_worth assert_in_delta expected, @family.net_worth.amount, 0.01
end end
test "should calculate snapshot correctly" do test "calculates asset time series" do
asset_series = @family.snapshot[:asset_series] series = @family.snapshot[:asset_series]
liability_series = @family.snapshot[:liability_series] expected_series = get_expected_balances_for :assets
net_worth_series = @family.snapshot[:net_worth_series]
assert_equal @expected_snapshots.count, asset_series.values.count assert_time_series_balances series, expected_series
assert_equal @expected_snapshots.count, liability_series.values.count
assert_equal @expected_snapshots.count, net_worth_series.values.count
@expected_snapshots.each_with_index do |row, index|
expected_assets = TimeSeries::Value.new(date: row["date"], value: Money.new(row["assets"].to_d))
expected_liabilities = TimeSeries::Value.new(date: row["date"], value: Money.new(row["liabilities"].to_d))
expected_net_worth = TimeSeries::Value.new(date: row["date"], value: Money.new(row["net_worth"].to_d))
assert_in_delta expected_assets.value.amount, Money.new(asset_series.values[index].value).amount, 0.01
assert_in_delta expected_liabilities.value.amount, Money.new(liability_series.values[index].value).amount, 0.01
assert_in_delta expected_net_worth.value.amount, Money.new(net_worth_series.values[index].value).amount, 0.01
end
end end
test "should calculate transaction snapshot correctly" do test "calculates liability time series" do
spending_series = @family.snapshot_transactions[:spending_series] series = @family.snapshot[:liability_series]
income_series = @family.snapshot_transactions[:income_series] expected_series = get_expected_balances_for :liabilities
savings_rate_series = @family.snapshot_transactions[:savings_rate_series]
assert_equal @expected_snapshots.count, spending_series.values.count assert_time_series_balances series, expected_series
assert_equal @expected_snapshots.count, income_series.values.count end
assert_equal @expected_snapshots.count, savings_rate_series.values.count
@expected_snapshots.each_with_index do |row, index| test "calculates net worth time series" do
expected_spending = TimeSeries::Value.new(date: row["date"], value: Money.new(row["rolling_spend"].to_d)) series = @family.snapshot[:net_worth_series]
expected_income = TimeSeries::Value.new(date: row["date"], value: Money.new(row["rolling_income"].to_d)) expected_series = get_expected_balances_for :net_worth
expected_savings_rate = TimeSeries::Value.new(date: row["date"], value: Money.new(row["savings_rate"].to_d))
assert_in_delta expected_spending.value.amount, Money.new(spending_series.values[index].value).amount, 0.01 assert_time_series_balances series, expected_series
assert_in_delta expected_income.value.amount, Money.new(income_series.values[index].value).amount, 0.01 end
assert_in_delta expected_savings_rate.value.amount, savings_rate_series.values[index].value, 0.01
test "calculates rolling expenses" do
series = @family.snapshot_transactions[:spending_series]
expected_series = get_expected_balances_for :rolling_spend
assert_time_series_balances series, expected_series, ignore_count: true
end
test "calculates rolling income" do
series = @family.snapshot_transactions[:income_series]
expected_series = get_expected_balances_for :rolling_income
assert_time_series_balances series, expected_series, ignore_count: true
end
test "calculates savings rate series" do
series = @family.snapshot_transactions[:savings_rate_series]
expected_series = get_expected_balances_for :savings_rate
series.values.each do |tsb|
expected_balance = expected_series.find { |eb| eb[:date] == tsb.date }
assert_in_delta expected_balance[:balance], tsb.value, 0.0001, "Balance incorrect on date: #{tsb.date}"
end end
end end
@ -127,4 +119,15 @@ class FamilyTest < ActiveSupport::TestCase
assert_equal liabilities_before - disabled_cc.balance, @family.liabilities assert_equal liabilities_before - disabled_cc.balance, @family.liabilities
assert_equal net_worth_before - disabled_checking.balance + disabled_cc.balance, @family.net_worth assert_equal net_worth_before - disabled_checking.balance + disabled_cc.balance, @family.net_worth
end end
private
def assert_time_series_balances(time_series_balances, expected_balances, ignore_count: false)
assert_equal time_series_balances.values.count, expected_balances.count unless ignore_count
time_series_balances.values.each do |tsb|
expected_balance = expected_balances.find { |eb| eb[:date] == tsb.date }
assert_in_delta expected_balance[:balance], tsb.value.amount, 0.01, "Balance incorrect on date: #{tsb.date}"
end
end
end end

View file

@ -3,6 +3,7 @@ require "test_helper"
class TransactionTest < ActiveSupport::TestCase class TransactionTest < ActiveSupport::TestCase
setup do setup do
@transaction = transactions(:checking_one) @transaction = transactions(:checking_one)
@family = families(:dylan_family)
end end
# See: https://github.com/maybe-finance/maybe/wiki/vision#signage-of-money # See: https://github.com/maybe-finance/maybe/wiki/vision#signage-of-money
@ -41,4 +42,14 @@ class TransactionTest < ActiveSupport::TestCase
current_transaction.account.expects(:sync_later).with(prior_transaction.date) current_transaction.account.expects(:sync_later).with(prior_transaction.date)
current_transaction.sync_account_later current_transaction.sync_account_later
end end
test "can calculate total spending for a group of transactions" do
assert_equal Money.new(2135), @family.transactions.expense_total("USD")
assert_equal Money.new(1010.85, "EUR"), @family.transactions.expense_total("EUR")
end
test "can calculate total income for a group of transactions" do
assert_equal -Money.new(2075), @family.transactions.income_total("USD")
assert_equal -Money.new(250, "EUR"), @family.transactions.income_total("EUR")
end
end end

View file

@ -0,0 +1,49 @@
require "test_helper"
class TransferTest < ActiveSupport::TestCase
setup do
# Transfers can be posted on different dates
@outflow = accounts(:checking).transactions.create! date: 1.day.ago.to_date, name: "Transfer to Savings", amount: 100, marked_as_transfer: true
@inflow = accounts(:savings).transactions.create! date: Date.current, name: "Transfer from Savings", amount: -100, marked_as_transfer: true
end
test "transfer valid if it has inflow and outflow from different accounts for the same amount" do
transfer = Transfer.create! transactions: [ @inflow, @outflow ]
assert transfer.valid?
end
test "transfer must have 2 transactions" do
invalid_transfer_1 = Transfer.new transactions: [ @outflow ]
invalid_transfer_2 = Transfer.new transactions: [ @inflow, @outflow, transactions(:savings_four) ]
assert invalid_transfer_1.invalid?
assert invalid_transfer_2.invalid?
end
test "transfer cannot have 2 transactions from the same account" do
account = accounts(:checking)
inflow = account.transactions.create! date: Date.current, name: "Inflow", amount: -100
outflow = account.transactions.create! date: Date.current, name: "Outflow", amount: 100
assert_raise ActiveRecord::RecordInvalid do
Transfer.create! transactions: [ inflow, outflow ]
end
end
test "all transfer transactions must be marked as transfers" do
@inflow.update! marked_as_transfer: false
assert_raise ActiveRecord::RecordInvalid do
Transfer.create! transactions: [ @inflow, @outflow ]
end
end
test "transfer transactions must net to zero" do
@outflow.update! amount: 105
assert_raises ActiveRecord::RecordInvalid do
Transfer.create! transactions: [ @inflow, @outflow ]
end
end
end

View file

@ -3,7 +3,7 @@ require "ostruct"
class ValueGroupTest < ActiveSupport::TestCase class ValueGroupTest < ActiveSupport::TestCase
setup do setup do
checking = accounts(:checking) checking = accounts(:checking)
savings = accounts(:savings_with_valuation_overrides) savings = accounts(:savings)
collectable = accounts(:collectable) collectable = accounts(:collectable)
# Level 1 # Level 1

View file

@ -0,0 +1,21 @@
module FamilySnapshotTestHelper
# See: https://docs.google.com/spreadsheets/d/18LN5N-VLq4b49Mq1fNwF7_eBiHSQB46qQduRtdAEN98/edit?usp=sharing
def get_expected_balances_for(key)
expected_results_file.map do |row|
{
date: (Date.current - row["date_offset"].to_i.days).to_date,
balance: row[key.to_s].to_d
}
end
end
def get_today_snapshot_value_for(metric)
expected_results_file[-1][metric.to_s].to_d
end
private
def expected_results_file
CSV.read("test/fixtures/files/expected_family_snapshots.csv", headers: true)
end
end

View file

@ -134,7 +134,7 @@ class TransactionsTest < ApplicationSystemTestCase
def number_of_transactions_on_page def number_of_transactions_on_page
page_size = 50 page_size = 50
[ @user.family.transactions.count, page_size ].min [ @user.family.transactions.where(transfer_id: nil).count, page_size ].min
end end
def all_transactions_checkbox def all_transactions_checkbox