double_entry 0.6.1 → 0.7.0
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 +4 -4
- data/.gitignore +26 -19
- data/.travis.yml +4 -4
- data/Gemfile +0 -1
- data/README.md +16 -3
- data/double_entry.gemspec +4 -5
- data/lib/active_record/locking_extensions.rb +1 -1
- data/lib/double_entry.rb +8 -1
- data/lib/double_entry/account.rb +15 -3
- data/lib/double_entry/account_balance.rb +13 -3
- data/lib/double_entry/balance_calculator.rb +3 -2
- data/lib/double_entry/errors.rb +2 -1
- data/lib/double_entry/line.rb +31 -12
- data/lib/double_entry/locking.rb +0 -1
- data/lib/double_entry/reporting/aggregate.rb +9 -5
- data/lib/double_entry/reporting/aggregate_array.rb +5 -1
- data/lib/double_entry/reporting/line_aggregate.rb +0 -1
- data/lib/double_entry/transfer.rb +5 -3
- data/lib/double_entry/validation/line_check.rb +3 -3
- data/lib/double_entry/version.rb +1 -1
- data/lib/generators/double_entry/install/templates/migration.rb +1 -1
- data/spec/double_entry/account_spec.rb +12 -0
- data/spec/double_entry/balance_calculator_spec.rb +4 -21
- data/spec/double_entry/line_spec.rb +49 -38
- data/spec/double_entry/locking_spec.rb +2 -2
- data/spec/double_entry/reporting/aggregate_array_spec.rb +18 -3
- data/spec/double_entry/reporting/aggregate_spec.rb +13 -0
- data/spec/double_entry/validation/line_check_spec.rb +16 -0
- data/spec/double_entry_spec.rb +40 -10
- data/spec/spec_helper.rb +14 -8
- data/spec/support/accounts.rb +9 -5
- data/spec/support/blueprints.rb +9 -0
- data/spec/support/database.example.yml +1 -1
- data/spec/support/database.travis.yml +1 -1
- data/spec/support/double_entry_spec_helper.rb +8 -0
- data/{gemfiles/Gemfile.rails-3.2.0 → spec/support/gemfiles/Gemfile.rails-3.2.x} +2 -2
- data/spec/support/gemfiles/Gemfile.rails-4.0.x +5 -0
- data/spec/support/gemfiles/Gemfile.rails-4.1.x +5 -0
- metadata +16 -28
- data/db/.gitkeep +0 -0
- data/gemfiles/Gemfile.rails-4.0.0 +0 -5
- data/gemfiles/Gemfile.rails-4.1.0 +0 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 05aef36c2326f83e2a5e81ef732b88eb375d40db
|
|
4
|
+
data.tar.gz: 14e53582a9aaa42cfb5acbff3273d036d29f7c5b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6affae6cedb93232bd40af21f8e2d9386e90014aaed30bfa41d4bf0ff3117d16879e21afc07eaea78e5308e7ccb5ae4b12d6d7e27a7854613a3df21df39bf267
|
|
7
|
+
data.tar.gz: 8176ff4543e6ffaad668ef63fe2862e472ebb7a160971f6d6749446ed350e334ebe309536e20e2c315b57a2849a17a21fb6a0c931ae93a0f068b40416fb63a17
|
data/.gitignore
CHANGED
|
@@ -1,22 +1,29 @@
|
|
|
1
1
|
*.gem
|
|
2
2
|
*.rbc
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
log/
|
|
3
|
+
/.config
|
|
4
|
+
/coverage/
|
|
5
|
+
/InstalledFiles
|
|
6
|
+
/pkg/
|
|
7
|
+
/spec/reports/
|
|
8
|
+
/test/tmp/
|
|
9
|
+
/test/version_tmp/
|
|
10
|
+
/tmp/
|
|
11
|
+
|
|
12
|
+
## Documentation cache and generated files:
|
|
13
|
+
/.yardoc/
|
|
14
|
+
/_yardoc/
|
|
15
|
+
/doc/
|
|
16
|
+
/rdoc/
|
|
17
|
+
|
|
18
|
+
## Environment normalisation:
|
|
19
|
+
/.bundle/
|
|
20
|
+
/lib/bundler/man/
|
|
22
21
|
/Gemfile.lock
|
|
22
|
+
/.ruby-version
|
|
23
|
+
/.ruby-gemset
|
|
24
|
+
|
|
25
|
+
# DoubleEntry specific
|
|
26
|
+
/bin/
|
|
27
|
+
/log/
|
|
28
|
+
/spec/reports/
|
|
29
|
+
/spec/support/database.yml
|
data/.travis.yml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
language: ruby
|
|
2
2
|
rvm:
|
|
3
|
-
- 2.1.
|
|
3
|
+
- 2.1.4
|
|
4
4
|
- 2.0.0
|
|
5
5
|
- 1.9.3
|
|
6
6
|
env:
|
|
@@ -15,6 +15,6 @@ script:
|
|
|
15
15
|
- rake spec
|
|
16
16
|
- ruby script/jack_hammer -t 2000
|
|
17
17
|
gemfile:
|
|
18
|
-
- gemfiles/Gemfile.rails-3.2.
|
|
19
|
-
- gemfiles/Gemfile.rails-4.0.
|
|
20
|
-
- gemfiles/Gemfile.rails-4.1.
|
|
18
|
+
- spec/support/gemfiles/Gemfile.rails-3.2.x
|
|
19
|
+
- spec/support/gemfiles/Gemfile.rails-4.0.x
|
|
20
|
+
- spec/support/gemfiles/Gemfile.rails-4.1.x
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -26,6 +26,7 @@ Rails Versions: Rails 3.2.x, 4.0.x, 4.1.x
|
|
|
26
26
|
**Databases Supported:**
|
|
27
27
|
* MySQL
|
|
28
28
|
* PostgreSQL
|
|
29
|
+
* SQLite
|
|
29
30
|
|
|
30
31
|
## Installation
|
|
31
32
|
|
|
@@ -124,7 +125,7 @@ manually lock the accounts you're using:
|
|
|
124
125
|
```ruby
|
|
125
126
|
DoubleEntry.lock_accounts(account_a, account_b) do
|
|
126
127
|
# Perhaps transfer some money
|
|
127
|
-
DoubleEntry.transfer(
|
|
128
|
+
DoubleEntry.transfer(Money.new(20_00), :from => account_a, :to => account_b, :code => :purchase)
|
|
128
129
|
# Perform other tasks that should be commited atomically with the transfer of funds...
|
|
129
130
|
end
|
|
130
131
|
```
|
|
@@ -181,6 +182,19 @@ DoubleEntry.configure do |config|
|
|
|
181
182
|
end
|
|
182
183
|
```
|
|
183
184
|
|
|
185
|
+
By default an account's currency is the same as Money.default_currency from the money gem.
|
|
186
|
+
|
|
187
|
+
You can also specify a currency on a per account basis.
|
|
188
|
+
Transfers between accounts of different currencies are not allowed.
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
DoubleEntry.configure do |config|
|
|
192
|
+
config.define_accounts do |accounts|
|
|
193
|
+
accounts.define(:identifier => :savings, :scope_identifier => user_scope, :currency => :aud)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
```
|
|
197
|
+
|
|
184
198
|
## Jackhammer
|
|
185
199
|
|
|
186
200
|
Run a concurrency test on the code.
|
|
@@ -240,7 +254,7 @@ See the Github project [issues](https://github.com/envato/double_entry/issues).
|
|
|
240
254
|
./script/setup.sh
|
|
241
255
|
```
|
|
242
256
|
|
|
243
|
-
3. Install MySQL and
|
|
257
|
+
3. Install MySQL, PostgreSQL and SQLite. We run tests against all three databases.
|
|
244
258
|
4. Create a database in MySQL.
|
|
245
259
|
|
|
246
260
|
```sh
|
|
@@ -265,4 +279,3 @@ See the Github project [issues](https://github.com/envato/double_entry/issues).
|
|
|
265
279
|
```sh
|
|
266
280
|
bundle exec rake
|
|
267
281
|
```
|
|
268
|
-
|
data/double_entry.gemspec
CHANGED
|
@@ -19,11 +19,10 @@ Gem::Specification.new do |gem|
|
|
|
19
19
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
|
20
20
|
gem.require_paths = ['lib']
|
|
21
21
|
|
|
22
|
-
gem.add_dependency 'money', '>=
|
|
23
|
-
gem.add_dependency '
|
|
24
|
-
gem.add_dependency '
|
|
25
|
-
gem.add_dependency '
|
|
26
|
-
gem.add_dependency 'railties', '>= 3.0.0'
|
|
22
|
+
gem.add_dependency 'money', '>= 6.0.0'
|
|
23
|
+
gem.add_dependency 'activerecord', '>= 3.2.0'
|
|
24
|
+
gem.add_dependency 'activesupport', '>= 3.2.0'
|
|
25
|
+
gem.add_dependency 'railties', '>= 3.2.0'
|
|
27
26
|
|
|
28
27
|
gem.add_development_dependency 'rake'
|
|
29
28
|
gem.add_development_dependency 'mysql2'
|
|
@@ -48,7 +48,7 @@ module ActiveRecord
|
|
|
48
48
|
begin
|
|
49
49
|
yield
|
|
50
50
|
rescue ActiveRecord::StatementInvalid, ActiveRecord::RecordNotUnique => exception
|
|
51
|
-
if exception.message =~ /duplicate/i || exception.message =~ /
|
|
51
|
+
if exception.message =~ /duplicate/i || exception.message =~ /ConstraintException/
|
|
52
52
|
# Just ignore it...someone else has already created the record.
|
|
53
53
|
else
|
|
54
54
|
raise
|
data/lib/double_entry.rb
CHANGED
|
@@ -3,7 +3,6 @@ require 'active_record'
|
|
|
3
3
|
require 'active_record/locking_extensions'
|
|
4
4
|
require 'active_support/all'
|
|
5
5
|
require 'money'
|
|
6
|
-
require 'encapsulate_as_money'
|
|
7
6
|
|
|
8
7
|
require 'double_entry/version'
|
|
9
8
|
require 'double_entry/errors'
|
|
@@ -129,6 +128,14 @@ module DoubleEntry
|
|
|
129
128
|
BalanceCalculator.calculate(account, options)
|
|
130
129
|
end
|
|
131
130
|
|
|
131
|
+
# Get the currency of an account.
|
|
132
|
+
#
|
|
133
|
+
# @param [DoubleEntry::Account:Instance, Symbol] account Find the currency for this account
|
|
134
|
+
# @return [Currency] the currency
|
|
135
|
+
def currency(account)
|
|
136
|
+
Account.currency(configuration.accounts, account)
|
|
137
|
+
end
|
|
138
|
+
|
|
132
139
|
# Lock accounts in preparation for transfers.
|
|
133
140
|
#
|
|
134
141
|
# This creates a transaction, and uses database-level locking to ensure
|
data/lib/double_entry/account.rb
CHANGED
|
@@ -8,6 +8,17 @@ module DoubleEntry
|
|
|
8
8
|
DoubleEntry::Account::Instance.new(:account => account, :scope => options[:scope])
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
+
# @api private
|
|
12
|
+
def self.currency(defined_accounts, account)
|
|
13
|
+
code = account.is_a?(Symbol) ? account : account.identifier
|
|
14
|
+
|
|
15
|
+
found_account = defined_accounts.detect do |account|
|
|
16
|
+
account.identifier == code
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
found_account.currency
|
|
20
|
+
end
|
|
21
|
+
|
|
11
22
|
# @api private
|
|
12
23
|
class Set < Array
|
|
13
24
|
def define(attributes)
|
|
@@ -47,7 +58,7 @@ module DoubleEntry
|
|
|
47
58
|
|
|
48
59
|
class Instance
|
|
49
60
|
attr_accessor :account, :scope
|
|
50
|
-
delegate :identifier, :scope_identifier, :scoped?, :positive_only, :to => :account
|
|
61
|
+
delegate :identifier, :scope_identifier, :scoped?, :positive_only, :currency, :to => :account
|
|
51
62
|
|
|
52
63
|
def initialize(attributes)
|
|
53
64
|
attributes.each { |name, value| send("#{name}=", value) }
|
|
@@ -93,7 +104,7 @@ module DoubleEntry
|
|
|
93
104
|
end
|
|
94
105
|
|
|
95
106
|
def to_s
|
|
96
|
-
"\#{Account account: #{identifier} scope: #{scope}}"
|
|
107
|
+
"\#{Account account: #{identifier} scope: #{scope} currency: #{currency}}"
|
|
97
108
|
end
|
|
98
109
|
|
|
99
110
|
def inspect
|
|
@@ -101,10 +112,11 @@ module DoubleEntry
|
|
|
101
112
|
end
|
|
102
113
|
end
|
|
103
114
|
|
|
104
|
-
attr_accessor :identifier, :scope_identifier, :positive_only
|
|
115
|
+
attr_accessor :identifier, :scope_identifier, :positive_only, :currency
|
|
105
116
|
|
|
106
117
|
def initialize(attributes)
|
|
107
118
|
attributes.each { |name, value| send("#{name}=", value) }
|
|
119
|
+
self.currency ||= Money.default_currency
|
|
108
120
|
end
|
|
109
121
|
|
|
110
122
|
def scoped?
|
|
@@ -9,9 +9,16 @@ module DoubleEntry
|
|
|
9
9
|
#
|
|
10
10
|
# Account balances are created on demand when transfers occur.
|
|
11
11
|
class AccountBalance < ActiveRecord::Base
|
|
12
|
-
extend EncapsulateAsMoney
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
delegate :currency, :to => :account
|
|
14
|
+
|
|
15
|
+
def balance
|
|
16
|
+
self[:balance] && Money.new(self[:balance], currency)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def balance=(money)
|
|
20
|
+
self[:balance] = (money && money.fractional)
|
|
21
|
+
end
|
|
15
22
|
|
|
16
23
|
def account=(account)
|
|
17
24
|
self[:account] = account.identifier.to_s
|
|
@@ -19,6 +26,10 @@ module DoubleEntry
|
|
|
19
26
|
account
|
|
20
27
|
end
|
|
21
28
|
|
|
29
|
+
def account
|
|
30
|
+
DoubleEntry.account(self[:account].to_sym, :scope => self[:scope])
|
|
31
|
+
end
|
|
32
|
+
|
|
22
33
|
def self.find_by_account(account, options = {})
|
|
23
34
|
scope = where(:scope => account.scope_identity, :account => account.identifier.to_s)
|
|
24
35
|
scope = scope.lock(true) if options[:lock]
|
|
@@ -28,4 +39,3 @@ module DoubleEntry
|
|
|
28
39
|
end
|
|
29
40
|
|
|
30
41
|
end
|
|
31
|
-
|
|
@@ -18,10 +18,11 @@ module DoubleEntry
|
|
|
18
18
|
options = Options.new(account, args)
|
|
19
19
|
relations = RelationBuilder.new(options)
|
|
20
20
|
lines = relations.build
|
|
21
|
+
currency = DoubleEntry.currency(account)
|
|
21
22
|
|
|
22
23
|
if options.between? || options.code?
|
|
23
24
|
# from and to or code lookups have to be done via sum
|
|
24
|
-
Money.new(lines.sum(:amount))
|
|
25
|
+
Money.new(lines.sum(:amount), currency)
|
|
25
26
|
else
|
|
26
27
|
# all other lookups can be performed with running balances
|
|
27
28
|
result = lines.
|
|
@@ -29,7 +30,7 @@ module DoubleEntry
|
|
|
29
30
|
order('id DESC').
|
|
30
31
|
limit(1).
|
|
31
32
|
pluck(:balance)
|
|
32
|
-
result.empty? ? Money.
|
|
33
|
+
result.empty? ? Money.zero(currency) : Money.new(result.first, currency)
|
|
33
34
|
end
|
|
34
35
|
end
|
|
35
36
|
|
data/lib/double_entry/errors.rb
CHANGED
|
@@ -7,5 +7,6 @@ module DoubleEntry
|
|
|
7
7
|
class DuplicateAccount < RuntimeError; end
|
|
8
8
|
class DuplicateTransfer < RuntimeError; end
|
|
9
9
|
class AccountWouldBeSentNegative < RuntimeError; end
|
|
10
|
-
|
|
10
|
+
class MismatchedCurrencies < RuntimeError; end
|
|
11
|
+
class MissingAccountError < RuntimeError; end;
|
|
11
12
|
end
|
data/lib/double_entry/line.rb
CHANGED
|
@@ -56,11 +56,24 @@ module DoubleEntry
|
|
|
56
56
|
# by account, or account and code, over a particular period.
|
|
57
57
|
#
|
|
58
58
|
class Line < ActiveRecord::Base
|
|
59
|
-
extend EncapsulateAsMoney
|
|
60
59
|
|
|
61
60
|
belongs_to :detail, :polymorphic => true
|
|
62
61
|
|
|
63
|
-
|
|
62
|
+
def amount
|
|
63
|
+
self[:amount] && Money.new(self[:amount], currency)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def amount=(money)
|
|
67
|
+
self[:amount] = (money && money.fractional)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def balance
|
|
71
|
+
self[:balance] && Money.new(self[:balance], currency)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def balance=(money)
|
|
75
|
+
self[:balance] = (money && money.fractional)
|
|
76
|
+
end
|
|
64
77
|
|
|
65
78
|
def save(*)
|
|
66
79
|
check_balance_will_not_be_sent_negative
|
|
@@ -81,20 +94,26 @@ module DoubleEntry
|
|
|
81
94
|
self[:code].try(:to_sym)
|
|
82
95
|
end
|
|
83
96
|
|
|
84
|
-
def account=(
|
|
85
|
-
self[:account] =
|
|
86
|
-
self.scope =
|
|
87
|
-
account
|
|
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
|
|
88
102
|
end
|
|
89
103
|
|
|
90
104
|
def account
|
|
91
105
|
DoubleEntry.account(self[:account].to_sym, :scope => scope)
|
|
92
106
|
end
|
|
93
107
|
|
|
94
|
-
def
|
|
95
|
-
self[:
|
|
96
|
-
|
|
97
|
-
|
|
108
|
+
def currency
|
|
109
|
+
account.currency if self[:account]
|
|
110
|
+
end
|
|
111
|
+
|
|
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
|
|
98
117
|
end
|
|
99
118
|
|
|
100
119
|
def partner_account
|
|
@@ -114,11 +133,11 @@ module DoubleEntry
|
|
|
114
133
|
end
|
|
115
134
|
|
|
116
135
|
def decrease?
|
|
117
|
-
amount < Money.
|
|
136
|
+
amount < Money.zero
|
|
118
137
|
end
|
|
119
138
|
|
|
120
139
|
def increase?
|
|
121
|
-
amount > Money.
|
|
140
|
+
amount > Money.zero
|
|
122
141
|
end
|
|
123
142
|
|
|
124
143
|
# Query out just the id and created_at fields for lines, without
|
data/lib/double_entry/locking.rb
CHANGED
|
@@ -162,7 +162,6 @@ module DoubleEntry
|
|
|
162
162
|
@accounts_without_balances.each do |account|
|
|
163
163
|
# Get the initial balance from the lines table.
|
|
164
164
|
balance = account.balance
|
|
165
|
-
|
|
166
165
|
# Try to create the balance record, but ignore it if someone else has done it in the meantime.
|
|
167
166
|
AccountBalance.create_ignoring_duplicates!(:account => account, :balance => balance)
|
|
168
167
|
end
|
|
@@ -8,7 +8,7 @@ module DoubleEntry
|
|
|
8
8
|
@function = function.to_s
|
|
9
9
|
raise AggregateFunctionNotSupported unless %w[sum count average].include?(@function)
|
|
10
10
|
|
|
11
|
-
@account = account
|
|
11
|
+
@account = account
|
|
12
12
|
@code = code ? code.to_s : nil
|
|
13
13
|
@options = options
|
|
14
14
|
@range = options[:range]
|
|
@@ -25,17 +25,17 @@ module DoubleEntry
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def formatted_amount
|
|
28
|
-
Aggregate.formatted_amount(function, amount)
|
|
28
|
+
Aggregate.formatted_amount(function, amount, currency)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
def self.formatted_amount(function, amount)
|
|
31
|
+
def self.formatted_amount(function, amount, currency)
|
|
32
32
|
safe_amount = amount || 0
|
|
33
33
|
|
|
34
34
|
case function.to_s
|
|
35
35
|
when 'count'
|
|
36
36
|
safe_amount
|
|
37
37
|
else
|
|
38
|
-
Money.new(safe_amount)
|
|
38
|
+
Money.new(safe_amount, currency)
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
41
|
|
|
@@ -46,6 +46,10 @@ module DoubleEntry
|
|
|
46
46
|
aggregate.amount if aggregate
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
+
def currency
|
|
50
|
+
DoubleEntry.currency(account)
|
|
51
|
+
end
|
|
52
|
+
|
|
49
53
|
def clear_old_aggregates
|
|
50
54
|
LineAggregate.delete_all(field_hash)
|
|
51
55
|
end
|
|
@@ -75,7 +79,7 @@ module DoubleEntry
|
|
|
75
79
|
when 'average'
|
|
76
80
|
calculate_yearly_average
|
|
77
81
|
else
|
|
78
|
-
zero = Aggregate.formatted_amount(function, 0)
|
|
82
|
+
zero = Aggregate.formatted_amount(function, 0, currency)
|
|
79
83
|
|
|
80
84
|
result = (1..12).inject(zero) do |total, month|
|
|
81
85
|
total += Reporting.aggregate(function, account, code,
|
|
@@ -54,7 +54,7 @@ module DoubleEntry
|
|
|
54
54
|
where(:filter => filter.inspect).
|
|
55
55
|
where(LineAggregate.arel_table[range_type].not_eq(nil)).
|
|
56
56
|
inject({}) do |hash, result|
|
|
57
|
-
hash[result.key] = Aggregate.formatted_amount(function, result.amount)
|
|
57
|
+
hash[result.key] = Aggregate.formatted_amount(function, result.amount, currency)
|
|
58
58
|
hash
|
|
59
59
|
end
|
|
60
60
|
end
|
|
@@ -62,6 +62,10 @@ module DoubleEntry
|
|
|
62
62
|
def all_periods
|
|
63
63
|
TimeRangeArray.make(range_type, start, finish)
|
|
64
64
|
end
|
|
65
|
+
|
|
66
|
+
def currency
|
|
67
|
+
DoubleEntry.currency(account)
|
|
68
|
+
end
|
|
65
69
|
end
|
|
66
70
|
end
|
|
67
71
|
end
|
|
@@ -4,7 +4,7 @@ module DoubleEntry
|
|
|
4
4
|
|
|
5
5
|
# @api private
|
|
6
6
|
def self.transfer(defined_transfers, amount, options = {})
|
|
7
|
-
raise TransferIsNegative if amount < Money.
|
|
7
|
+
raise TransferIsNegative if amount < Money.zero
|
|
8
8
|
from, to, code, detail = options[:from], options[:to], options[:code], options[:detail]
|
|
9
9
|
defined_transfers.
|
|
10
10
|
find!(from, to, code).
|
|
@@ -52,9 +52,11 @@ module DoubleEntry
|
|
|
52
52
|
|
|
53
53
|
def process(amount, from, to, code, detail)
|
|
54
54
|
if from.scope_identity == to.scope_identity and from.identifier == to.identifier
|
|
55
|
-
raise TransferNotAllowed.new
|
|
55
|
+
raise TransferNotAllowed.new("from and to are identical")
|
|
56
|
+
end
|
|
57
|
+
if to.currency != from.currency
|
|
58
|
+
raise MismatchedCurrencies.new("Missmatched currency (#{to.currency} <> #{from.currency})")
|
|
56
59
|
end
|
|
57
|
-
|
|
58
60
|
Locking.lock_accounts(from, to) do
|
|
59
61
|
credit, debit = Line.new, Line.new
|
|
60
62
|
|
|
@@ -4,7 +4,6 @@ require 'set'
|
|
|
4
4
|
module DoubleEntry
|
|
5
5
|
module Validation
|
|
6
6
|
class LineCheck < ActiveRecord::Base
|
|
7
|
-
extend EncapsulateAsMoney
|
|
8
7
|
|
|
9
8
|
default_scope -> { order('created_at') }
|
|
10
9
|
|
|
@@ -65,7 +64,8 @@ module DoubleEntry
|
|
|
65
64
|
# yes, it needs to be find_by_sql, because any other find will be affected
|
|
66
65
|
# by the find_each call in perform!
|
|
67
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])
|
|
68
|
-
|
|
67
|
+
|
|
68
|
+
previous_balance = previous_line.length == 1 ? previous_line[0].balance : Money.zero(line.account.currency)
|
|
69
69
|
|
|
70
70
|
if line.balance != (line.amount + previous_balance)
|
|
71
71
|
log << line_error_message(line, previous_line, previous_balance)
|
|
@@ -93,7 +93,7 @@ module DoubleEntry
|
|
|
93
93
|
|
|
94
94
|
def recalculate_account(account)
|
|
95
95
|
DoubleEntry.lock_accounts(account) do
|
|
96
|
-
recalculated_balance = Money.
|
|
96
|
+
recalculated_balance = Money.zero(account.currency)
|
|
97
97
|
|
|
98
98
|
lines_for_account(account).each do |line|
|
|
99
99
|
recalculated_balance += line.amount
|
data/lib/double_entry/version.rb
CHANGED
|
@@ -41,9 +41,9 @@ class CreateDoubleEntryTables < ActiveRecord::Migration
|
|
|
41
41
|
t.integer "day"
|
|
42
42
|
t.integer "hour"
|
|
43
43
|
t.integer "amount"
|
|
44
|
-
t.timestamps
|
|
45
44
|
t.string "filter"
|
|
46
45
|
t.string "range_type"
|
|
46
|
+
t.timestamps
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
add_index "double_entry_line_aggregates", ["function", "account", "code", "year", "month", "week", "day"], :name => "line_aggregate_idx"
|
|
@@ -20,6 +20,18 @@ module DoubleEntry
|
|
|
20
20
|
expect(a1.hash).to eq a2.hash
|
|
21
21
|
expect(a1.hash).to_not eq b.hash
|
|
22
22
|
end
|
|
23
|
+
|
|
24
|
+
describe "currency" do
|
|
25
|
+
it "defaults to USD currency" do
|
|
26
|
+
account = DoubleEntry::Account.new(:identifier => "savings", :scope_identifier => identity_scope)
|
|
27
|
+
expect(DoubleEntry::Account::Instance.new(:account => account).currency).to eq("USD")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "allows the currency to be set" do
|
|
31
|
+
account = DoubleEntry::Account.new(:identifier => "savings", :scope_identifier => identity_scope, :currency => "AUD")
|
|
32
|
+
expect(DoubleEntry::Account::Instance.new(:account => account).currency).to eq("AUD")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
23
35
|
end
|
|
24
36
|
|
|
25
37
|
describe Account::Set do
|
|
@@ -4,8 +4,8 @@ require 'spec_helper'
|
|
|
4
4
|
describe DoubleEntry::BalanceCalculator do
|
|
5
5
|
|
|
6
6
|
describe '#calculate' do
|
|
7
|
-
let(:account) {
|
|
8
|
-
let(:scope) {
|
|
7
|
+
let(:account) { DoubleEntry::account(:test, :scope => scope) }
|
|
8
|
+
let(:scope) { double(:id => 1) }
|
|
9
9
|
let(:from) { nil }
|
|
10
10
|
let(:to) { nil }
|
|
11
11
|
let(:at) { nil }
|
|
@@ -28,10 +28,10 @@ describe DoubleEntry::BalanceCalculator do
|
|
|
28
28
|
|
|
29
29
|
describe 'what happens with different accounts' do
|
|
30
30
|
context 'when the given account is a symbol' do
|
|
31
|
-
let(:account) { :
|
|
31
|
+
let(:account) { :test }
|
|
32
32
|
|
|
33
33
|
it 'scopes the lines summed by the account symbol' do
|
|
34
|
-
expect(DoubleEntry::Line).to have_received(:where).with(:account => '
|
|
34
|
+
expect(DoubleEntry::Line).to have_received(:where).with(:account => 'test')
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
context 'with a scopeable entity provided' do
|
|
@@ -48,23 +48,6 @@ describe DoubleEntry::BalanceCalculator do
|
|
|
48
48
|
end
|
|
49
49
|
end
|
|
50
50
|
end
|
|
51
|
-
|
|
52
|
-
context 'when the given account is DoubleEntry::Account-like' do
|
|
53
|
-
let(:account) do
|
|
54
|
-
DoubleEntry::Account::Instance.new(
|
|
55
|
-
:account => DoubleEntry::Account.new(
|
|
56
|
-
:identifier => 'account_identity',
|
|
57
|
-
:scope_identifier => lambda { |scope_id| scope_id },
|
|
58
|
-
),
|
|
59
|
-
:scope => 'account_scope_identity'
|
|
60
|
-
)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
it 'scopes the lines summed by the accounts identifier and its scope identity' do
|
|
64
|
-
expect(DoubleEntry::Line).to have_received(:where).with(:account => 'account_identity')
|
|
65
|
-
expect(relation).to have_received(:where).with(:scope => 'account_scope_identity')
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
51
|
end
|
|
69
52
|
|
|
70
53
|
describe 'what happens with different times' do
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# encoding: utf-8
|
|
2
2
|
require "spec_helper"
|
|
3
3
|
describe DoubleEntry::Line do
|
|
4
|
+
it "has a table name prefixed with double_entry_" do
|
|
5
|
+
expect(DoubleEntry::Line.table_name).to eq "double_entry_lines"
|
|
6
|
+
end
|
|
4
7
|
|
|
5
|
-
describe "
|
|
8
|
+
describe "persistance" do
|
|
6
9
|
let(:persisted_line) {
|
|
7
10
|
DoubleEntry::Line.new(
|
|
8
11
|
:amount => Money.new(10_00),
|
|
9
|
-
:balance => Money.
|
|
12
|
+
:balance => Money.zero,
|
|
10
13
|
:account => account,
|
|
11
14
|
:partner_account => partner_account,
|
|
12
15
|
:code => code,
|
|
@@ -15,53 +18,61 @@ describe DoubleEntry::Line do
|
|
|
15
18
|
let(:account) { DoubleEntry.account(:test, :scope => "17") }
|
|
16
19
|
let(:partner_account) { DoubleEntry.account(:test, :scope => "72") }
|
|
17
20
|
let(:code) { :test_code }
|
|
18
|
-
before { persisted_line.save! }
|
|
19
21
|
subject { DoubleEntry::Line.last }
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
its(:code) { should eq :the_code }
|
|
24
|
-
end
|
|
23
|
+
describe "attributes" do
|
|
24
|
+
before { persisted_line.save! }
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
context "given code = :the_code" do
|
|
27
|
+
let(:code) { :the_code }
|
|
28
|
+
its(:code) { should eq :the_code }
|
|
29
|
+
end
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
context "given code = nil" do
|
|
32
|
+
let(:code) { nil }
|
|
33
|
+
its(:code) { should eq nil }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
context "given account = :test, 54 " do
|
|
37
|
+
let(:account) { DoubleEntry.account(:test, :scope => "54") }
|
|
38
|
+
its("account.account.identifier") { should eq :test }
|
|
39
|
+
its("account.scope") { should eq "54" }
|
|
40
|
+
end
|
|
36
41
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
context "given partner_account = :test, 91 " do
|
|
43
|
+
let(:partner_account) { DoubleEntry.account(:test, :scope => "91") }
|
|
44
|
+
its("partner_account.account.identifier") { should eq :test }
|
|
45
|
+
its("partner_account.scope") { should eq "91" }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
context "currency" do
|
|
49
|
+
let(:account) { DoubleEntry.account(:btc_test, :scope => "17") }
|
|
50
|
+
let(:partner_account) { DoubleEntry.account(:btc_test, :scope => "72") }
|
|
51
|
+
its(:currency) { should eq "BTC" }
|
|
52
|
+
end
|
|
41
53
|
end
|
|
42
|
-
end
|
|
43
54
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
describe '#save' do
|
|
56
|
+
context 'when balance is sent negative' do
|
|
57
|
+
let(:account) {
|
|
58
|
+
DoubleEntry.account(:savings, :scope => '17', :positive_only => true)
|
|
59
|
+
}
|
|
49
60
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
let(:line) {
|
|
62
|
+
DoubleEntry::Line.new(
|
|
63
|
+
:balance => Money.new(-1),
|
|
64
|
+
:account => account,
|
|
65
|
+
)
|
|
66
|
+
}
|
|
56
67
|
|
|
57
|
-
|
|
58
|
-
|
|
68
|
+
it 'raises AccountWouldBeSentNegative exception' do
|
|
69
|
+
expect { line.save }.to raise_error DoubleEntry::AccountWouldBeSentNegative
|
|
70
|
+
end
|
|
59
71
|
end
|
|
60
72
|
end
|
|
61
|
-
end
|
|
62
73
|
|
|
63
|
-
|
|
64
|
-
|
|
74
|
+
it "has a table name prefixed with double_entry_" do
|
|
75
|
+
expect(DoubleEntry::Line.table_name).to eq "double_entry_lines"
|
|
76
|
+
end
|
|
65
77
|
end
|
|
66
|
-
|
|
67
78
|
end
|
|
@@ -51,8 +51,8 @@ describe DoubleEntry::Locking do
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
it "takes the balance for new account balance records from the lines table" do
|
|
54
|
-
DoubleEntry::Line.create!(:account => @account_a, :amount => Money.new(3_00), :balance => Money.new( 3_00), :code => :test)
|
|
55
|
-
DoubleEntry::Line.create!(:account => @account_a, :amount => Money.new(7_00), :balance => Money.new(10_00), :code => :test)
|
|
54
|
+
DoubleEntry::Line.create!(:account => @account_a, :partner_account => @account_b, :amount => Money.new(3_00), :balance => Money.new( 3_00), :code => :test)
|
|
55
|
+
DoubleEntry::Line.create!(:account => @account_a, :partner_account => @account_b, :amount => Money.new(7_00), :balance => Money.new(10_00), :code => :test)
|
|
56
56
|
|
|
57
57
|
expect do
|
|
58
58
|
DoubleEntry::Locking.lock_accounts(@account_a) { }
|
|
@@ -8,12 +8,13 @@ module DoubleEntry
|
|
|
8
8
|
let(:finish) { nil }
|
|
9
9
|
let(:range_type) { 'year' }
|
|
10
10
|
let(:function) { :sum }
|
|
11
|
-
|
|
11
|
+
let(:account) { :savings }
|
|
12
|
+
let(:transfer_code) { :bonus }
|
|
12
13
|
subject(:aggregate_array) {
|
|
13
14
|
Reporting.aggregate_array(
|
|
14
15
|
function,
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
account,
|
|
17
|
+
transfer_code,
|
|
17
18
|
:range_type => range_type,
|
|
18
19
|
:start => start,
|
|
19
20
|
:finish => finish,
|
|
@@ -69,6 +70,20 @@ module DoubleEntry
|
|
|
69
70
|
end
|
|
70
71
|
end
|
|
71
72
|
|
|
73
|
+
context 'when account is in BTC currency' do
|
|
74
|
+
let(:account) { :btc_savings }
|
|
75
|
+
let(:range_type) { 'year' }
|
|
76
|
+
let(:start) { "#{Time.now.year}-01-01" }
|
|
77
|
+
let(:transfer_code) { :btc_test_transfer }
|
|
78
|
+
|
|
79
|
+
before do
|
|
80
|
+
perform_btc_deposit(user, 100_000_000)
|
|
81
|
+
perform_btc_deposit(user, 100_000_000)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it { should eq [ Money.new(200_000_000, :btc) ] }
|
|
85
|
+
end
|
|
86
|
+
|
|
72
87
|
context 'when called with range type of "invalid_and_should_not_work"' do
|
|
73
88
|
let(:range_type) { 'invalid_and_should_not_work' }
|
|
74
89
|
it 'raises an argument error' do
|
|
@@ -203,5 +203,18 @@ module DoubleEntry
|
|
|
203
203
|
end
|
|
204
204
|
end
|
|
205
205
|
end
|
|
206
|
+
describe Aggregate, "currencies" do
|
|
207
|
+
let(:user) { User.make! }
|
|
208
|
+
before do
|
|
209
|
+
perform_btc_deposit(user, 100_000_000)
|
|
210
|
+
perform_btc_deposit(user, 200_000_000)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
it 'should calculate the sum in the correct currency' do
|
|
214
|
+
expect(
|
|
215
|
+
Reporting.aggregate(:sum, :btc_savings, :btc_test_transfer, :range => TimeRange.make(:year => Time.now.year))
|
|
216
|
+
).to eq Money.new(300_000_000, :btc)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
206
219
|
end
|
|
207
220
|
end
|
|
@@ -80,6 +80,22 @@ module DoubleEntry::Validation
|
|
|
80
80
|
end
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
+
|
|
84
|
+
context 'Given a user with a non default currency balance' do
|
|
85
|
+
before { User.make!(:bitcoin_balance => Money.new(100_00, 'BTC')) }
|
|
86
|
+
its(:errors_found) { should eq false }
|
|
87
|
+
context 'And there is a consistency error in lines' do
|
|
88
|
+
before { DoubleEntry::Line.order(:id).limit(1).update_all('balance = balance + 1') }
|
|
89
|
+
|
|
90
|
+
its(:errors_found) { should eq true }
|
|
91
|
+
it 'should correct the running balance' do
|
|
92
|
+
expect {
|
|
93
|
+
LineCheck.perform!
|
|
94
|
+
}.to change { DoubleEntry::Line.order(:id).first.balance }.by Money.new(-1, 'BTC')
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
83
99
|
it "has a table name prefixed with double_entry_" do
|
|
84
100
|
expect(LineCheck.table_name).to eq "double_entry_line_checks"
|
|
85
101
|
end
|
data/spec/double_entry_spec.rb
CHANGED
|
@@ -97,17 +97,20 @@ describe DoubleEntry do
|
|
|
97
97
|
accounts.define(:identifier => :savings)
|
|
98
98
|
accounts.define(:identifier => :cash)
|
|
99
99
|
accounts.define(:identifier => :trash)
|
|
100
|
+
accounts.define(:identifier => :bitbucket, :currency => :btc)
|
|
100
101
|
end
|
|
101
102
|
|
|
102
103
|
config.define_transfers do |transfers|
|
|
103
104
|
transfers.define(:from => :savings, :to => :cash, :code => :xfer)
|
|
105
|
+
transfers.define(:from => :trash, :to => :bitbucket, :code => :mismatch_xfer)
|
|
104
106
|
end
|
|
105
107
|
end
|
|
106
108
|
end
|
|
107
109
|
|
|
108
|
-
let(:savings)
|
|
109
|
-
let(:cash)
|
|
110
|
-
let(:trash)
|
|
110
|
+
let(:savings) { DoubleEntry.account(:savings) }
|
|
111
|
+
let(:cash) { DoubleEntry.account(:cash) }
|
|
112
|
+
let(:trash) { DoubleEntry.account(:trash) }
|
|
113
|
+
let(:bitbucket) { DoubleEntry.account(:bitbucket) }
|
|
111
114
|
|
|
112
115
|
it 'can transfer from an account to an account, if the transfer is allowed' do
|
|
113
116
|
DoubleEntry.transfer(
|
|
@@ -149,6 +152,17 @@ describe DoubleEntry do
|
|
|
149
152
|
)
|
|
150
153
|
}.to raise_error DoubleEntry::TransferNotAllowed
|
|
151
154
|
end
|
|
155
|
+
|
|
156
|
+
it 'raises an exception when the transfer is not allowed (mismatched currencies)' do
|
|
157
|
+
expect {
|
|
158
|
+
DoubleEntry.transfer(
|
|
159
|
+
Money.new(100_00),
|
|
160
|
+
:from => trash,
|
|
161
|
+
:to => bitbucket,
|
|
162
|
+
:code => :mismatch_xfer
|
|
163
|
+
)
|
|
164
|
+
}.to raise_error DoubleEntry::MismatchedCurrencies
|
|
165
|
+
end
|
|
152
166
|
end
|
|
153
167
|
|
|
154
168
|
describe 'lines' do
|
|
@@ -219,10 +233,12 @@ describe DoubleEntry do
|
|
|
219
233
|
|
|
220
234
|
describe 'balances' do
|
|
221
235
|
|
|
222
|
-
let(:work)
|
|
223
|
-
let(:savings)
|
|
224
|
-
let(:cash)
|
|
225
|
-
let(:store)
|
|
236
|
+
let(:work) { DoubleEntry.account(:work) }
|
|
237
|
+
let(:savings) { DoubleEntry.account(:savings) }
|
|
238
|
+
let(:cash) { DoubleEntry.account(:cash) }
|
|
239
|
+
let(:store) { DoubleEntry.account(:store) }
|
|
240
|
+
let(:btc_store) { DoubleEntry.account(:btc_store) }
|
|
241
|
+
let(:btc_wallet) { DoubleEntry.account(:btc_wallet) }
|
|
226
242
|
|
|
227
243
|
before do
|
|
228
244
|
DoubleEntry.configure do |config|
|
|
@@ -231,6 +247,8 @@ describe DoubleEntry do
|
|
|
231
247
|
accounts.define(:identifier => :cash)
|
|
232
248
|
accounts.define(:identifier => :savings)
|
|
233
249
|
accounts.define(:identifier => :store)
|
|
250
|
+
accounts.define(:identifier => :btc_store, :currency => 'BTC')
|
|
251
|
+
accounts.define(:identifier => :btc_wallet, :currency => 'BTC')
|
|
234
252
|
end
|
|
235
253
|
|
|
236
254
|
config.define_transfers do |transfers|
|
|
@@ -240,6 +258,7 @@ describe DoubleEntry do
|
|
|
240
258
|
transfers.define(:code => :purchase, :from => :cash, :to => :store)
|
|
241
259
|
transfers.define(:code => :layby, :from => :cash, :to => :store)
|
|
242
260
|
transfers.define(:code => :deposit, :from => :cash, :to => :store)
|
|
261
|
+
transfers.define(:code => :btc_ex, :from => :btc_store, :to => :btc_wallet)
|
|
243
262
|
end
|
|
244
263
|
end
|
|
245
264
|
|
|
@@ -267,7 +286,8 @@ describe DoubleEntry do
|
|
|
267
286
|
end
|
|
268
287
|
|
|
269
288
|
Timecop.freeze 1.week.from_now do
|
|
270
|
-
#
|
|
289
|
+
# it's the future, man
|
|
290
|
+
DoubleEntry.transfer(Money.new(200_00, 'BTC'), :from => btc_store, :code => :btc_ex, :to => btc_wallet)
|
|
271
291
|
end
|
|
272
292
|
end
|
|
273
293
|
|
|
@@ -276,14 +296,19 @@ describe DoubleEntry do
|
|
|
276
296
|
expect(cash.balance).to eq Money.new(100_00)
|
|
277
297
|
expect(savings.balance).to eq Money.new(300_00)
|
|
278
298
|
expect(store.balance).to eq Money.new(600_00)
|
|
299
|
+
expect(btc_wallet.balance).to eq Money.new(200_00, 'BTC')
|
|
279
300
|
end
|
|
280
301
|
|
|
281
302
|
it 'should have correct account balance records' do
|
|
282
|
-
[work, cash, savings, store].each do |account|
|
|
303
|
+
[work, cash, savings, store, btc_wallet].each do |account|
|
|
283
304
|
expect(DoubleEntry::AccountBalance.find_by_account(account).balance).to eq account.balance
|
|
284
305
|
end
|
|
285
306
|
end
|
|
286
307
|
|
|
308
|
+
it 'should have correct account balance currencies' do
|
|
309
|
+
expect(DoubleEntry::AccountBalance.find_by_account(btc_wallet).balance.currency).to eq 'BTC'
|
|
310
|
+
end
|
|
311
|
+
|
|
287
312
|
it 'affects origin/destination balance after transfer' do
|
|
288
313
|
savings_balance = savings.balance
|
|
289
314
|
cash_balance = cash.balance
|
|
@@ -303,6 +328,10 @@ describe DoubleEntry do
|
|
|
303
328
|
expect(cash.balance(:from => 3.weeks.ago, :to => 2.weeks.ago)).to eq Money.new(500_00)
|
|
304
329
|
end
|
|
305
330
|
|
|
331
|
+
it 'can be queried between two points in time, even in the future' do
|
|
332
|
+
expect(btc_wallet.balance(:from => Time.now, :to => 2.weeks.from_now)).to eq Money.new(200_00, 'BTC')
|
|
333
|
+
end
|
|
334
|
+
|
|
306
335
|
it 'can report on balances, scoped by code' do
|
|
307
336
|
expect(cash.balance(:code => :salary)).to eq Money.new(1_000_00)
|
|
308
337
|
end
|
|
@@ -341,6 +370,8 @@ describe DoubleEntry do
|
|
|
341
370
|
end
|
|
342
371
|
|
|
343
372
|
let(:bank) { DoubleEntry.account(:bank) }
|
|
373
|
+
let(:cash) { DoubleEntry.account(:cash) }
|
|
374
|
+
let(:savings) { DoubleEntry.account(:savings) }
|
|
344
375
|
|
|
345
376
|
let(:john) { User.make! }
|
|
346
377
|
let(:johns_cash) { DoubleEntry.account(:cash, :scope => john) }
|
|
@@ -386,5 +417,4 @@ describe DoubleEntry do
|
|
|
386
417
|
expect(DoubleEntry.balance(:savings)).to eq ryans_savings.balance + johns_savings.balance
|
|
387
418
|
end
|
|
388
419
|
end
|
|
389
|
-
|
|
390
420
|
end
|
data/spec/spec_helper.rb
CHANGED
|
@@ -5,11 +5,19 @@ require 'active_support'
|
|
|
5
5
|
|
|
6
6
|
db_engine = ENV['DB'] || 'mysql'
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
FileUtils.mkdir_p 'tmp'
|
|
10
9
|
FileUtils.mkdir_p 'log'
|
|
11
10
|
FileUtils.rm 'log/test.log', :force => true
|
|
12
11
|
|
|
12
|
+
database_config_file = File.expand_path("../support/database.yml", __FILE__)
|
|
13
|
+
if File.exists?(database_config_file)
|
|
14
|
+
ActiveRecord::Base.establish_connection YAML.load_file(database_config_file)[db_engine]
|
|
15
|
+
else
|
|
16
|
+
puts "Please configure your spec/support/database.yml file."
|
|
17
|
+
puts "See spec/support/database.example.yml"
|
|
18
|
+
exit 1
|
|
19
|
+
end
|
|
20
|
+
|
|
13
21
|
# Buffered Logger was deprecated in ActiveSupport 4.0.0 and was removed in 4.1.0
|
|
14
22
|
# Logger was added in ActiveSupport 4.0.0
|
|
15
23
|
if defined? ActiveSupport::Logger
|
|
@@ -33,16 +41,14 @@ RSpec.configure do |config|
|
|
|
33
41
|
config.include DoubleEntrySpecHelper
|
|
34
42
|
|
|
35
43
|
config.before(:suite) do
|
|
36
|
-
DatabaseCleaner.strategy = :
|
|
37
|
-
DatabaseCleaner.clean_with(:deletion)
|
|
44
|
+
DatabaseCleaner.strategy = :truncation
|
|
38
45
|
end
|
|
39
46
|
|
|
40
|
-
config.before
|
|
41
|
-
DatabaseCleaner.
|
|
47
|
+
config.before do
|
|
48
|
+
DatabaseCleaner.clean
|
|
42
49
|
end
|
|
43
50
|
|
|
44
|
-
config.after
|
|
51
|
+
config.after do
|
|
45
52
|
Timecop.return
|
|
46
|
-
DatabaseCleaner.clean
|
|
47
53
|
end
|
|
48
54
|
end
|
data/spec/support/accounts.rb
CHANGED
|
@@ -5,15 +5,19 @@ DoubleEntry.configure do |config|
|
|
|
5
5
|
# A set of accounts to test with
|
|
6
6
|
config.define_accounts do |accounts|
|
|
7
7
|
user_scope = accounts.active_record_scope_identifier(User)
|
|
8
|
-
accounts.define(:identifier => :savings,
|
|
9
|
-
accounts.define(:identifier => :checking,
|
|
10
|
-
accounts.define(:identifier => :test,
|
|
8
|
+
accounts.define(:identifier => :savings, :scope_identifier => user_scope, :positive_only => true)
|
|
9
|
+
accounts.define(:identifier => :checking, :scope_identifier => user_scope, :positive_only => true)
|
|
10
|
+
accounts.define(:identifier => :test, :scope_identifier => user_scope)
|
|
11
|
+
accounts.define(:identifier => :btc_test, :scope_identifier => user_scope, :currency => "BTC")
|
|
12
|
+
accounts.define(:identifier => :btc_savings, :scope_identifier => user_scope, :currency => "BTC")
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
# A set of allowed transfers between accounts
|
|
14
16
|
config.define_transfers do |transfers|
|
|
15
|
-
transfers.define(:from => :test,
|
|
16
|
-
transfers.define(:from => :test,
|
|
17
|
+
transfers.define(:from => :test, :to => :savings, :code => :bonus)
|
|
18
|
+
transfers.define(:from => :test, :to => :checking, :code => :pay)
|
|
19
|
+
transfers.define(:from => :savings, :to => :test, :code => :test_withdrawal)
|
|
20
|
+
transfers.define(:from => :btc_test, :to => :btc_savings, :code => :btc_test_transfer)
|
|
17
21
|
end
|
|
18
22
|
|
|
19
23
|
end
|
data/spec/support/blueprints.rb
CHANGED
|
@@ -2,6 +2,7 @@ class UserBlueprint < Machinist::ActiveRecord::Blueprint
|
|
|
2
2
|
def make!(attributes = {})
|
|
3
3
|
savings_balance = attributes.delete(:savings_balance)
|
|
4
4
|
checking_balance = attributes.delete(:checking_balance)
|
|
5
|
+
bitcoin_balance = attributes.delete(:bitcoin_balance)
|
|
5
6
|
user = super(attributes)
|
|
6
7
|
if savings_balance
|
|
7
8
|
DoubleEntry.transfer(
|
|
@@ -19,6 +20,14 @@ class UserBlueprint < Machinist::ActiveRecord::Blueprint
|
|
|
19
20
|
:code => :pay,
|
|
20
21
|
)
|
|
21
22
|
end
|
|
23
|
+
if bitcoin_balance
|
|
24
|
+
DoubleEntry.transfer(
|
|
25
|
+
bitcoin_balance,
|
|
26
|
+
:from => DoubleEntry.account(:btc_test, :scope => user),
|
|
27
|
+
:to => DoubleEntry.account(:btc_savings, :scope => user),
|
|
28
|
+
:code => :btc_test_transfer,
|
|
29
|
+
)
|
|
30
|
+
end
|
|
22
31
|
user
|
|
23
32
|
end
|
|
24
33
|
end
|
|
@@ -16,4 +16,12 @@ module DoubleEntrySpecHelper
|
|
|
16
16
|
)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
def perform_btc_deposit(user, amount)
|
|
20
|
+
DoubleEntry.transfer(Money.new(amount, :btc),
|
|
21
|
+
:from => DoubleEntry.account(:btc_test, :scope => user),
|
|
22
|
+
:to => DoubleEntry.account(:btc_savings, :scope => user),
|
|
23
|
+
:code => :btc_test_transfer,
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
19
27
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: double_entry
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Anthony Sellitti
|
|
@@ -15,7 +15,7 @@ authors:
|
|
|
15
15
|
autorequire:
|
|
16
16
|
bindir: bin
|
|
17
17
|
cert_chain: []
|
|
18
|
-
date: 2014-
|
|
18
|
+
date: 2014-11-12 00:00:00.000000000 Z
|
|
19
19
|
dependencies:
|
|
20
20
|
- !ruby/object:Gem::Dependency
|
|
21
21
|
name: money
|
|
@@ -23,70 +23,56 @@ dependencies:
|
|
|
23
23
|
requirements:
|
|
24
24
|
- - ">="
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version:
|
|
26
|
+
version: 6.0.0
|
|
27
27
|
type: :runtime
|
|
28
28
|
prerelease: false
|
|
29
29
|
version_requirements: !ruby/object:Gem::Requirement
|
|
30
30
|
requirements:
|
|
31
31
|
- - ">="
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version:
|
|
34
|
-
- !ruby/object:Gem::Dependency
|
|
35
|
-
name: encapsulate_as_money
|
|
36
|
-
requirement: !ruby/object:Gem::Requirement
|
|
37
|
-
requirements:
|
|
38
|
-
- - ">="
|
|
39
|
-
- !ruby/object:Gem::Version
|
|
40
|
-
version: '0'
|
|
41
|
-
type: :runtime
|
|
42
|
-
prerelease: false
|
|
43
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
44
|
-
requirements:
|
|
45
|
-
- - ">="
|
|
46
|
-
- !ruby/object:Gem::Version
|
|
47
|
-
version: '0'
|
|
33
|
+
version: 6.0.0
|
|
48
34
|
- !ruby/object:Gem::Dependency
|
|
49
35
|
name: activerecord
|
|
50
36
|
requirement: !ruby/object:Gem::Requirement
|
|
51
37
|
requirements:
|
|
52
38
|
- - ">="
|
|
53
39
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: 3.2.
|
|
40
|
+
version: 3.2.0
|
|
55
41
|
type: :runtime
|
|
56
42
|
prerelease: false
|
|
57
43
|
version_requirements: !ruby/object:Gem::Requirement
|
|
58
44
|
requirements:
|
|
59
45
|
- - ">="
|
|
60
46
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: 3.2.
|
|
47
|
+
version: 3.2.0
|
|
62
48
|
- !ruby/object:Gem::Dependency
|
|
63
49
|
name: activesupport
|
|
64
50
|
requirement: !ruby/object:Gem::Requirement
|
|
65
51
|
requirements:
|
|
66
52
|
- - ">="
|
|
67
53
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: 3.
|
|
54
|
+
version: 3.2.0
|
|
69
55
|
type: :runtime
|
|
70
56
|
prerelease: false
|
|
71
57
|
version_requirements: !ruby/object:Gem::Requirement
|
|
72
58
|
requirements:
|
|
73
59
|
- - ">="
|
|
74
60
|
- !ruby/object:Gem::Version
|
|
75
|
-
version: 3.
|
|
61
|
+
version: 3.2.0
|
|
76
62
|
- !ruby/object:Gem::Dependency
|
|
77
63
|
name: railties
|
|
78
64
|
requirement: !ruby/object:Gem::Requirement
|
|
79
65
|
requirements:
|
|
80
66
|
- - ">="
|
|
81
67
|
- !ruby/object:Gem::Version
|
|
82
|
-
version: 3.
|
|
68
|
+
version: 3.2.0
|
|
83
69
|
type: :runtime
|
|
84
70
|
prerelease: false
|
|
85
71
|
version_requirements: !ruby/object:Gem::Requirement
|
|
86
72
|
requirements:
|
|
87
73
|
- - ">="
|
|
88
74
|
- !ruby/object:Gem::Version
|
|
89
|
-
version: 3.
|
|
75
|
+
version: 3.2.0
|
|
90
76
|
- !ruby/object:Gem::Dependency
|
|
91
77
|
name: rake
|
|
92
78
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -249,11 +235,7 @@ files:
|
|
|
249
235
|
- LICENSE.md
|
|
250
236
|
- README.md
|
|
251
237
|
- Rakefile
|
|
252
|
-
- db/.gitkeep
|
|
253
238
|
- double_entry.gemspec
|
|
254
|
-
- gemfiles/Gemfile.rails-3.2.0
|
|
255
|
-
- gemfiles/Gemfile.rails-4.0.0
|
|
256
|
-
- gemfiles/Gemfile.rails-4.1.0
|
|
257
239
|
- lib/active_record/locking_extensions.rb
|
|
258
240
|
- lib/double_entry.rb
|
|
259
241
|
- lib/double_entry/account.rb
|
|
@@ -308,6 +290,9 @@ files:
|
|
|
308
290
|
- spec/support/database.example.yml
|
|
309
291
|
- spec/support/database.travis.yml
|
|
310
292
|
- spec/support/double_entry_spec_helper.rb
|
|
293
|
+
- spec/support/gemfiles/Gemfile.rails-3.2.x
|
|
294
|
+
- spec/support/gemfiles/Gemfile.rails-4.0.x
|
|
295
|
+
- spec/support/gemfiles/Gemfile.rails-4.1.x
|
|
311
296
|
- spec/support/reporting_configuration.rb
|
|
312
297
|
- spec/support/schema.rb
|
|
313
298
|
homepage: https://github.com/envato/double_entry
|
|
@@ -359,5 +344,8 @@ test_files:
|
|
|
359
344
|
- spec/support/database.example.yml
|
|
360
345
|
- spec/support/database.travis.yml
|
|
361
346
|
- spec/support/double_entry_spec_helper.rb
|
|
347
|
+
- spec/support/gemfiles/Gemfile.rails-3.2.x
|
|
348
|
+
- spec/support/gemfiles/Gemfile.rails-4.0.x
|
|
349
|
+
- spec/support/gemfiles/Gemfile.rails-4.1.x
|
|
362
350
|
- spec/support/reporting_configuration.rb
|
|
363
351
|
- spec/support/schema.rb
|
data/db/.gitkeep
DELETED
|
File without changes
|