Module: DoubleEntry

Defined in:
lib/double_entry.rb,
lib/double_entry/line.rb,
lib/double_entry/account.rb,
lib/double_entry/locking.rb,
lib/double_entry/version.rb,
lib/double_entry/transfer.rb,
lib/double_entry/aggregate.rb,
lib/double_entry/day_range.rb,
lib/double_entry/reporting.rb,
lib/double_entry/hour_range.rb,
lib/double_entry/line_check.rb,
lib/double_entry/time_range.rb,
lib/double_entry/week_range.rb,
lib/double_entry/year_range.rb,
lib/double_entry/month_range.rb,
lib/double_entry/configurable.rb,
lib/double_entry/line_aggregate.rb,
lib/double_entry/account_balance.rb,
lib/double_entry/aggregate_array.rb,
lib/double_entry/time_range_array.rb,
lib/generators/double_entry/install/install_generator.rb

Overview

Keep track of all the monies!

This module provides the public interfaces for everything to do with transferring money around the system.

Defined Under Namespace

Modules: Configurable, Generators, Locking, Reporting Classes: Account, AccountBalance, AccountWouldBeSentNegative, Aggregate, AggregateArray, DayRange, DuplicateAccount, DuplicateTransfer, HourRange, Line, LineAggregate, LineCheck, MonthRange, RequiredMetaMissing, TimeRange, TimeRangeArray, Transfer, TransferIsNegative, TransferNotAllowed, UnknownAccount, UserAccountNotLocked, WeekRange, YearRange

Constant Summary collapse

VERSION =
"0.1.0"

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.accountsObject

Returns the value of attribute accounts.



47
48
49
# File 'lib/double_entry.rb', line 47

def accounts
  @accounts
end

.transfersObject

Returns the value of attribute transfers.



47
48
49
# File 'lib/double_entry.rb', line 47

def transfers
  @transfers
end

Class Method Details

.account(identifier, options = {}) ⇒ DoubleEntry::Account::Instance

Get the particular account instance with the provided identifier and scope.

Examples:

Obtain the 'cash' account for a user

DoubleEntry.(:cash, scope: user)

Parameters:

  • identifier (Symbol)

    The symbol identifying the desired account. As specified in the account configuration.

  • options (Hash) (defaults to: {})

    a customizable set of options

Options Hash (options):

  • :scope (Object)

    Limit the account to the given scope. As specified in the account configuration.

Returns:

Raises:



62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/double_entry.rb', line 62

def (identifier, options = {})
   = @accounts.detect do ||
    .identifier == identifier and
      (options[:scope] ? .scoped? : !.scoped?)
  end

  if 
    DoubleEntry::Account::Instance.new(:account => , :scope => options[:scope])
  else
    raise UnknownAccount.new("account: #{identifier} scope: #{options[:scope]}")
  end
end

.aggregate(function, account, code, options = {}) ⇒ Object



234
235
236
# File 'lib/double_entry.rb', line 234

def aggregate(function, , code, options = {})
  DoubleEntry::Aggregate.new(function, , code, options).formatted_amount
end

.aggregate_array(function, account, code, options = {}) ⇒ Object



238
239
240
# File 'lib/double_entry.rb', line 238

def aggregate_array(function, , code, options = {})
  DoubleEntry::AggregateArray.new(function, , code, options)
end

.balance(account, options = {}) ⇒ Money

Get the current balance of an account, as a Money object.

Parameters:

  • account (DoubleEntry::Account:Instance, Symbol)
  • options (Hash) (defaults to: {})

    a customizable set of options

Options Hash (options):

  • :scope (Symbol)
  • :from (Time)
  • :to (Time)
  • :at (Time)
  • :code (Symbol)
  • :codes (Array<Symbol>)

Returns:

  • (Money)


129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/double_entry.rb', line 129

def balance(, options = {})
  scope_arg = options[:scope] ? options[:scope].id.to_s : nil
  scope = (.is_a?(Symbol) ? scope_arg : .scope_identity)
   = (.is_a?(Symbol) ?  : .identifier).to_s
  from, to, at = options[:from], options[:to], options[:at]
  code, codes = options[:code], options[:codes]

  # time based scoping
  conditions = if at
    # lookup method could use running balance, with a order by limit one clause
    # (unless it's a reporting call, i.e. account == symbol and not an instance)
    ['account = ? and created_at <= ?', , at] # index this??
  elsif from and to
    ['account = ? and created_at >= ? and created_at <= ?', , from, to] # index this??
  else
    # lookup method could use running balance, with a order by limit one clause
    # (unless it's a reporting call, i.e. account == symbol and not an instance)
    ['account = ?', ]
  end

  # code based scoping
  if code
    conditions[0] << ' and code = ?' # index this??
    conditions << code.to_s
  elsif codes
    conditions[0] << ' and code in (?)' # index this??
    conditions << codes.collect { |c| c.to_s }
  end

  # account based scoping
  if scope
    conditions[0] << ' and scope = ?'
    conditions << scope

    # This is to work around a MySQL 5.1 query optimiser bug that causes the ORDER BY
    # on the query to fail in some circumstances, resulting in an old balance being
    # returned. This was biting us intermittently in spec runs.
    # See http://bugs.mysql.com/bug.php?id=51431
    if Line.connection.adapter_name.match /mysql/i
      use_index = "USE INDEX (lines_scope_account_id_idx)"
    end
  end

  if (from and to) or (code or codes)
    # from and to or code lookups have to be done via sum
    Money.new(Line.where(conditions).sum(:amount))
  else
    # all other lookups can be performed with running balances
    line = Line.select("id, balance").from("#{Line.quoted_table_name} #{use_index}").where(conditions).order('id desc').first
    line ? line.balance : Money.empty
  end
end

.describe(line) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



224
225
226
227
228
229
230
231
232
# File 'lib/double_entry.rb', line 224

def describe(line)
  # make sure we have a test for this refactoring, the test
  # conditions are: i forget... but it's important!
  if line.credit?
    @transfers.find(line., line., line.code)
  else
    @transfers.find(line., line., line.code)
  end.description.call(line)
end

.lock_accounts(*accounts) { ... } ⇒ Object

Lock accounts in preparation for transfers.

This creates a transaction, and uses database-level locking to ensure that we're the only ones who can transfer to or from the given accounts for the duration of the transaction.

Examples:

Lock the savings and checking accounts for a user

 = DoubleEntry.(:checking, scope: user)
  = DoubleEntry.(:savings,  scope: user)
DoubleEntry.lock_accounts(, ) do
  # ...
end

Yields:

  • Hold the locks while the provided block is processed.

Raises:



219
220
221
# File 'lib/double_entry.rb', line 219

def lock_accounts(*accounts, &block)
  DoubleEntry::Locking.lock_accounts(*accounts, &block)
end

.reconciled?(account) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This is used by the concurrency test script.

Returns:

  • (Boolean)

    true if all the amounts for an account add up to the final balance, which they always should.



247
248
249
250
251
252
253
# File 'lib/double_entry.rb', line 247

def reconciled?()
  scoped_lines = Line.where(:account => "#{.identifier}", :scope => "#{.scope}")
  sum_of_amounts = scoped_lines.sum(:amount)
  final_balance  = scoped_lines.order(:id).last[:balance]
  cached_balance = AccountBalance.()[:balance]
  final_balance == sum_of_amounts && final_balance == cached_balance
end

.scopes_with_minimum_balance_for_account(minimum_balance, account_identifier) ⇒ Array<Fixnum>

Identify the scopes with the given account identifier holding at least the provided minimum balance.

Examples:

Find users with at lease $1,000,000 in their savings accounts

DoubleEntry.(
  Money.new(1_000_000_00),
  :savings
) # might return user ids: [ 1423, 12232, 34729 ]

Parameters:

  • minimum_balance (Money)

    Minimum account balance a scope must have to be included in the result set.

  • account_identifier (Symbol)

Returns:

  • (Array<Fixnum>)

    Scopes



194
195
196
197
198
199
200
201
# File 'lib/double_entry.rb', line 194

def (minimum_balance, )
  select_values(sanitize_sql_array([<<-SQL, , minimum_balance.cents])).map {|scope| scope.to_i }
    SELECT scope
      FROM #{AccountBalance.table_name}
     WHERE account = ?
       AND balance >= ?
  SQL
end

.table_name_prefixObject



255
256
257
# File 'lib/double_entry.rb', line 255

def table_name_prefix
  'double_entry_'
end

.transfer(amount, options = {}) ⇒ Object

Transfer money from one account to another.

Only certain transfers are allowed. Define legal transfers in your configuration file.

If you're doing more than one transfer in one hit, or you're doing other database operations along with your transfer, you'll need to use the lock_accounts method.

Examples:

Transfer $20 from a user's checking to savings account

 = DoubleEntry.(:checking, scope: user)
  = DoubleEntry.(:savings,  scope: user)
DoubleEntry.transfer(
  Money.new(20_00),
  from: ,
  to:   ,
  code: :save,
)

Parameters:

  • amount (Money)

    The quantity of money to transfer from one account to the other.

  • options (Hash) (defaults to: {})

    a customizable set of options

Options Hash (options):

  • :from (DoubleEntry::Account::Instance)

    Transfer money out of this account.

  • :to (DoubleEntry::Account::Instance)

    Transfer money into this account.

  • :code (Symbol)

    Your application specific code for this type of transfer. As specified in the transfer configuration.

  • :meta (String)

    Metadata to associate with this transfer.

  • :detail (ActiveRecord::Base)

    ActiveRecord model associated (via a polymorphic association) with the transfer.

Raises:



108
109
110
111
112
113
114
115
116
117
# File 'lib/double_entry.rb', line 108

def transfer(amount, options = {})
  raise TransferIsNegative if amount < Money.new(0)
  from, to, code, meta, detail = options[:from], options[:to], options[:code], options[:meta], options[:detail]
  transfer = @transfers.find(from, to, code)
  if transfer
    transfer.process!(amount, from, to, code, meta, detail)
  else
    raise TransferNotAllowed.new([from.identifier, to.identifier, code].inspect)
  end
end