rvgp 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.rubocop.yml +23 -0
  4. data/LICENSE +504 -0
  5. data/README.md +223 -0
  6. data/Rakefile +32 -0
  7. data/bin/rvgp +8 -0
  8. data/lib/rvgp/application/config.rb +159 -0
  9. data/lib/rvgp/application/descendant_registry.rb +122 -0
  10. data/lib/rvgp/application/status_output.rb +139 -0
  11. data/lib/rvgp/application.rb +170 -0
  12. data/lib/rvgp/base/command.rb +457 -0
  13. data/lib/rvgp/base/grid.rb +531 -0
  14. data/lib/rvgp/base/reader.rb +29 -0
  15. data/lib/rvgp/base/reconciler.rb +434 -0
  16. data/lib/rvgp/base/validation.rb +261 -0
  17. data/lib/rvgp/commands/cashflow.rb +160 -0
  18. data/lib/rvgp/commands/grid.rb +70 -0
  19. data/lib/rvgp/commands/ireconcile.rb +95 -0
  20. data/lib/rvgp/commands/new_project.rb +296 -0
  21. data/lib/rvgp/commands/plot.rb +41 -0
  22. data/lib/rvgp/commands/publish_gsheets.rb +83 -0
  23. data/lib/rvgp/commands/reconcile.rb +58 -0
  24. data/lib/rvgp/commands/rotate_year.rb +202 -0
  25. data/lib/rvgp/commands/validate_journal.rb +59 -0
  26. data/lib/rvgp/commands/validate_system.rb +44 -0
  27. data/lib/rvgp/commands.rb +160 -0
  28. data/lib/rvgp/dashboard.rb +252 -0
  29. data/lib/rvgp/fakers/fake_feed.rb +245 -0
  30. data/lib/rvgp/fakers/fake_journal.rb +57 -0
  31. data/lib/rvgp/fakers/fake_reconciler.rb +88 -0
  32. data/lib/rvgp/fakers/faker_helpers.rb +25 -0
  33. data/lib/rvgp/gem.rb +80 -0
  34. data/lib/rvgp/journal/commodity.rb +453 -0
  35. data/lib/rvgp/journal/complex_commodity.rb +214 -0
  36. data/lib/rvgp/journal/currency.rb +101 -0
  37. data/lib/rvgp/journal/journal.rb +141 -0
  38. data/lib/rvgp/journal/posting.rb +156 -0
  39. data/lib/rvgp/journal/pricer.rb +267 -0
  40. data/lib/rvgp/journal.rb +24 -0
  41. data/lib/rvgp/plot/gnuplot.rb +478 -0
  42. data/lib/rvgp/plot/google-drive/output_csv.rb +44 -0
  43. data/lib/rvgp/plot/google-drive/output_google_sheets.rb +434 -0
  44. data/lib/rvgp/plot/google-drive/sheet.rb +67 -0
  45. data/lib/rvgp/plot.rb +293 -0
  46. data/lib/rvgp/pta/hledger.rb +237 -0
  47. data/lib/rvgp/pta/ledger.rb +308 -0
  48. data/lib/rvgp/pta.rb +311 -0
  49. data/lib/rvgp/reconcilers/csv_reconciler.rb +424 -0
  50. data/lib/rvgp/reconcilers/journal_reconciler.rb +41 -0
  51. data/lib/rvgp/reconcilers/shorthand/finance_gem_hacks.rb +48 -0
  52. data/lib/rvgp/reconcilers/shorthand/international_atm.rb +152 -0
  53. data/lib/rvgp/reconcilers/shorthand/investment.rb +144 -0
  54. data/lib/rvgp/reconcilers/shorthand/mortgage.rb +195 -0
  55. data/lib/rvgp/utilities/grid_query.rb +190 -0
  56. data/lib/rvgp/utilities/yaml.rb +131 -0
  57. data/lib/rvgp/utilities.rb +44 -0
  58. data/lib/rvgp/validations/balance_validation.rb +68 -0
  59. data/lib/rvgp/validations/duplicate_tags_validation.rb +48 -0
  60. data/lib/rvgp/validations/uncategorized_validation.rb +15 -0
  61. data/lib/rvgp.rb +66 -0
  62. data/resources/README.MD/2022-cashflow-google.png +0 -0
  63. data/resources/README.MD/2022-cashflow.png +0 -0
  64. data/resources/README.MD/all-wealth-growth-google.png +0 -0
  65. data/resources/README.MD/all-wealth-growth.png +0 -0
  66. data/resources/gnuplot/default.yml +80 -0
  67. data/resources/i18n/en.yml +192 -0
  68. data/resources/iso-4217-currencies.json +171 -0
  69. data/resources/skel/Rakefile +5 -0
  70. data/resources/skel/app/grids/cashflow_grid.rb +27 -0
  71. data/resources/skel/app/grids/monthly_income_and_expenses_grid.rb +25 -0
  72. data/resources/skel/app/grids/wealth_growth_grid.rb +35 -0
  73. data/resources/skel/app/plots/cashflow.yml +33 -0
  74. data/resources/skel/app/plots/monthly-income-and-expenses.yml +17 -0
  75. data/resources/skel/app/plots/wealth-growth.yml +20 -0
  76. data/resources/skel/config/csv-format-acme-checking.yml +9 -0
  77. data/resources/skel/config/google-secrets.yml +5 -0
  78. data/resources/skel/config/rvgp.yml +0 -0
  79. data/resources/skel/journals/prices.db +0 -0
  80. data/rvgp.gemspec +6 -0
  81. data/test/assets/ledger_total_monthly_liabilities_with_empty.xml +383 -0
  82. data/test/assets/ledger_total_monthly_liabilities_with_empty2.xml +428 -0
  83. data/test/test_command_base.rb +61 -0
  84. data/test/test_commodity.rb +270 -0
  85. data/test/test_csv_reconciler.rb +60 -0
  86. data/test/test_currency.rb +24 -0
  87. data/test/test_fake_feed.rb +228 -0
  88. data/test/test_fake_journal.rb +98 -0
  89. data/test/test_fake_reconciler.rb +60 -0
  90. data/test/test_journal_parse.rb +545 -0
  91. data/test/test_ledger.rb +102 -0
  92. data/test/test_plot.rb +133 -0
  93. data/test/test_posting.rb +50 -0
  94. data/test/test_pricer.rb +139 -0
  95. data/test/test_pta_adapter.rb +575 -0
  96. data/test/test_utilities.rb +45 -0
  97. 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