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