double_entry 0.10.0 → 0.10.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +2 -1
  3. data/.rubocop.yml +55 -0
  4. data/.travis.yml +23 -12
  5. data/README.md +5 -1
  6. data/Rakefile +8 -3
  7. data/double_entry.gemspec +4 -3
  8. data/lib/active_record/locking_extensions.rb +28 -40
  9. data/lib/active_record/locking_extensions/log_subscriber.rb +4 -4
  10. data/lib/double_entry.rb +0 -2
  11. data/lib/double_entry/account.rb +13 -16
  12. data/lib/double_entry/account_balance.rb +0 -4
  13. data/lib/double_entry/balance_calculator.rb +4 -5
  14. data/lib/double_entry/configurable.rb +0 -2
  15. data/lib/double_entry/configuration.rb +2 -3
  16. data/lib/double_entry/errors.rb +2 -2
  17. data/lib/double_entry/line.rb +13 -16
  18. data/lib/double_entry/locking.rb +13 -18
  19. data/lib/double_entry/reporting.rb +2 -3
  20. data/lib/double_entry/reporting/aggregate.rb +90 -88
  21. data/lib/double_entry/reporting/aggregate_array.rb +58 -58
  22. data/lib/double_entry/reporting/day_range.rb +37 -35
  23. data/lib/double_entry/reporting/hour_range.rb +40 -37
  24. data/lib/double_entry/reporting/line_aggregate.rb +27 -28
  25. data/lib/double_entry/reporting/month_range.rb +67 -67
  26. data/lib/double_entry/reporting/time_range.rb +40 -38
  27. data/lib/double_entry/reporting/time_range_array.rb +3 -5
  28. data/lib/double_entry/reporting/week_range.rb +77 -78
  29. data/lib/double_entry/reporting/year_range.rb +27 -27
  30. data/lib/double_entry/transfer.rb +14 -15
  31. data/lib/double_entry/validation/line_check.rb +92 -86
  32. data/lib/double_entry/version.rb +1 -1
  33. data/lib/generators/double_entry/install/install_generator.rb +1 -2
  34. data/lib/generators/double_entry/install/templates/migration.rb +0 -2
  35. data/script/jack_hammer +1 -1
  36. data/spec/active_record/locking_extensions_spec.rb +45 -38
  37. data/spec/double_entry/account_balance_spec.rb +4 -5
  38. data/spec/double_entry/account_spec.rb +43 -44
  39. data/spec/double_entry/balance_calculator_spec.rb +6 -8
  40. data/spec/double_entry/configuration_spec.rb +14 -16
  41. data/spec/double_entry/line_spec.rb +25 -26
  42. data/spec/double_entry/locking_spec.rb +34 -39
  43. data/spec/double_entry/reporting/aggregate_array_spec.rb +8 -10
  44. data/spec/double_entry/reporting/aggregate_spec.rb +84 -44
  45. data/spec/double_entry/reporting/line_aggregate_spec.rb +7 -6
  46. data/spec/double_entry/reporting/month_range_spec.rb +109 -103
  47. data/spec/double_entry/reporting/time_range_array_spec.rb +145 -135
  48. data/spec/double_entry/reporting/time_range_spec.rb +36 -35
  49. data/spec/double_entry/reporting/week_range_spec.rb +82 -76
  50. data/spec/double_entry/reporting_spec.rb +9 -13
  51. data/spec/double_entry/transfer_spec.rb +13 -15
  52. data/spec/double_entry/validation/line_check_spec.rb +73 -79
  53. data/spec/double_entry_spec.rb +65 -68
  54. data/spec/generators/double_entry/install/install_generator_spec.rb +7 -10
  55. data/spec/spec_helper.rb +68 -10
  56. data/spec/support/accounts.rb +2 -4
  57. data/spec/support/double_entry_spec_helper.rb +4 -4
  58. data/spec/support/gemfiles/Gemfile.rails-3.2.x +1 -0
  59. metadata +31 -2
@@ -24,10 +24,10 @@ module DoubleEntry
24
24
  else
25
25
  # all other lookups can be performed with running balances
26
26
  result = lines.
27
- from(lines_table_name(options)).
28
- order('id DESC').
29
- limit(1).
30
- pluck(:balance)
27
+ from(lines_table_name(options)).
28
+ order('id DESC').
29
+ limit(1).
30
+ pluck(:balance)
31
31
  result.empty? ? Money.zero(account.currency) : Money.new(result.first, account.currency)
32
32
  end
33
33
  end
@@ -94,6 +94,5 @@ module DoubleEntry
94
94
  lines
95
95
  end
96
96
  end
97
-
98
97
  end
99
98
  end
@@ -1,6 +1,5 @@
1
1
  # encoding: utf-8
2
2
  module DoubleEntry
3
-
4
3
  # Make configuring a module or a class simple.
5
4
  #
6
5
  # class MyClass
@@ -48,5 +47,4 @@ module DoubleEntry
48
47
  end
49
48
  end
50
49
  end
51
-
52
50
  end
@@ -3,7 +3,6 @@ module DoubleEntry
3
3
  include Configurable
4
4
 
5
5
  class Configuration
6
-
7
6
  delegate(
8
7
  :accounts,
9
8
  :accounts=,
@@ -11,7 +10,7 @@ module DoubleEntry
11
10
  :scope_identifier_max_length=,
12
11
  :account_identifier_max_length,
13
12
  :account_identifier_max_length=,
14
- :to => "DoubleEntry::Account",
13
+ :to => 'DoubleEntry::Account',
15
14
  )
16
15
 
17
16
  delegate(
@@ -19,7 +18,7 @@ module DoubleEntry
19
18
  :transfers=,
20
19
  :code_max_length,
21
20
  :code_max_length=,
22
- :to => "DoubleEntry::Transfer",
21
+ :to => 'DoubleEntry::Transfer',
23
22
  )
24
23
 
25
24
  def define_accounts
@@ -11,6 +11,6 @@ module DoubleEntry
11
11
  class AccountWouldBeSentNegative < RuntimeError; end
12
12
  class AccountWouldBeSentPositiveError < RuntimeError; end
13
13
  class MismatchedCurrencies < RuntimeError; end
14
- class MissingAccountError < RuntimeError; end;
15
- class AccountScopeMismatchError < RuntimeError; end;
14
+ class MissingAccountError < RuntimeError; end
15
+ class AccountScopeMismatchError < RuntimeError; end
16
16
  end
@@ -1,7 +1,6 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module DoubleEntry
4
-
5
4
  # This is the table to end all tables!
6
5
  #
7
6
  # Every financial transaction gets two entries in here: one for the source
@@ -56,7 +55,6 @@ module DoubleEntry
56
55
  # by account, or account and code, over a particular period.
57
56
  #
58
57
  class Line < ActiveRecord::Base
59
-
60
58
  belongs_to :detail, :polymorphic => true
61
59
 
62
60
  def amount
@@ -94,11 +92,11 @@ module DoubleEntry
94
92
  self[:code].try(:to_sym)
95
93
  end
96
94
 
97
- def account=(_account)
98
- self[:account] = _account.identifier.to_s
99
- self.scope = _account.scope_identity
100
- raise "Missing Account" unless account
101
- _account
95
+ def account=(account)
96
+ self[:account] = account.identifier.to_s
97
+ self.scope = account.scope_identity
98
+ fail 'Missing Account' unless self.account
99
+ account
102
100
  end
103
101
 
104
102
  def account
@@ -109,11 +107,11 @@ module DoubleEntry
109
107
  account.currency if self[:account]
110
108
  end
111
109
 
112
- def partner_account=(_partner_account)
113
- self[:partner_account] = _partner_account.identifier.to_s
114
- self.partner_scope = _partner_account.scope_identity
115
- raise "Missing Partner Account" unless partner_account
116
- _partner_account
110
+ def partner_account=(partner_account)
111
+ self[:partner_account] = partner_account.identifier.to_s
112
+ self.partner_scope = partner_account.scope_identity
113
+ fail 'Missing Partner Account' unless self.partner_account
114
+ partner_account
117
115
  end
118
116
 
119
117
  def partner_account
@@ -149,16 +147,15 @@ module DoubleEntry
149
147
  SQL
150
148
  end
151
149
 
152
- private
150
+ private
153
151
 
154
152
  def check_balance_will_remain_valid
155
153
  if account.positive_only && balance < Money.zero
156
- raise AccountWouldBeSentNegative.new(account)
154
+ fail AccountWouldBeSentNegative, account
157
155
  end
158
156
  if account.negative_only && balance > Money.zero
159
- raise AccountWouldBeSentPositiveError.new(account)
157
+ fail AccountWouldBeSentPositiveError, account
160
158
  end
161
159
  end
162
160
  end
163
-
164
161
  end
@@ -1,6 +1,5 @@
1
1
  # encoding: utf-8
2
2
  module DoubleEntry
3
-
4
3
  # Lock financial accounts to ensure consistency.
5
4
  #
6
5
  # In order to ensure financial transactions always keep track of balances
@@ -51,7 +50,7 @@ module DoubleEntry
51
50
  end
52
51
 
53
52
  class Lock
54
- @@locks = Hash.new
53
+ @@locks = {}
55
54
 
56
55
  def initialize(accounts)
57
56
  # Make sure we always lock in the same order, to avoid deadlocks.
@@ -65,9 +64,7 @@ module DoubleEntry
65
64
 
66
65
  unless lock_and_call(&block)
67
66
  create_missing_account_balances
68
- unless lock_and_call(&block)
69
- raise LockDisaster
70
- end
67
+ fail LockDisaster unless lock_and_call(&block)
71
68
  end
72
69
  end
73
70
 
@@ -78,7 +75,9 @@ module DoubleEntry
78
75
 
79
76
  def ensure_locked!
80
77
  @accounts.each do |account|
81
- raise LockNotHeld.new(account) unless have_lock?(account)
78
+ unless lock?(account)
79
+ fail LockNotHeld, "No lock held for account: #{account.identifier}, scope #{account.scope}"
80
+ end
82
81
  end
83
82
  end
84
83
 
@@ -88,7 +87,7 @@ module DoubleEntry
88
87
  locks[account]
89
88
  end
90
89
 
91
- private
90
+ private
92
91
 
93
92
  def locks
94
93
  @@locks[Thread.current.object_id]
@@ -103,19 +102,18 @@ module DoubleEntry
103
102
  end
104
103
 
105
104
  # Return true if there's a lock on the given account.
106
- def have_lock?(account)
107
- in_a_locked_transaction? && locks.has_key?(account)
105
+ def lock?(account)
106
+ in_a_locked_transaction? && locks.key?(account)
108
107
  end
109
108
 
110
109
  # Raise an exception unless we're outside any transactions.
111
110
  def ensure_outermost_transaction!
112
111
  minimum_transaction_level = Locking.configuration.running_inside_transactional_fixtures ? 1 : 0
113
112
  unless AccountBalance.connection.open_transactions <= minimum_transaction_level
114
- raise LockMustBeOutermostTransaction
113
+ fail LockMustBeOutermostTransaction
115
114
  end
116
115
  end
117
116
 
118
-
119
117
  # Start a transaction, grab locks on the given accounts, then call the block
120
118
  # from within the transaction.
121
119
  #
@@ -144,12 +142,12 @@ module DoubleEntry
144
142
  # If one or more account balance records don't exist, set
145
143
  # accounts_with_balances to the corresponding accounts, and return false.
146
144
  def grab_locks
147
- account_balances = @accounts.map {|account| AccountBalance.find_by_account(account, :lock => true) }
145
+ account_balances = @accounts.map { |account| AccountBalance.find_by_account(account, :lock => true) }
148
146
 
149
147
  if account_balances.any?(&:nil?)
150
- @accounts_without_balances = @accounts.zip(account_balances).
151
- select {|account, account_balance| account_balance.nil? }.
152
- collect {|account, account_balance| account }
148
+ @accounts_without_balances = @accounts.zip(account_balances).
149
+ select { |_account, account_balance| account_balance.nil? }.
150
+ collect { |account, _account_balance| account }
153
151
  false
154
152
  else
155
153
  self.locks = Hash[*@accounts.zip(account_balances).flatten]
@@ -174,9 +172,6 @@ module DoubleEntry
174
172
 
175
173
  # Raised when attempting a transfer on an account that's not locked.
176
174
  class LockNotHeld < RuntimeError
177
- def initialize(account)
178
- super "No lock held for account: #{account.identifier}, scope #{account.scope}"
179
- end
180
175
  end
181
176
 
182
177
  # Raised if things go horribly, horribly wrong. This should never happen.
@@ -11,7 +11,6 @@ require 'double_entry/reporting/line_aggregate'
11
11
  require 'double_entry/reporting/time_range_array'
12
12
 
13
13
  module DoubleEntry
14
-
15
14
  # @api private
16
15
  module Reporting
17
16
  include Configurable
@@ -131,11 +130,11 @@ module DoubleEntry
131
130
  # ) # might return the user ids: [ 1423, 12232, 34729 ]
132
131
  # @param [Money] minimum_balance Minimum account balance a scope must have
133
132
  # to be included in the result set.
134
- # @param [Symbol] account_identifier
133
+ # @param [Symbol] account_identifier
135
134
  # @return [Array<Fixnum>] Scopes
136
135
  #
137
136
  def scopes_with_minimum_balance_for_account(minimum_balance, account_identifier)
138
- select_values(sanitize_sql_array([<<-SQL, account_identifier, minimum_balance.cents])).map {|scope| scope.to_i }
137
+ select_values(sanitize_sql_array([<<-SQL, account_identifier, minimum_balance.cents])).map(&:to_i)
139
138
  SELECT scope
140
139
  FROM #{AccountBalance.table_name}
141
140
  WHERE account = ?
@@ -1,112 +1,114 @@
1
1
  # encoding: utf-8
2
2
  module DoubleEntry
3
- module Reporting
4
- class Aggregate
5
- attr_reader :function, :account, :code, :range, :options, :filter, :currency
3
+ module Reporting
4
+ class Aggregate
5
+ attr_reader :function, :account, :code, :range, :options, :filter, :currency
6
6
 
7
- def initialize(function, account, code, options)
8
- @function = function.to_s
9
- raise AggregateFunctionNotSupported unless %w[sum count average].include?(@function)
7
+ def initialize(function, account, code, options)
8
+ @function = function.to_s
9
+ fail AggregateFunctionNotSupported unless %w(sum count average).include?(@function)
10
10
 
11
- @account = account
12
- @code = code ? code.to_s : nil
13
- @options = options
14
- @range = options[:range]
15
- @filter = options[:filter]
16
- @currency = DoubleEntry::Account.currency(account)
17
- end
11
+ @account = account
12
+ @code = code ? code.to_s : nil
13
+ @options = options
14
+ @range = options[:range]
15
+ @filter = options[:filter]
16
+ @currency = DoubleEntry::Account.currency(account)
17
+ end
18
18
 
19
- def amount(force_recalculation = false)
20
- if force_recalculation
21
- clear_old_aggregates
22
- calculate
23
- else
24
- retrieve || calculate
19
+ def amount(force_recalculation = false)
20
+ if force_recalculation
21
+ clear_old_aggregates
22
+ calculate
23
+ else
24
+ retrieve || calculate
25
+ end
25
26
  end
26
- end
27
27
 
28
- def formatted_amount(amount = amount)
29
- amount ||= 0
30
- if function == "count"
31
- amount
32
- else
33
- Money.new(amount, currency)
28
+ def formatted_amount(value = amount())
29
+ value ||= 0
30
+ if function == 'count'
31
+ value
32
+ else
33
+ Money.new(value, currency)
34
+ end
34
35
  end
35
- end
36
36
 
37
37
  private
38
38
 
39
- def retrieve
40
- aggregate = LineAggregate.where(field_hash).first
41
- aggregate.amount if aggregate
42
- end
43
-
44
- def clear_old_aggregates
45
- LineAggregate.delete_all(field_hash)
46
- end
47
-
48
- def calculate
49
- if range.class == YearRange
50
- aggregate = calculate_yearly_aggregate
51
- else
52
- aggregate = LineAggregate.aggregate(function, account, code, range, filter)
39
+ def retrieve
40
+ aggregate = LineAggregate.where(field_hash).first
41
+ aggregate.amount if aggregate
53
42
  end
54
43
 
55
- if range_is_complete?
56
- fields = field_hash
57
- fields[:amount] = aggregate || 0
58
- LineAggregate.create! fields
44
+ def clear_old_aggregates
45
+ LineAggregate.delete_all(field_hash)
59
46
  end
60
47
 
61
- aggregate
62
- end
48
+ def calculate
49
+ if range.class == YearRange
50
+ aggregate = calculate_yearly_aggregate
51
+ else
52
+ aggregate = LineAggregate.aggregate(function, account, code, range, filter)
53
+ end
63
54
 
64
- def calculate_yearly_aggregate
65
- # We calculate yearly aggregates by combining monthly aggregates
66
- # otherwise they will get excruciatingly slow to calculate
67
- # as the year progresses. (I am thinking mainly of the 'current' year.)
68
- # Combining monthly aggregates will mean that the figure will be partially memoized
69
- if function == "average"
70
- calculate_yearly_average
71
- else
72
- zero = formatted_amount(0)
73
- result = (1..12).inject(zero) do |total, month|
74
- total += Reporting.aggregate(
75
- function, account, code,
76
- :range => MonthRange.new(:year => range.year, :month => month),
77
- :filter => filter,
78
- )
55
+ if range_is_complete?
56
+ fields = field_hash
57
+ fields[:amount] = aggregate || 0
58
+ LineAggregate.create! fields
79
59
  end
80
- result.is_a?(Money) ? result.cents : result
60
+
61
+ aggregate
81
62
  end
82
- end
83
63
 
84
- def calculate_yearly_average
85
- # need this seperate function, because an average of averages is not the correct average
86
- year_range = YearRange.new(:year => range.year)
87
- sum = Reporting.aggregate(:sum, account, code, :range => year_range, :filter => filter)
88
- count = Reporting.aggregate(:count, account, code, :range => year_range, :filter => filter)
89
- (count == 0) ? 0 : (sum / count).cents
90
- end
64
+ def calculate_yearly_aggregate
65
+ # We calculate yearly aggregates by combining monthly aggregates
66
+ # otherwise they will get excruciatingly slow to calculate
67
+ # as the year progresses. (I am thinking mainly of the 'current' year.)
68
+ # Combining monthly aggregates will mean that the figure will be partially memoized
69
+ if function == 'average'
70
+ calculate_yearly_average
71
+ else
72
+ zero = formatted_amount(0)
73
+ result = (1..12).inject(zero) do |total, month|
74
+ total + Reporting.aggregate(
75
+ function,
76
+ account,
77
+ code,
78
+ :range => MonthRange.new(:year => range.year, :month => month),
79
+ :filter => filter,
80
+ )
81
+ end
82
+ result.is_a?(Money) ? result.cents : result
83
+ end
84
+ end
91
85
 
92
- def range_is_complete?
93
- Time.now > range.finish
94
- end
86
+ def calculate_yearly_average
87
+ # need this seperate function, because an average of averages is not the correct average
88
+ year_range = YearRange.new(:year => range.year)
89
+ sum = Reporting.aggregate(:sum, account, code, :range => year_range, :filter => filter)
90
+ count = Reporting.aggregate(:count, account, code, :range => year_range, :filter => filter)
91
+ (count == 0) ? 0 : (sum / count).cents
92
+ end
95
93
 
96
- def field_hash
97
- {
98
- :function => function,
99
- :account => account,
100
- :code => code,
101
- :year => range.year,
102
- :month => range.month,
103
- :week => range.week,
104
- :day => range.day,
105
- :hour => range.hour,
106
- :filter => filter.inspect,
107
- :range_type => range.range_type.to_s
108
- }
94
+ def range_is_complete?
95
+ Time.now > range.finish
96
+ end
97
+
98
+ def field_hash
99
+ {
100
+ :function => function,
101
+ :account => account,
102
+ :code => code,
103
+ :year => range.year,
104
+ :month => range.month,
105
+ :week => range.week,
106
+ :day => range.day,
107
+ :hour => range.hour,
108
+ :filter => filter.inspect,
109
+ :range_type => range.range_type.to_s,
110
+ }
111
+ end
109
112
  end
110
113
  end
111
- end
112
114
  end