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.
- checksums.yaml +4 -4
- data/.rspec +2 -1
- data/.rubocop.yml +55 -0
- data/.travis.yml +23 -12
- data/README.md +5 -1
- data/Rakefile +8 -3
- data/double_entry.gemspec +4 -3
- data/lib/active_record/locking_extensions.rb +28 -40
- data/lib/active_record/locking_extensions/log_subscriber.rb +4 -4
- data/lib/double_entry.rb +0 -2
- data/lib/double_entry/account.rb +13 -16
- data/lib/double_entry/account_balance.rb +0 -4
- data/lib/double_entry/balance_calculator.rb +4 -5
- data/lib/double_entry/configurable.rb +0 -2
- data/lib/double_entry/configuration.rb +2 -3
- data/lib/double_entry/errors.rb +2 -2
- data/lib/double_entry/line.rb +13 -16
- data/lib/double_entry/locking.rb +13 -18
- data/lib/double_entry/reporting.rb +2 -3
- data/lib/double_entry/reporting/aggregate.rb +90 -88
- data/lib/double_entry/reporting/aggregate_array.rb +58 -58
- data/lib/double_entry/reporting/day_range.rb +37 -35
- data/lib/double_entry/reporting/hour_range.rb +40 -37
- data/lib/double_entry/reporting/line_aggregate.rb +27 -28
- data/lib/double_entry/reporting/month_range.rb +67 -67
- data/lib/double_entry/reporting/time_range.rb +40 -38
- data/lib/double_entry/reporting/time_range_array.rb +3 -5
- data/lib/double_entry/reporting/week_range.rb +77 -78
- data/lib/double_entry/reporting/year_range.rb +27 -27
- data/lib/double_entry/transfer.rb +14 -15
- data/lib/double_entry/validation/line_check.rb +92 -86
- data/lib/double_entry/version.rb +1 -1
- data/lib/generators/double_entry/install/install_generator.rb +1 -2
- data/lib/generators/double_entry/install/templates/migration.rb +0 -2
- data/script/jack_hammer +1 -1
- data/spec/active_record/locking_extensions_spec.rb +45 -38
- data/spec/double_entry/account_balance_spec.rb +4 -5
- data/spec/double_entry/account_spec.rb +43 -44
- data/spec/double_entry/balance_calculator_spec.rb +6 -8
- data/spec/double_entry/configuration_spec.rb +14 -16
- data/spec/double_entry/line_spec.rb +25 -26
- data/spec/double_entry/locking_spec.rb +34 -39
- data/spec/double_entry/reporting/aggregate_array_spec.rb +8 -10
- data/spec/double_entry/reporting/aggregate_spec.rb +84 -44
- data/spec/double_entry/reporting/line_aggregate_spec.rb +7 -6
- data/spec/double_entry/reporting/month_range_spec.rb +109 -103
- data/spec/double_entry/reporting/time_range_array_spec.rb +145 -135
- data/spec/double_entry/reporting/time_range_spec.rb +36 -35
- data/spec/double_entry/reporting/week_range_spec.rb +82 -76
- data/spec/double_entry/reporting_spec.rb +9 -13
- data/spec/double_entry/transfer_spec.rb +13 -15
- data/spec/double_entry/validation/line_check_spec.rb +73 -79
- data/spec/double_entry_spec.rb +65 -68
- data/spec/generators/double_entry/install/install_generator_spec.rb +7 -10
- data/spec/spec_helper.rb +68 -10
- data/spec/support/accounts.rb +2 -4
- data/spec/support/double_entry_spec_helper.rb +4 -4
- data/spec/support/gemfiles/Gemfile.rails-3.2.x +1 -0
- metadata +31 -2
@@ -2,119 +2,125 @@
|
|
2
2
|
require 'set'
|
3
3
|
|
4
4
|
module DoubleEntry
|
5
|
-
|
6
|
-
|
5
|
+
module Validation
|
6
|
+
class LineCheck < ActiveRecord::Base
|
7
|
+
default_scope -> { order('created_at') }
|
7
8
|
|
8
|
-
|
9
|
+
def self.perform!
|
10
|
+
new.perform
|
11
|
+
end
|
9
12
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
+
def perform
|
14
|
+
log = ''
|
15
|
+
current_line_id = nil
|
13
16
|
|
14
|
-
|
15
|
-
|
16
|
-
current_line_id = nil
|
17
|
+
active_accounts = Set.new
|
18
|
+
incorrect_accounts = Set.new
|
17
19
|
|
18
|
-
|
19
|
-
|
20
|
+
new_lines_since_last_run.find_each do |line|
|
21
|
+
incorrect_accounts << line.account unless running_balance_correct?(line, log)
|
22
|
+
active_accounts << line.account
|
23
|
+
current_line_id = line.id
|
24
|
+
end
|
20
25
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
+
active_accounts.each do |account|
|
27
|
+
incorrect_accounts << account unless cached_balance_correct?(account)
|
28
|
+
end
|
29
|
+
|
30
|
+
incorrect_accounts.each { |account| recalculate_account(account) }
|
26
31
|
|
27
|
-
|
28
|
-
|
32
|
+
unless active_accounts.empty?
|
33
|
+
LineCheck.create!(
|
34
|
+
:errors_found => incorrect_accounts.any?,
|
35
|
+
:last_line_id => current_line_id,
|
36
|
+
:log => log,
|
37
|
+
)
|
38
|
+
end
|
29
39
|
end
|
30
40
|
|
31
|
-
|
41
|
+
private
|
32
42
|
|
33
|
-
|
34
|
-
LineCheck.
|
35
|
-
|
36
|
-
:last_line_id => current_line_id,
|
37
|
-
:log => log,
|
38
|
-
)
|
43
|
+
def last_run_line_id
|
44
|
+
latest = LineCheck.last
|
45
|
+
latest ? latest.last_line_id : 0
|
39
46
|
end
|
40
|
-
end
|
41
|
-
|
42
|
-
private
|
43
47
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
end
|
48
|
+
def new_lines_since_last_run
|
49
|
+
Line.where('id > ?', last_run_line_id)
|
50
|
+
end
|
48
51
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
+
def running_balance_correct?(line, log)
|
53
|
+
# Another work around for the MySQL 5.1 query optimiser bug that causes the ORDER BY
|
54
|
+
# on the query to fail in some circumstances, resulting in an old balance being
|
55
|
+
# returned. This was biting us intermittently in spec runs.
|
56
|
+
# See http://bugs.mysql.com/bug.php?id=51431
|
57
|
+
force_index = if Line.connection.adapter_name.match(/mysql/i)
|
58
|
+
'FORCE INDEX (lines_scope_account_id_idx)'
|
59
|
+
else
|
60
|
+
''
|
61
|
+
end
|
62
|
+
|
63
|
+
# yes, it needs to be find_by_sql, because any other find will be affected
|
64
|
+
# by the find_each call in perform!
|
65
|
+
previous_line = Line.find_by_sql([<<-SQL, line.account.identifier.to_s, line.scope, line.id])
|
66
|
+
SELECT * FROM #{Line.quoted_table_name} #{force_index}
|
67
|
+
WHERE account = ?
|
68
|
+
AND scope = ?
|
69
|
+
AND id < ?
|
70
|
+
ORDER BY id DESC
|
71
|
+
LIMIT 1
|
72
|
+
SQL
|
73
|
+
|
74
|
+
previous_balance = previous_line.length == 1 ? previous_line[0].balance : Money.zero(line.account.currency)
|
75
|
+
|
76
|
+
if line.balance != (line.amount + previous_balance)
|
77
|
+
log << line_error_message(line, previous_line, previous_balance)
|
78
|
+
end
|
52
79
|
|
53
|
-
|
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
|
-
|
68
|
-
previous_balance = previous_line.length == 1 ? previous_line[0].balance : Money.zero(line.account.currency)
|
69
|
-
|
70
|
-
if line.balance != (line.amount + previous_balance)
|
71
|
-
log << line_error_message(line, previous_line, previous_balance)
|
80
|
+
line.balance == previous_balance + line.amount
|
72
81
|
end
|
73
82
|
|
74
|
-
line
|
75
|
-
|
76
|
-
|
77
|
-
def line_error_message(line, previous_line, previous_balance)
|
78
|
-
<<-END_OF_MESSAGE.strip_heredoc
|
83
|
+
def line_error_message(line, previous_line, previous_balance)
|
84
|
+
<<-END_OF_MESSAGE.strip_heredoc
|
79
85
|
*********************************
|
80
86
|
Error on line ##{line.id}: balance:#{line.balance} != #{previous_balance} + #{line.amount}
|
81
87
|
*********************************
|
82
|
-
|
83
|
-
|
88
|
+
#{previous_line.inspect}
|
89
|
+
#{line.inspect}
|
84
90
|
|
85
|
-
|
86
|
-
|
91
|
+
END_OF_MESSAGE
|
92
|
+
end
|
87
93
|
|
88
|
-
|
89
|
-
|
90
|
-
|
94
|
+
def cached_balance_correct?(account)
|
95
|
+
DoubleEntry.lock_accounts(account) do
|
96
|
+
return AccountBalance.find_by_account(account).balance == account.balance
|
97
|
+
end
|
91
98
|
end
|
92
|
-
end
|
93
99
|
|
94
|
-
|
95
|
-
|
96
|
-
|
100
|
+
def recalculate_account(account)
|
101
|
+
DoubleEntry.lock_accounts(account) do
|
102
|
+
recalculated_balance = Money.zero(account.currency)
|
97
103
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
104
|
+
lines_for_account(account).each do |line|
|
105
|
+
recalculated_balance += line.amount
|
106
|
+
line.update_attribute(:balance, recalculated_balance) if line.balance != recalculated_balance
|
107
|
+
end
|
102
108
|
|
103
|
-
|
109
|
+
update_balance_for_account(account, recalculated_balance)
|
110
|
+
end
|
104
111
|
end
|
105
|
-
end
|
106
112
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
+
def lines_for_account(account)
|
114
|
+
Line.where(
|
115
|
+
:account => account.identifier.to_s,
|
116
|
+
:scope => account.scope_identity.to_s,
|
117
|
+
).order(:id)
|
118
|
+
end
|
113
119
|
|
114
|
-
|
115
|
-
|
116
|
-
|
120
|
+
def update_balance_for_account(account, balance)
|
121
|
+
account_balance = Locking.balance_for_locked_account(account)
|
122
|
+
account_balance.update_attribute(:balance, balance)
|
123
|
+
end
|
117
124
|
end
|
118
125
|
end
|
119
|
-
end
|
120
126
|
end
|
data/lib/double_entry/version.rb
CHANGED
@@ -54,7 +54,6 @@ class CreateDoubleEntryTables < ActiveRecord::Migration
|
|
54
54
|
t.text "log"
|
55
55
|
t.timestamps :null => false
|
56
56
|
end
|
57
|
-
|
58
57
|
end
|
59
58
|
|
60
59
|
def self.down
|
@@ -63,5 +62,4 @@ class CreateDoubleEntryTables < ActiveRecord::Migration
|
|
63
62
|
drop_table "double_entry_lines"
|
64
63
|
drop_table "double_entry_account_balances"
|
65
64
|
end
|
66
|
-
|
67
65
|
end
|
data/script/jack_hammer
CHANGED
@@ -1,12 +1,11 @@
|
|
1
1
|
# encoding: utf-8
|
2
|
-
require 'spec_helper'
|
3
2
|
|
4
|
-
describe ActiveRecord::LockingExtensions do
|
5
|
-
PG_DEADLOCK = ActiveRecord::StatementInvalid.new(
|
6
|
-
MYSQL_DEADLOCK = ActiveRecord::StatementInvalid.new(
|
7
|
-
SQLITE3_LOCK = ActiveRecord::StatementInvalid.new(
|
3
|
+
RSpec.describe ActiveRecord::LockingExtensions do
|
4
|
+
PG_DEADLOCK = ActiveRecord::StatementInvalid.new('PG::Error: ERROR: deadlock detected')
|
5
|
+
MYSQL_DEADLOCK = ActiveRecord::StatementInvalid.new('Mysql::Error: Deadlock found when trying to get lock')
|
6
|
+
SQLITE3_LOCK = ActiveRecord::StatementInvalid.new('SQLite3::BusyException: database is locked: UPDATE...')
|
8
7
|
|
9
|
-
context
|
8
|
+
context '#restartable_transaction' do
|
10
9
|
it "keeps running the lock until a ActiveRecord::RestartTransaction isn't raised" do
|
11
10
|
expect(User).to receive(:create!).ordered.and_raise(ActiveRecord::RestartTransaction)
|
12
11
|
expect(User).to receive(:create!).ordered.and_raise(ActiveRecord::RestartTransaction)
|
@@ -16,88 +15,96 @@ describe ActiveRecord::LockingExtensions do
|
|
16
15
|
end
|
17
16
|
end
|
18
17
|
|
19
|
-
context
|
20
|
-
shared_examples
|
21
|
-
it
|
22
|
-
expect { User.with_restart_on_deadlock {
|
18
|
+
context '#with_restart_on_deadlock' do
|
19
|
+
shared_examples 'abstract adapter' do
|
20
|
+
it 'raises a ActiveRecord::RestartTransaction error if a deadlock occurs' do
|
21
|
+
expect { User.with_restart_on_deadlock { fail exception } }.
|
22
|
+
to raise_error(ActiveRecord::RestartTransaction)
|
23
23
|
end
|
24
24
|
|
25
|
-
it
|
26
|
-
expect(ActiveSupport::Notifications).
|
27
|
-
|
25
|
+
it 'publishes a notification' do
|
26
|
+
expect(ActiveSupport::Notifications).
|
27
|
+
to receive(:publish).
|
28
|
+
with('deadlock_restart.active_record', hash_including(:exception => exception))
|
29
|
+
expect { User.with_restart_on_deadlock { fail exception } }.to raise_error
|
28
30
|
end
|
29
31
|
end
|
30
32
|
|
31
|
-
context
|
33
|
+
context 'mysql' do
|
32
34
|
let(:exception) { MYSQL_DEADLOCK }
|
33
35
|
|
34
|
-
it_behaves_like
|
36
|
+
it_behaves_like 'abstract adapter'
|
35
37
|
end
|
36
38
|
|
37
|
-
context
|
39
|
+
context 'postgres' do
|
38
40
|
let(:exception) { PG_DEADLOCK }
|
39
41
|
|
40
|
-
it_behaves_like
|
42
|
+
it_behaves_like 'abstract adapter'
|
41
43
|
end
|
42
44
|
|
43
|
-
context
|
45
|
+
context 'sqlite' do
|
44
46
|
let(:exception) { SQLITE3_LOCK }
|
45
47
|
|
46
|
-
it_behaves_like
|
48
|
+
it_behaves_like 'abstract adapter'
|
47
49
|
end
|
48
50
|
end
|
49
51
|
|
50
|
-
context
|
51
|
-
it
|
52
|
-
User.make! :username =>
|
52
|
+
context '#create_ignoring_duplicates' do
|
53
|
+
it 'does not raise an error if a duplicate index error is raised in the database' do
|
54
|
+
User.make! :username => 'keith'
|
53
55
|
|
54
|
-
expect { User.make! :username =>
|
55
|
-
expect { User.create_ignoring_duplicates! :username =>
|
56
|
+
expect { User.make! :username => 'keith' }.to raise_error
|
57
|
+
expect { User.create_ignoring_duplicates! :username => 'keith' }.to_not raise_error
|
56
58
|
end
|
57
59
|
|
58
|
-
it
|
59
|
-
User.make! :username =>
|
60
|
+
it 'publishes a notification when a duplicate is encountered' do
|
61
|
+
User.make! :username => 'keith'
|
60
62
|
|
61
|
-
expect(ActiveSupport::Notifications).
|
63
|
+
expect(ActiveSupport::Notifications).
|
64
|
+
to receive(:publish).
|
65
|
+
with('duplicate_ignore.active_record', hash_including(:exception => kind_of(ActiveRecord::RecordNotUnique)))
|
62
66
|
|
63
|
-
expect { User.create_ignoring_duplicates! :username =>
|
67
|
+
expect { User.create_ignoring_duplicates! :username => 'keith' }.to_not raise_error
|
64
68
|
end
|
65
69
|
|
66
|
-
shared_examples
|
67
|
-
it
|
70
|
+
shared_examples 'abstract adapter' do
|
71
|
+
it 'retries the creation if a deadlock error is raised from the database' do
|
68
72
|
expect(User).to receive(:create!).ordered.and_raise(exception)
|
69
73
|
expect(User).to receive(:create!).ordered.and_return(true)
|
70
74
|
|
71
75
|
expect { User.create_ignoring_duplicates! }.to_not raise_error
|
72
76
|
end
|
73
77
|
|
74
|
-
it
|
78
|
+
it 'publishes a notification on each retry' do
|
75
79
|
expect(User).to receive(:create!).ordered.and_raise(exception)
|
76
80
|
expect(User).to receive(:create!).ordered.and_raise(exception)
|
77
81
|
expect(User).to receive(:create!).ordered.and_return(true)
|
78
82
|
|
79
|
-
expect(ActiveSupport::Notifications).
|
83
|
+
expect(ActiveSupport::Notifications).
|
84
|
+
to receive(:publish).
|
85
|
+
with('deadlock_retry.active_record', hash_including(:exception => exception)).
|
86
|
+
twice
|
80
87
|
|
81
88
|
expect { User.create_ignoring_duplicates! }.to_not raise_error
|
82
89
|
end
|
83
90
|
end
|
84
91
|
|
85
|
-
context
|
92
|
+
context 'mysql' do
|
86
93
|
let(:exception) { MYSQL_DEADLOCK }
|
87
94
|
|
88
|
-
it_behaves_like
|
95
|
+
it_behaves_like 'abstract adapter'
|
89
96
|
end
|
90
97
|
|
91
|
-
context
|
98
|
+
context 'postgres' do
|
92
99
|
let(:exception) { PG_DEADLOCK }
|
93
100
|
|
94
|
-
it_behaves_like
|
101
|
+
it_behaves_like 'abstract adapter'
|
95
102
|
end
|
96
103
|
|
97
|
-
context
|
104
|
+
context 'sqlite' do
|
98
105
|
let(:exception) { SQLITE3_LOCK }
|
99
106
|
|
100
|
-
it_behaves_like
|
107
|
+
it_behaves_like 'abstract adapter'
|
101
108
|
end
|
102
109
|
end
|
103
110
|
end
|
@@ -1,8 +1,7 @@
|
|
1
1
|
# encoding: utf-8
|
2
|
-
|
3
|
-
describe
|
4
|
-
|
5
|
-
|
6
|
-
expect(DoubleEntry::AccountBalance.table_name).to eq "double_entry_account_balances"
|
2
|
+
RSpec.describe DoubleEntry::AccountBalance do
|
3
|
+
describe '.table_name' do
|
4
|
+
subject { DoubleEntry::AccountBalance.table_name }
|
5
|
+
it { should eq('double_entry_account_balances') }
|
7
6
|
end
|
8
7
|
end
|
@@ -1,19 +1,18 @@
|
|
1
1
|
# encoding: utf-8
|
2
|
-
require 'spec_helper'
|
3
2
|
module DoubleEntry
|
4
|
-
describe Account do
|
3
|
+
RSpec.describe Account do
|
5
4
|
let(:identity_scope) { ->(value) { value } }
|
6
5
|
|
7
|
-
describe
|
8
|
-
context
|
9
|
-
let(:identifier) {
|
6
|
+
describe '::new' do
|
7
|
+
context 'given an identifier 31 characters in length' do
|
8
|
+
let(:identifier) { 'xxxxxxxx 31 characters xxxxxxxx' }
|
10
9
|
specify do
|
11
10
|
expect { Account.new(:identifier => identifier) }.to_not raise_error
|
12
11
|
end
|
13
12
|
end
|
14
13
|
|
15
|
-
context
|
16
|
-
let(:identifier) {
|
14
|
+
context 'given an identifier 32 characters in length' do
|
15
|
+
let(:identifier) { 'xxxxxxxx 32 characters xxxxxxxxx' }
|
17
16
|
specify do
|
18
17
|
expect { Account.new(:identifier => identifier) }.to raise_error AccountIdentifierTooLongError, /'#{identifier}'/
|
19
18
|
end
|
@@ -21,64 +20,64 @@ module DoubleEntry
|
|
21
20
|
end
|
22
21
|
|
23
22
|
describe Account::Instance do
|
24
|
-
it
|
25
|
-
account = Account.new(:identifier =>
|
26
|
-
a = Account::Instance.new(:account => account, :scope =>
|
27
|
-
b = Account::Instance.new(:account => account, :scope =>
|
23
|
+
it 'is sortable' do
|
24
|
+
account = Account.new(:identifier => 'savings', :scope_identifier => identity_scope)
|
25
|
+
a = Account::Instance.new(:account => account, :scope => '123')
|
26
|
+
b = Account::Instance.new(:account => account, :scope => '456')
|
28
27
|
expect([b, a].sort).to eq [a, b]
|
29
28
|
end
|
30
29
|
|
31
|
-
it
|
32
|
-
account = Account.new(:identifier =>
|
33
|
-
a1 = Account::Instance.new(:account => account, :scope =>
|
34
|
-
a2 = Account::Instance.new(:account => account, :scope =>
|
35
|
-
b = Account::Instance.new(:account => account, :scope =>
|
30
|
+
it 'is hashable' do
|
31
|
+
account = Account.new(:identifier => 'savings', :scope_identifier => identity_scope)
|
32
|
+
a1 = Account::Instance.new(:account => account, :scope => '123')
|
33
|
+
a2 = Account::Instance.new(:account => account, :scope => '123')
|
34
|
+
b = Account::Instance.new(:account => account, :scope => '456')
|
36
35
|
|
37
36
|
expect(a1.hash).to eq a2.hash
|
38
37
|
expect(a1.hash).to_not eq b.hash
|
39
38
|
end
|
40
39
|
|
41
|
-
describe
|
42
|
-
let(:account) { Account.new(:identifier =>
|
40
|
+
describe '::new' do
|
41
|
+
let(:account) { Account.new(:identifier => 'x', :scope_identifier => identity_scope) }
|
43
42
|
subject(:initialize_account_instance) { Account::Instance.new(:account => account, :scope => scope) }
|
44
43
|
|
45
|
-
context
|
46
|
-
let(:scope) {
|
44
|
+
context 'given a scope identifier 23 characters in length' do
|
45
|
+
let(:scope) { 'xxxx 23 characters xxxx' }
|
47
46
|
specify { expect { initialize_account_instance }.to_not raise_error }
|
48
47
|
end
|
49
48
|
|
50
|
-
context
|
51
|
-
let(:scope) {
|
49
|
+
context 'given a scope identifier 24 characters in length' do
|
50
|
+
let(:scope) { 'xxxx 24 characters xxxxx' }
|
52
51
|
specify { expect { initialize_account_instance }.to raise_error ScopeIdentifierTooLongError, /'#{scope}'/ }
|
53
52
|
end
|
54
53
|
end
|
55
54
|
end
|
56
55
|
|
57
|
-
describe
|
58
|
-
it
|
59
|
-
account = DoubleEntry::Account.new(:identifier =>
|
60
|
-
expect(DoubleEntry::Account::Instance.new(:account => account).currency).to eq(
|
56
|
+
describe 'currency' do
|
57
|
+
it 'defaults to USD currency' do
|
58
|
+
account = DoubleEntry::Account.new(:identifier => 'savings', :scope_identifier => identity_scope)
|
59
|
+
expect(DoubleEntry::Account::Instance.new(:account => account).currency).to eq('USD')
|
61
60
|
end
|
62
61
|
|
63
|
-
it
|
64
|
-
account = DoubleEntry::Account.new(:identifier =>
|
65
|
-
expect(DoubleEntry::Account::Instance.new(:account => account).currency).to eq(
|
62
|
+
it 'allows the currency to be set' do
|
63
|
+
account = DoubleEntry::Account.new(:identifier => 'savings', :scope_identifier => identity_scope, :currency => 'AUD')
|
64
|
+
expect(DoubleEntry::Account::Instance.new(:account => account).currency).to eq('AUD')
|
66
65
|
end
|
67
66
|
end
|
68
67
|
|
69
68
|
describe Account::Set do
|
70
|
-
describe
|
69
|
+
describe '#define' do
|
71
70
|
context "given a 'savings' account is defined" do
|
72
|
-
before { subject.define(:identifier =>
|
71
|
+
before { subject.define(:identifier => 'savings') }
|
73
72
|
its(:first) { should be_an Account }
|
74
|
-
its(
|
73
|
+
its('first.identifier') { should eq 'savings' }
|
75
74
|
end
|
76
75
|
end
|
77
76
|
|
78
|
-
describe
|
77
|
+
describe '#active_record_scope_identifier' do
|
79
78
|
subject(:scope) { Account::Set.new.active_record_scope_identifier(ar_class) }
|
80
79
|
|
81
|
-
context
|
80
|
+
context 'given ActiveRecordScopeFactory is stubbed' do
|
82
81
|
let(:scope_identifier) { double(:scope_identifier) }
|
83
82
|
let(:scope_factory) { double(:scope_factory, :scope_identifier => scope_identifier) }
|
84
83
|
let(:ar_class) { double(:ar_class) }
|
@@ -90,36 +89,36 @@ module DoubleEntry
|
|
90
89
|
end
|
91
90
|
end
|
92
91
|
|
93
|
-
describe Account::ActiveRecordScopeFactory do
|
94
|
-
context
|
92
|
+
RSpec.describe Account::ActiveRecordScopeFactory do
|
93
|
+
context 'given the class User' do
|
95
94
|
subject(:factory) { Account::ActiveRecordScopeFactory.new(User) }
|
96
95
|
|
97
|
-
describe
|
96
|
+
describe '#scope_identifier' do
|
98
97
|
subject(:scope_identifier) { factory.scope_identifier }
|
99
98
|
|
100
|
-
describe
|
99
|
+
describe '#call' do
|
101
100
|
subject(:scope) { scope_identifier.call(value) }
|
102
101
|
|
103
|
-
context
|
102
|
+
context 'given a User instance with ID 32' do
|
104
103
|
let(:value) { User.make(:id => 32) }
|
105
104
|
|
106
105
|
it { should eq 32 }
|
107
106
|
end
|
108
107
|
|
109
|
-
context
|
108
|
+
context 'given differing model instance with ID 32' do
|
110
109
|
let(:value) { double(:id => 32) }
|
111
|
-
it
|
110
|
+
it 'raises an error' do
|
112
111
|
expect { scope_identifier.call(value) }.to raise_error DoubleEntry::AccountScopeMismatchError
|
113
112
|
end
|
114
113
|
end
|
115
114
|
|
116
115
|
context "given the String 'I am a bearded lady'" do
|
117
|
-
let(:value) {
|
116
|
+
let(:value) { 'I am a bearded lady' }
|
118
117
|
|
119
|
-
it { should eq
|
118
|
+
it { should eq 'I am a bearded lady' }
|
120
119
|
end
|
121
120
|
|
122
|
-
context
|
121
|
+
context 'given the Integer 42' do
|
123
122
|
let(:value) { 42 }
|
124
123
|
|
125
124
|
it { should eq 42 }
|