double_entry 0.0.1.pre → 0.1.0.pre.pre.alpha
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.
- checksums.yaml +13 -5
- data/.gitignore +5 -6
- data/.rspec +1 -0
- data/.travis.yml +19 -0
- data/.yardopts +2 -0
- data/Gemfile +0 -1
- data/LICENSE.md +19 -0
- data/README.md +221 -14
- data/Rakefile +12 -0
- data/double_entry.gemspec +30 -15
- data/gemfiles/Gemfile.rails-3.2.0 +5 -0
- data/gemfiles/Gemfile.rails-4.0.0 +5 -0
- data/gemfiles/Gemfile.rails-4.1.0 +5 -0
- data/lib/active_record/locking_extensions.rb +61 -0
- data/lib/double_entry.rb +267 -2
- data/lib/double_entry/account.rb +82 -0
- data/lib/double_entry/account_balance.rb +31 -0
- data/lib/double_entry/aggregate.rb +118 -0
- data/lib/double_entry/aggregate_array.rb +65 -0
- data/lib/double_entry/configurable.rb +52 -0
- data/lib/double_entry/day_range.rb +38 -0
- data/lib/double_entry/hour_range.rb +40 -0
- data/lib/double_entry/line.rb +147 -0
- data/lib/double_entry/line_aggregate.rb +37 -0
- data/lib/double_entry/line_check.rb +118 -0
- data/lib/double_entry/locking.rb +187 -0
- data/lib/double_entry/month_range.rb +92 -0
- data/lib/double_entry/reporting.rb +16 -0
- data/lib/double_entry/time_range.rb +55 -0
- data/lib/double_entry/time_range_array.rb +43 -0
- data/lib/double_entry/transfer.rb +70 -0
- data/lib/double_entry/version.rb +3 -1
- data/lib/double_entry/week_range.rb +99 -0
- data/lib/double_entry/year_range.rb +39 -0
- data/lib/generators/double_entry/install/install_generator.rb +22 -0
- data/lib/generators/double_entry/install/templates/migration.rb +68 -0
- data/script/jack_hammer +201 -0
- data/script/setup.sh +8 -0
- data/spec/active_record/locking_extensions_spec.rb +54 -0
- data/spec/double_entry/account_balance_spec.rb +8 -0
- data/spec/double_entry/account_spec.rb +23 -0
- data/spec/double_entry/aggregate_array_spec.rb +75 -0
- data/spec/double_entry/aggregate_spec.rb +168 -0
- data/spec/double_entry/double_entry_spec.rb +391 -0
- data/spec/double_entry/line_aggregate_spec.rb +8 -0
- data/spec/double_entry/line_check_spec.rb +88 -0
- data/spec/double_entry/line_spec.rb +72 -0
- data/spec/double_entry/locking_spec.rb +154 -0
- data/spec/double_entry/month_range_spec.rb +131 -0
- data/spec/double_entry/reporting_spec.rb +25 -0
- data/spec/double_entry/time_range_array_spec.rb +149 -0
- data/spec/double_entry/time_range_spec.rb +43 -0
- data/spec/double_entry/week_range_spec.rb +88 -0
- data/spec/generators/double_entry/install/install_generator_spec.rb +33 -0
- data/spec/spec_helper.rb +47 -0
- data/spec/support/accounts.rb +26 -0
- data/spec/support/blueprints.rb +34 -0
- data/spec/support/database.example.yml +16 -0
- data/spec/support/database.travis.yml +18 -0
- data/spec/support/double_entry_spec_helper.rb +19 -0
- data/spec/support/reporting_configuration.rb +6 -0
- data/spec/support/schema.rb +71 -0
- metadata +277 -18
- data/LICENSE.txt +0 -22
@@ -0,0 +1,65 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module DoubleEntry
|
3
|
+
class AggregateArray < Array
|
4
|
+
# An AggregateArray is awesome
|
5
|
+
# It is useful for making reports
|
6
|
+
# It is basically an array of aggregate results,
|
7
|
+
# representing a column of data in a report.
|
8
|
+
#
|
9
|
+
# For example, you could request all sales
|
10
|
+
# broken down by month and it would return an array of values
|
11
|
+
attr_reader :function, :account, :code, :filter, :range_type, :start, :finish
|
12
|
+
|
13
|
+
def initialize(function, account, code, options)
|
14
|
+
@function = function
|
15
|
+
@account = account
|
16
|
+
@code = code
|
17
|
+
@filter = options[:filter]
|
18
|
+
@range_type = options[:range_type]
|
19
|
+
@start = options[:start]
|
20
|
+
@finish = options[:finish]
|
21
|
+
|
22
|
+
retrieve_aggregates
|
23
|
+
fill_in_missing_aggregates
|
24
|
+
populate_self
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def populate_self
|
30
|
+
all_periods.each do |period|
|
31
|
+
self << @aggregates[period.key]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def fill_in_missing_aggregates
|
36
|
+
# some aggregates may not have been previously calculated, so we can request them now
|
37
|
+
# (this includes aggregates for the still-running period)
|
38
|
+
all_periods.each do |period|
|
39
|
+
unless @aggregates[period.key]
|
40
|
+
@aggregates[period.key] = DoubleEntry.aggregate(function, account, code, :filter => filter, :range => period)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# get any previously calculated aggregates
|
46
|
+
def retrieve_aggregates
|
47
|
+
raise ArgumentError.new("Invalid range type '#{range_type}'") unless %w(year month week day hour).include? range_type
|
48
|
+
@aggregates = LineAggregate.
|
49
|
+
where(:function => function.to_s).
|
50
|
+
where(:range_type => 'normal').
|
51
|
+
where(:account => account.to_s).
|
52
|
+
where(:code => code.to_s).
|
53
|
+
where(:filter => filter.inspect).
|
54
|
+
where(LineAggregate.arel_table[range_type].not_eq(nil)).
|
55
|
+
inject({}) do |hash, result|
|
56
|
+
hash[result.key] = Aggregate.formatted_amount(function, result.amount)
|
57
|
+
hash
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def all_periods
|
62
|
+
TimeRangeArray.make(range_type, start, finish)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module DoubleEntry
|
3
|
+
|
4
|
+
# Make configuring a module or a class simple.
|
5
|
+
#
|
6
|
+
# class MyClass
|
7
|
+
# include Configurable
|
8
|
+
#
|
9
|
+
# class Configuration
|
10
|
+
# attr_accessor :my_config_option
|
11
|
+
#
|
12
|
+
# def initialize #:nodoc:
|
13
|
+
# @my_config_option = "default value"
|
14
|
+
# end
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# Then in an initializer (or environments/*.rb) do:
|
19
|
+
#
|
20
|
+
# MyClass.configure do |config|
|
21
|
+
# config.my_config_option = "custom value"
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# And inside methods in your class you can access your config:
|
25
|
+
#
|
26
|
+
# class MyClass
|
27
|
+
# def my_method
|
28
|
+
# puts configuration.my_config_option
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# This is all based on this article:
|
33
|
+
#
|
34
|
+
# http://robots.thoughtbot.com/post/344833329/mygem-configure-block
|
35
|
+
#
|
36
|
+
module Configurable
|
37
|
+
def self.included(base) #:nodoc:
|
38
|
+
base.extend(ClassMethods)
|
39
|
+
end
|
40
|
+
|
41
|
+
module ClassMethods #:nodoc:
|
42
|
+
def configuration
|
43
|
+
@configuration ||= self::Configuration.new
|
44
|
+
end
|
45
|
+
|
46
|
+
def configure
|
47
|
+
yield(configuration)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module DoubleEntry
|
3
|
+
class DayRange < TimeRange
|
4
|
+
attr_reader :year, :week, :day
|
5
|
+
|
6
|
+
def initialize(options)
|
7
|
+
super options
|
8
|
+
|
9
|
+
@week = options[:week]
|
10
|
+
@day = options[:day]
|
11
|
+
week_range = WeekRange.new(options)
|
12
|
+
|
13
|
+
@start = week_range.start + (options[:day] - 1).days
|
14
|
+
@finish = @start.end_of_day
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.from_time(time)
|
18
|
+
week_range = WeekRange.from_time(time)
|
19
|
+
DayRange.new(:year => week_range.year, :week => week_range.week, :day => time.wday == 0 ? 7 : time.wday)
|
20
|
+
end
|
21
|
+
|
22
|
+
def previous
|
23
|
+
DayRange.from_time(@start - 1.day)
|
24
|
+
end
|
25
|
+
|
26
|
+
def next
|
27
|
+
DayRange.from_time(@start + 1.day)
|
28
|
+
end
|
29
|
+
|
30
|
+
def ==(other)
|
31
|
+
(self.week == other.week) and (self.year == other.year) and (self.day == other.day)
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_s
|
35
|
+
start.strftime('%Y, %a %b %d')
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module DoubleEntry
|
3
|
+
class HourRange < TimeRange
|
4
|
+
attr_reader :year, :week, :day, :hour
|
5
|
+
|
6
|
+
def initialize(options)
|
7
|
+
super options
|
8
|
+
|
9
|
+
@week = options[:week]
|
10
|
+
@day = options[:day]
|
11
|
+
@hour = options[:hour]
|
12
|
+
|
13
|
+
day_range = DayRange.new(options)
|
14
|
+
|
15
|
+
@start = day_range.start + options[:hour].hours
|
16
|
+
@finish = @start.end_of_hour
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.from_time(time)
|
20
|
+
day = DayRange.from_time(time)
|
21
|
+
HourRange.new :year => day.year, :week => day.week, :day => day.day, :hour => time.hour
|
22
|
+
end
|
23
|
+
|
24
|
+
def previous
|
25
|
+
HourRange.from_time(@start - 1.hour)
|
26
|
+
end
|
27
|
+
|
28
|
+
def next
|
29
|
+
HourRange.from_time(@start + 1.hour)
|
30
|
+
end
|
31
|
+
|
32
|
+
def ==(other)
|
33
|
+
(self.week == other.week) and (self.year == other.year) and (self.day == other.day) and (self.hour == other.hour)
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_s
|
37
|
+
"#{start.hour}:00:00 - #{start.hour}:59:59"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module DoubleEntry
|
4
|
+
|
5
|
+
# This is the table to end all tables!
|
6
|
+
#
|
7
|
+
# Every financial transaction gets two entries in here: one for the source
|
8
|
+
# account, and one for the destination account. Normal double-entry
|
9
|
+
# accounting principles are followed.
|
10
|
+
#
|
11
|
+
# This is a log table, and should (ideally) never be updated.
|
12
|
+
#
|
13
|
+
# ## Indexes
|
14
|
+
#
|
15
|
+
# The indexes on this table are carefully chosen, as it's both big and heavily loaded.
|
16
|
+
#
|
17
|
+
# ### lines_scope_account_id_idx
|
18
|
+
#
|
19
|
+
# ```sql
|
20
|
+
# ADD INDEX `lines_scope_account_id_idx` (scope, account, id)
|
21
|
+
# ```
|
22
|
+
#
|
23
|
+
# This is the important one. It's used primarily for querying the current
|
24
|
+
# balance of an account. eg:
|
25
|
+
#
|
26
|
+
# ```sql
|
27
|
+
# SELECT * FROM `lines` WHERE scope = ? AND account = ? ORDER BY id DESC LIMIT 1
|
28
|
+
# ```
|
29
|
+
#
|
30
|
+
# ### lines_scope_account_created_at_idx
|
31
|
+
#
|
32
|
+
# ```sql
|
33
|
+
# ADD INDEX `lines_scope_account_created_at_idx` (scope, account, created_at)
|
34
|
+
# ```
|
35
|
+
#
|
36
|
+
# Used for querying historic balances:
|
37
|
+
#
|
38
|
+
# ```sql
|
39
|
+
# SELECT * FROM `lines` WHERE scope = ? AND account = ? AND created_at < ? ORDER BY id DESC LIMIT 1
|
40
|
+
# ```
|
41
|
+
#
|
42
|
+
# And for reporting on account changes over a time period:
|
43
|
+
#
|
44
|
+
# ```sql
|
45
|
+
# SELECT SUM(amount) FROM `lines` WHERE scope = ? AND account = ? AND created_at BETWEEN ? AND ?
|
46
|
+
# ```
|
47
|
+
#
|
48
|
+
# ### lines_account_created_at_idx and lines_account_code_created_at_idx
|
49
|
+
#
|
50
|
+
# ```sql
|
51
|
+
# ADD INDEX `lines_account_created_at_idx` (account, created_at);
|
52
|
+
# ADD INDEX `lines_account_code_created_at_idx` (account, code, created_at);
|
53
|
+
# ```
|
54
|
+
#
|
55
|
+
# These two are used for generating reports, which need to sum things
|
56
|
+
# by account, or account and code, over a particular period.
|
57
|
+
#
|
58
|
+
class Line < ActiveRecord::Base
|
59
|
+
extend EncapsulateAsMoney
|
60
|
+
|
61
|
+
belongs_to :detail, :polymorphic => true
|
62
|
+
before_save :check_balance_will_not_be_sent_negative
|
63
|
+
|
64
|
+
encapsulate_as_money :amount, :balance
|
65
|
+
|
66
|
+
def code=(code)
|
67
|
+
self[:code] = code.try(:to_s)
|
68
|
+
code
|
69
|
+
end
|
70
|
+
|
71
|
+
def code
|
72
|
+
self[:code].try(:to_sym)
|
73
|
+
end
|
74
|
+
|
75
|
+
def meta=(meta)
|
76
|
+
self[:meta] = Marshal.dump(meta)
|
77
|
+
meta
|
78
|
+
end
|
79
|
+
|
80
|
+
def meta
|
81
|
+
meta = self[:meta]
|
82
|
+
meta ? Marshal.load(meta) : {}
|
83
|
+
end
|
84
|
+
|
85
|
+
def account=(account)
|
86
|
+
self[:account] = account.identifier.to_s
|
87
|
+
self.scope = account.scope_identity
|
88
|
+
account
|
89
|
+
end
|
90
|
+
|
91
|
+
def account
|
92
|
+
DoubleEntry.account(self[:account].to_sym, :scope => scope)
|
93
|
+
end
|
94
|
+
|
95
|
+
def partner_account=(partner_account)
|
96
|
+
self[:partner_account] = partner_account.identifier.to_s
|
97
|
+
self.partner_scope = partner_account.scope_identity
|
98
|
+
partner_account
|
99
|
+
end
|
100
|
+
|
101
|
+
def partner_account
|
102
|
+
DoubleEntry.account(self[:partner_account].to_sym, :scope => partner_scope)
|
103
|
+
end
|
104
|
+
|
105
|
+
def partner
|
106
|
+
self.class.find(partner_id)
|
107
|
+
end
|
108
|
+
|
109
|
+
def pair
|
110
|
+
if credit?
|
111
|
+
[self, partner]
|
112
|
+
else
|
113
|
+
[partner, self]
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def credit?
|
118
|
+
amount < Money.empty
|
119
|
+
end
|
120
|
+
|
121
|
+
def debit?
|
122
|
+
amount > Money.empty
|
123
|
+
end
|
124
|
+
|
125
|
+
def description
|
126
|
+
DoubleEntry.describe(self)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Query out just the id and created_at fields for lines, without
|
130
|
+
# instantiating any ActiveRecord objects.
|
131
|
+
def self.find_id_and_created_at(options)
|
132
|
+
connection.select_rows <<-SQL
|
133
|
+
SELECT id, created_at FROM #{Line.quoted_table_name} #{options[:joins]}
|
134
|
+
WHERE #{sanitize_sql_for_conditions(options[:conditions])}
|
135
|
+
SQL
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
def check_balance_will_not_be_sent_negative
|
141
|
+
if self.account.positive_only and self.balance < Money.new(0)
|
142
|
+
raise AccountWouldBeSentNegative.new(account)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module DoubleEntry
|
3
|
+
class LineAggregate < ActiveRecord::Base
|
4
|
+
extend EncapsulateAsMoney
|
5
|
+
|
6
|
+
def self.aggregate(function, account, code, scope, range, named_scopes)
|
7
|
+
collection = aggregate_collection(named_scopes)
|
8
|
+
collection = collection.where(:account => account)
|
9
|
+
collection = collection.where(:created_at => range.start..range.finish)
|
10
|
+
collection = collection.where(:code => code) if code
|
11
|
+
collection.send(function, :amount)
|
12
|
+
end
|
13
|
+
|
14
|
+
# a lot of the trickier reports will use filters defined
|
15
|
+
# in named_scopes to bring in data from other tables.
|
16
|
+
def self.aggregate_collection(named_scopes)
|
17
|
+
if named_scopes
|
18
|
+
collection = Line
|
19
|
+
named_scopes.each do |named_scope|
|
20
|
+
if named_scope.is_a?(Hash)
|
21
|
+
method_name = named_scope.keys[0]
|
22
|
+
collection = collection.send(method_name, named_scope[method_name])
|
23
|
+
else
|
24
|
+
collection = collection.send(named_scope)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
collection
|
28
|
+
else
|
29
|
+
Line
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def key
|
34
|
+
"#{year}:#{month}:#{week}:#{day}:#{hour}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
module DoubleEntry
|
5
|
+
class LineCheck < ActiveRecord::Base
|
6
|
+
extend EncapsulateAsMoney
|
7
|
+
|
8
|
+
default_scope -> { order('created_at') }
|
9
|
+
|
10
|
+
def self.perform!
|
11
|
+
new.perform
|
12
|
+
end
|
13
|
+
|
14
|
+
def perform
|
15
|
+
log = ''
|
16
|
+
current_line_id = nil
|
17
|
+
|
18
|
+
active_accounts = Set.new
|
19
|
+
incorrect_accounts = Set.new
|
20
|
+
|
21
|
+
new_lines_since_last_run.find_each do |line|
|
22
|
+
incorrect_accounts << line.account unless running_balance_correct?(line, log)
|
23
|
+
active_accounts << line.account
|
24
|
+
current_line_id = line.id
|
25
|
+
end
|
26
|
+
|
27
|
+
active_accounts.each do |account|
|
28
|
+
incorrect_accounts << account unless cached_balance_correct?(account)
|
29
|
+
end
|
30
|
+
|
31
|
+
incorrect_accounts.each { |account| recalculate_account(account) }
|
32
|
+
|
33
|
+
unless active_accounts.empty?
|
34
|
+
LineCheck.create!(
|
35
|
+
:errors_found => incorrect_accounts.any?,
|
36
|
+
:last_line_id => current_line_id,
|
37
|
+
:log => log,
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def last_run_line_id
|
45
|
+
latest = LineCheck.last
|
46
|
+
latest ? latest.last_line_id : 0
|
47
|
+
end
|
48
|
+
|
49
|
+
def new_lines_since_last_run
|
50
|
+
Line.where('id > ?', last_run_line_id)
|
51
|
+
end
|
52
|
+
|
53
|
+
def running_balance_correct?(line, log)
|
54
|
+
# Another work around for the MySQL 5.1 query optimiser bug that causes the ORDER BY
|
55
|
+
# on the query to fail in some circumstances, resulting in an old balance being
|
56
|
+
# returned. This was biting us intermittently in spec runs.
|
57
|
+
# See http://bugs.mysql.com/bug.php?id=51431
|
58
|
+
force_index = if Line.connection.adapter_name.match /mysql/i
|
59
|
+
"FORCE INDEX (lines_scope_account_id_idx)"
|
60
|
+
else
|
61
|
+
""
|
62
|
+
end
|
63
|
+
|
64
|
+
# yes, it needs to be find_by_sql, because any other find will be affected
|
65
|
+
# by the find_each call in perform!
|
66
|
+
previous_line = Line.find_by_sql(["SELECT * FROM #{Line.quoted_table_name} #{force_index} WHERE account = ? AND scope = ? AND id < ? ORDER BY id DESC LIMIT 1", line.account.identifier.to_s, line.scope, line.id])
|
67
|
+
previous_balance = previous_line.length == 1 ? previous_line[0].balance : Money.empty
|
68
|
+
|
69
|
+
if line.balance != (line.amount + previous_balance)
|
70
|
+
log << line_error_message(line, previous_line, previous_balance)
|
71
|
+
end
|
72
|
+
|
73
|
+
line.balance == previous_balance + line.amount
|
74
|
+
end
|
75
|
+
|
76
|
+
def line_error_message(line, previous_line, previous_balance)
|
77
|
+
<<-END_OF_MESSAGE.strip_heredoc
|
78
|
+
*********************************
|
79
|
+
Error on line ##{line.id}: balance:#{line.balance} != #{previous_balance} + #{line.amount}
|
80
|
+
*********************************
|
81
|
+
#{previous_line.inspect}
|
82
|
+
#{line.inspect}
|
83
|
+
|
84
|
+
END_OF_MESSAGE
|
85
|
+
end
|
86
|
+
|
87
|
+
def cached_balance_correct?(account)
|
88
|
+
DoubleEntry.lock_accounts(account) do
|
89
|
+
return AccountBalance.find_by_account(account).balance == account.balance
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def recalculate_account(account)
|
94
|
+
DoubleEntry.lock_accounts(account) do
|
95
|
+
recalculated_balance = Money.empty
|
96
|
+
|
97
|
+
lines_for_account(account).each do |line|
|
98
|
+
recalculated_balance += line.amount
|
99
|
+
line.update_attribute(:balance, recalculated_balance) if line.balance != recalculated_balance
|
100
|
+
end
|
101
|
+
|
102
|
+
update_balance_for_account(account, recalculated_balance)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def lines_for_account(account)
|
107
|
+
Line.where(
|
108
|
+
:account => account.identifier.to_s,
|
109
|
+
:scope => account.scope_identity.to_s
|
110
|
+
).order(:id)
|
111
|
+
end
|
112
|
+
|
113
|
+
def update_balance_for_account(account, balance)
|
114
|
+
account_balance = Locking.balance_for_locked_account(account)
|
115
|
+
account_balance.update_attribute(:balance, balance)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|