mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-02 20:15:22 +02:00
Multi-Currency Part 2 (#543)
* Support all currencies, handle outside DB * Remove currencies from seed * Fix account balance namespace * Set default currency on authentication * Cache currency instances * Implement multi-currency syncs with tests * Series fallback, passing tests * Fix conflicts * Make value group concrete class that works with currency values * Fix migration conflict * Update tests to expect multi-currency results * Update account list to use group method * Namespace updates * Fetch unknown exchange rates from API * Fix date range bug * Ensure demo data works without external API * Enforce cascades only at DB level
This commit is contained in:
parent
de0cba9fed
commit
110855d077
55 changed files with 1226 additions and 714 deletions
|
@ -2,7 +2,7 @@ require "test_helper"
|
|||
|
||||
class PagesControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
sign_in users(:family_admin)
|
||||
sign_in @user = users(:family_admin)
|
||||
end
|
||||
|
||||
test "dashboard" do
|
||||
|
|
4
test/fixtures/account/depositories.yml
vendored
4
test/fixtures/account/depositories.yml
vendored
|
@ -2,3 +2,7 @@ checking:
|
|||
id: "123e4567-e89b-12d3-a456-426614174000"
|
||||
savings:
|
||||
id: "123e4567-e89b-12d3-a456-426614174001"
|
||||
eur_checking:
|
||||
id: "123e4567-e89b-12d3-a456-426614174004"
|
||||
multi_currency:
|
||||
id: "123e4567-e89b-12d3-a456-426614174005"
|
||||
|
|
32
test/fixtures/account/expected_balances.csv
vendored
Normal file
32
test/fixtures/account/expected_balances.csv
vendored
Normal file
|
@ -0,0 +1,32 @@
|
|||
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,20000,1000,12000,13000.8,10000
|
|
17
test/fixtures/accounts.yml
vendored
17
test/fixtures/accounts.yml
vendored
|
@ -29,3 +29,20 @@ credit_card:
|
|||
balance: 1000
|
||||
accountable_type: Account::Credit
|
||||
accountable_id: "123e4567-e89b-12d3-a456-426614174003"
|
||||
|
||||
eur_checking:
|
||||
family: dylan_family
|
||||
name: Euro Checking Account
|
||||
currency: EUR
|
||||
balance: 12000
|
||||
accountable_type: Account::Depository
|
||||
accountable_id: "123e4567-e89b-12d3-a456-426614174004"
|
||||
|
||||
# Multi-currency account (e.g. Wise, Revolut, etc.)
|
||||
multi_currency:
|
||||
family: dylan_family
|
||||
name: Multi Currency Account
|
||||
currency: USD # multi-currency accounts still have a "primary" currency
|
||||
balance: 10000
|
||||
accountable_type: Account::Depository
|
||||
accountable_id: "123e4567-e89b-12d3-a456-426614174005"
|
||||
|
|
9
test/fixtures/currencies.yml
vendored
9
test/fixtures/currencies.yml
vendored
|
@ -1,9 +0,0 @@
|
|||
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||
|
||||
# one:
|
||||
# name: MyString
|
||||
# iso_code: MyString
|
||||
|
||||
# two:
|
||||
# name: MyString
|
||||
# iso_code: MyString
|
323
test/fixtures/exchange_rates.yml
vendored
323
test/fixtures/exchange_rates.yml
vendored
|
@ -1,13 +1,310 @@
|
|||
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||
|
||||
# one:
|
||||
# base_currency: one
|
||||
# converted_currency: one
|
||||
# rate: 9.99
|
||||
# date: 2024-02-09
|
||||
|
||||
# two:
|
||||
# base_currency: two
|
||||
# converted_currency: two
|
||||
# rate: 9.99
|
||||
# date: 2024-02-09
|
||||
day_30_ago_eur_to_usd:
|
||||
base_currency: EUR
|
||||
converted_currency: USD
|
||||
rate: 1.0926
|
||||
date: <%= 30.days.ago.to_date %>
|
||||
day_29_ago_eur_to_usd:
|
||||
base_currency: EUR
|
||||
converted_currency: USD
|
||||
rate: 1.094
|
||||
date: <%= 29.days.ago.to_date %>
|
||||
day_28_ago_eur_to_usd:
|
||||
base_currency: EUR
|
||||
converted_currency: USD
|
||||
rate: 1.095
|
||||
date: <%= 28.days.ago.to_date %>
|
||||
day_27_ago_eur_to_usd:
|
||||
base_currency: EUR
|
||||
converted_currency: USD
|
||||
rate: 1.0898
|
||||
date: <%= 27.days.ago.to_date %>
|
||||
day_26_ago_eur_to_usd:
|
||||
base_currency: EUR
|
||||
converted_currency: USD
|
||||
rate: 1.0858
|
||||
date: <%= 26.days.ago.to_date %>
|
||||
day_25_ago_eur_to_usd:
|
||||
base_currency: EUR
|
||||
converted_currency: USD
|
||||
rate: 1.0856
|
||||
date: <%= 25.days.ago.to_date %>
|
||||
day_24_ago_eur_to_usd:
|
||||
base_currency: EUR
|
||||
converted_currency: USD
|
||||
rate: 1.084
|
||||
date: <%= 24.days.ago.to_date %>
|
||||
day_23_ago_eur_to_usd:
|
||||
base_currency: EUR
|
||||
converted_currency: USD
|
||||
rate: 1.0807
|
||||
date: <%= 23.days.ago.to_date %>
|
||||
day_22_ago_eur_to_usd:
|
||||
base_currency: EUR
|
||||
converted_currency: USD
|
||||
rate: 1.0839
|
||||
date: <%= 22.days.ago.to_date %>
|
||||
day_21_ago_eur_to_usd:
|
||||
base_currency: EUR
|
||||
converted_currency: USD
|
||||
rate: 1.0845
|
||||
date: <%= 21.days.ago.to_date %>
|
||||
day_20_ago_eur_to_usd:
|
||||
base_currency: EUR
|
||||
converted_currency: USD
|
||||
rate: 1.0854
|
||||
date: <%= 20.days.ago.to_date %>
|
||||
day_19_ago_eur_to_usd:
|
||||
base_currency: EUR
|
||||
converted_currency: USD
|
||||
rate: 1.0822
|
||||
date: <%= 19.days.ago.to_date %>
|
||||
day_18_ago_eur_to_usd:
|
||||
base_currency: EUR
|
||||
converted_currency: USD
|
||||
rate: 1.0824
|
||||
date: <%= 18.days.ago.to_date %>
|
||||
day_17_ago_eur_to_usd:
|
||||
base_currency: EUR
|
||||
converted_currency: USD
|
||||
rate: 1.0818
|
||||
date: <%= 17.days.ago.to_date %>
|
||||
day_16_ago_eur_to_usd:
|
||||
base_currency: EUR
|
||||
converted_currency: USD
|
||||
rate: 1.0809
|
||||
date: <%= 16.days.ago.to_date %>
|
||||
day_15_ago_eur_to_usd:
|
||||
base_currency: EUR
|
||||
converted_currency: USD
|
||||
rate: 1.078
|
||||
date: <%= 15.days.ago.to_date %>
|
||||
day_14_ago_eur_to_usd:
|
||||
base_currency: EUR
|
||||
converted_currency: USD
|
||||
rate: 1.0778
|
||||
date: <%= 14.days.ago.to_date %>
|
||||
day_13_ago_eur_to_usd:
|
||||
base_currency: EUR
|
||||
converted_currency: USD
|
||||
rate: 1.0773
|
||||
date: <%= 13.days.ago.to_date %>
|
||||
day_12_ago_eur_to_usd:
|
||||
base_currency: EUR
|
||||
converted_currency: USD
|
||||
rate: 1.0729
|
||||
date: <%= 12.days.ago.to_date %>
|
||||
day_11_ago_eur_to_usd:
|
||||
base_currency: EUR
|
||||
converted_currency: USD
|
||||
rate: 1.0709
|
||||
date: <%= 11.days.ago.to_date %>
|
||||
day_10_ago_eur_to_usd:
|
||||
base_currency: EUR
|
||||
converted_currency: USD
|
||||
rate: 1.0773
|
||||
date: <%= 10.days.ago.to_date %>
|
||||
day_9_ago_eur_to_usd:
|
||||
base_currency: EUR
|
||||
converted_currency: USD
|
||||
rate: 1.0783
|
||||
date: <%= 9.days.ago.to_date %>
|
||||
day_8_ago_eur_to_usd:
|
||||
base_currency: EUR
|
||||
converted_currency: USD
|
||||
rate: 1.0778
|
||||
date: <%= 8.days.ago.to_date %>
|
||||
day_7_ago_eur_to_usd:
|
||||
base_currency: EUR
|
||||
converted_currency: USD
|
||||
rate: 1.0774
|
||||
date: <%= 7.days.ago.to_date %>
|
||||
day_6_ago_eur_to_usd:
|
||||
base_currency: EUR
|
||||
converted_currency: USD
|
||||
rate: 1.0755
|
||||
date: <%= 6.days.ago.to_date %>
|
||||
day_5_ago_eur_to_usd:
|
||||
base_currency: EUR
|
||||
converted_currency: USD
|
||||
rate: 1.0743
|
||||
date: <%= 5.days.ago.to_date %>
|
||||
day_4_ago_eur_to_usd:
|
||||
base_currency: EUR
|
||||
converted_currency: USD
|
||||
rate: 1.0788
|
||||
date: <%= 4.days.ago.to_date %>
|
||||
day_3_ago_eur_to_usd:
|
||||
base_currency: EUR
|
||||
converted_currency: USD
|
||||
rate: 1.0872
|
||||
date: <%= 3.days.ago.to_date %>
|
||||
day_2_ago_eur_to_usd:
|
||||
base_currency: EUR
|
||||
converted_currency: USD
|
||||
rate: 1.0819
|
||||
date: <%= 2.days.ago.to_date %>
|
||||
day_1_ago_eur_to_usd:
|
||||
base_currency: EUR
|
||||
converted_currency: USD
|
||||
rate: 1.0845
|
||||
date: <%= 1.days.ago.to_date %>
|
||||
today_eur_to_usd:
|
||||
base_currency: EUR
|
||||
converted_currency: USD
|
||||
rate: 1.0834
|
||||
date: <%= Date.current %>
|
||||
day_30_ago_usd_to_eur:
|
||||
base_currency: USD
|
||||
converted_currency: EUR
|
||||
rate: 0.9179
|
||||
date: <%= 30.days.ago.to_date %>
|
||||
day_29_ago_usd_to_eur:
|
||||
base_currency: USD
|
||||
converted_currency: EUR
|
||||
rate: 0.9154
|
||||
date: <%= 29.days.ago.to_date %>
|
||||
day_28_ago_usd_to_eur:
|
||||
base_currency: USD
|
||||
converted_currency: EUR
|
||||
rate: 0.9107
|
||||
date: <%= 28.days.ago.to_date %>
|
||||
day_27_ago_usd_to_eur:
|
||||
base_currency: USD
|
||||
converted_currency: EUR
|
||||
rate: 0.9139
|
||||
date: <%= 27.days.ago.to_date %>
|
||||
day_26_ago_usd_to_eur:
|
||||
base_currency: USD
|
||||
converted_currency: EUR
|
||||
rate: 0.9082
|
||||
date: <%= 26.days.ago.to_date %>
|
||||
day_25_ago_usd_to_eur:
|
||||
base_currency: USD
|
||||
converted_currency: EUR
|
||||
rate: 0.9077
|
||||
date: <%= 25.days.ago.to_date %>
|
||||
day_24_ago_usd_to_eur:
|
||||
base_currency: USD
|
||||
converted_currency: EUR
|
||||
rate: 0.9054
|
||||
date: <%= 24.days.ago.to_date %>
|
||||
day_23_ago_usd_to_eur:
|
||||
base_currency: USD
|
||||
converted_currency: EUR
|
||||
rate: 0.9004
|
||||
date: <%= 23.days.ago.to_date %>
|
||||
day_22_ago_usd_to_eur:
|
||||
base_currency: USD
|
||||
converted_currency: EUR
|
||||
rate: 0.9040
|
||||
date: <%= 22.days.ago.to_date %>
|
||||
day_21_ago_usd_to_eur:
|
||||
base_currency: USD
|
||||
converted_currency: EUR
|
||||
rate: 0.9060
|
||||
date: <%= 21.days.ago.to_date %>
|
||||
day_20_ago_usd_to_eur:
|
||||
base_currency: USD
|
||||
converted_currency: EUR
|
||||
rate: 0.9052
|
||||
date: <%= 20.days.ago.to_date %>
|
||||
day_19_ago_usd_to_eur:
|
||||
base_currency: USD
|
||||
converted_currency: EUR
|
||||
rate: 0.9139
|
||||
date: <%= 19.days.ago.to_date %>
|
||||
day_18_ago_usd_to_eur:
|
||||
base_currency: USD
|
||||
converted_currency: EUR
|
||||
rate: 0.9155
|
||||
date: <%= 18.days.ago.to_date %>
|
||||
day_17_ago_usd_to_eur:
|
||||
base_currency: USD
|
||||
converted_currency: EUR
|
||||
rate: 0.9135
|
||||
date: <%= 17.days.ago.to_date %>
|
||||
day_16_ago_usd_to_eur:
|
||||
base_currency: USD
|
||||
converted_currency: EUR
|
||||
rate: 0.9141
|
||||
date: <%= 16.days.ago.to_date %>
|
||||
day_15_ago_usd_to_eur:
|
||||
base_currency: USD
|
||||
converted_currency: EUR
|
||||
rate: 0.9131
|
||||
date: <%= 15.days.ago.to_date %>
|
||||
day_14_ago_usd_to_eur:
|
||||
base_currency: USD
|
||||
converted_currency: EUR
|
||||
rate: 0.9147
|
||||
date: <%= 14.days.ago.to_date %>
|
||||
day_13_ago_usd_to_eur:
|
||||
base_currency: USD
|
||||
converted_currency: EUR
|
||||
rate: 0.9112
|
||||
date: <%= 13.days.ago.to_date %>
|
||||
day_12_ago_usd_to_eur:
|
||||
base_currency: USD
|
||||
converted_currency: EUR
|
||||
rate: 0.9115
|
||||
date: <%= 12.days.ago.to_date %>
|
||||
day_11_ago_usd_to_eur:
|
||||
base_currency: USD
|
||||
converted_currency: EUR
|
||||
rate: 0.9132
|
||||
date: <%= 11.days.ago.to_date %>
|
||||
day_10_ago_usd_to_eur:
|
||||
base_currency: USD
|
||||
converted_currency: EUR
|
||||
rate: 0.9130
|
||||
date: <%= 10.days.ago.to_date %>
|
||||
day_9_ago_usd_to_eur:
|
||||
base_currency: USD
|
||||
converted_currency: EUR
|
||||
rate: 0.9192
|
||||
date: <%= 9.days.ago.to_date %>
|
||||
day_8_ago_usd_to_eur:
|
||||
base_currency: USD
|
||||
converted_currency: EUR
|
||||
rate: 0.9188
|
||||
date: <%= 8.days.ago.to_date %>
|
||||
day_7_ago_usd_to_eur:
|
||||
base_currency: USD
|
||||
converted_currency: EUR
|
||||
rate: 0.9194
|
||||
date: <%= 7.days.ago.to_date %>
|
||||
day_6_ago_usd_to_eur:
|
||||
base_currency: USD
|
||||
converted_currency: EUR
|
||||
rate: 0.9177
|
||||
date: <%= 6.days.ago.to_date %>
|
||||
day_5_ago_usd_to_eur:
|
||||
base_currency: USD
|
||||
converted_currency: EUR
|
||||
rate: 0.9187
|
||||
date: <%= 5.days.ago.to_date %>
|
||||
day_4_ago_usd_to_eur:
|
||||
base_currency: USD
|
||||
converted_currency: EUR
|
||||
rate: 0.9213
|
||||
date: <%= 4.days.ago.to_date %>
|
||||
day_3_ago_usd_to_eur:
|
||||
base_currency: USD
|
||||
converted_currency: EUR
|
||||
rate: 0.9186
|
||||
date: <%= 3.days.ago.to_date %>
|
||||
day_2_ago_usd_to_eur:
|
||||
base_currency: USD
|
||||
converted_currency: EUR
|
||||
rate: 0.9218
|
||||
date: <%= 2.days.ago.to_date %>
|
||||
day_1_ago_usd_to_eur:
|
||||
base_currency: USD
|
||||
converted_currency: EUR
|
||||
rate: 0.9213
|
||||
date: <%= 1.days.ago.to_date %>
|
||||
today_usd_to_eur:
|
||||
base_currency: USD
|
||||
converted_currency: EUR
|
||||
rate: 0.9141
|
||||
date: <%= Date.current %>
|
||||
|
|
62
test/fixtures/family/expected_snapshots.csv
vendored
62
test/fixtures/family/expected_snapshots.csv
vendored
|
@ -1,32 +1,32 @@
|
|||
date_offset,net_worth,assets,liabilities,depositories,investments,loans,credits,properties,vehicles,other_assets,other_liabilities
|
||||
-30,24610,25650,1040,25250,0,0,1040,0,0,400,0
|
||||
-29,25195,26135,940,25735,0,0,940,0,0,400,0
|
||||
-28,25195,26135,940,25735,0,0,940,0,0,400,0
|
||||
-27,25195,26135,940,25735,0,0,940,0,0,400,0
|
||||
-26,25195,26135,940,25735,0,0,940,0,0,400,0
|
||||
-25,24445,25385,940,24985,0,0,940,0,0,400,0
|
||||
-24,24445,25385,940,24985,0,0,940,0,0,400,0
|
||||
-23,24445,25385,940,24985,0,0,940,0,0,400,0
|
||||
-22,25520,26460,940,26060,0,0,940,0,0,400,0
|
||||
-21,25520,26460,940,26060,0,0,940,0,0,400,0
|
||||
-20,25520,26460,940,26060,0,0,940,0,0,400,0
|
||||
-19,25520,26460,940,26060,0,0,940,0,0,400,0
|
||||
-18,23520,24460,940,24060,0,0,940,0,0,400,0
|
||||
-17,23520,24460,940,24060,0,0,940,0,0,400,0
|
||||
-16,23520,24460,940,24060,0,0,940,0,0,400,0
|
||||
-15,23480,24440,960,24040,0,0,960,0,0,400,0
|
||||
-14,23480,24440,960,24040,0,0,960,0,0,400,0
|
||||
-13,23480,24440,960,24040,0,0,960,0,0,400,0
|
||||
-12,24220,25210,990,24510,0,0,990,0,0,700,0
|
||||
-11,24220,25210,990,24510,0,0,990,0,0,700,0
|
||||
-10,24220,25210,990,24510,0,0,990,0,0,700,0
|
||||
-9,24220,25210,990,24510,0,0,990,0,0,700,0
|
||||
-8,24220,25210,990,24510,0,0,990,0,0,700,0
|
||||
-7,24220,25210,990,24510,0,0,990,0,0,700,0
|
||||
-6,24220,25210,990,24510,0,0,990,0,0,700,0
|
||||
-5,24400,25400,1000,24700,0,0,1000,0,0,700,0
|
||||
-4,24250,25250,1000,24700,0,0,1000,0,0,550,0
|
||||
-3,25050,26050,1000,25500,0,0,1000,0,0,550,0
|
||||
-2,25050,26050,1000,25500,0,0,1000,0,0,550,0
|
||||
-1,25050,26050,1000,25500,0,0,1000,0,0,550,0
|
||||
0,24550,25550,1000,25000,0,0,1000,0,0,550,0
|
||||
-30,48278.57,49318.57,1040.00,48918.57,0.00,0.00,1040.00,0.00,0.00,400.00,0.00
|
||||
-29,49298.96,50238.96,940.00,49838.96,0.00,0.00,940.00,0.00,0.00,400.00,0.00
|
||||
-28,49311.01,50251.01,940.00,49851.01,0.00,0.00,940.00,0.00,0.00,400.00,0.00
|
||||
-27,49248.35,50188.35,940.00,49788.35,0.00,0.00,940.00,0.00,0.00,400.00,0.00
|
||||
-26,49200.15,50140.15,940.00,49740.15,0.00,0.00,940.00,0.00,0.00,400.00,0.00
|
||||
-25,48447.74,49387.74,940.00,48987.74,0.00,0.00,940.00,0.00,0.00,400.00,0.00
|
||||
-24,48428.46,49368.46,940.00,48968.46,0.00,0.00,940.00,0.00,0.00,400.00,0.00
|
||||
-23,48388.70,49328.70,940.00,48928.70,0.00,0.00,940.00,0.00,0.00,400.00,0.00
|
||||
-22,49502.26,50442.26,940.00,50042.26,0.00,0.00,940.00,0.00,0.00,400.00,0.00
|
||||
-21,49509.49,50449.49,940.00,50049.49,0.00,0.00,940.00,0.00,0.00,400.00,0.00
|
||||
-20,49520.33,50460.33,940.00,50060.33,0.00,0.00,940.00,0.00,0.00,400.00,0.00
|
||||
-19,49265.33,50205.33,940.00,49805.33,0.00,0.00,940.00,0.00,0.00,400.00,0.00
|
||||
-18,47267.72,48207.72,940.00,47807.72,0.00,0.00,940.00,0.00,0.00,400.00,0.00
|
||||
-17,47260.55,48200.55,940.00,47800.55,0.00,0.00,940.00,0.00,0.00,400.00,0.00
|
||||
-16,47249.80,48189.80,940.00,47789.80,0.00,0.00,940.00,0.00,0.00,400.00,0.00
|
||||
-15,47175.14,48135.14,960.00,47735.14,0.00,0.00,960.00,0.00,0.00,400.00,0.00
|
||||
-14,47172.75,48132.75,960.00,47732.75,0.00,0.00,960.00,0.00,0.00,400.00,0.00
|
||||
-13,47166.78,48126.78,960.00,47726.78,0.00,0.00,960.00,0.00,0.00,400.00,0.00
|
||||
-12,47854.20,48844.20,990.00,48144.20,0.00,0.00,990.00,0.00,0.00,700.00,0.00
|
||||
-11,47830.30,48820.30,990.00,48120.30,0.00,0.00,990.00,0.00,0.00,700.00,0.00
|
||||
-10,47906.78,48896.78,990.00,48196.78,0.00,0.00,990.00,0.00,0.00,700.00,0.00
|
||||
-9,48022.64,49012.64,990.00,48312.64,0.00,0.00,990.00,0.00,0.00,700.00,0.00
|
||||
-8,48016.64,49006.64,990.00,48306.64,0.00,0.00,990.00,0.00,0.00,700.00,0.00
|
||||
-7,48011.84,49001.84,990.00,48301.84,0.00,0.00,990.00,0.00,0.00,700.00,0.00
|
||||
-6,47989.04,48979.04,990.00,48279.04,0.00,0.00,990.00,0.00,0.00,700.00,0.00
|
||||
-5,48154.64,49154.64,1000.00,48454.64,0.00,0.00,1000.00,0.00,0.00,700.00,0.00
|
||||
-4,47195.60,48195.60,1000.00,47645.60,0.00,0.00,1000.00,0.00,0.00,550.00,0.00
|
||||
-3,48096.40,49096.40,1000.00,48546.40,0.00,0.00,1000.00,0.00,0.00,550.00,0.00
|
||||
-2,48032.80,49032.80,1000.00,48482.80,0.00,0.00,1000.00,0.00,0.00,550.00,0.00
|
||||
-1,48064.00,49064.00,1000.00,48514.00,0.00,0.00,1000.00,0.00,0.00,550.00,0.00
|
||||
0,47550.80,48550.80,1000.00,48000.80,0.00,0.00,1000.00,0.00,0.00,550.00,0.00
|
|
64
test/fixtures/transactions.yml
vendored
64
test/fixtures/transactions.yml
vendored
|
@ -5,6 +5,7 @@ checking_one:
|
|||
amount: 10
|
||||
account: checking
|
||||
category: food_and_drink
|
||||
currency: USD
|
||||
|
||||
checking_two:
|
||||
name: Chipotle
|
||||
|
@ -12,12 +13,14 @@ checking_two:
|
|||
amount: 30
|
||||
account: checking
|
||||
category: food_and_drink
|
||||
currency: USD
|
||||
|
||||
checking_three:
|
||||
name: Amazon
|
||||
date: <%= 15.days.ago.to_date %>
|
||||
amount: 20
|
||||
account: checking
|
||||
currency: USD
|
||||
|
||||
checking_four:
|
||||
name: Paycheck
|
||||
|
@ -25,12 +28,14 @@ checking_four:
|
|||
amount: -1075
|
||||
account: checking
|
||||
category: income
|
||||
currency: USD
|
||||
|
||||
checking_five:
|
||||
name: Netflix
|
||||
date: <%= 29.days.ago.to_date %>
|
||||
amount: 15
|
||||
account: checking
|
||||
currency: USD
|
||||
|
||||
# Savings account that has these transactions and valuation overrides
|
||||
savings_one:
|
||||
|
@ -39,6 +44,7 @@ savings_one:
|
|||
amount: -200
|
||||
account: savings_with_valuation_overrides
|
||||
category: income
|
||||
currency: USD
|
||||
|
||||
savings_two:
|
||||
name: Check Deposit
|
||||
|
@ -46,12 +52,14 @@ savings_two:
|
|||
amount: -50
|
||||
account: savings_with_valuation_overrides
|
||||
category: income
|
||||
currency: USD
|
||||
|
||||
savings_three:
|
||||
name: Withdrawal
|
||||
date: <%= 18.days.ago.to_date %>
|
||||
amount: 2000
|
||||
account: savings_with_valuation_overrides
|
||||
currency: USD
|
||||
|
||||
savings_four:
|
||||
name: Check Deposit
|
||||
|
@ -59,6 +67,7 @@ savings_four:
|
|||
amount: -500
|
||||
account: savings_with_valuation_overrides
|
||||
category: income
|
||||
currency: USD
|
||||
|
||||
# Credit card account transactions
|
||||
credit_card_one:
|
||||
|
@ -67,6 +76,7 @@ credit_card_one:
|
|||
amount: 10
|
||||
account: credit_card
|
||||
category: food_and_drink
|
||||
currency: USD
|
||||
|
||||
credit_card_two:
|
||||
name: Chipotle
|
||||
|
@ -74,15 +84,69 @@ credit_card_two:
|
|||
amount: 30
|
||||
account: credit_card
|
||||
category: food_and_drink
|
||||
currency: USD
|
||||
|
||||
credit_card_three:
|
||||
name: Amazon
|
||||
date: <%= 15.days.ago.to_date %>
|
||||
amount: 20
|
||||
account: credit_card
|
||||
currency: USD
|
||||
|
||||
credit_card_four:
|
||||
name: CC Payment
|
||||
date: <%= 29.days.ago.to_date %>
|
||||
amount: -100
|
||||
account: credit_card
|
||||
currency: USD
|
||||
|
||||
# eur_checking transactions
|
||||
eur_checking_one:
|
||||
name: Check
|
||||
date: <%= 9.days.ago.to_date %>
|
||||
amount: -50
|
||||
currency: EUR
|
||||
account: eur_checking
|
||||
|
||||
eur_checking_two:
|
||||
name: Shopping trip
|
||||
date: <%= 19.days.ago.to_date %>
|
||||
amount: 100
|
||||
currency: EUR
|
||||
account: eur_checking
|
||||
|
||||
eur_checking_three:
|
||||
name: Check
|
||||
date: <%= 29.days.ago.to_date %>
|
||||
amount: -200
|
||||
currency: EUR
|
||||
account: eur_checking
|
||||
|
||||
# multi_currency transactions
|
||||
multi_currency_one:
|
||||
name: Outflow 1
|
||||
date: <%= 4.days.ago.to_date %>
|
||||
amount: 800
|
||||
currency: EUR
|
||||
account: multi_currency
|
||||
|
||||
multi_currency_two:
|
||||
name: Inflow 1
|
||||
date: <%= 9.days.ago.to_date %>
|
||||
amount: -50
|
||||
currency: USD
|
||||
account: multi_currency
|
||||
|
||||
multi_currency_three:
|
||||
name: Outflow 2
|
||||
date: <%= 19.days.ago.to_date %>
|
||||
amount: 100
|
||||
currency: EUR
|
||||
account: multi_currency
|
||||
|
||||
multi_currency_four:
|
||||
name: Inflow 2
|
||||
date: <%= 29.days.ago.to_date %>
|
||||
amount: -200
|
||||
currency: USD
|
||||
account: multi_currency
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
require "test_helper"
|
||||
|
||||
class AccountBalanceSyncJobTest < ActiveJob::TestCase
|
||||
class Account::BalanceSyncJobTest < ActiveJob::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
|
|
|
@ -87,4 +87,13 @@ class MoneyTest < ActiveSupport::TestCase
|
|||
assert_equal "$1,000.90", Money.new(1000.899).format
|
||||
assert_equal "€1.000,12", Money.new(1000.12, :eur).format
|
||||
end
|
||||
|
||||
test "can exchange to another currency" do
|
||||
er = exchange_rates(:today_usd_to_eur)
|
||||
assert_equal Money.new(1000).exchange_to(:eur), Money.new(1000 * er.rate, :eur)
|
||||
end
|
||||
|
||||
test "returns nil if exchange rate not available" do
|
||||
assert_nil Money.new(1000).exchange_to(:jpy)
|
||||
end
|
||||
end
|
||||
|
|
98
test/models/account/balance/calculator_test.rb
Normal file
98
test/models/account/balance/calculator_test.rb
Normal file
|
@ -0,0 +1,98 @@
|
|||
require "test_helper"
|
||||
require "csv"
|
||||
|
||||
class Account::Balance::CalculatorTest < ActiveSupport::TestCase
|
||||
# See: https://docs.google.com/spreadsheets/d/18LN5N-VLq4b49Mq1fNwF7_eBiHSQB46qQduRtdAEN98/edit?usp=sharing
|
||||
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
|
||||
"eur_checking_eur" => row["eur_checking_eur"],
|
||||
"eur_checking_usd" => row["eur_checking_usd"]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
test "syncs account with only valuations" do
|
||||
account = accounts(:collectable)
|
||||
|
||||
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
|
||||
end
|
|
@ -1,61 +0,0 @@
|
|||
require "test_helper"
|
||||
|
||||
class Account::BalanceCalculatorTest < ActiveSupport::TestCase
|
||||
test "syncs account with only valuations" do
|
||||
account = accounts(:collectable)
|
||||
|
||||
daily_balances = Account::BalanceCalculator.new(account).daily_balances
|
||||
|
||||
expected_balances = [
|
||||
400, 400, 400, 400, 400, 400, 400, 400, 400, 400,
|
||||
400, 400, 400, 400, 400, 400, 400, 400, 700, 700,
|
||||
700, 700, 700, 700, 700, 700, 550, 550, 550, 550,
|
||||
550
|
||||
].map(&:to_d)
|
||||
|
||||
assert_equal expected_balances, daily_balances.map { |b| b[:balance] }
|
||||
end
|
||||
|
||||
test "syncs account with only transactions" do
|
||||
account = accounts(:checking)
|
||||
|
||||
daily_balances = Account::BalanceCalculator.new(account).daily_balances
|
||||
|
||||
expected_balances = [
|
||||
4000, 3985, 3985, 3985, 3985, 3985, 3985, 3985, 5060, 5060,
|
||||
5060, 5060, 5060, 5060, 5060, 5040, 5040, 5040, 5010, 5010,
|
||||
5010, 5010, 5010, 5010, 5010, 5000, 5000, 5000, 5000, 5000,
|
||||
5000
|
||||
].map(&:to_d)
|
||||
|
||||
assert_equal expected_balances, daily_balances.map { |b| b[:balance] }
|
||||
end
|
||||
|
||||
test "syncs account with both valuations and transactions" do
|
||||
account = accounts(:savings_with_valuation_overrides)
|
||||
daily_balances = Account::BalanceCalculator.new(account).daily_balances
|
||||
|
||||
expected_balances = [
|
||||
21250, 21750, 21750, 21750, 21750, 21000, 21000, 21000, 21000, 21000,
|
||||
21000, 21000, 19000, 19000, 19000, 19000, 19000, 19000, 19500, 19500,
|
||||
19500, 19500, 19500, 19500, 19500, 19700, 19700, 20500, 20500, 20500,
|
||||
20000
|
||||
].map(&:to_d)
|
||||
|
||||
assert_equal expected_balances, daily_balances.map { |b| b[:balance] }
|
||||
end
|
||||
|
||||
test "syncs liability account" do
|
||||
account = accounts(:credit_card)
|
||||
daily_balances = Account::BalanceCalculator.new(account).daily_balances
|
||||
|
||||
expected_balances = [
|
||||
1040, 940, 940, 940, 940, 940, 940, 940, 940, 940,
|
||||
940, 940, 940, 940, 940, 960, 960, 960, 990, 990,
|
||||
990, 990, 990, 990, 990, 1000, 1000, 1000, 1000, 1000,
|
||||
1000
|
||||
].map(&:to_d)
|
||||
|
||||
assert_equal expected_balances, daily_balances.map { |b| b[:balance] }
|
||||
end
|
||||
end
|
|
@ -14,6 +14,15 @@ class Account::SyncableTest < ActiveSupport::TestCase
|
|||
assert_equal 31, account.balances.count
|
||||
end
|
||||
|
||||
test "foreign currency account has balances in each currency after syncing" do
|
||||
account = accounts(:eur_checking)
|
||||
account.sync
|
||||
|
||||
assert_equal 62, account.balances.count
|
||||
assert_equal 31, account.balances.where(currency: "EUR").count
|
||||
assert_equal 31, account.balances.where(currency: "USD").count
|
||||
end
|
||||
|
||||
test "stale balances are purged after syncing" do
|
||||
account = accounts(:savings_with_valuation_overrides)
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
require "test_helper"
|
||||
|
||||
class AccountBalanceTest < ActiveSupport::TestCase
|
||||
class Account::BalanceTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
|
|
|
@ -1,8 +1,20 @@
|
|||
require "test_helper"
|
||||
require "csv"
|
||||
|
||||
class AccountTest < ActiveSupport::TestCase
|
||||
def setup
|
||||
@account = accounts(:checking)
|
||||
@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
|
||||
|
||||
test "new account should be valid" do
|
||||
|
@ -10,4 +22,92 @@ class AccountTest < ActiveSupport::TestCase
|
|||
assert_not_nil @account.accountable_id
|
||||
assert_not_nil @account.accountable
|
||||
end
|
||||
|
||||
test "recognizes foreign currency account" do
|
||||
regular_account = accounts(:checking)
|
||||
foreign_account = accounts(:eur_checking)
|
||||
assert_not regular_account.foreign_currency?
|
||||
assert foreign_account.foreign_currency?
|
||||
end
|
||||
|
||||
test "recognizes multi currency account" do
|
||||
regular_account = accounts(:checking)
|
||||
multi_currency_account = accounts(:multi_currency)
|
||||
assert_not regular_account.multi_currency?
|
||||
assert multi_currency_account.multi_currency?
|
||||
end
|
||||
|
||||
test "multi currency and foreign currency are different concepts" do
|
||||
multi_currency_account = accounts(:multi_currency)
|
||||
assert_equal multi_currency_account.family.currency, multi_currency_account.currency
|
||||
assert multi_currency_account.multi_currency?
|
||||
assert_not multi_currency_account.foreign_currency?
|
||||
end
|
||||
|
||||
test "syncs regular account" do
|
||||
@account.sync
|
||||
assert_equal "ok", @account.status
|
||||
assert_equal 31, @account.balances.count
|
||||
end
|
||||
|
||||
test "syncs foreign currency account" do
|
||||
account = accounts(:eur_checking)
|
||||
account.sync
|
||||
assert_equal "ok", account.status
|
||||
assert_equal 31, account.balances.where(currency: "USD").count
|
||||
assert_equal 31, account.balances.where(currency: "EUR").count
|
||||
end
|
||||
test "groups accounts by type" do
|
||||
@family.accounts.each do |account|
|
||||
account.sync
|
||||
end
|
||||
|
||||
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]
|
||||
liabilities = result[:liabilities]
|
||||
|
||||
assert_equal @family.assets, assets.sum
|
||||
assert_equal @family.liabilities, liabilities.sum
|
||||
|
||||
depositories = assets.children.find { |group| group.name == "Account::Depository" }
|
||||
properties = assets.children.find { |group| group.name == "Account::Property" }
|
||||
vehicles = assets.children.find { |group| group.name == "Account::Vehicle" }
|
||||
investments = assets.children.find { |group| group.name == "Account::Investment" }
|
||||
other_assets = assets.children.find { |group| group.name == "Account::OtherAsset" }
|
||||
|
||||
credits = liabilities.children.find { |group| group.name == "Account::Credit" }
|
||||
loans = liabilities.children.find { |group| group.name == "Account::Loan" }
|
||||
other_liabilities = liabilities.children.find { |group| group.name == "Account::OtherLiability" }
|
||||
|
||||
assert_equal 4, depositories.children.count
|
||||
assert_equal 0, properties.children.count
|
||||
assert_equal 0, vehicles.children.count
|
||||
assert_equal 0, investments.children.count
|
||||
assert_equal 1, other_assets.children.count
|
||||
|
||||
assert_equal 1, credits.children.count
|
||||
assert_equal 0, loans.children.count
|
||||
assert_equal 0, other_liabilities.children.count
|
||||
end
|
||||
|
||||
test "generates series with last balance equal to current account balance" do
|
||||
# If account hasn't been synced, series falls back to a single point with the current balance
|
||||
assert_equal @account.balance_money, @account.series.last.value
|
||||
|
||||
@account.sync
|
||||
|
||||
# Synced series will always have final balance equal to the current account balance
|
||||
assert_equal @account.balance_money, @account.series.last.value
|
||||
end
|
||||
|
||||
test "generates empty series for foreign currency if no exchange rate" do
|
||||
account = accounts(:eur_checking)
|
||||
|
||||
# We know EUR -> NZD exchange rate is not available in fixtures
|
||||
assert_equal 0, account.series(currency: "NZD").values.count
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
require "test_helper"
|
||||
|
||||
class CurrencyTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
|
@ -8,6 +8,17 @@ class FamilyTest < ActiveSupport::TestCase
|
|||
@family.accounts.each do |account|
|
||||
account.sync
|
||||
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"]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
test "should have many users" do
|
||||
|
@ -38,45 +49,37 @@ class FamilyTest < ActiveSupport::TestCase
|
|||
end
|
||||
|
||||
test "should calculate total assets" do
|
||||
assert_equal Money.new(25550), @family.assets_money
|
||||
expected = @expected_snapshots.last["assets"].to_d
|
||||
assert_equal Money.new(expected), @family.assets
|
||||
end
|
||||
|
||||
test "should calculate total liabilities" do
|
||||
assert_equal Money.new(1000), @family.liabilities_money
|
||||
expected = @expected_snapshots.last["liabilities"].to_d
|
||||
assert_equal Money.new(expected), @family.liabilities
|
||||
end
|
||||
|
||||
test "should calculate net worth" do
|
||||
assert_equal Money.new(24550), @family.net_worth_money
|
||||
expected = @expected_snapshots.last["net_worth"].to_d
|
||||
assert_equal Money.new(expected), @family.net_worth
|
||||
end
|
||||
|
||||
test "should calculate snapshot correctly" do
|
||||
# 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"]
|
||||
}
|
||||
end
|
||||
|
||||
asset_series = @family.snapshot[:asset_series]
|
||||
liability_series = @family.snapshot[:liability_series]
|
||||
net_worth_series = @family.snapshot[:net_worth_series]
|
||||
|
||||
assert_equal expected_snapshots.count, asset_series.values.count
|
||||
assert_equal expected_snapshots.count, liability_series.values.count
|
||||
assert_equal expected_snapshots.count, net_worth_series.values.count
|
||||
assert_equal @expected_snapshots.count, asset_series.values.count
|
||||
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_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_equal expected_assets, asset_series.values[index]
|
||||
assert_equal expected_liabilities, liability_series.values[index]
|
||||
assert_equal expected_net_worth, net_worth_series.values[index]
|
||||
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
|
||||
|
||||
|
|
|
@ -7,20 +7,20 @@ class ValueGroupTest < ActiveSupport::TestCase
|
|||
collectable = accounts(:collectable)
|
||||
|
||||
# Level 1
|
||||
@assets = ValueGroup.new("Assets")
|
||||
@assets = ValueGroup.new("Assets", :usd)
|
||||
|
||||
# Level 2
|
||||
@depositories = @assets.add_child_node("Depositories")
|
||||
@other_assets = @assets.add_child_node("Other Assets")
|
||||
@depositories = @assets.add_child_group("Depositories", :usd)
|
||||
@other_assets = @assets.add_child_group("Other Assets", :usd)
|
||||
|
||||
# Level 3 (leaf/value nodes)
|
||||
@checking_node = @depositories.add_value_node(checking)
|
||||
@savings_node = @depositories.add_value_node(savings)
|
||||
@collectable_node = @other_assets.add_value_node(collectable)
|
||||
@checking_node = @depositories.add_value_node(OpenStruct.new({ name: "Checking", value: Money.new(5000) }), Money.new(5000))
|
||||
@savings_node = @depositories.add_value_node(OpenStruct.new({ name: "Savings", value: Money.new(20000) }), Money.new(20000))
|
||||
@collectable_node = @other_assets.add_value_node(OpenStruct.new({ name: "Collectable", value: Money.new(550) }), Money.new(550))
|
||||
end
|
||||
|
||||
test "empty group works" do
|
||||
group = ValueGroup.new
|
||||
group = ValueGroup.new("Root", :usd)
|
||||
|
||||
assert_equal "Root", group.name
|
||||
assert_equal [], group.children
|
||||
|
@ -32,7 +32,7 @@ class ValueGroupTest < ActiveSupport::TestCase
|
|||
|
||||
test "group without value nodes has no value" do
|
||||
assets = ValueGroup.new("Assets")
|
||||
depositories = assets.add_child_node("Depositories")
|
||||
depositories = assets.add_child_group("Depositories")
|
||||
|
||||
assert_equal 0, assets.sum
|
||||
assert_equal 0, depositories.sum
|
||||
|
@ -57,24 +57,24 @@ class ValueGroupTest < ActiveSupport::TestCase
|
|||
end
|
||||
|
||||
test "group with value nodes aggregates totals correctly" do
|
||||
assert_equal 5000, @checking_node.sum
|
||||
assert_equal 20000, @savings_node.sum
|
||||
assert_equal 550, @collectable_node.sum
|
||||
assert_equal Money.new(5000), @checking_node.sum
|
||||
assert_equal Money.new(20000), @savings_node.sum
|
||||
assert_equal Money.new(550), @collectable_node.sum
|
||||
|
||||
assert_equal 25000, @depositories.sum
|
||||
assert_equal 550, @other_assets.sum
|
||||
assert_equal Money.new(25000), @depositories.sum
|
||||
assert_equal Money.new(550), @other_assets.sum
|
||||
|
||||
assert_equal 25550, @assets.sum
|
||||
assert_equal Money.new(25550), @assets.sum
|
||||
end
|
||||
|
||||
test "group averages leaf nodes" do
|
||||
assert_equal 5000, @checking_node.avg
|
||||
assert_equal 20000, @savings_node.avg
|
||||
assert_equal 550, @collectable_node.avg
|
||||
assert_equal Money.new(5000), @checking_node.avg
|
||||
assert_equal Money.new(20000), @savings_node.avg
|
||||
assert_equal Money.new(550), @collectable_node.avg
|
||||
|
||||
assert_in_delta 12500, @depositories.avg, 0.01
|
||||
assert_in_delta 550, @other_assets.avg, 0.01
|
||||
assert_in_delta 8516.67, @assets.avg, 0.01
|
||||
assert_in_delta 12500, @depositories.avg.amount, 0.01
|
||||
assert_in_delta 550, @other_assets.avg.amount, 0.01
|
||||
assert_in_delta 8516.67, @assets.avg.amount, 0.01
|
||||
end
|
||||
|
||||
# Percentage of parent group (i.e. collectable is 100% of "Other Assets" group)
|
||||
|
@ -88,19 +88,19 @@ class ValueGroupTest < ActiveSupport::TestCase
|
|||
end
|
||||
|
||||
test "handles unbalanced tree" do
|
||||
vehicles = @assets.add_child_node("Vehicles")
|
||||
vehicles = @assets.add_child_group("Vehicles")
|
||||
|
||||
# Since we didn't add any value nodes to vehicles, shouldn't affect rollups
|
||||
assert_equal 25550, @assets.sum
|
||||
assert_equal Money.new(25550), @assets.sum
|
||||
end
|
||||
|
||||
|
||||
test "can attach and aggregate time series" do
|
||||
checking_series = TimeSeries.new([ { date: 1.day.ago.to_date, value: 4000 }, { date: Date.current, value: 5000 } ])
|
||||
savings_series = TimeSeries.new([ { date: 1.day.ago.to_date, value: 19000 }, { date: Date.current, value: 20000 } ])
|
||||
checking_series = TimeSeries.new([ { date: 1.day.ago.to_date, value: Money.new(4000) }, { date: Date.current, value: Money.new(5000) } ])
|
||||
savings_series = TimeSeries.new([ { date: 1.day.ago.to_date, value: Money.new(19000) }, { date: Date.current, value: Money.new(20000) } ])
|
||||
|
||||
@checking_node.attach_series(checking_series)
|
||||
@savings_node.attach_series(savings_series)
|
||||
@checking_node.series = checking_series
|
||||
@savings_node.series = savings_series
|
||||
|
||||
assert_not_nil @checking_node.series
|
||||
assert_not_nil @savings_node.series
|
||||
|
@ -108,8 +108,8 @@ class ValueGroupTest < ActiveSupport::TestCase
|
|||
assert_equal @checking_node.sum, @checking_node.series.last.value
|
||||
assert_equal @savings_node.sum, @savings_node.series.last.value
|
||||
|
||||
aggregated_depository_series = TimeSeries.new([ { date: 1.day.ago.to_date, value: 23000 }, { date: Date.current, value: 25000 } ])
|
||||
aggregated_assets_series = TimeSeries.new([ { date: 1.day.ago.to_date, value: 23000 }, { date: Date.current, value: 25000 } ])
|
||||
aggregated_depository_series = TimeSeries.new([ { date: 1.day.ago.to_date, value: Money.new(23000) }, { date: Date.current, value: Money.new(25000) } ])
|
||||
aggregated_assets_series = TimeSeries.new([ { date: 1.day.ago.to_date, value: Money.new(23000) }, { date: Date.current, value: Money.new(25000) } ])
|
||||
|
||||
assert_equal aggregated_depository_series.values, @depositories.series.values
|
||||
assert_equal aggregated_assets_series.values, @assets.series.values
|
||||
|
@ -117,29 +117,29 @@ class ValueGroupTest < ActiveSupport::TestCase
|
|||
|
||||
test "attached series must be a TimeSeries" do
|
||||
assert_raises(RuntimeError) do
|
||||
@checking_node.attach_series([])
|
||||
@checking_node.series = []
|
||||
end
|
||||
end
|
||||
|
||||
test "cannot add time series to non-leaf node" do
|
||||
assert_raises(RuntimeError) do
|
||||
@assets.attach_series(TimeSeries.new([]))
|
||||
@assets.series = TimeSeries.new([])
|
||||
end
|
||||
end
|
||||
|
||||
test "can only add value node at leaf level of tree" do
|
||||
root = ValueGroup.new("Root Level")
|
||||
grandparent = root.add_child_node("Grandparent")
|
||||
parent = grandparent.add_child_node("Parent")
|
||||
grandparent = root.add_child_group("Grandparent")
|
||||
parent = grandparent.add_child_group("Parent")
|
||||
|
||||
value_node = parent.add_value_node(OpenStruct.new({ name: "Value Node", value: 100 }))
|
||||
value_node = parent.add_value_node(OpenStruct.new({ name: "Value Node", value: Money.new(100) }), Money.new(100))
|
||||
|
||||
assert_raises(RuntimeError) do
|
||||
value_node.add_value_node(OpenStruct.new({ name: "Value Node", value: 100 }))
|
||||
value_node.add_value_node(OpenStruct.new({ name: "Value Node", value: Money.new(100) }), Money.new(100))
|
||||
end
|
||||
|
||||
assert_raises(RuntimeError) do
|
||||
grandparent.add_value_node(OpenStruct.new({ name: "Value Node", value: 100 }))
|
||||
grandparent.add_value_node(OpenStruct.new({ name: "Value Node", value: Money.new(100) }), Money.new(100))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue