double_entry 1.0.1 → 2.0.0.beta5
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 +5 -5
- data/CHANGELOG.md +497 -0
- data/README.md +107 -44
- data/double_entry.gemspec +22 -49
- data/lib/active_record/locking_extensions.rb +3 -3
- data/lib/active_record/locking_extensions/log_subscriber.rb +1 -1
- data/lib/double_entry.rb +29 -21
- data/lib/double_entry/account.rb +39 -46
- data/lib/double_entry/account_balance.rb +20 -3
- data/lib/double_entry/balance_calculator.rb +5 -5
- data/lib/double_entry/configurable.rb +1 -0
- data/lib/double_entry/configuration.rb +8 -2
- data/lib/double_entry/errors.rb +13 -13
- data/lib/double_entry/line.rb +7 -6
- data/lib/double_entry/locking.rb +5 -5
- data/lib/double_entry/transfer.rb +37 -30
- data/lib/double_entry/validation.rb +1 -0
- data/lib/double_entry/validation/account_fixer.rb +36 -0
- data/lib/double_entry/validation/line_check.rb +25 -43
- data/lib/double_entry/version.rb +1 -1
- data/lib/generators/double_entry/install/install_generator.rb +22 -1
- data/lib/generators/double_entry/install/templates/initializer.rb +20 -0
- data/lib/generators/double_entry/install/templates/migration.rb +45 -55
- metadata +35 -256
- data/.gitignore +0 -32
- data/.rspec +0 -2
- data/.travis.yml +0 -29
- data/.yardopts +0 -2
- data/Gemfile +0 -2
- data/Rakefile +0 -15
- data/lib/double_entry/reporting.rb +0 -181
- data/lib/double_entry/reporting/aggregate.rb +0 -110
- data/lib/double_entry/reporting/aggregate_array.rb +0 -76
- data/lib/double_entry/reporting/day_range.rb +0 -42
- data/lib/double_entry/reporting/hour_range.rb +0 -45
- data/lib/double_entry/reporting/line_aggregate.rb +0 -16
- data/lib/double_entry/reporting/line_aggregate_filter.rb +0 -79
- data/lib/double_entry/reporting/month_range.rb +0 -94
- data/lib/double_entry/reporting/time_range.rb +0 -59
- data/lib/double_entry/reporting/time_range_array.rb +0 -49
- data/lib/double_entry/reporting/week_range.rb +0 -107
- data/lib/double_entry/reporting/year_range.rb +0 -40
- data/script/jack_hammer +0 -210
- data/script/setup.sh +0 -8
- data/spec/active_record/locking_extensions_spec.rb +0 -110
- data/spec/double_entry/account_balance_spec.rb +0 -7
- data/spec/double_entry/account_spec.rb +0 -130
- data/spec/double_entry/balance_calculator_spec.rb +0 -88
- data/spec/double_entry/configuration_spec.rb +0 -50
- data/spec/double_entry/line_spec.rb +0 -80
- data/spec/double_entry/locking_spec.rb +0 -214
- data/spec/double_entry/performance/double_entry_performance_spec.rb +0 -32
- data/spec/double_entry/performance/reporting/aggregate_performance_spec.rb +0 -50
- data/spec/double_entry/reporting/aggregate_array_spec.rb +0 -123
- data/spec/double_entry/reporting/aggregate_spec.rb +0 -205
- data/spec/double_entry/reporting/line_aggregate_filter_spec.rb +0 -90
- data/spec/double_entry/reporting/line_aggregate_spec.rb +0 -39
- data/spec/double_entry/reporting/month_range_spec.rb +0 -139
- data/spec/double_entry/reporting/time_range_array_spec.rb +0 -169
- data/spec/double_entry/reporting/time_range_spec.rb +0 -45
- data/spec/double_entry/reporting/week_range_spec.rb +0 -103
- data/spec/double_entry/reporting_spec.rb +0 -181
- data/spec/double_entry/transfer_spec.rb +0 -93
- data/spec/double_entry/validation/line_check_spec.rb +0 -99
- data/spec/double_entry_spec.rb +0 -428
- data/spec/generators/double_entry/install/install_generator_spec.rb +0 -30
- data/spec/spec_helper.rb +0 -118
- data/spec/support/accounts.rb +0 -21
- data/spec/support/blueprints.rb +0 -43
- data/spec/support/database.example.yml +0 -21
- data/spec/support/database.travis.yml +0 -24
- data/spec/support/double_entry_spec_helper.rb +0 -27
- data/spec/support/gemfiles/Gemfile.rails-3.2.x +0 -8
- data/spec/support/gemfiles/Gemfile.rails-4.1.x +0 -6
- data/spec/support/gemfiles/Gemfile.rails-4.2.x +0 -5
- data/spec/support/gemfiles/Gemfile.rails-5.0.x +0 -5
- data/spec/support/performance_helper.rb +0 -26
- data/spec/support/reporting_configuration.rb +0 -6
- data/spec/support/schema.rb +0 -74
@@ -1,40 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
module DoubleEntry
|
3
|
-
module Reporting
|
4
|
-
class YearRange < TimeRange
|
5
|
-
attr_reader :year
|
6
|
-
|
7
|
-
def initialize(options)
|
8
|
-
super options
|
9
|
-
|
10
|
-
year_start = Time.local(@year, 1, 1)
|
11
|
-
@start = year_start
|
12
|
-
@finish = year_start.end_of_year
|
13
|
-
end
|
14
|
-
|
15
|
-
def self.current
|
16
|
-
new(:year => Time.now.year)
|
17
|
-
end
|
18
|
-
|
19
|
-
def self.from_time(time)
|
20
|
-
new(:year => time.year)
|
21
|
-
end
|
22
|
-
|
23
|
-
def ==(other)
|
24
|
-
year == other.year
|
25
|
-
end
|
26
|
-
|
27
|
-
def previous
|
28
|
-
YearRange.new(:year => year - 1)
|
29
|
-
end
|
30
|
-
|
31
|
-
def next
|
32
|
-
YearRange.new(:year => year + 1)
|
33
|
-
end
|
34
|
-
|
35
|
-
def to_s
|
36
|
-
year.to_s
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
data/script/jack_hammer
DELETED
@@ -1,210 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
# Run a concurrency test on the double_entry code.
|
4
|
-
#
|
5
|
-
# This spawns a bunch of processes, and does random transactions between a set
|
6
|
-
# of accounts, then validates that all the numbers add up at the end.
|
7
|
-
#
|
8
|
-
# You can also tell it to flush our the account balances table at regular
|
9
|
-
# intervals, to validate that new account balances records get created with the
|
10
|
-
# correct balances from the lines table.
|
11
|
-
#
|
12
|
-
# Run it without arguments to get the usage.
|
13
|
-
|
14
|
-
require 'optparse'
|
15
|
-
require 'bundler/setup'
|
16
|
-
require 'active_record'
|
17
|
-
require 'database_cleaner'
|
18
|
-
|
19
|
-
support = File.expand_path("../../spec/support/", __FILE__)
|
20
|
-
|
21
|
-
db_engine = ENV['DB'] || 'mysql'
|
22
|
-
|
23
|
-
if db_engine == 'sqlite'
|
24
|
-
puts "Skipping jackhammer for SQLITE..."
|
25
|
-
exit 0
|
26
|
-
end
|
27
|
-
|
28
|
-
ActiveRecord::Base.establish_connection YAML.load_file(File.join(support, "database.yml"))[db_engine]
|
29
|
-
require "#{support}/schema"
|
30
|
-
|
31
|
-
lib = File.expand_path('../../lib', __FILE__)
|
32
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
33
|
-
require 'double_entry'
|
34
|
-
|
35
|
-
def parse_options
|
36
|
-
$account_count = 5
|
37
|
-
$process_count = 20
|
38
|
-
$transfer_count = 20000
|
39
|
-
$balance_flush_count = 1
|
40
|
-
$use_threads = false
|
41
|
-
|
42
|
-
options = OptionParser.new
|
43
|
-
|
44
|
-
options.on("-a", "--accounts=COUNT", Integer, "Number of accounts (default: #{$account_count})") do |value|
|
45
|
-
$account_count = value
|
46
|
-
end
|
47
|
-
|
48
|
-
options.on("-p", "--processes=COUNT", Integer, "Number of processes (default: #{$process_count})") do |value|
|
49
|
-
$process_count = value
|
50
|
-
end
|
51
|
-
|
52
|
-
options.on("-t", "--transfers=COUNT", Integer, "Number of transfers (default: #{$transfer_count})") do |value|
|
53
|
-
$transfer_count = value
|
54
|
-
end
|
55
|
-
|
56
|
-
options.on("-f", "--flush-balances=COUNT", Integer, "Flush account balances table COUNT times") do |value|
|
57
|
-
$balance_flush_count = value
|
58
|
-
end
|
59
|
-
|
60
|
-
options.on("-z", "--threads", "Use threads instead of processes") do |value|
|
61
|
-
$use_threads = !!value
|
62
|
-
end
|
63
|
-
|
64
|
-
options.parse(*ARGV)
|
65
|
-
end
|
66
|
-
|
67
|
-
|
68
|
-
def clean_out_database
|
69
|
-
puts "Cleaning out the database..."
|
70
|
-
|
71
|
-
DatabaseCleaner.clean_with(:truncation)
|
72
|
-
end
|
73
|
-
|
74
|
-
def create_accounts_and_transfers
|
75
|
-
puts "Setting up #{$account_count} accounts..."
|
76
|
-
|
77
|
-
DoubleEntry.configure do |config|
|
78
|
-
|
79
|
-
# Create the accounts.
|
80
|
-
config.define_accounts do |accounts|
|
81
|
-
scope = ->(x) { x }
|
82
|
-
$account_count.times do |i|
|
83
|
-
accounts.define(:identifier => :"account-#{i}", :scope_identifier => scope)
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
# Create all the possible transfers.
|
88
|
-
config.define_transfers do |transfers|
|
89
|
-
config.accounts.each do |from|
|
90
|
-
config.accounts.each do |to|
|
91
|
-
transfers.define(:from => from.identifier, :to => to.identifier, :code => :test)
|
92
|
-
end
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
# Find account instances so we have something to work with.
|
97
|
-
$accounts = config.accounts.map do |account|
|
98
|
-
DoubleEntry.account(account.identifier, :scope => 1)
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
|
104
|
-
def run_tests
|
105
|
-
puts "Spawning #{$process_count} processes..."
|
106
|
-
|
107
|
-
iterations_per_process = [ ($transfer_count / $process_count / $balance_flush_count), 1 ].max
|
108
|
-
|
109
|
-
$balance_flush_count.times do
|
110
|
-
puts "Flushing balances"
|
111
|
-
DoubleEntry::AccountBalance.delete_all
|
112
|
-
ActiveRecord::Base.connection_pool.disconnect!
|
113
|
-
|
114
|
-
if $use_threads
|
115
|
-
puts "Using threads as workers"
|
116
|
-
threads = []
|
117
|
-
$process_count.times do |process_num|
|
118
|
-
threads << Thread.new { run_process(iterations_per_process, process_num) }
|
119
|
-
end
|
120
|
-
|
121
|
-
threads.each(&:join)
|
122
|
-
else
|
123
|
-
puts "Using processes as workers"
|
124
|
-
pids = []
|
125
|
-
$process_count.times do |process_num|
|
126
|
-
pids << fork { run_process(iterations_per_process, process_num) }
|
127
|
-
end
|
128
|
-
|
129
|
-
pids.each {|pid| Process.wait2(pid) }
|
130
|
-
end
|
131
|
-
end
|
132
|
-
end
|
133
|
-
|
134
|
-
|
135
|
-
def run_process(iterations, process_num)
|
136
|
-
srand # Seed the random number generator separately for each process.
|
137
|
-
|
138
|
-
puts "Process #{process_num} running #{iterations} transfers..."
|
139
|
-
|
140
|
-
iterations.times do |i|
|
141
|
-
account_a = $accounts.sample
|
142
|
-
account_b = ($accounts - [account_a]).sample
|
143
|
-
amount = rand(1000) + 1
|
144
|
-
|
145
|
-
DoubleEntry.transfer(Money.new(amount), :from => account_a, :to => account_b, :code => :test)
|
146
|
-
|
147
|
-
puts "Process #{process_num} completed #{i+1} transfers" if (i+1) % 100 == 0
|
148
|
-
end
|
149
|
-
end
|
150
|
-
|
151
|
-
|
152
|
-
def reconcile
|
153
|
-
error_count = 0
|
154
|
-
puts "Reconciling..."
|
155
|
-
|
156
|
-
if DoubleEntry::Line.count == $transfer_count * 2
|
157
|
-
puts "All the Line records were written, FTW!"
|
158
|
-
else
|
159
|
-
puts "Not enough Line records written. :("
|
160
|
-
error_count += 1
|
161
|
-
end
|
162
|
-
|
163
|
-
if $accounts.all? {|account| DoubleEntry::Reporting.reconciled?(account) }
|
164
|
-
puts "All accounts reconciled, FTW!"
|
165
|
-
else
|
166
|
-
$accounts.each do |account|
|
167
|
-
if !DoubleEntry::Reporting.reconciled?(account)
|
168
|
-
puts "Account #{account.identifier} failed to reconcile. :("
|
169
|
-
|
170
|
-
# See http://bugs.mysql.com/bug.php?id=51431
|
171
|
-
use_index = if DoubleEntry::Line.connection.adapter_name.match /mysql/i
|
172
|
-
"USE INDEX (lines_scope_account_id_idx)"
|
173
|
-
else
|
174
|
-
""
|
175
|
-
end
|
176
|
-
|
177
|
-
rows = ActiveRecord::Base.connection.select_all(<<-SQL)
|
178
|
-
SELECT id, amount, balance
|
179
|
-
FROM #{DoubleEntry::Line.quoted_table_name} #{use_index}
|
180
|
-
WHERE scope = '#{account.scope_identity}'
|
181
|
-
AND account = '#{account.identifier}'
|
182
|
-
ORDER BY id
|
183
|
-
SQL
|
184
|
-
|
185
|
-
rows.each_cons(2) do |a, b|
|
186
|
-
if a["balance"].to_i + b["amount"].to_i != b["balance"].to_i
|
187
|
-
puts "Bad lines entry id = #{b['id']}"
|
188
|
-
error_count += 1
|
189
|
-
end
|
190
|
-
end
|
191
|
-
end
|
192
|
-
end
|
193
|
-
end
|
194
|
-
|
195
|
-
error_count == 0
|
196
|
-
end
|
197
|
-
|
198
|
-
|
199
|
-
parse_options
|
200
|
-
clean_out_database
|
201
|
-
create_accounts_and_transfers
|
202
|
-
run_tests
|
203
|
-
|
204
|
-
if reconcile
|
205
|
-
puts "Done successfully :)"
|
206
|
-
exit 0
|
207
|
-
else
|
208
|
-
puts "Done with errors :("
|
209
|
-
exit 1
|
210
|
-
end
|
data/script/setup.sh
DELETED
@@ -1,110 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
|
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...')
|
7
|
-
|
8
|
-
context '#restartable_transaction' do
|
9
|
-
it "keeps running the lock until a ActiveRecord::RestartTransaction isn't raised" do
|
10
|
-
expect(User).to receive(:create!).ordered.and_raise(ActiveRecord::RestartTransaction)
|
11
|
-
expect(User).to receive(:create!).ordered.and_raise(ActiveRecord::RestartTransaction)
|
12
|
-
expect(User).to receive(:create!).ordered.and_return(true)
|
13
|
-
|
14
|
-
expect { User.restartable_transaction { User.create! } }.to_not raise_error
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
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
|
-
end
|
24
|
-
|
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
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
context 'mysql' do
|
34
|
-
let(:exception) { MYSQL_DEADLOCK }
|
35
|
-
|
36
|
-
it_behaves_like 'abstract adapter'
|
37
|
-
end
|
38
|
-
|
39
|
-
context 'postgres' do
|
40
|
-
let(:exception) { PG_DEADLOCK }
|
41
|
-
|
42
|
-
it_behaves_like 'abstract adapter'
|
43
|
-
end
|
44
|
-
|
45
|
-
context 'sqlite' do
|
46
|
-
let(:exception) { SQLITE3_LOCK }
|
47
|
-
|
48
|
-
it_behaves_like 'abstract adapter'
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
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'
|
55
|
-
|
56
|
-
expect { User.make! :username => 'keith' }.to raise_error
|
57
|
-
expect { User.create_ignoring_duplicates! :username => 'keith' }.to_not raise_error
|
58
|
-
end
|
59
|
-
|
60
|
-
it 'publishes a notification when a duplicate is encountered' do
|
61
|
-
User.make! :username => 'keith'
|
62
|
-
|
63
|
-
expect(ActiveSupport::Notifications).
|
64
|
-
to receive(:publish).
|
65
|
-
with('duplicate_ignore.active_record', hash_including(:exception => kind_of(ActiveRecord::RecordNotUnique)))
|
66
|
-
|
67
|
-
expect { User.create_ignoring_duplicates! :username => 'keith' }.to_not raise_error
|
68
|
-
end
|
69
|
-
|
70
|
-
shared_examples 'abstract adapter' do
|
71
|
-
it 'retries the creation if a deadlock error is raised from the database' do
|
72
|
-
expect(User).to receive(:create!).ordered.and_raise(exception)
|
73
|
-
expect(User).to receive(:create!).ordered.and_return(true)
|
74
|
-
|
75
|
-
expect { User.create_ignoring_duplicates! }.to_not raise_error
|
76
|
-
end
|
77
|
-
|
78
|
-
it 'publishes a notification on each retry' do
|
79
|
-
expect(User).to receive(:create!).ordered.and_raise(exception)
|
80
|
-
expect(User).to receive(:create!).ordered.and_raise(exception)
|
81
|
-
expect(User).to receive(:create!).ordered.and_return(true)
|
82
|
-
|
83
|
-
expect(ActiveSupport::Notifications).
|
84
|
-
to receive(:publish).
|
85
|
-
with('deadlock_retry.active_record', hash_including(:exception => exception)).
|
86
|
-
twice
|
87
|
-
|
88
|
-
expect { User.create_ignoring_duplicates! }.to_not raise_error
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
context 'mysql' do
|
93
|
-
let(:exception) { MYSQL_DEADLOCK }
|
94
|
-
|
95
|
-
it_behaves_like 'abstract adapter'
|
96
|
-
end
|
97
|
-
|
98
|
-
context 'postgres' do
|
99
|
-
let(:exception) { PG_DEADLOCK }
|
100
|
-
|
101
|
-
it_behaves_like 'abstract adapter'
|
102
|
-
end
|
103
|
-
|
104
|
-
context 'sqlite' do
|
105
|
-
let(:exception) { SQLITE3_LOCK }
|
106
|
-
|
107
|
-
it_behaves_like 'abstract adapter'
|
108
|
-
end
|
109
|
-
end
|
110
|
-
end
|
@@ -1,130 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
module DoubleEntry
|
3
|
-
RSpec.describe Account do
|
4
|
-
let(:identity_scope) { ->(value) { value } }
|
5
|
-
|
6
|
-
describe '::new' do
|
7
|
-
context 'given an identifier 31 characters in length' do
|
8
|
-
let(:identifier) { 'xxxxxxxx 31 characters xxxxxxxx' }
|
9
|
-
specify do
|
10
|
-
expect { Account.new(:identifier => identifier) }.to_not raise_error
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
context 'given an identifier 32 characters in length' do
|
15
|
-
let(:identifier) { 'xxxxxxxx 32 characters xxxxxxxxx' }
|
16
|
-
specify do
|
17
|
-
expect { Account.new(:identifier => identifier) }.to raise_error AccountIdentifierTooLongError, /'#{identifier}'/
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
describe Account::Instance do
|
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')
|
27
|
-
expect([b, a].sort).to eq [a, b]
|
28
|
-
end
|
29
|
-
|
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')
|
35
|
-
|
36
|
-
expect(a1.hash).to eq a2.hash
|
37
|
-
expect(a1.hash).to_not eq b.hash
|
38
|
-
end
|
39
|
-
|
40
|
-
describe '::new' do
|
41
|
-
let(:account) { Account.new(:identifier => 'x', :scope_identifier => identity_scope) }
|
42
|
-
subject(:initialize_account_instance) { Account::Instance.new(:account => account, :scope => scope) }
|
43
|
-
|
44
|
-
context 'given a scope identifier 23 characters in length' do
|
45
|
-
let(:scope) { 'xxxx 23 characters xxxx' }
|
46
|
-
specify { expect { initialize_account_instance }.to_not raise_error }
|
47
|
-
end
|
48
|
-
|
49
|
-
context 'given a scope identifier 24 characters in length' do
|
50
|
-
let(:scope) { 'xxxx 24 characters xxxxx' }
|
51
|
-
specify { expect { initialize_account_instance }.to raise_error ScopeIdentifierTooLongError, /'#{scope}'/ }
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
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')
|
60
|
-
end
|
61
|
-
|
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')
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
describe Account::Set do
|
69
|
-
describe '#define' do
|
70
|
-
context "given a 'savings' account is defined" do
|
71
|
-
before { subject.define(:identifier => 'savings') }
|
72
|
-
its(:first) { should be_an Account }
|
73
|
-
its('first.identifier') { should eq 'savings' }
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
describe '#active_record_scope_identifier' do
|
78
|
-
subject(:scope) { Account::Set.new.active_record_scope_identifier(ar_class) }
|
79
|
-
|
80
|
-
context 'given ActiveRecordScopeFactory is stubbed' do
|
81
|
-
let(:scope_identifier) { double(:scope_identifier) }
|
82
|
-
let(:scope_factory) { double(:scope_factory, :scope_identifier => scope_identifier) }
|
83
|
-
let(:ar_class) { double(:ar_class) }
|
84
|
-
before { allow(Account::ActiveRecordScopeFactory).to receive(:new).with(ar_class).and_return(scope_factory) }
|
85
|
-
|
86
|
-
it { should eq scope_identifier }
|
87
|
-
end
|
88
|
-
end
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
RSpec.describe Account::ActiveRecordScopeFactory do
|
93
|
-
context 'given the class User' do
|
94
|
-
subject(:factory) { Account::ActiveRecordScopeFactory.new(User) }
|
95
|
-
|
96
|
-
describe '#scope_identifier' do
|
97
|
-
subject(:scope_identifier) { factory.scope_identifier }
|
98
|
-
|
99
|
-
describe '#call' do
|
100
|
-
subject(:scope) { scope_identifier.call(value) }
|
101
|
-
|
102
|
-
context 'given a User instance with ID 32' do
|
103
|
-
let(:value) { User.make(:id => 32) }
|
104
|
-
|
105
|
-
it { should eq 32 }
|
106
|
-
end
|
107
|
-
|
108
|
-
context 'given differing model instance with ID 32' do
|
109
|
-
let(:value) { double(:id => 32) }
|
110
|
-
it 'raises an error' do
|
111
|
-
expect { scope_identifier.call(value) }.to raise_error DoubleEntry::AccountScopeMismatchError
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
context "given the String 'I am a bearded lady'" do
|
116
|
-
let(:value) { 'I am a bearded lady' }
|
117
|
-
|
118
|
-
it { should eq 'I am a bearded lady' }
|
119
|
-
end
|
120
|
-
|
121
|
-
context 'given the Integer 42' do
|
122
|
-
let(:value) { 42 }
|
123
|
-
|
124
|
-
it { should eq 42 }
|
125
|
-
end
|
126
|
-
end
|
127
|
-
end
|
128
|
-
end
|
129
|
-
end
|
130
|
-
end
|