double_entry 0.1.0 → 0.2.0

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 (40) hide show
  1. data/README.md +16 -14
  2. data/lib/double_entry.rb +9 -62
  3. data/lib/double_entry/account.rb +5 -1
  4. data/lib/double_entry/configuration.rb +21 -0
  5. data/lib/double_entry/reporting.rb +51 -0
  6. data/lib/double_entry/{aggregate.rb → reporting/aggregate.rb} +9 -7
  7. data/lib/double_entry/{aggregate_array.rb → reporting/aggregate_array.rb} +3 -1
  8. data/lib/double_entry/{day_range.rb → reporting/day_range.rb} +2 -0
  9. data/lib/double_entry/{hour_range.rb → reporting/hour_range.rb} +2 -0
  10. data/lib/double_entry/{line_aggregate.rb → reporting/line_aggregate.rb} +4 -2
  11. data/lib/double_entry/{month_range.rb → reporting/month_range.rb} +4 -2
  12. data/lib/double_entry/{time_range.rb → reporting/time_range.rb} +7 -5
  13. data/lib/double_entry/{time_range_array.rb → reporting/time_range_array.rb} +2 -0
  14. data/lib/double_entry/{week_range.rb → reporting/week_range.rb} +3 -1
  15. data/lib/double_entry/{year_range.rb → reporting/year_range.rb} +4 -3
  16. data/lib/double_entry/transfer.rb +4 -0
  17. data/lib/double_entry/validation.rb +1 -0
  18. data/lib/double_entry/{line_check.rb → validation/line_check.rb} +2 -0
  19. data/lib/double_entry/version.rb +1 -1
  20. data/script/jack_hammer +21 -16
  21. data/spec/double_entry/account_spec.rb +9 -0
  22. data/spec/double_entry/configuration_spec.rb +23 -0
  23. data/spec/double_entry/locking_spec.rb +24 -13
  24. data/spec/double_entry/{aggregate_array_spec.rb → reporting/aggregate_array_spec.rb} +2 -2
  25. data/spec/double_entry/reporting/aggregate_spec.rb +171 -0
  26. data/spec/double_entry/reporting/line_aggregate_spec.rb +10 -0
  27. data/spec/double_entry/{month_range_spec.rb → reporting/month_range_spec.rb} +23 -21
  28. data/spec/double_entry/{time_range_array_spec.rb → reporting/time_range_array_spec.rb} +41 -39
  29. data/spec/double_entry/{time_range_spec.rb → reporting/time_range_spec.rb} +10 -9
  30. data/spec/double_entry/{week_range_spec.rb → reporting/week_range_spec.rb} +26 -25
  31. data/spec/double_entry/reporting_spec.rb +24 -0
  32. data/spec/double_entry/transfer_spec.rb +17 -0
  33. data/spec/double_entry/{line_check_spec.rb → validation/line_check_spec.rb} +17 -16
  34. data/spec/double_entry_spec.rb +409 -0
  35. data/spec/support/accounts.rb +16 -17
  36. metadata +70 -35
  37. checksums.yaml +0 -15
  38. data/spec/double_entry/aggregate_spec.rb +0 -168
  39. data/spec/double_entry/double_entry_spec.rb +0 -391
  40. data/spec/double_entry/line_aggregate_spec.rb +0 -8
@@ -1,391 +0,0 @@
1
- # encoding: utf-8
2
- require 'spec_helper'
3
-
4
- describe DoubleEntry do
5
-
6
- # these specs blat the DoubleEntry configuration, so take
7
- # a copy and clean up after ourselves
8
- before do
9
- @config_accounts = DoubleEntry.accounts
10
- @config_transfers = DoubleEntry.transfers
11
- end
12
-
13
- after do
14
- DoubleEntry.accounts = @config_accounts
15
- DoubleEntry.transfers = @config_transfers
16
- end
17
-
18
-
19
- describe 'configuration' do
20
- it 'checks for duplicates of accounts' do
21
- expect do
22
- DoubleEntry.accounts = DoubleEntry::Account::Set.new.tap do |accounts|
23
- accounts << DoubleEntry::Account.new(:identifier => :gah!)
24
- accounts << DoubleEntry::Account.new(:identifier => :gah!)
25
- end
26
- end.to raise_error(DoubleEntry::DuplicateAccount)
27
- end
28
-
29
- it 'checks for duplicates of transfers' do
30
- expect do
31
- DoubleEntry.transfers = DoubleEntry::Transfer::Set.new.tap do |transfers|
32
- transfers << DoubleEntry::Transfer.new(:from => :savings, :to => :cash, :code => :xfer)
33
- transfers << DoubleEntry::Transfer.new(:from => :savings, :to => :cash, :code => :xfer)
34
- end
35
- end.to raise_error(DoubleEntry::DuplicateTransfer)
36
- end
37
- end
38
-
39
- describe 'accounts' do
40
- before do
41
- @scope = double('a scope', :id => 1)
42
-
43
- DoubleEntry.accounts = DoubleEntry::Account::Set.new.tap do |accounts|
44
- accounts << DoubleEntry::Account.new(:identifier => :unscoped)
45
- accounts << DoubleEntry::Account.new(:identifier => :scoped, :scope_identifier => lambda { |u| u.id })
46
- end
47
- end
48
-
49
- describe 'fetching' do
50
- it 'can find an unscoped account by identifier' do
51
- expect(DoubleEntry.account(:unscoped)).to_not be_nil
52
- end
53
-
54
- it 'can find a scoped account by identifier' do
55
- expect(DoubleEntry.account(:scoped, :scope => @scope)).to_not be_nil
56
- end
57
-
58
- it 'raises an exception when it cannot find an account' do
59
- expect { DoubleEntry.account(:invalid) }.to raise_error(DoubleEntry::UnknownAccount)
60
- end
61
-
62
- it 'raises exception when you ask for an unscoped account w/ scope' do
63
- expect { DoubleEntry.account(:unscoped, :scope => @scope) }.to raise_error(DoubleEntry::UnknownAccount)
64
- end
65
-
66
- it 'raises exception when you ask for a scoped account w/ out scope' do
67
- expect { DoubleEntry.account(:scoped) }.to raise_error(DoubleEntry::UnknownAccount)
68
- end
69
- end
70
-
71
- context "an unscoped account" do
72
- subject(:unscoped) { DoubleEntry.account(:unscoped) }
73
-
74
- it "has an identifier" do
75
- expect(unscoped.identifier).to eq :unscoped
76
- end
77
- end
78
- context "a scoped account" do
79
- subject(:scoped) { DoubleEntry.account(:scoped, :scope => @scope) }
80
-
81
- it "has an identifier" do
82
- expect(scoped.identifier).to eq :scoped
83
- end
84
- end
85
- end
86
-
87
- describe 'transfers' do
88
- before do
89
- DoubleEntry.accounts = DoubleEntry::Account::Set.new.tap do |accounts|
90
- accounts << DoubleEntry::Account.new(:identifier => :savings)
91
- accounts << DoubleEntry::Account.new(:identifier => :cash)
92
- accounts << DoubleEntry::Account.new(:identifier => :trash)
93
- end
94
-
95
- DoubleEntry.transfers = DoubleEntry::Transfer::Set.new.tap do |transfers|
96
- transfers << DoubleEntry::Transfer.new(:from => :savings, :to => :cash, :code => :xfer, :meta_requirement => [:ref])
97
- end
98
-
99
- @savings = DoubleEntry.account(:savings)
100
- @cash = DoubleEntry.account(:cash)
101
- @trash = DoubleEntry.account(:trash)
102
- end
103
-
104
- it 'can transfer from an account to an account, if the transfer is allowed' do
105
- expect do
106
- DoubleEntry.transfer(Money.new(100_00), :from => @savings, :to => @cash, :code => :xfer, :meta => {:ref => 'shopping!'})
107
- end.to_not raise_error
108
- end
109
-
110
- it 'raises an exception when the transfer is not allowed (wrong direction)' do
111
- expect do
112
- DoubleEntry.transfer(Money.new(100_00), :from => @cash, :to => @savings, :code => :xfer)
113
- end.to raise_error(DoubleEntry::TransferNotAllowed)
114
- end
115
-
116
- it 'raises an exception when the transfer is not allowed (wrong code)' do
117
- expect do
118
- DoubleEntry.transfer(Money.new(100_00), :from => @savings, :to => @cash, :code => :yfer, :meta => {:ref => 'shopping!'})
119
- end.to raise_error(DoubleEntry::TransferNotAllowed)
120
- end
121
-
122
- it 'raises an exception when the transfer is not allowed (does not exist, at all)' do
123
- expect do
124
- DoubleEntry.transfer(Money.new(100_00), :from => @cash, :to => @trash)
125
- end.to raise_error(DoubleEntry::TransferNotAllowed)
126
- end
127
-
128
- it 'raises an exception when required meta data is omitted' do
129
- expect do
130
- DoubleEntry.transfer(Money.new(100_00), :from => @savings, :to => @cash, :code => :xfer, :meta => {})
131
- end.to raise_error(DoubleEntry::RequiredMetaMissing)
132
- end
133
- end
134
-
135
- describe 'lines' do
136
- before do
137
- DoubleEntry.accounts = DoubleEntry::Account::Set.new.tap do |accounts|
138
- accounts << DoubleEntry::Account.new(:identifier => :a)
139
- accounts << DoubleEntry::Account.new(:identifier => :b)
140
- end
141
-
142
- DoubleEntry.transfers = DoubleEntry::Transfer::Set.new.tap do |transfers|
143
- description = lambda do |line|
144
- "Money goes #{line.credit? ? 'out' : 'in'}: #{line.amount.format}"
145
- end
146
- transfers << DoubleEntry::Transfer.new(:code => :xfer, :from => :a, :to => :b, :description => description)
147
- end
148
-
149
- @a, @b = DoubleEntry.account(:a), DoubleEntry.account(:b)
150
- DoubleEntry.transfer(Money.new(10_00), :from => @a, :to => @b, :code => :xfer)
151
- @credit = lines_for_account(@a).first
152
- @debit = lines_for_account(@b).first
153
- end
154
-
155
- it 'has an amount' do
156
- expect(@credit.amount).to eq -Money.new(10_00)
157
- expect(@debit.amount).to eq Money.new(10_00)
158
- end
159
-
160
- it 'has a code' do
161
- expect(@credit.code).to eq :xfer
162
- expect(@debit.code).to eq :xfer
163
- end
164
-
165
- it 'auto-sets scope when assigning account (and partner_accout, is this implementation?)' do
166
- expect(@credit[:account]).to eq 'a'
167
- expect(@credit[:scope]).to be_nil
168
- expect(@credit[:partner_account]).to eq 'b'
169
- expect(@credit[:partner_scope]).to be_nil
170
- end
171
-
172
- it 'has a partner_account (or is this implementation?)' do
173
- expect(@credit.partner_account).to eq @debit.account
174
- end
175
-
176
- it 'knows if it is a credit or debit' do
177
- expect(@credit).to be_credit
178
- expect(@debit).to be_debit
179
- expect(@credit).to_not be_debit
180
- expect(@debit).to_not be_credit
181
- end
182
-
183
- it 'can describe itself' do
184
- expect(@credit.description).to eq 'Money goes out: $-10.00'
185
- expect(@debit.description).to eq 'Money goes in: $10.00'
186
- end
187
-
188
- it 'can reference its partner' do
189
- expect(@credit.partner).to eq @debit
190
- expect(@debit.partner).to eq @credit
191
- end
192
-
193
- it 'can ask for its pair (credit always coming first)' do
194
- expect(@credit.pair).to eq [@credit, @debit]
195
- expect(@debit.pair).to eq [@credit, @debit]
196
- end
197
-
198
- it 'can ask for the account (and get an instance)' do
199
- expect(@credit.account).to eq @a
200
- expect(@debit.account).to eq @b
201
- end
202
- end
203
-
204
- describe 'balances' do
205
- before do
206
- DoubleEntry.accounts = DoubleEntry::Account::Set.new.tap do |accounts|
207
- accounts << DoubleEntry::Account.new(:identifier => :work)
208
- accounts << DoubleEntry::Account.new(:identifier => :cash)
209
- accounts << DoubleEntry::Account.new(:identifier => :savings)
210
- accounts << DoubleEntry::Account.new(:identifier => :store)
211
- end
212
-
213
- DoubleEntry.transfers = DoubleEntry::Transfer::Set.new.tap do |transfers|
214
- transfers << DoubleEntry::Transfer.new(:code => :salary, :from => :work, :to => :cash)
215
- transfers << DoubleEntry::Transfer.new(:code => :xfer, :from => :cash, :to => :savings)
216
- transfers << DoubleEntry::Transfer.new(:code => :xfer, :from => :savings, :to => :cash)
217
- transfers << DoubleEntry::Transfer.new(:code => :purchase, :from => :cash, :to => :store)
218
- transfers << DoubleEntry::Transfer.new(:code => :layby, :from => :cash, :to => :store)
219
- transfers << DoubleEntry::Transfer.new(:code => :deposit, :from => :cash, :to => :store)
220
- end
221
-
222
- @work = DoubleEntry.account(:work)
223
- @savings = DoubleEntry.account(:savings)
224
- @cash = DoubleEntry.account(:cash)
225
- @store = DoubleEntry.account(:store)
226
-
227
- Timecop.freeze 3.weeks.ago+1.day do
228
- # got paid from work
229
- DoubleEntry.transfer(Money.new(1_000_00), :from => @work, :code => :salary, :to => @cash)
230
- # transfer half salary into savings
231
- DoubleEntry.transfer(Money.new(500_00), :from => @cash, :code => :xfer, :to => @savings)
232
- end
233
-
234
- Timecop.freeze 2.weeks.ago+1.day do
235
- # got myself a darth vader helmet
236
- DoubleEntry.transfer(Money.new(200_00), :from => @cash, :code => :purchase, :to => @store)
237
- # paid off some of my darth vader suit layby (to go with the helmet)
238
- DoubleEntry.transfer(Money.new(100_00), :from => @cash, :code => :layby, :to => @store)
239
- # put a deposit on the darth vader voice changer module (for the helmet)
240
- DoubleEntry.transfer(Money.new(100_00), :from => @cash, :code => :deposit, :to => @store)
241
- end
242
-
243
- Timecop.freeze 1.week.ago+1.day do
244
- # transfer 200 out of savings
245
- DoubleEntry.transfer(Money.new(200_00), :from => @savings, :code => :xfer, :to => @cash)
246
- # pay the remaining balance on the darth vader voice changer module
247
- DoubleEntry.transfer(Money.new(200_00), :from => @cash, :code => :purchase, :to => @store)
248
- end
249
-
250
- Timecop.freeze 1.week.from_now do
251
- # go to the star wars convention AND ROCK OUT IN YOUR ACE DARTH VADER COSTUME!!!
252
- end
253
- end
254
-
255
- it 'has the initial balances that we expect' do
256
- expect(@work.balance).to eq -Money.new(1_000_00)
257
- expect(@cash.balance).to eq Money.new(100_00)
258
- expect(@savings.balance).to eq Money.new(300_00)
259
- expect(@store.balance).to eq Money.new(600_00)
260
- end
261
-
262
- it 'should have correct account balance records' do
263
- [@work, @cash, @savings, @store].each do |account|
264
- expect(DoubleEntry::AccountBalance.find_by_account(account).balance).to eq account.balance
265
- end
266
- end
267
-
268
- it 'affects origin/destination balance after transfer' do
269
- @savings_balance = @savings.balance
270
- @cash_balance = @cash.balance
271
- @amount = Money.new(10_00)
272
-
273
- DoubleEntry.transfer(@amount, :from => @savings, :code => :xfer, :to => @cash)
274
-
275
- expect(@savings.balance).to eq @savings_balance - @amount
276
- expect(@cash.balance).to eq @cash_balance + @amount
277
- end
278
-
279
- it 'can be queried at a given point in time' do
280
- expect(@cash.balance(:at => 1.week.ago)).to eq Money.new(100_00)
281
- end
282
-
283
- it 'can be queries between two points in time' do
284
- expect(@cash.balance(:from => 3.weeks.ago, :to => 2.weeks.ago)).to eq Money.new(500_00)
285
- end
286
-
287
- it 'can report on balances, scoped by code' do
288
- expect(@cash.balance(:code => :salary)).to eq Money.new(1_000_00)
289
- end
290
-
291
- it 'can report on balances, scoped by many codes' do
292
- expect(@store.balance(:codes => [:layby, :deposit])).to eq Money.new(200_00)
293
- end
294
-
295
- it 'has running balances for each line' do
296
- @lines = lines_for_account(@cash)
297
- expect(@lines[0].balance).to eq Money.new(1_000_00) # salary
298
- expect(@lines[1].balance).to eq Money.new(500_00) # savings
299
- expect(@lines[2].balance).to eq Money.new(300_00) # purchase
300
- expect(@lines[3].balance).to eq Money.new(200_00) # layby
301
- expect(@lines[4].balance).to eq Money.new(100_00) # deposit
302
- expect(@lines[5].balance).to eq Money.new(300_00) # savings
303
- expect(@lines[6].balance).to eq Money.new(100_00) # purchase
304
- end
305
- end
306
-
307
- describe 'scoping of accounts' do
308
- before do
309
- DoubleEntry.accounts = DoubleEntry::Account::Set.new.tap do |accounts|
310
- accounts << DoubleEntry::Account.new(:identifier => :bank)
311
- accounts << DoubleEntry::Account.new(:identifier => :cash, :scope_identifier => lambda { |user| user.id })
312
- accounts << DoubleEntry::Account.new(:identifier => :savings, :scope_identifier => lambda { |user| user.id })
313
- end
314
-
315
- DoubleEntry.transfers = DoubleEntry::Transfer::Set.new.tap do |transfers|
316
- transfers << DoubleEntry::Transfer.new(:from => :bank, :to => :cash, :code => :xfer)
317
- transfers << DoubleEntry::Transfer.new(:from => :cash, :to => :cash, :code => :xfer)
318
- transfers << DoubleEntry::Transfer.new(:from => :cash, :to => :savings, :code => :xfer)
319
- end
320
-
321
- @john = User.make!
322
- @ryan = User.make!
323
-
324
- @bank = DoubleEntry.account(:bank)
325
- @johns_cash = DoubleEntry.account(:cash, :scope => @john)
326
- @johns_savings = DoubleEntry.account(:savings, :scope => @john)
327
- @ryans_cash = DoubleEntry.account(:cash, :scope => @ryan)
328
- @ryans_savings = DoubleEntry.account(:savings, :scope => @ryan)
329
- end
330
-
331
- it 'treats each separately scoped account having their own separate balances' do
332
- DoubleEntry.transfer(Money.new(20_00), :from => @bank, :to => @johns_cash, :code => :xfer)
333
- DoubleEntry.transfer(Money.new(10_00), :from => @bank, :to => @ryans_cash, :code => :xfer)
334
- expect(@johns_cash.balance).to eq Money.new(20_00)
335
- expect(@ryans_cash.balance).to eq Money.new(10_00)
336
- end
337
-
338
- it 'allows transfer between two separately scoped accounts' do
339
- DoubleEntry.transfer(Money.new(10_00), :from => @ryans_cash, :to => @johns_cash, :code => :xfer)
340
- expect(@ryans_cash.balance).to eq -Money.new(10_00)
341
- expect(@johns_cash.balance).to eq Money.new(10_00)
342
- end
343
-
344
- it 'reports balance correctly if called from either account or finances object' do
345
- DoubleEntry.transfer(Money.new(10_00), :from => @ryans_cash, :to => @johns_cash, :code => :xfer)
346
- expect(@ryans_cash.balance).to eq -Money.new(10_00)
347
- expect(DoubleEntry.balance(:cash, :scope => @ryan)).to eq -Money.new(10_00)
348
- end
349
-
350
- it 'raises exception if you try to transfer between the same account, despite it being scoped' do
351
- expect do
352
- DoubleEntry.transfer(Money.new(10_00), :from => @ryans_cash, :to => @ryans_cash, :code => :xfer)
353
- end.to raise_error(DoubleEntry::TransferNotAllowed)
354
- end
355
-
356
- it 'allows transfer from one persons account to the same persons other kind of account' do
357
- DoubleEntry.transfer(Money.new(100_00), :from => @ryans_cash, :to => @ryans_savings, :code => :xfer)
358
- expect(@ryans_cash.balance).to eq -Money.new(100_00)
359
- expect(@ryans_savings.balance).to eq Money.new(100_00)
360
- end
361
-
362
- it 'allows you to report on scoped accounts globally' do
363
- expect(DoubleEntry.balance(:cash)).to eq @ryans_cash.balance+@johns_cash.balance
364
- expect(DoubleEntry.balance(:savings)).to eq @ryans_savings.balance+@johns_savings.balance
365
- end
366
- end
367
-
368
- describe "::scopes_with_minimum_balance_for_account" do
369
- subject(:scopes) { DoubleEntry.scopes_with_minimum_balance_for_account(minimum_balance, :checking) }
370
-
371
- context "a 'checking' account with balance $100" do
372
- let!(:user) { User.make!(:checking_balance => Money.new(100_00)) }
373
-
374
- context "when searching for balance $99" do
375
- let(:minimum_balance) { Money.new(99_00) }
376
- it { should include user.id }
377
- end
378
-
379
- context "when searching for balance $100" do
380
- let(:minimum_balance) { Money.new(100_00) }
381
- it { should include user.id }
382
- end
383
-
384
- context "when searching for balance $101" do
385
- let(:minimum_balance) { Money.new(101_00) }
386
- it { should_not include user.id }
387
- end
388
- end
389
- end
390
-
391
- end
@@ -1,8 +0,0 @@
1
- # encoding: utf-8
2
- require 'spec_helper'
3
- describe DoubleEntry::LineAggregate do
4
-
5
- it "has a table name prefixed with double_entry_" do
6
- expect(DoubleEntry::LineAggregate.table_name).to eq "double_entry_line_aggregates"
7
- end
8
- end