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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a89a7b1a2bfa39a0c51b309f107fc625af9643f0
|
4
|
+
data.tar.gz: 1ac1862d044333c98bb26e78c6ba7d2c508203b4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 235592b9bce67bfdbb4aa474e722a9dd54573ad495797fc1338521d6807e7d51648c7d3eb6258bbde8bdfa8b4b04a5fb6993dca63f21aadd123b9d76d4364dd3
|
7
|
+
data.tar.gz: 28f0afca58a850bca2efc4a28bfa967639f54e383b5b16cc9e5959cef65b5eb8f3d8a72efb0e519f745e9491252fe2ff5c7ca86fa90168e6e41da9a2e1152765
|
data/.rspec
CHANGED
@@ -1 +1,2 @@
|
|
1
|
-
--
|
1
|
+
--color
|
2
|
+
--require spec_helper
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
AllCops:
|
2
|
+
Exclude:
|
3
|
+
- .bundle/**/*
|
4
|
+
- bin/**/*
|
5
|
+
- tmp/**/*
|
6
|
+
- script/jack_hammer
|
7
|
+
- spec/support/schema.rb
|
8
|
+
- lib/generators/double_entry/install/templates/**/*
|
9
|
+
|
10
|
+
Style/AccessModifierIndentation:
|
11
|
+
EnforcedStyle: outdent
|
12
|
+
|
13
|
+
Style/DotPosition:
|
14
|
+
EnforcedStyle: trailing
|
15
|
+
|
16
|
+
Style/GuardClause:
|
17
|
+
Enabled: false
|
18
|
+
|
19
|
+
Style/HashSyntax:
|
20
|
+
EnforcedStyle: hash_rockets
|
21
|
+
|
22
|
+
Style/SpecialGlobalVars:
|
23
|
+
Enabled: false
|
24
|
+
|
25
|
+
Style/TrailingComma:
|
26
|
+
EnforcedStyleForMultiline: comma
|
27
|
+
|
28
|
+
# TODO Enable these checks and fix codes
|
29
|
+
|
30
|
+
Style/ClassVars:
|
31
|
+
Enabled: false
|
32
|
+
|
33
|
+
Style/Documentation:
|
34
|
+
Enabled: false
|
35
|
+
|
36
|
+
Style/DoubleNegation:
|
37
|
+
Enabled: false
|
38
|
+
|
39
|
+
Style/ModuleFunction:
|
40
|
+
Enabled: false
|
41
|
+
|
42
|
+
Metrics/AbcSize:
|
43
|
+
Max: 47 #15
|
44
|
+
|
45
|
+
Metrics/CyclomaticComplexity:
|
46
|
+
Max: 13 #6
|
47
|
+
|
48
|
+
Metrics/LineLength:
|
49
|
+
Max: 185
|
50
|
+
|
51
|
+
Metrics/MethodLength:
|
52
|
+
Max: 30 # 10
|
53
|
+
|
54
|
+
Metrics/PerceivedComplexity:
|
55
|
+
Max: 13 #7
|
data/.travis.yml
CHANGED
@@ -1,12 +1,4 @@
|
|
1
1
|
language: ruby
|
2
|
-
rvm:
|
3
|
-
- 1.9.3
|
4
|
-
- 2.0.0
|
5
|
-
- 2.1.5
|
6
|
-
env:
|
7
|
-
- DB=mysql
|
8
|
-
- DB=postgres
|
9
|
-
- DB=sqlite
|
10
2
|
before_script:
|
11
3
|
- cp spec/support/database.travis.yml spec/support/database.yml
|
12
4
|
- mysql -e 'create database double_entry_test;'
|
@@ -14,7 +6,26 @@ before_script:
|
|
14
6
|
script:
|
15
7
|
- rake spec
|
16
8
|
- ruby script/jack_hammer -t 2000
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
9
|
+
matrix:
|
10
|
+
include:
|
11
|
+
- rvm: 1.9.3
|
12
|
+
gemfile: spec/support/gemfiles/Gemfile.rails-4.2.x
|
13
|
+
env: DB=mysql
|
14
|
+
- rvm: 2.0.0
|
15
|
+
gemfile: spec/support/gemfiles/Gemfile.rails-4.2.x
|
16
|
+
env: DB=mysql
|
17
|
+
- rvm: 2.2.0
|
18
|
+
gemfile: spec/support/gemfiles/Gemfile.rails-4.1.x
|
19
|
+
env: DB=mysql
|
20
|
+
- rvm: 2.2.0
|
21
|
+
gemfile: spec/support/gemfiles/Gemfile.rails-4.2.x
|
22
|
+
env: DB=mysql
|
23
|
+
- rvm: 2.2.0
|
24
|
+
gemfile: spec/support/gemfiles/Gemfile.rails-4.2.x
|
25
|
+
env: DB=sqlite
|
26
|
+
- rvm: 2.2.0
|
27
|
+
gemfile: spec/support/gemfiles/Gemfile.rails-4.2.x
|
28
|
+
env: DB=postgres
|
29
|
+
- rvm: 2.1.5
|
30
|
+
gemfile: spec/support/gemfiles/Gemfile.rails-3.2.x
|
31
|
+
env: DB=mysql
|
data/README.md
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
# DoubleEntry
|
2
2
|
|
3
|
+
|
4
|
+
[![License MIT](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://github.com/envato/double_entry/blob/master/LICENSE.md)
|
3
5
|
[![Gem Version](https://badge.fury.io/rb/double_entry.svg)](http://badge.fury.io/rb/double_entry)
|
4
|
-
[![Build Status](https://travis-ci.org/envato/double_entry.svg)](https://travis-ci.org/envato/double_entry)
|
6
|
+
[![Build Status](https://travis-ci.org/envato/double_entry.svg?branch=master)](https://travis-ci.org/envato/double_entry)
|
5
7
|
[![Code Climate](https://codeclimate.com/github/envato/double_entry.png)](https://codeclimate.com/github/envato/double_entry)
|
6
8
|
|
7
9
|
![Show me the Money](http://24.media.tumblr.com/tumblr_m3bwbqNJIG1rrgbmqo1_500.gif)
|
@@ -21,7 +23,9 @@ DoubleEntry is tested against:
|
|
21
23
|
|
22
24
|
Ruby
|
23
25
|
* 1.9.3
|
26
|
+
* 2.0.0
|
24
27
|
* 2.1.5
|
28
|
+
* 2.2.0
|
25
29
|
|
26
30
|
Rails
|
27
31
|
* 3.2.x
|
data/Rakefile
CHANGED
@@ -1,14 +1,19 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require 'rspec/core/rake_task'
|
2
|
+
require 'bundler/gem_tasks'
|
3
|
+
require 'rubocop/rake_task'
|
3
4
|
|
4
5
|
RSpec::Core::RakeTask.new(:spec) do |t|
|
5
6
|
t.verbose = false
|
7
|
+
t.ruby_opts = '-w'
|
6
8
|
end
|
7
9
|
|
10
|
+
RuboCop::RakeTask.new(:rubocop)
|
11
|
+
|
8
12
|
task :default do
|
9
13
|
%w(mysql postgres sqlite).each do |db|
|
10
14
|
puts "Running tests with `DB=#{db}`"
|
11
15
|
ENV['DB'] = db
|
12
|
-
Rake::Task[
|
16
|
+
Rake::Task['spec'].execute
|
13
17
|
end
|
18
|
+
Rake::Task['rubocop'].execute
|
14
19
|
end
|
data/double_entry.gemspec
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
# encoding: utf-8
|
2
|
-
|
3
2
|
lib = File.expand_path('../lib', __FILE__)
|
4
3
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
4
|
|
@@ -10,12 +9,11 @@ Gem::Specification.new do |gem|
|
|
10
9
|
gem.version = DoubleEntry::VERSION
|
11
10
|
gem.authors = ['Anthony Sellitti', 'Keith Pitt', 'Martin Jagusch', 'Martin Spickermann', 'Mark Turnley', 'Orien Madgwick', 'Pete Yandall', 'Stephanie Staub']
|
12
11
|
gem.email = ['anthony.sellitti@envato.com', 'me@keithpitt.com', '_@mj.io', 'spickemann@gmail.com', 'mark@envato.com', '_@orien.io', 'pete@envato.com', 'staub.steph@gmail.com']
|
13
|
-
# gem.description = %q{}
|
14
12
|
gem.summary = 'Tools to build your double entry financial ledger'
|
15
13
|
gem.homepage = 'https://github.com/envato/double_entry'
|
16
14
|
|
17
15
|
gem.files = `git ls-files`.split($/)
|
18
|
-
gem.executables = gem.files.grep(%r{
|
16
|
+
gem.executables = gem.files.grep(%r{bin/}).map { |f| File.basename(f) }
|
19
17
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
20
18
|
gem.require_paths = ['lib']
|
21
19
|
|
@@ -28,10 +26,13 @@ Gem::Specification.new do |gem|
|
|
28
26
|
gem.add_development_dependency 'mysql2'
|
29
27
|
gem.add_development_dependency 'pg'
|
30
28
|
gem.add_development_dependency 'sqlite3'
|
29
|
+
|
31
30
|
gem.add_development_dependency 'rspec'
|
32
31
|
gem.add_development_dependency 'rspec-its'
|
32
|
+
gem.add_development_dependency 'rspec-instafail'
|
33
33
|
gem.add_development_dependency 'database_cleaner'
|
34
34
|
gem.add_development_dependency 'generator_spec'
|
35
35
|
gem.add_development_dependency 'machinist'
|
36
36
|
gem.add_development_dependency 'timecop'
|
37
|
+
gem.add_development_dependency 'rubocop'
|
37
38
|
end
|
@@ -1,34 +1,28 @@
|
|
1
1
|
# encoding: utf-8
|
2
|
-
require
|
2
|
+
require 'active_support/notifications'
|
3
3
|
|
4
4
|
module ActiveRecord
|
5
|
-
|
6
5
|
# These methods are available as class methods on ActiveRecord::Base.
|
7
6
|
module LockingExtensions
|
8
|
-
|
9
7
|
# Execute the given block within a database transaction, and retry the
|
10
8
|
# transaction from the beginning if a RestartTransaction exception is raised.
|
11
9
|
def restartable_transaction(&block)
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
retry
|
16
|
-
end
|
10
|
+
transaction(&block)
|
11
|
+
rescue ActiveRecord::RestartTransaction
|
12
|
+
retry
|
17
13
|
end
|
18
14
|
|
19
15
|
# Execute the given block, and retry the current restartable transaction if a
|
20
16
|
# MySQL deadlock occurs.
|
21
17
|
def with_restart_on_deadlock
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
ActiveSupport::Notifications.publish("deadlock_restart.active_record", :exception => exception)
|
18
|
+
yield
|
19
|
+
rescue ActiveRecord::StatementInvalid => exception
|
20
|
+
if exception.message =~ /deadlock/i || exception.message =~ /database is locked/i
|
21
|
+
ActiveSupport::Notifications.publish('deadlock_restart.active_record', :exception => exception)
|
27
22
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
end
|
23
|
+
raise ActiveRecord::RestartTransaction
|
24
|
+
else
|
25
|
+
raise
|
32
26
|
end
|
33
27
|
end
|
34
28
|
|
@@ -42,23 +36,21 @@ module ActiveRecord
|
|
42
36
|
end
|
43
37
|
end
|
44
38
|
|
45
|
-
|
39
|
+
private
|
46
40
|
|
47
41
|
def ignoring_duplicates
|
48
42
|
# Error examples:
|
49
43
|
# PG::Error: ERROR: duplicate key value violates unique constraint
|
50
44
|
# Mysql2::Error: Duplicate entry 'keith' for key 'index_users_on_username': INSERT INTO `users...
|
51
45
|
# ActiveRecord::RecordNotUnique SQLite3::ConstraintException: column username is not unique: INSERT INTO "users"...
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
ActiveSupport::Notifications.publish("duplicate_ignore.active_record", :exception => exception)
|
46
|
+
yield
|
47
|
+
rescue ActiveRecord::StatementInvalid, ActiveRecord::RecordNotUnique => exception
|
48
|
+
if exception.message =~ /duplicate/i || exception.message =~ /ConstraintException/
|
49
|
+
ActiveSupport::Notifications.publish('duplicate_ignore.active_record', :exception => exception)
|
57
50
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
end
|
51
|
+
# Just ignore it...someone else has already created the record.
|
52
|
+
else
|
53
|
+
raise
|
62
54
|
end
|
63
55
|
end
|
64
56
|
|
@@ -66,18 +58,16 @@ module ActiveRecord
|
|
66
58
|
# Error examples:
|
67
59
|
# PG::Error: ERROR: deadlock detected
|
68
60
|
# Mysql::Error: Deadlock found when trying to get lock
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
ActiveSupport::Notifications.publish("deadlock_retry.active_record", :exception => exception)
|
61
|
+
yield
|
62
|
+
rescue ActiveRecord::StatementInvalid, ActiveRecord::RecordNotUnique => exception
|
63
|
+
if exception.message =~ /deadlock/i || exception.message =~ /database is locked/i
|
64
|
+
# Somebody else is in the midst of creating the record. We'd better
|
65
|
+
# retry, so we ensure they're done before we move on.
|
66
|
+
ActiveSupport::Notifications.publish('deadlock_retry.active_record', :exception => exception)
|
76
67
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
end
|
68
|
+
retry
|
69
|
+
else
|
70
|
+
raise
|
81
71
|
end
|
82
72
|
end
|
83
73
|
end
|
@@ -85,8 +75,6 @@ module ActiveRecord
|
|
85
75
|
# Raise this inside a restartable_transaction to retry the transaction from the beginning.
|
86
76
|
class RestartTransaction < RuntimeError
|
87
77
|
end
|
88
|
-
|
89
78
|
end
|
90
79
|
|
91
|
-
|
92
80
|
ActiveRecord::Base.extend(ActiveRecord::LockingExtensions)
|
@@ -1,21 +1,21 @@
|
|
1
1
|
# encoding: utf-8
|
2
|
-
require
|
2
|
+
require 'active_support/log_subscriber'
|
3
3
|
|
4
4
|
module ActiveRecord
|
5
5
|
module LockingExtensions
|
6
6
|
class LogSubscriber < ActiveSupport::LogSubscriber
|
7
7
|
def deadlock_restart(event)
|
8
|
-
info
|
8
|
+
info 'Deadlock causing restart'
|
9
9
|
debug event[:exception]
|
10
10
|
end
|
11
11
|
|
12
12
|
def deadlock_retry(event)
|
13
|
-
info
|
13
|
+
info 'Deadlock causing retry'
|
14
14
|
debug event[:exception]
|
15
15
|
end
|
16
16
|
|
17
17
|
def duplicate_ignore(event)
|
18
|
-
info
|
18
|
+
info 'Duplicate ignored'
|
19
19
|
debug event[:exception]
|
20
20
|
end
|
21
21
|
|
data/lib/double_entry.rb
CHANGED
@@ -23,9 +23,7 @@ require 'double_entry/validation'
|
|
23
23
|
# This module provides the public interfaces for everything to do with
|
24
24
|
# transferring money around the system.
|
25
25
|
module DoubleEntry
|
26
|
-
|
27
26
|
class << self
|
28
|
-
|
29
27
|
# Get the particular account instance with the provided identifier and
|
30
28
|
# scope.
|
31
29
|
#
|
data/lib/double_entry/account.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
module DoubleEntry
|
3
3
|
class Account
|
4
|
-
|
5
4
|
class << self
|
6
5
|
attr_writer :accounts, :scope_identifier_max_length, :account_identifier_max_length
|
7
6
|
|
@@ -39,18 +38,18 @@ module DoubleEntry
|
|
39
38
|
end
|
40
39
|
|
41
40
|
def find(identifier, scoped)
|
42
|
-
|
41
|
+
found_account = detect do |account|
|
43
42
|
account.identifier == identifier && account.scoped? == scoped
|
44
43
|
end
|
45
|
-
|
46
|
-
|
44
|
+
fail UnknownAccount, "account: #{identifier} scoped?: #{scoped}" unless found_account
|
45
|
+
found_account
|
47
46
|
end
|
48
47
|
|
49
48
|
def <<(account)
|
50
49
|
if any? { |a| a.identifier == account.identifier }
|
51
|
-
|
50
|
+
fail DuplicateAccount
|
52
51
|
else
|
53
|
-
super
|
52
|
+
super
|
54
53
|
end
|
55
54
|
end
|
56
55
|
|
@@ -72,7 +71,7 @@ module DoubleEntry
|
|
72
71
|
when String, Fixnum
|
73
72
|
value
|
74
73
|
else
|
75
|
-
|
74
|
+
fail AccountScopeMismatchError, "Expected instance of `#{@active_record_class}`, received instance of `#{value.class}`"
|
76
75
|
end
|
77
76
|
end
|
78
77
|
end
|
@@ -115,8 +114,8 @@ module DoubleEntry
|
|
115
114
|
self == other
|
116
115
|
end
|
117
116
|
|
118
|
-
def <=>(
|
119
|
-
[scope_identity.to_s, identifier.to_s] <=> [
|
117
|
+
def <=>(other)
|
118
|
+
[scope_identity.to_s, identifier.to_s] <=> [other.scope_identity.to_s, other.identifier.to_s]
|
120
119
|
end
|
121
120
|
|
122
121
|
def hash
|
@@ -135,14 +134,13 @@ module DoubleEntry
|
|
135
134
|
to_s
|
136
135
|
end
|
137
136
|
|
138
|
-
|
137
|
+
private
|
139
138
|
|
140
139
|
def ensure_scope_is_valid
|
141
140
|
identity = scope_identity
|
142
141
|
if identity && identity.length > Account.scope_identifier_max_length
|
143
|
-
|
144
|
-
|
145
|
-
)
|
142
|
+
fail ScopeIdentifierTooLongError,
|
143
|
+
"scope identifier '#{identity}' is too long. Please limit it to #{Account.scope_identifier_max_length} characters."
|
146
144
|
end
|
147
145
|
end
|
148
146
|
end
|
@@ -156,9 +154,8 @@ module DoubleEntry
|
|
156
154
|
@negative_only = args[:negative_only]
|
157
155
|
@currency = args[:currency] || Money.default_currency
|
158
156
|
if identifier.length > Account.account_identifier_max_length
|
159
|
-
|
160
|
-
|
161
|
-
)
|
157
|
+
fail AccountIdentifierTooLongError,
|
158
|
+
"account identifier '#{identifier}' is too long. Please limit it to #{Account.account_identifier_max_length} characters."
|
162
159
|
end
|
163
160
|
end
|
164
161
|
|
@@ -1,6 +1,5 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
module DoubleEntry
|
3
|
-
|
4
3
|
# Account balance records cache the current balance for each account. They
|
5
4
|
# also provide a database representation of an account that we can use to do
|
6
5
|
# DB level locking.
|
@@ -9,7 +8,6 @@ module DoubleEntry
|
|
9
8
|
#
|
10
9
|
# Account balances are created on demand when transfers occur.
|
11
10
|
class AccountBalance < ActiveRecord::Base
|
12
|
-
|
13
11
|
delegate :currency, :to => :account
|
14
12
|
|
15
13
|
def balance
|
@@ -35,7 +33,5 @@ module DoubleEntry
|
|
35
33
|
scope = scope.lock(true) if options[:lock]
|
36
34
|
scope.first
|
37
35
|
end
|
38
|
-
|
39
36
|
end
|
40
|
-
|
41
37
|
end
|