rvgp 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.rubocop.yml +23 -0
- data/LICENSE +504 -0
- data/README.md +223 -0
- data/Rakefile +32 -0
- data/bin/rvgp +8 -0
- data/lib/rvgp/application/config.rb +159 -0
- data/lib/rvgp/application/descendant_registry.rb +122 -0
- data/lib/rvgp/application/status_output.rb +139 -0
- data/lib/rvgp/application.rb +170 -0
- data/lib/rvgp/base/command.rb +457 -0
- data/lib/rvgp/base/grid.rb +531 -0
- data/lib/rvgp/base/reader.rb +29 -0
- data/lib/rvgp/base/reconciler.rb +434 -0
- data/lib/rvgp/base/validation.rb +261 -0
- data/lib/rvgp/commands/cashflow.rb +160 -0
- data/lib/rvgp/commands/grid.rb +70 -0
- data/lib/rvgp/commands/ireconcile.rb +95 -0
- data/lib/rvgp/commands/new_project.rb +296 -0
- data/lib/rvgp/commands/plot.rb +41 -0
- data/lib/rvgp/commands/publish_gsheets.rb +83 -0
- data/lib/rvgp/commands/reconcile.rb +58 -0
- data/lib/rvgp/commands/rotate_year.rb +202 -0
- data/lib/rvgp/commands/validate_journal.rb +59 -0
- data/lib/rvgp/commands/validate_system.rb +44 -0
- data/lib/rvgp/commands.rb +160 -0
- data/lib/rvgp/dashboard.rb +252 -0
- data/lib/rvgp/fakers/fake_feed.rb +245 -0
- data/lib/rvgp/fakers/fake_journal.rb +57 -0
- data/lib/rvgp/fakers/fake_reconciler.rb +88 -0
- data/lib/rvgp/fakers/faker_helpers.rb +25 -0
- data/lib/rvgp/gem.rb +80 -0
- data/lib/rvgp/journal/commodity.rb +453 -0
- data/lib/rvgp/journal/complex_commodity.rb +214 -0
- data/lib/rvgp/journal/currency.rb +101 -0
- data/lib/rvgp/journal/journal.rb +141 -0
- data/lib/rvgp/journal/posting.rb +156 -0
- data/lib/rvgp/journal/pricer.rb +267 -0
- data/lib/rvgp/journal.rb +24 -0
- data/lib/rvgp/plot/gnuplot.rb +478 -0
- data/lib/rvgp/plot/google-drive/output_csv.rb +44 -0
- data/lib/rvgp/plot/google-drive/output_google_sheets.rb +434 -0
- data/lib/rvgp/plot/google-drive/sheet.rb +67 -0
- data/lib/rvgp/plot.rb +293 -0
- data/lib/rvgp/pta/hledger.rb +237 -0
- data/lib/rvgp/pta/ledger.rb +308 -0
- data/lib/rvgp/pta.rb +311 -0
- data/lib/rvgp/reconcilers/csv_reconciler.rb +424 -0
- data/lib/rvgp/reconcilers/journal_reconciler.rb +41 -0
- data/lib/rvgp/reconcilers/shorthand/finance_gem_hacks.rb +48 -0
- data/lib/rvgp/reconcilers/shorthand/international_atm.rb +152 -0
- data/lib/rvgp/reconcilers/shorthand/investment.rb +144 -0
- data/lib/rvgp/reconcilers/shorthand/mortgage.rb +195 -0
- data/lib/rvgp/utilities/grid_query.rb +190 -0
- data/lib/rvgp/utilities/yaml.rb +131 -0
- data/lib/rvgp/utilities.rb +44 -0
- data/lib/rvgp/validations/balance_validation.rb +68 -0
- data/lib/rvgp/validations/duplicate_tags_validation.rb +48 -0
- data/lib/rvgp/validations/uncategorized_validation.rb +15 -0
- data/lib/rvgp.rb +66 -0
- data/resources/README.MD/2022-cashflow-google.png +0 -0
- data/resources/README.MD/2022-cashflow.png +0 -0
- data/resources/README.MD/all-wealth-growth-google.png +0 -0
- data/resources/README.MD/all-wealth-growth.png +0 -0
- data/resources/gnuplot/default.yml +80 -0
- data/resources/i18n/en.yml +192 -0
- data/resources/iso-4217-currencies.json +171 -0
- data/resources/skel/Rakefile +5 -0
- data/resources/skel/app/grids/cashflow_grid.rb +27 -0
- data/resources/skel/app/grids/monthly_income_and_expenses_grid.rb +25 -0
- data/resources/skel/app/grids/wealth_growth_grid.rb +35 -0
- data/resources/skel/app/plots/cashflow.yml +33 -0
- data/resources/skel/app/plots/monthly-income-and-expenses.yml +17 -0
- data/resources/skel/app/plots/wealth-growth.yml +20 -0
- data/resources/skel/config/csv-format-acme-checking.yml +9 -0
- data/resources/skel/config/google-secrets.yml +5 -0
- data/resources/skel/config/rvgp.yml +0 -0
- data/resources/skel/journals/prices.db +0 -0
- data/rvgp.gemspec +6 -0
- data/test/assets/ledger_total_monthly_liabilities_with_empty.xml +383 -0
- data/test/assets/ledger_total_monthly_liabilities_with_empty2.xml +428 -0
- data/test/test_command_base.rb +61 -0
- data/test/test_commodity.rb +270 -0
- data/test/test_csv_reconciler.rb +60 -0
- data/test/test_currency.rb +24 -0
- data/test/test_fake_feed.rb +228 -0
- data/test/test_fake_journal.rb +98 -0
- data/test/test_fake_reconciler.rb +60 -0
- data/test/test_journal_parse.rb +545 -0
- data/test/test_ledger.rb +102 -0
- data/test/test_plot.rb +133 -0
- data/test/test_posting.rb +50 -0
- data/test/test_pricer.rb +139 -0
- data/test/test_pta_adapter.rb +575 -0
- data/test/test_utilities.rb +45 -0
- metadata +268 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'minitest/autorun'
|
|
5
|
+
|
|
6
|
+
require_relative '../lib/rvgp'
|
|
7
|
+
|
|
8
|
+
# Minitest class, used to test RVGP::Journal::Commodity
|
|
9
|
+
class TestCommodity < Minitest::Test
|
|
10
|
+
def test_commodity_comparision_when_precision_not_equal
|
|
11
|
+
assert_equal commodity('$ 23.01'), commodity('$ 23.010')
|
|
12
|
+
assert_equal commodity('$ 23.010'), commodity('$ 23.01')
|
|
13
|
+
|
|
14
|
+
assert_equal commodity('$ 23.010'), commodity('$ 23.01')
|
|
15
|
+
assert_equal commodity('$ 23.01'), commodity('$ 23.010')
|
|
16
|
+
|
|
17
|
+
assert_equal commodity('$ 23.00'), commodity('$ 23.000000')
|
|
18
|
+
assert_equal commodity('$ 23.000000'), commodity('$ 23.00')
|
|
19
|
+
|
|
20
|
+
assert(commodity('$ 13.000') != commodity('$ 23.00'))
|
|
21
|
+
assert(commodity('$ 23.00') != commodity('$ 13.000'))
|
|
22
|
+
|
|
23
|
+
assert(commodity('$ 23.00') != commodity('$ 13.000'))
|
|
24
|
+
assert(commodity('$ 13.000') != commodity('$ 23.00'))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_commodity_add_and_sub_when_precision_not_equal
|
|
28
|
+
assert_equal '$ 32.00', commodity_op('$ 2', :+, '$ 30')
|
|
29
|
+
assert_equal '$ 25.01', commodity_op('$ 24.01', :+, '$ 1')
|
|
30
|
+
assert_equal '$ 46.02', commodity_op('$ 23.01', :+, '$ 23.010')
|
|
31
|
+
assert_equal '$ 15.01002', commodity_op('$ 5.000020', :+, '$ 10.010')
|
|
32
|
+
assert_equal '$ 110.00001', commodity_op('$ 10.000004', :+, '$ 100.000006')
|
|
33
|
+
assert_equal '$ 15.20', commodity_op('$ 5.1', :+, '$ 10.1')
|
|
34
|
+
assert_equal '$ 15.10', commodity_op('$ 5.10', :+, '$ 10')
|
|
35
|
+
assert_equal '$ 10.01', commodity_op('$ 5.005', :+, '$ 5.005')
|
|
36
|
+
|
|
37
|
+
assert_equal '$ 0.00', commodity_op('$ 1', :-, '$ 1')
|
|
38
|
+
assert_equal '$ 1.9996', commodity_op('$ 3.0009', :-, '$ 1.0013')
|
|
39
|
+
assert_equal '$ -1.00', commodity_op('$ 1.000000000000001', :-, '$ 2.000000000000001')
|
|
40
|
+
assert_equal '$ 0.10', commodity_op('$ 0.2', :-, '$ 0.1')
|
|
41
|
+
assert_equal '$ 0.01', commodity_op('$ 0.02', :-, '$ 0.01')
|
|
42
|
+
assert_equal '$ 0.05', commodity_op('$ 0.1', :-, '$ 0.05')
|
|
43
|
+
assert_equal '$ 0.90', commodity_op('$ 1.0', :-, '$ 0.1')
|
|
44
|
+
assert_equal '$ 0.99', commodity_op('$ 1.0', :-, '$ 0.01')
|
|
45
|
+
|
|
46
|
+
assert_equal '$ -0.10', commodity_op('$ -0.2', :-, '$ -0.1')
|
|
47
|
+
assert_equal '$ -0.01', commodity_op('$ -0.02', :-, '$ -0.01')
|
|
48
|
+
assert_equal '$ -0.05', commodity_op('$ -0.1', :-, '$ -0.05')
|
|
49
|
+
assert_equal '$ -0.90', commodity_op('$ -1.0', :-, '$ -0.1')
|
|
50
|
+
assert_equal '$ -0.99', commodity_op('$ -1.0', :-, '$ -0.01')
|
|
51
|
+
|
|
52
|
+
assert_equal '$ -0.30', commodity_op('$ -0.2', :-, '$ 0.1')
|
|
53
|
+
assert_equal '$ -0.03', commodity_op('$ -0.02', :-, '$ 0.01')
|
|
54
|
+
assert_equal '$ -0.15', commodity_op('$ -0.1', :-, '$ 0.05')
|
|
55
|
+
assert_equal '$ -1.10', commodity_op('$ -1.0', :-, '$ 0.1')
|
|
56
|
+
assert_equal '$ -1.01', commodity_op('$ -1.0', :-, '$ 0.01')
|
|
57
|
+
|
|
58
|
+
assert_equal '$ 0.30', commodity_op('$ 0.2', :-, '$ -0.1')
|
|
59
|
+
assert_equal '$ 0.03', commodity_op('$ 0.02', :-, '$ -0.01')
|
|
60
|
+
assert_equal '$ 0.15', commodity_op('$ 0.1', :-, '$ -0.05')
|
|
61
|
+
assert_equal '$ 1.10', commodity_op('$ 1.0', :-, '$ -0.1')
|
|
62
|
+
assert_equal '$ 1.01', commodity_op('$ 1.0', :-, '$ -0.01')
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def test_sum
|
|
66
|
+
assert_equal commodity('$ 100.00'), ['$ 22.00', '$ 60.00', '$ 8.00', '$ 10.00'].collect { |s| commodity s }.sum
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def test_commodity_mul_by_numeric
|
|
70
|
+
assert_equal commodity('$ 32.00'), commodity('$ 2') * 16
|
|
71
|
+
assert_equal commodity('$ 17.00'), commodity('$ 2') * 8.5
|
|
72
|
+
assert_equal commodity('$ 2.00'), commodity('$ 0.5') * 4
|
|
73
|
+
|
|
74
|
+
assert_equal commodity('$ -32.00'), commodity('$ -2') * 16
|
|
75
|
+
assert_equal commodity('$ -17.00'), commodity('$ -2') * 8.5
|
|
76
|
+
assert_equal commodity('$ -2.00'), commodity('$ -0.5') * 4
|
|
77
|
+
|
|
78
|
+
assert_equal commodity('$ -32.00'), commodity('$ 2') * -16
|
|
79
|
+
assert_equal commodity('$ -17.00'), commodity('$ 2') * -8.5
|
|
80
|
+
assert_equal commodity('$ -2.00'), commodity('$ 0.5') * -4
|
|
81
|
+
|
|
82
|
+
assert_equal commodity('$ 0.5000125'), commodity('$ 2.00005') * 0.25
|
|
83
|
+
assert_equal commodity('$ 0.5000125'), commodity('$ 0.25') * 2.00005
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def test_commodity_div_by_numeric
|
|
87
|
+
assert_equal commodity('$ 2'), commodity('$ 32.00') / 16
|
|
88
|
+
assert_equal commodity('$ 2'), commodity('$ 17.00') / 8.5
|
|
89
|
+
assert_equal commodity('$ 0.5'), commodity('$ 2.00') / 4
|
|
90
|
+
|
|
91
|
+
assert_equal commodity('$ -2'), commodity('$ -32.00') / 16
|
|
92
|
+
assert_equal commodity('$ -2'), commodity('$ -17.00') / 8.5
|
|
93
|
+
assert_equal commodity('$ -0.5'), commodity('$ -2.00') / 4
|
|
94
|
+
|
|
95
|
+
assert_equal commodity('$ 2'), commodity('$ -32.00') / -16
|
|
96
|
+
assert_equal commodity('$ 2'), commodity('$ -17.00') / -8.5
|
|
97
|
+
assert_equal commodity('$ 0.5'), commodity('$ -2.00') / -4
|
|
98
|
+
|
|
99
|
+
assert_equal commodity('$ 2.00005'), commodity('$ 0.5000125') / 0.25
|
|
100
|
+
assert_equal commodity('$ 0.25'), commodity('$ 0.5000125') / 2.00005
|
|
101
|
+
assert_equal commodity('$ 0.005'), commodity('$ 0.01') / 2
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def test_huge_decimal_sum_when_decimal_zeros
|
|
105
|
+
# This bug showed up while testing the HLedger::balance ...
|
|
106
|
+
assert_equal '$ 493.00', ['$ 443.0000000000'.to_commodity, '$ 50.0000000000'.to_commodity].sum.to_s
|
|
107
|
+
assert_equal '$ 493.10', ['$ 443.0500000000'.to_commodity, '$ 50.0500000000'.to_commodity].sum.to_s
|
|
108
|
+
assert_equal '$ 0.00', ['$ 0.0000000000'.to_commodity, '$ 0.0000000000'.to_commodity].sum.to_s
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def test_round
|
|
112
|
+
assert_equal '$ 123.46', '$ 123.455'.to_commodity.round(2).to_s
|
|
113
|
+
assert_equal '$ -123.46', '$ -123.455'.to_commodity.round(2).to_s
|
|
114
|
+
assert_equal '$ 123.46', '$ 123.459999999999'.to_commodity.round(2).to_s
|
|
115
|
+
assert_equal '$ 123.454545', '$ 123.4545454545454'.to_commodity.round(6).to_s
|
|
116
|
+
assert_equal '$ -123.46', '$ -123.459999999999'.to_commodity.round(2).to_s
|
|
117
|
+
assert_equal '$ 123.45', '$ 123.454444449'.to_commodity.round(2).to_s
|
|
118
|
+
assert_equal '$ -123.45', '$ -123.454444449'.to_commodity.round(2).to_s
|
|
119
|
+
assert_equal '$ 123.44', '$ 123.44'.to_commodity.round(2).to_s
|
|
120
|
+
assert_equal '$ -123.44', '$ -123.44'.to_commodity.round(2).to_s
|
|
121
|
+
assert_equal '$ 123.4000', '$ 123.4'.to_commodity.round(4).to_s
|
|
122
|
+
assert_equal '$ -123.4000', '$ -123.4'.to_commodity.round(4).to_s
|
|
123
|
+
assert_equal '$ 123.0000', '$ 123'.to_commodity.round(4).to_s
|
|
124
|
+
assert_equal '$ -123.0000', '$ -123'.to_commodity.round(4).to_s
|
|
125
|
+
assert_equal '$ 123', '$ 123.0455555555'.to_commodity.round(0).to_s
|
|
126
|
+
assert_equal '$ -123', '$ -123.0455555555'.to_commodity.round(0).to_s
|
|
127
|
+
assert_equal '$ 1000.00', '$ 999.99999'.to_commodity.round(2).to_s
|
|
128
|
+
assert_equal '$ -1000.00', '$ -999.99999'.to_commodity.round(2).to_s
|
|
129
|
+
|
|
130
|
+
assert_equal '$ 123.45', '$ 123.454'.to_commodity.round(2).to_s
|
|
131
|
+
assert_equal '$ -123.45', '$ -123.454'.to_commodity.round(2).to_s
|
|
132
|
+
|
|
133
|
+
# Found/fixed on 2021-10-25:
|
|
134
|
+
assert_equal '$ 2584.09', '$ 2584.09'.to_commodity.round(2).to_s
|
|
135
|
+
assert_equal '$ 2584.009', '$ 2584.009'.to_commodity.round(3).to_s
|
|
136
|
+
assert_equal '$ 2584.009', '$ 2584.009'.to_commodity.round(3).to_s
|
|
137
|
+
assert_equal '$ 2584.900', '$ 2584.900'.to_commodity.round(3).to_s
|
|
138
|
+
assert_equal '$ 2584.9', '$ 2584.9'.to_commodity.round(1).to_s
|
|
139
|
+
|
|
140
|
+
# Found/fixed on 2022-11-13:
|
|
141
|
+
# The reason this is a bug, is because the mantissa begins with zero's. We
|
|
142
|
+
# had to cheange the implementation to switch to precision, from a log10() operation
|
|
143
|
+
assert_equal '$ 5.01', ('$ 139.76'.to_commodity * (22_290.0 / 622_290.0)).round(2).to_s
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def test_floor
|
|
147
|
+
assert_equal '$ 123.45', '$ 123.455'.to_commodity.floor(2).to_s
|
|
148
|
+
assert_equal '$ -123.45', '$ -123.455'.to_commodity.floor(2).to_s
|
|
149
|
+
assert_equal '$ 123.45', '$ 123.459999999999'.to_commodity.floor(2).to_s
|
|
150
|
+
assert_equal '$ 123.454545', '$ 123.4545454545454'.to_commodity.floor(6).to_s
|
|
151
|
+
assert_equal '$ -123.45', '$ -123.459999999999'.to_commodity.floor(2).to_s
|
|
152
|
+
assert_equal '$ 123.45', '$ 123.454444449'.to_commodity.floor(2).to_s
|
|
153
|
+
assert_equal '$ -123.45', '$ -123.454444449'.to_commodity.floor(2).to_s
|
|
154
|
+
assert_equal '$ 123.44', '$ 123.44'.to_commodity.floor(2).to_s
|
|
155
|
+
assert_equal '$ -123.44', '$ -123.44'.to_commodity.floor(2).to_s
|
|
156
|
+
assert_equal '$ 123.4000', '$ 123.4'.to_commodity.floor(4).to_s
|
|
157
|
+
assert_equal '$ -123.4000', '$ -123.4'.to_commodity.floor(4).to_s
|
|
158
|
+
assert_equal '$ 123.0000', '$ 123'.to_commodity.floor(4).to_s
|
|
159
|
+
assert_equal '$ -123.0000', '$ -123'.to_commodity.floor(4).to_s
|
|
160
|
+
assert_equal '$ 123', '$ 123.0455555555'.to_commodity.floor(0).to_s
|
|
161
|
+
assert_equal '$ -123', '$ -123.0455555555'.to_commodity.floor(0).to_s
|
|
162
|
+
assert_equal '$ 999.99', '$ 999.99999'.to_commodity.floor(2).to_s
|
|
163
|
+
assert_equal '$ -999.99', '$ -999.99999'.to_commodity.floor(2).to_s
|
|
164
|
+
|
|
165
|
+
assert_equal '$ 123.45', '$ 123.454'.to_commodity.floor(2).to_s
|
|
166
|
+
assert_equal '$ -123.45', '$ -123.454'.to_commodity.floor(2).to_s
|
|
167
|
+
|
|
168
|
+
# Found/fixed on 2021-10-25:
|
|
169
|
+
assert_equal '$ 2584.09', '$ 2584.09'.to_commodity.floor(2).to_s
|
|
170
|
+
assert_equal '$ 2584.009', '$ 2584.009'.to_commodity.floor(3).to_s
|
|
171
|
+
assert_equal '$ 2584.009', '$ 2584.009'.to_commodity.floor(3).to_s
|
|
172
|
+
assert_equal '$ 2584.900', '$ 2584.900'.to_commodity.floor(3).to_s
|
|
173
|
+
assert_equal '$ 2584.9', '$ 2584.9'.to_commodity.floor(1).to_s
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def test_to_s_features
|
|
177
|
+
assert_equal '$ 0.00', '$ 0'.to_commodity.to_s(commatize: true)
|
|
178
|
+
assert_equal '$ 1.00', '$ 1.00'.to_commodity.to_s(commatize: true)
|
|
179
|
+
assert_equal '$ 12.00', '$ 12.00'.to_commodity.to_s(commatize: true)
|
|
180
|
+
assert_equal '$ 123.00', '$ 123.00'.to_commodity.to_s(commatize: true)
|
|
181
|
+
assert_equal '$ 1,234.00', '$ 1234.00'.to_commodity.to_s(commatize: true)
|
|
182
|
+
assert_equal '$ 12,345.00', '$ 12345.00'.to_commodity.to_s(commatize: true)
|
|
183
|
+
assert_equal '$ 123,456.00', '$ 123456.00'.to_commodity.to_s(commatize: true)
|
|
184
|
+
assert_equal '$ 1,234,567.00', '$ 1234567.00'.to_commodity.to_s(commatize: true)
|
|
185
|
+
|
|
186
|
+
assert_equal '$ 123', '$ 123.00'.to_commodity.to_s(precision: 0)
|
|
187
|
+
assert_equal '$ 123.0', '$ 123.00'.to_commodity.to_s(precision: 1)
|
|
188
|
+
assert_equal '$ 123.00', '$ 123.00'.to_commodity.to_s(precision: 2)
|
|
189
|
+
assert_equal '$ 123.000', '$ 123.00'.to_commodity.to_s(precision: 3)
|
|
190
|
+
assert_equal '$ 123.0000', '$ 123.00'.to_commodity.to_s(precision: 4)
|
|
191
|
+
|
|
192
|
+
assert_equal '$ 123.46', '$ 123.455'.to_commodity.to_s(precision: 2)
|
|
193
|
+
assert_equal '$ 123.45', '$ 123.454'.to_commodity.to_s(precision: 2)
|
|
194
|
+
assert_equal '$ 124', '$ 123.5'.to_commodity.to_s(precision: 0)
|
|
195
|
+
assert_equal '$ 123', '$ 123.4'.to_commodity.to_s(precision: 0)
|
|
196
|
+
assert_equal '$ 1000', '$ 999.5'.to_commodity.to_s(precision: 0)
|
|
197
|
+
|
|
198
|
+
assert_equal '$ 0.00000', '$ 0'.to_commodity.to_s(precision: 5)
|
|
199
|
+
|
|
200
|
+
assert_equal '$ 1,234,567.00000', '$ 1234567.00'.to_commodity.to_s(commatize: true, precision: 5)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def test_commodity_from_s_with_double_quotes
|
|
204
|
+
# Seems like we need to support commodities in these forms, after spending
|
|
205
|
+
# some time going through the ledger documentation:
|
|
206
|
+
|
|
207
|
+
palette_cleanser = '1 FLORIDAHOME'.to_commodity
|
|
208
|
+
assert_equal 1, palette_cleanser.quantity
|
|
209
|
+
assert_equal 'FLORIDAHOME', palette_cleanser.alphabetic_code
|
|
210
|
+
assert_equal 'FLORIDAHOME', palette_cleanser.code
|
|
211
|
+
assert_equal '1 FLORIDAHOME', palette_cleanser.to_s
|
|
212
|
+
|
|
213
|
+
crab_apples = '100 "crab apples"'.to_commodity
|
|
214
|
+
assert_equal 100, crab_apples.quantity
|
|
215
|
+
assert_equal 'crab apples', crab_apples.alphabetic_code
|
|
216
|
+
assert_equal 'crab apples', crab_apples.code
|
|
217
|
+
assert_equal '100 "crab apples"', crab_apples.to_s
|
|
218
|
+
|
|
219
|
+
reverse_crab_apples = '"crab apples" 100'.to_commodity
|
|
220
|
+
assert_equal 100, reverse_crab_apples.quantity
|
|
221
|
+
assert_equal 'crab apples', reverse_crab_apples.alphabetic_code
|
|
222
|
+
assert_equal 'crab apples', reverse_crab_apples.code
|
|
223
|
+
# NOTE: We're opinionated on this, in our to_s. So, we don't return the
|
|
224
|
+
# original string
|
|
225
|
+
assert_equal '100 "crab apples"', reverse_crab_apples.to_s
|
|
226
|
+
|
|
227
|
+
pesky_code = '1 "test \\" ing"'.to_commodity
|
|
228
|
+
assert_equal 1, pesky_code.quantity
|
|
229
|
+
assert_equal 'test \\" ing', pesky_code.alphabetic_code
|
|
230
|
+
assert_equal 'test \\" ing', pesky_code.code
|
|
231
|
+
assert_equal '1 "test \\" ing"', pesky_code.to_s
|
|
232
|
+
|
|
233
|
+
reverse_pesky_code = '"test \\" ing" 1'.to_commodity
|
|
234
|
+
assert_equal 1, reverse_pesky_code.quantity
|
|
235
|
+
assert_equal 'test \\" ing', reverse_pesky_code.alphabetic_code
|
|
236
|
+
assert_equal 'test \\" ing', reverse_pesky_code.code
|
|
237
|
+
# NOTE: We're opinionated on this, in our to_s. So, we don't return the
|
|
238
|
+
# original string
|
|
239
|
+
assert_equal '1 "test \\" ing"', reverse_pesky_code.to_s
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# This code path appeared, when parsing the results of '--empty' in register
|
|
243
|
+
# queries. Here's what's in the xml:
|
|
244
|
+
# <post-amount>
|
|
245
|
+
# <amount>
|
|
246
|
+
# <quantity>0</quantity>
|
|
247
|
+
# </amount>
|
|
248
|
+
# </post-amount>
|
|
249
|
+
# For some months, when 'nothing' happens, a commodity of type '0'
|
|
250
|
+
# appears. I don't know what the 'best' thing to do is here. But, atm, I think
|
|
251
|
+
# this output suffices....
|
|
252
|
+
def test_commodity_from_string_of_zero
|
|
253
|
+
zero = RVGP::Journal::Commodity.from_symbol_and_amount(nil, '0')
|
|
254
|
+
|
|
255
|
+
assert_equal 0, zero.quantity
|
|
256
|
+
assert_nil zero.alphabetic_code
|
|
257
|
+
assert_nil zero.code
|
|
258
|
+
assert_equal '0', zero.to_s
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
private
|
|
262
|
+
|
|
263
|
+
def commodity(str)
|
|
264
|
+
RVGP::Journal::Commodity.from_s str
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def commodity_op(lstr, operation, rstr)
|
|
268
|
+
commodity(lstr).send(operation, commodity(rstr)).to_s
|
|
269
|
+
end
|
|
270
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'minitest/autorun'
|
|
5
|
+
|
|
6
|
+
require_relative '../lib/rvgp'
|
|
7
|
+
|
|
8
|
+
# Minitest class, used to test RVGP::Reconcilers::CsvReconciler
|
|
9
|
+
class TestCsvReconciler < Minitest::Test
|
|
10
|
+
THREE_LINES = "Line 1\nLine 2\nLine 3\n"
|
|
11
|
+
THREE_LINES_WO_ENDLINE = "Line 1\nLine 2\nLine 3"
|
|
12
|
+
THREE_LINES_W_ENDLINE_AS_CHAR0 = "\nLine 2\nLine 3\n"
|
|
13
|
+
|
|
14
|
+
def test_input_file_contents
|
|
15
|
+
assert_equal THREE_LINES, input_file_contents(THREE_LINES)
|
|
16
|
+
assert_equal "Line 2\nLine 3\n", input_file_contents(THREE_LINES, 1)
|
|
17
|
+
assert_equal "Line 3\n", input_file_contents(THREE_LINES, 2)
|
|
18
|
+
assert_equal '', input_file_contents(THREE_LINES, 3)
|
|
19
|
+
|
|
20
|
+
assert_equal "Line 1\nLine 2\n", input_file_contents(THREE_LINES, nil, 1)
|
|
21
|
+
assert_equal "Line 1\n", input_file_contents(THREE_LINES, nil, 2)
|
|
22
|
+
assert_equal '', input_file_contents(THREE_LINES, nil, 3)
|
|
23
|
+
|
|
24
|
+
assert_equal "Line 2\n", input_file_contents(THREE_LINES, 1, 1)
|
|
25
|
+
assert_equal '', input_file_contents(THREE_LINES, 0, 3)
|
|
26
|
+
assert_equal '', input_file_contents(THREE_LINES, nil, 3)
|
|
27
|
+
assert_equal '', input_file_contents(THREE_LINES, 3, 0)
|
|
28
|
+
assert_equal '', input_file_contents(THREE_LINES, 3)
|
|
29
|
+
assert_equal '', input_file_contents(THREE_LINES, 1, 2)
|
|
30
|
+
assert_equal '', input_file_contents(THREE_LINES, 2, 1)
|
|
31
|
+
|
|
32
|
+
# Test the case of the first line being empty
|
|
33
|
+
assert_equal THREE_LINES_W_ENDLINE_AS_CHAR0, input_file_contents(THREE_LINES_W_ENDLINE_AS_CHAR0)
|
|
34
|
+
|
|
35
|
+
assert_equal "Line 2\nLine 3\n", input_file_contents(THREE_LINES_W_ENDLINE_AS_CHAR0, 1)
|
|
36
|
+
assert_equal "Line 3\n", input_file_contents(THREE_LINES_W_ENDLINE_AS_CHAR0, 2)
|
|
37
|
+
assert_equal '', input_file_contents(THREE_LINES_W_ENDLINE_AS_CHAR0, 3)
|
|
38
|
+
|
|
39
|
+
# Test to see what happens if we exceed the number of lines in the file
|
|
40
|
+
assert_equal '', input_file_contents(THREE_LINES, 4, 1)
|
|
41
|
+
assert_equal '', input_file_contents(THREE_LINES, 1, 4)
|
|
42
|
+
assert_equal '', input_file_contents(THREE_LINES, 4, nil)
|
|
43
|
+
assert_equal '', input_file_contents(THREE_LINES, nil, 4)
|
|
44
|
+
assert_equal '', input_file_contents(THREE_LINES, 2, 2)
|
|
45
|
+
|
|
46
|
+
# Test the case of more trims/skips than lines
|
|
47
|
+
assert_equal THREE_LINES_WO_ENDLINE, input_file_contents(THREE_LINES_WO_ENDLINE)
|
|
48
|
+
assert_equal "Line 1\nLine 2\n", input_file_contents(THREE_LINES_WO_ENDLINE, nil, 1)
|
|
49
|
+
|
|
50
|
+
assert_equal "Line 1\n", input_file_contents(THREE_LINES_WO_ENDLINE, nil, 2)
|
|
51
|
+
assert_equal '', input_file_contents(THREE_LINES_WO_ENDLINE, nil, 3)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# This is just to may the tests a bit easier to type
|
|
57
|
+
def input_file_contents(*args)
|
|
58
|
+
RVGP::Reconcilers::CsvReconciler.input_file_contents(*args)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'minitest/autorun'
|
|
5
|
+
|
|
6
|
+
require_relative '../lib/rvgp'
|
|
7
|
+
|
|
8
|
+
# Minitest class, used to test RVGP::Journal::Currency
|
|
9
|
+
class TestCurrency < Minitest::Test
|
|
10
|
+
def test_currency
|
|
11
|
+
currency = RVGP::Journal::Currency.from_code_or_symbol 'USD'
|
|
12
|
+
assert_equal 'UNITED STATES', currency.entity
|
|
13
|
+
assert_equal 'US Dollar', currency.currency
|
|
14
|
+
assert_equal 'USD', currency.alphabetic_code
|
|
15
|
+
assert_equal 840, currency.numeric_code
|
|
16
|
+
assert_equal 2, currency.minor_unit
|
|
17
|
+
assert_equal '$', currency.symbol
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def test_currency_nil
|
|
21
|
+
currency = RVGP::Journal::Currency.from_code_or_symbol nil
|
|
22
|
+
assert_nil nil, currency
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'minitest/autorun'
|
|
5
|
+
|
|
6
|
+
require_relative '../lib/rvgp'
|
|
7
|
+
require_relative '../lib/rvgp/fakers/fake_feed'
|
|
8
|
+
require_relative '../lib/rvgp/fakers/fake_reconciler'
|
|
9
|
+
|
|
10
|
+
# Minitest class, used to test RVGP::Fakers::FakeJournal
|
|
11
|
+
class TestFakeFeed < Minitest::Test
|
|
12
|
+
def test_basic_feed
|
|
13
|
+
feed = RVGP::Fakers::FakeFeed.basic_checking from: Date.new(2020, 1, 1),
|
|
14
|
+
to: Date.new(2020, 3, 31),
|
|
15
|
+
post_count: 300
|
|
16
|
+
|
|
17
|
+
assert_kind_of String, feed
|
|
18
|
+
|
|
19
|
+
csv = CSV.parse feed, headers: true
|
|
20
|
+
assert_equal 300, csv.length
|
|
21
|
+
|
|
22
|
+
assert_equal ['Date', 'Type', 'Description', 'Withdrawal (-)', 'Deposit (+)',
|
|
23
|
+
'RunningBalance'], csv.headers
|
|
24
|
+
assert_equal '01/01/2020', csv.first['Date']
|
|
25
|
+
assert_equal '03/31/2020', csv[-1]['Date']
|
|
26
|
+
|
|
27
|
+
csv.each do |row|
|
|
28
|
+
assert_kind_of String, row['Date']
|
|
29
|
+
assert_kind_of String, row['Type']
|
|
30
|
+
assert_kind_of String, row['Description']
|
|
31
|
+
assert_kind_of String, row['Withdrawal (-)']
|
|
32
|
+
assert_kind_of String, row['Deposit (+)']
|
|
33
|
+
assert_kind_of String, row['RunningBalance']
|
|
34
|
+
|
|
35
|
+
assert_match(%r{\A\d{2}/\d{2}/\d{4}\Z}, row['Date'])
|
|
36
|
+
assert !row['Type'].empty?
|
|
37
|
+
assert !row['Description'].empty?
|
|
38
|
+
assert_match(/\$ \d+\.\d{2}/,
|
|
39
|
+
row['Withdrawal (-)'].empty? ? row['Deposit (+)'] : row['Withdrawal (-)'])
|
|
40
|
+
assert_match(/\$ -?\d+\.\d{2}/, row['RunningBalance'])
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def test_descriptions_param
|
|
45
|
+
expense_descriptions = %w[Walmart Amazon Apple CVS Exxon Berkshire Google Microsoft Costco]
|
|
46
|
+
income_descriptions = %w[Uber Cyberdyne]
|
|
47
|
+
feed = RVGP::Fakers::FakeFeed.basic_checking post_count: 50,
|
|
48
|
+
expense_descriptions: expense_descriptions,
|
|
49
|
+
income_descriptions: income_descriptions
|
|
50
|
+
|
|
51
|
+
csv = CSV.parse feed, headers: true
|
|
52
|
+
assert_equal 50, csv.length
|
|
53
|
+
csv.each do |row|
|
|
54
|
+
if row['Withdrawal (-)'].empty?
|
|
55
|
+
assert(/\A(.+) DIRECT DEP\Z/.match(row['Description']))
|
|
56
|
+
assert_includes income_descriptions, ::Regexp.last_match(1)
|
|
57
|
+
else
|
|
58
|
+
assert_includes expense_descriptions, row['Description']
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def test_entries_param
|
|
64
|
+
# There isn't an easy way to sort this, atm... so, we just test that these lines are appended
|
|
65
|
+
additional_entries = [
|
|
66
|
+
{ 'Date' => Date.new(2019, 12, 31),
|
|
67
|
+
'Type' => 'VISA',
|
|
68
|
+
'Description' => 'Posting from Dec 2019',
|
|
69
|
+
'Withdrawal (-)' => '$ 9.00'.to_commodity },
|
|
70
|
+
{ 'Date' => Date.new(2020, 1, 31),
|
|
71
|
+
'Type' => 'VISA',
|
|
72
|
+
'Description' => 'Posting from Jan 2020',
|
|
73
|
+
'Withdrawal (-)' => '$ 12.00'.to_commodity },
|
|
74
|
+
{ 'Date' => Date.new(2020, 4, 1),
|
|
75
|
+
'Type' => 'ACH',
|
|
76
|
+
'Description' => 'Posting from Apr 2020',
|
|
77
|
+
'Deposit (+)' => '$ 8.00'.to_commodity }
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
starting_balance = '$ 10000.00'.to_commodity
|
|
81
|
+
|
|
82
|
+
feed = RVGP::Fakers::FakeFeed.basic_checking from: Date.new(2020, 1, 1),
|
|
83
|
+
to: Date.new(2020, 3, 31),
|
|
84
|
+
post_count: 300,
|
|
85
|
+
starting_balance: starting_balance,
|
|
86
|
+
entries: additional_entries
|
|
87
|
+
csv = CSV.parse feed, headers: true
|
|
88
|
+
|
|
89
|
+
# First:
|
|
90
|
+
assert_equal csv[0].to_h.values,
|
|
91
|
+
['12/31/2019', 'VISA', 'Posting from Dec 2019', '$ 9.00', '',
|
|
92
|
+
(starting_balance - '$ 9.00'.to_commodity).to_s]
|
|
93
|
+
|
|
94
|
+
# Middle (This just happened to consistently be element 100)
|
|
95
|
+
assert_equal csv[100].to_h.values,
|
|
96
|
+
['01/31/2020', 'VISA', 'Posting from Jan 2020', '$ 12.00', '',
|
|
97
|
+
(csv[99]['RunningBalance'].to_commodity - '$ 12.00'.to_commodity).to_s]
|
|
98
|
+
|
|
99
|
+
# Last:
|
|
100
|
+
assert_equal csv[-1].to_h.values,
|
|
101
|
+
['04/01/2020', 'ACH', 'Posting from Apr 2020', '', '$ 8.00',
|
|
102
|
+
(csv[-2]['RunningBalance'].to_commodity + '$ 8.00'.to_commodity).to_s]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def test_personal_checking_feed
|
|
106
|
+
duration_in_months = 12
|
|
107
|
+
from = Date.new 2020, 1, 1
|
|
108
|
+
expense_sources = [Faker::Company.name.tr('^a-zA-Z0-9 ', '')]
|
|
109
|
+
income_sources = [Faker::Company.name.tr('^a-zA-Z0-9 ', '')]
|
|
110
|
+
liability_sources = [Faker::Company.name.tr('^a-zA-Z0-9 ', '')]
|
|
111
|
+
liabilities, assets = 2.times.map do |_|
|
|
112
|
+
duration_in_months.times.map do
|
|
113
|
+
RVGP::Journal::Commodity.from_symbol_and_amount '$', Faker::Number.between(from: 0, to: 2_000_000)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
categories = ['Personal:Expenses:Rent', 'Personal:Expenses:Food:Restaurants',
|
|
118
|
+
'Personal:Expenses:Food:Groceries', 'Personal:Expenses:Drug Stores']
|
|
119
|
+
|
|
120
|
+
category_to_company = (categories.map { |category| [category, Faker::Company.name] }).to_h
|
|
121
|
+
|
|
122
|
+
monthly_expenses = (category_to_company.keys.map do |category|
|
|
123
|
+
[category_to_company[category],
|
|
124
|
+
12.times.map do |_|
|
|
125
|
+
RVGP::Journal::Commodity.from_symbol_and_amount '$', Faker::Number.between(from: 10, to: 1000)
|
|
126
|
+
end]
|
|
127
|
+
end).to_h
|
|
128
|
+
|
|
129
|
+
# This one, we'll just try something different with:
|
|
130
|
+
monthly_expenses[category_to_company['Personal:Expenses:Rent']] = ['$ 1500.00'.to_commodity] * 12
|
|
131
|
+
|
|
132
|
+
feed = RVGP::Fakers::FakeFeed.personal_checking from: from,
|
|
133
|
+
to: from >> (duration_in_months - 1),
|
|
134
|
+
expense_sources: expense_sources,
|
|
135
|
+
income_sources: income_sources,
|
|
136
|
+
monthly_expenses: monthly_expenses,
|
|
137
|
+
liability_sources: liability_sources,
|
|
138
|
+
liabilities_by_month: liabilities,
|
|
139
|
+
assets_by_month: assets
|
|
140
|
+
|
|
141
|
+
# Ensure the running balance is making sense:
|
|
142
|
+
running_balance = nil
|
|
143
|
+
CSV.parse(feed, headers: true).each_with_index do |row, i|
|
|
144
|
+
if i.zero?
|
|
145
|
+
running_balance = row['RunningBalance'].to_commodity
|
|
146
|
+
next
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
running_balance -= row['Withdrawal (-)'].to_commodity unless row['Withdrawal (-)'].empty?
|
|
150
|
+
running_balance += row['Deposit (+)'].to_commodity unless row['Deposit (+)'].empty?
|
|
151
|
+
|
|
152
|
+
assert_equal running_balance.to_s, row['RunningBalance']
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Now let's ensure the Monthly balances:
|
|
156
|
+
liabilities_match, incomes_match, expenses_match = *[
|
|
157
|
+
'Personal:Liabilities', liability_sources,
|
|
158
|
+
'Personal:Income', income_sources,
|
|
159
|
+
'Personal:Expenses', expense_sources
|
|
160
|
+
].each_slice(2).map do |category, sources|
|
|
161
|
+
sources.map do |name|
|
|
162
|
+
{ match: format('/%s/', name), to: format([category, ':%s'].join, name.tr(' ', '')) }
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
monthly_expenses_match = category_to_company.each_with_object([]) do |pair, sum|
|
|
167
|
+
sum << { match: format('/%s/', pair.last), to: pair.first }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
journal = reconcile_journal feed,
|
|
171
|
+
income: incomes_match + liabilities_match,
|
|
172
|
+
expense: expenses_match + liabilities_match + monthly_expenses_match
|
|
173
|
+
|
|
174
|
+
# This was kinda copy pasta'd from the ReportBase
|
|
175
|
+
result_assets, result_liabilities = %w[Assets Liabilities].collect { |acct| account_by_month acct, journal }
|
|
176
|
+
|
|
177
|
+
assert_equal duration_in_months, result_liabilities.length
|
|
178
|
+
assert_equal liabilities, result_liabilities.map(&:invert!)
|
|
179
|
+
assert_equal duration_in_months, result_assets.length
|
|
180
|
+
assert_equal assets, result_assets
|
|
181
|
+
|
|
182
|
+
# Run the monthly for each of the monthly_expenses
|
|
183
|
+
categories.each do |category|
|
|
184
|
+
category_by_month = account_by_month category, journal, :amount_in
|
|
185
|
+
|
|
186
|
+
assert_equal duration_in_months, category_by_month.length
|
|
187
|
+
assert_equal monthly_expenses[category_to_company[category]], category_by_month
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
assert_equal [], account_by_month('Unknown', journal)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
private
|
|
194
|
+
|
|
195
|
+
def account_by_month(acct, journal_s, accrue_by = :total_in)
|
|
196
|
+
ledger = RVGP::Pta::Ledger.new
|
|
197
|
+
ledger.register(acct, monthly: true, from_s: journal_s)
|
|
198
|
+
.transactions.map do |tx|
|
|
199
|
+
assert_equal 1, tx.postings.length
|
|
200
|
+
tx.postings[0].send(accrue_by, '$')
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def reconcile_journal(feed, reconciler_opts)
|
|
205
|
+
journal_file = Tempfile.open %w[rvgp_test .journal]
|
|
206
|
+
|
|
207
|
+
feed_file = Tempfile.open %w[rvgp_test .csv]
|
|
208
|
+
feed_file.write feed
|
|
209
|
+
feed_file.close
|
|
210
|
+
|
|
211
|
+
yaml_file = Tempfile.open %w[rvgp_test .yaml]
|
|
212
|
+
|
|
213
|
+
yaml_file.write RVGP::Fakers::FakeReconciler.basic_checking(
|
|
214
|
+
**{ label: 'Personal AcmeBank:Checking',
|
|
215
|
+
input_path: feed_file.path,
|
|
216
|
+
output_path: journal_file.path }.merge(reconciler_opts)
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
yaml_file.close
|
|
220
|
+
|
|
221
|
+
RVGP::Reconcilers::CsvReconciler.new(RVGP::Utilities::Yaml.new(yaml_file.path)).to_ledger
|
|
222
|
+
ensure
|
|
223
|
+
[feed_file, yaml_file, journal_file].each do |f|
|
|
224
|
+
f.close
|
|
225
|
+
f.unlink
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|