double_entry 0.10.0 → 0.10.1

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