double_entry 0.0.1.pre → 0.1.0.pre.pre.alpha

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +13 -5
  2. data/.gitignore +5 -6
  3. data/.rspec +1 -0
  4. data/.travis.yml +19 -0
  5. data/.yardopts +2 -0
  6. data/Gemfile +0 -1
  7. data/LICENSE.md +19 -0
  8. data/README.md +221 -14
  9. data/Rakefile +12 -0
  10. data/double_entry.gemspec +30 -15
  11. data/gemfiles/Gemfile.rails-3.2.0 +5 -0
  12. data/gemfiles/Gemfile.rails-4.0.0 +5 -0
  13. data/gemfiles/Gemfile.rails-4.1.0 +5 -0
  14. data/lib/active_record/locking_extensions.rb +61 -0
  15. data/lib/double_entry.rb +267 -2
  16. data/lib/double_entry/account.rb +82 -0
  17. data/lib/double_entry/account_balance.rb +31 -0
  18. data/lib/double_entry/aggregate.rb +118 -0
  19. data/lib/double_entry/aggregate_array.rb +65 -0
  20. data/lib/double_entry/configurable.rb +52 -0
  21. data/lib/double_entry/day_range.rb +38 -0
  22. data/lib/double_entry/hour_range.rb +40 -0
  23. data/lib/double_entry/line.rb +147 -0
  24. data/lib/double_entry/line_aggregate.rb +37 -0
  25. data/lib/double_entry/line_check.rb +118 -0
  26. data/lib/double_entry/locking.rb +187 -0
  27. data/lib/double_entry/month_range.rb +92 -0
  28. data/lib/double_entry/reporting.rb +16 -0
  29. data/lib/double_entry/time_range.rb +55 -0
  30. data/lib/double_entry/time_range_array.rb +43 -0
  31. data/lib/double_entry/transfer.rb +70 -0
  32. data/lib/double_entry/version.rb +3 -1
  33. data/lib/double_entry/week_range.rb +99 -0
  34. data/lib/double_entry/year_range.rb +39 -0
  35. data/lib/generators/double_entry/install/install_generator.rb +22 -0
  36. data/lib/generators/double_entry/install/templates/migration.rb +68 -0
  37. data/script/jack_hammer +201 -0
  38. data/script/setup.sh +8 -0
  39. data/spec/active_record/locking_extensions_spec.rb +54 -0
  40. data/spec/double_entry/account_balance_spec.rb +8 -0
  41. data/spec/double_entry/account_spec.rb +23 -0
  42. data/spec/double_entry/aggregate_array_spec.rb +75 -0
  43. data/spec/double_entry/aggregate_spec.rb +168 -0
  44. data/spec/double_entry/double_entry_spec.rb +391 -0
  45. data/spec/double_entry/line_aggregate_spec.rb +8 -0
  46. data/spec/double_entry/line_check_spec.rb +88 -0
  47. data/spec/double_entry/line_spec.rb +72 -0
  48. data/spec/double_entry/locking_spec.rb +154 -0
  49. data/spec/double_entry/month_range_spec.rb +131 -0
  50. data/spec/double_entry/reporting_spec.rb +25 -0
  51. data/spec/double_entry/time_range_array_spec.rb +149 -0
  52. data/spec/double_entry/time_range_spec.rb +43 -0
  53. data/spec/double_entry/week_range_spec.rb +88 -0
  54. data/spec/generators/double_entry/install/install_generator_spec.rb +33 -0
  55. data/spec/spec_helper.rb +47 -0
  56. data/spec/support/accounts.rb +26 -0
  57. data/spec/support/blueprints.rb +34 -0
  58. data/spec/support/database.example.yml +16 -0
  59. data/spec/support/database.travis.yml +18 -0
  60. data/spec/support/double_entry_spec_helper.rb +19 -0
  61. data/spec/support/reporting_configuration.rb +6 -0
  62. data/spec/support/schema.rb +71 -0
  63. metadata +277 -18
  64. data/LICENSE.txt +0 -22
checksums.yaml CHANGED
@@ -1,7 +1,15 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 513e0ed61ff5a07f7a0cf4da5ff19264649a8c8f
4
- data.tar.gz: 2edc588b822646885c8965f5d10d8bc075849eea
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ Y2ZlMDRhZWIzMzM2NDg0MmRlNmNhY2ZkNzY0Nzc3NjhlYWMwNjJiMw==
5
+ data.tar.gz: !binary |-
6
+ NDFhYzZhYmY1Zjg1NzYxMzM4NzA1NzIzMWFkMTJmMDBlYzRjOGE4Yg==
5
7
  SHA512:
6
- metadata.gz: 034b7dacb2d35b0590b74cf4712ac21a54fb201cf8de6fba36a7a0dbd2d840ad519957b0c96ce2d94f50ec59f9c87c2e89e55bf219d23fef1a93600259301ca1
7
- data.tar.gz: 0f22cd5a079652573a20329cf8bf961f0842b3f13c95b8dc2f037b7cf512f0cfc042edbfbe1804fd84ecdf77fc0bfb3df2481c3c39b7f2ce28f2fdd860d417e5
8
+ metadata.gz: !binary |-
9
+ MzZmNTNmOGVlYTgyN2I4MjljZGU5ZGZlODJkMTMyM2JlNWQyMmY3OTQxY2Fh
10
+ NDVjZmYxNmQ1OGNiMTA5ODUwM2U5ZjJlMmEzMjY4MmMyOWY4Y2M0ZmI5YzJj
11
+ OTU5MDQ0MzMxODk4NGNjMGI3Yzk2ZjYxZjc4ODkyODU0MGNhM2Y=
12
+ data.tar.gz: !binary |-
13
+ MDcxOWE3OTFkNzQ3ODk1MTVhYzFjYTlmMWNiNGM4ZmU5N2UzMDc3M2U5Zjkz
14
+ YTU5MDVhM2JmODQ0ZTgzYmVkZjdiNDRhZWE0ZDI0YjQwYjBkNWEzZGEzOGQw
15
+ YWE3OTE2ODlmODc3ZjY2YTE1ODBjYzcxNmY4M2M0MTJkMTVhMWY=
data/.gitignore CHANGED
@@ -3,7 +3,7 @@
3
3
  .bundle
4
4
  .config
5
5
  .yardoc
6
- Gemfile.lock
6
+ .ruby-version
7
7
  InstalledFiles
8
8
  _yardoc
9
9
  coverage
@@ -12,11 +12,10 @@ lib/bundler/man
12
12
  pkg
13
13
  rdoc
14
14
  spec/reports
15
+ spec/support/database.yml
15
16
  test/tmp
16
17
  test/version_tmp
17
18
  tmp
18
- *.bundle
19
- *.so
20
- *.o
21
- *.a
22
- mkmf.log
19
+ bin/
20
+ log/
21
+ /Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour
data/.travis.yml ADDED
@@ -0,0 +1,19 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.2
4
+ - 2.0.0
5
+ - 1.9.3
6
+ env:
7
+ - DB=mysql
8
+ - DB=postgres
9
+ before_script:
10
+ - cp spec/support/database.travis.yml spec/support/database.yml
11
+ - mysql -e 'create database double_entry_test;'
12
+ - psql -c 'create database double_entry_test;' -U postgres
13
+ script:
14
+ - rake spec
15
+ - ruby script/jack_hammer -t 2000
16
+ gemfile:
17
+ - gemfiles/Gemfile.rails-3.2.0
18
+ - gemfiles/Gemfile.rails-4.0.0
19
+ - gemfiles/Gemfile.rails-4.1.0
data/.yardopts ADDED
@@ -0,0 +1,2 @@
1
+ --markup markdown
2
+ - LICENSE.md
data/Gemfile CHANGED
@@ -1,4 +1,3 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- # Specify your gem's dependencies in double_entry.gemspec
4
3
  gemspec
data/LICENSE.md ADDED
@@ -0,0 +1,19 @@
1
+ Copyright © 2014 Envato Pty Ltd
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md CHANGED
@@ -1,29 +1,236 @@
1
1
  # DoubleEntry
2
2
 
3
- TODO: Write a gem description
3
+ [![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)
5
+ [![Code Climate](https://codeclimate.com/github/envato/double_entry.png)](https://codeclimate.com/github/envato/double_entry)
6
+
7
+ ![Show me the Money](http://24.media.tumblr.com/tumblr_m3bwbqNJIG1rrgbmqo1_500.gif)
8
+
9
+ Keep track of all the monies!
10
+
11
+ DoubleEntry is an accounting system based on the principles of a
12
+ [Double-entry Bookkeeping](http://en.wikipedia.org/wiki/Double-entry_bookkeeping_system)
13
+ system. While this gem acts like a double-entry bookkeeping system, as it creates
14
+ two entries in the database for each transfer, it does *not* enforce accounting rules.
15
+
16
+ DoubleEntry uses the Money gem to avoid floating point rounding errors.
17
+
18
+ ## Compatibility
19
+
20
+ DoubleEntry has been tested with:
21
+
22
+ Ruby Versions: 1.9.3, 2.0.0, 2.1.2
23
+
24
+ Rails Versions: Rails 3.2.x, 4.0.x, 4.1.x
25
+
26
+ **Databases Supported:**
27
+ * MySQL
28
+ * PostgreSQL
4
29
 
5
30
  ## Installation
6
31
 
7
- Add this line to your application's Gemfile:
32
+ In your application's `Gemfile`, add:
8
33
 
9
34
  gem 'double_entry'
10
35
 
11
- And then execute:
36
+ Then run:
37
+
38
+ bundle
39
+ rails generate double_entry:install
40
+
41
+ Run migration files:
42
+
43
+ rake db:migrate
44
+
45
+
46
+ ## Interface
47
+
48
+ The entire API for recording financial transactions is available through a few
49
+ methods in the **DoubleEntry** module. For full details on
50
+ what the API provides, please view the documentation on these methods.
51
+
52
+ A configuration file should be used to define a set of accounts, and potential
53
+ transfers between those accounts. See the Configuration section for more details.
54
+
55
+
56
+ ### Accounts
57
+
58
+ Money is kept in Accounts.
59
+
60
+ Each Account has a scope, which is used to subdivide the account into smaller
61
+ accounts. For example, an account can be scoped by user to ensure that each
62
+ user has their own individual account.
63
+
64
+ Scoping accounts is recommended. Unscoped accounts may perform more slowly
65
+ than scoped accounts due to lock contention.
66
+
67
+ To get a particular account:
68
+
69
+ account = DoubleEntry.account(:spending, :scope => user)
70
+
71
+ (This actually returns an Account::Instance object.)
72
+
73
+ See **DoubleEntry::Account** for more info.
74
+
75
+
76
+ ### Balances
77
+
78
+ Calling:
79
+
80
+ account.balance
81
+
82
+ will return the current balance for an account as a Money object.
83
+
84
+
85
+ ### Transfers
86
+
87
+ To transfer money between accounts:
88
+
89
+ DoubleEntry.transfer(20.dollars, :from => account_a, :to => account_b, :code => :purchase)
90
+
91
+ The possible transfers, and their codes, should be defined in the configuration.
92
+
93
+ See **DoubleEntry::Transfer** for more info.
94
+
95
+
96
+ ### Locking
97
+
98
+ If you're doing more than one transfer in a single financial transaction, or
99
+ you're doing other database operations along with the transfer, you'll need to
100
+ manually lock the accounts you're using:
101
+
102
+ DoubleEntry.lock_accounts(account_a, account_b) do
103
+ # Do some other stuff in here...
104
+ DoubleEntry.transfer(20.dollars, :from => account_a, :to => account_b, :code => :purchase)
105
+ end
106
+
107
+ The lock_accounts call generates a database transaction, which must be the
108
+ outermost transaction.
109
+
110
+ See **DoubleEntry::Locking** for more info.
111
+
112
+
113
+ ## Implementation
114
+
115
+ All transfers and balances are stored in the lines table. As this is a
116
+ double-entry accounting system, each transfer generates two lines table
117
+ entries: one for the source account, and one for the destination.
118
+
119
+ Lines table entries also store the running balance for the account. To retrieve
120
+ the current balance for an account, we find the most recent lines table entry
121
+ for it.
122
+
123
+ See **DoubleEntry::Line** for more info.
124
+
125
+ AccountBalance records cache the current balance for each Account, and are used
126
+ to perform database level locking.
127
+
128
+ ## Configuration
129
+
130
+ A configuration file should be used to define a set of accounts, optional scopes on
131
+ the accounts, and permitted transfers between those accounts.
132
+
133
+ The configuration file should be kept in your application's load path. For example,
134
+ *config/initializers/double_entry.rb*
135
+
136
+ For example, the following specifies two accounts, account_a and account_b.
137
+ Each account is scoped by User (where User is an object with an ID), meaning
138
+ each user can have their own account of each type.
139
+
140
+ This configuration also specifies that money can be transferred between the two accounts.
141
+
142
+ ```ruby
143
+ require 'double_entry'
144
+
145
+ DoubleEntry.accounts = DoubleEntry::Account::Set.new.tap do |accounts|
146
+ user_scope = lambda do |user_identifier|
147
+ if user_identifier.is_a?(User)
148
+ user_identifier.id
149
+ else
150
+ user_identifier
151
+ end
152
+ end
153
+
154
+ accounts << DoubleEntry::Account.new(identifier: :savings, scope_identifier: user_scope, positive_only: true)
155
+ accounts << DoubleEntry::Account.new(identifier: :checking, scope_identifier: user_scope)
156
+ end
157
+
158
+ DoubleEntry.transfers = DoubleEntry::Transfer::Set.new.tap do |transfers|
159
+ transfers << DoubleEntry::Transfer.new(from: :checking, to: :savings, code: :deposit)
160
+ transfers << DoubleEntry::Transfer.new(from: :savings, to: :checking, code: :withdraw)
161
+ end
162
+ ```
163
+
164
+ ## Jackhammer
165
+
166
+ Run a concurrency test on the code.
167
+
168
+ This spawns a bunch of processes, and does random transactions between a set
169
+ of accounts, then validates that all the numbers add up at the end.
170
+
171
+ You can also tell out it to flush out the account balances table at regular
172
+ intervals, to validate that new account balances records get created with the
173
+ correct balances from the lines table.
174
+
175
+ ./script/jack_hammer -t 20
176
+ Cleaning out the database...
177
+ Setting up 5 accounts...
178
+ Spawning 20 processes...
179
+ Flushing balances
180
+ Process 1 running 1 transfers...
181
+ Process 0 running 1 transfers...
182
+ Process 3 running 1 transfers...
183
+ Process 2 running 1 transfers...
184
+ Process 4 running 1 transfers...
185
+ Process 5 running 1 transfers...
186
+ Process 6 running 1 transfers...
187
+ Process 7 running 1 transfers...
188
+ Process 8 running 1 transfers...
189
+ Process 9 running 1 transfers...
190
+ Process 10 running 1 transfers...
191
+ Process 11 running 1 transfers...
192
+ Process 12 running 1 transfers...
193
+ Process 13 running 1 transfers...
194
+ Process 14 running 1 transfers...
195
+ Process 16 running 1 transfers...
196
+ Process 15 running 1 transfers...
197
+ Process 17 running 1 transfers...
198
+ Process 19 running 1 transfers...
199
+ Process 18 running 1 transfers...
200
+ Reconciling...
201
+ All the Line records were written, FTW!
202
+ All accounts reconciled, FTW!
203
+ Done successfully :)
204
+
205
+ ## Future Direction
206
+
207
+ No immediate to-do's.
208
+
209
+ ## Development Environment Setup
210
+
211
+ 1. Clone this repo.
212
+
213
+ git clone git@github.com:envato/double_entry.git && cd double_entry
214
+
215
+ 2. Run the included setup script to install the gem dependencies.
216
+
217
+ ./script/setup.sh
218
+
219
+ 3. Install MySQL and PostgreSQL. The tests run using both databases.
220
+ 4. Create a database in MySQL.
221
+
222
+ mysql -u root -e 'create database double_entry_test;'
12
223
 
13
- $ bundle
224
+ 5. Create a database in PostgreSQL.
14
225
 
15
- Or install it yourself as:
226
+ psql -c 'create database double_entry_test;' -U postgres
16
227
 
17
- $ gem install double_entry
228
+ 6. Specify how the tests should connect to the database
18
229
 
19
- ## Usage
230
+ cp spec/support/database.example.yml spec/support/database.yml
231
+ vim spec/support/database.yml
20
232
 
21
- TODO: Write usage instructions here
233
+ 7. Run the tests
22
234
 
23
- ## Contributing
235
+ bundle exec rake
24
236
 
25
- 1. Fork it ( https://github.com/[my-github-username]/double_entry/fork )
26
- 2. Create your feature branch (`git checkout -b my-new-feature`)
27
- 3. Commit your changes (`git commit -am 'Add some feature'`)
28
- 4. Push to the branch (`git push origin my-new-feature`)
29
- 5. Create a new Pull Request
data/Rakefile CHANGED
@@ -1,2 +1,14 @@
1
+ require "rspec/core/rake_task"
1
2
  require "bundler/gem_tasks"
2
3
 
4
+ RSpec::Core::RakeTask.new(:spec) do |t|
5
+ t.verbose = false
6
+ end
7
+
8
+ task :default do
9
+ %w(mysql postgres).each do |db|
10
+ puts "Running tests with `DB=#{db}`"
11
+ ENV['DB'] = db
12
+ Rake::Task["spec"].execute
13
+ end
14
+ end
data/double_entry.gemspec CHANGED
@@ -1,22 +1,37 @@
1
- # coding: utf-8
1
+ # encoding: utf-8
2
+
2
3
  lib = File.expand_path('../lib', __FILE__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+
4
6
  require 'double_entry/version'
5
7
 
6
- Gem::Specification.new do |spec|
7
- spec.name = "double_entry"
8
- spec.version = DoubleEntry::VERSION
9
- spec.authors = ["Orien Madgwick"]
10
- spec.email = ["_@orien.io"]
11
- spec.summary = "Tools to build your double entry financial ledger"
12
- spec.homepage = "https://github.com/envato/double_entry"
13
- spec.license = "MIT"
8
+ Gem::Specification.new do |gem|
9
+ gem.name = 'double_entry'
10
+ gem.version = DoubleEntry::VERSION
11
+ gem.authors = ['Anthony Sellitti', 'Keith Pitt', 'Martin Jagusch', 'Martin Spickermann', 'Mark Turnley', 'Orien Madgwick', 'Pete Yandall', 'Stephanie Staub']
12
+ 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
+ gem.summary = 'Tools to build your double entry financial ledger'
15
+ gem.homepage = 'https://github.com/envato/double_entry'
16
+
17
+ gem.files = `git ls-files`.split($/)
18
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
19
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
20
+ gem.require_paths = ['lib']
14
21
 
15
- spec.files = `git ls-files -z`.split("\x0")
16
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
- spec.require_paths = ["lib"]
22
+ gem.add_dependency 'money', '>= 5.1.0'
23
+ gem.add_dependency 'encapsulate_as_money'
24
+ gem.add_dependency 'activerecord', '>= 3.2.9'
25
+ gem.add_dependency 'activesupport', '>= 3.0.0'
26
+ gem.add_dependency 'railties', '>= 3.0.0'
19
27
 
20
- spec.add_development_dependency "bundler", "~> 1.6"
21
- spec.add_development_dependency "rake"
28
+ gem.add_development_dependency 'rake'
29
+ gem.add_development_dependency 'mysql2'
30
+ gem.add_development_dependency 'pg'
31
+ gem.add_development_dependency 'rspec'
32
+ gem.add_development_dependency 'rspec-its'
33
+ gem.add_development_dependency 'database_cleaner'
34
+ gem.add_development_dependency 'generator_spec'
35
+ gem.add_development_dependency 'machinist'
36
+ gem.add_development_dependency 'timecop'
22
37
  end
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec :path => '../'
4
+
5
+ gem 'rails', '~>3.2.0'
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec :path => '../'
4
+
5
+ gem 'rails', '~>4.0.0'
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec :path => '../'
4
+
5
+ gem 'rails', '~>4.1.0'
@@ -0,0 +1,61 @@
1
+ # encoding: utf-8
2
+ module ActiveRecord
3
+
4
+ # These methods are available as class methods on ActiveRecord::Base.
5
+ module LockingExtensions
6
+
7
+ # Execute the given block within a database transaction, and retry the
8
+ # transaction from the beginning if a RestartTransaction exception is raised.
9
+ def restartable_transaction(&block)
10
+ begin
11
+ transaction(&block)
12
+ rescue ActiveRecord::RestartTransaction
13
+ retry
14
+ end
15
+ end
16
+
17
+ # Execute the given block, and retry the current restartable transaction if a
18
+ # MySQL deadlock occurs.
19
+ def with_restart_on_deadlock
20
+ begin
21
+ yield
22
+ rescue ActiveRecord::StatementInvalid => exception
23
+ if exception.message =~ /deadlock/i
24
+ raise ActiveRecord::RestartTransaction
25
+ else
26
+ raise
27
+ end
28
+ end
29
+ end
30
+
31
+ # Create the record, but ignore the exception if there's a duplicate.
32
+ def create_ignoring_duplicates!(*args)
33
+ # Error examples:
34
+ # PG::Error: ERROR: deadlock detected
35
+ # Mysql::Error: Deadlock found when trying to get lock
36
+ # PG::Error: ERROR: duplicate key value violates unique constraint
37
+ # Mysql2::Error: Duplicate entry 'keith' for key 'index_users_on_username': INSERT INTO `users...
38
+ begin
39
+ create!(*args)
40
+ rescue ActiveRecord::StatementInvalid => exception
41
+ if exception.message =~ /duplicate/i
42
+ # Just ignore it...someone else has already created the record.
43
+ elsif exception.message =~ /deadlock/i
44
+ # Somebody else is in the midst of creating the record. We'd better
45
+ # retry, so we ensure they're done before we move on.
46
+ retry
47
+ else
48
+ raise
49
+ end
50
+ end
51
+ end
52
+
53
+ end
54
+
55
+ # Raise this inside a restartable_transaction to retry the transaction from the beginning.
56
+ class RestartTransaction < RuntimeError
57
+ end
58
+
59
+ end
60
+
61
+ ActiveRecord::Base.extend(ActiveRecord::LockingExtensions)