double_entry 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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