double_entry 0.10.0 → 0.10.1
Sign up to get free protection for your applications and to get access to all the features.
- 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 }
|