double_entry 2.0.0.beta4 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2c695b54b7cec66c99e9e6e62e0452883412b2a73596d5ea6deae695d48403f7
4
- data.tar.gz: e12facb9d3741e7d461788534826c93f0827fd08f90898d831ea0777879d9204
3
+ metadata.gz: 496d452f002312016c033d18ba23257b677a07072fdea0d5dfc85f77563a3cc3
4
+ data.tar.gz: a53729a0facc3b920c70c87050cacd93743cea8f2895974fbddbd32925126807
5
5
  SHA512:
6
- metadata.gz: f99f15db2934b0f5ca49f640a1373aefef4ca37fc62303afa440ff71c671783f586345149e681566cc9c27543c8f6c4979921ea457ccd5e46c68f3d20363d010
7
- data.tar.gz: 215d09cbc9a0aafdd1fa6ebb518a0a86aea0ea8c757afe528126dbb95e0772ade2edb8948ddfa46445258102a12f09b41f81192f96495e26dbade111cab3ef01
6
+ metadata.gz: 626f6ee8bc46253bec15f21ef9a619d894fc5748dca7aa2a07518032777f0c2313889393f7d8d31f974bdd11c26aa309b329f816e1648915978131e228a9d740
7
+ data.tar.gz: bf503a7c96e23fcde06a04fc64e38799341a60be9c73443eda3d3ffe2583191f33e3ddfb61acb349e4062b1ae568784a49a0d3bbb54c1efce70ccc966057e395
data/CHANGELOG.md CHANGED
@@ -7,7 +7,51 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- [Unreleased]: https://github.com/envato/double_entry/compare/v2.0.0.beta4...HEAD
10
+ [Unreleased]: https://github.com/envato/double_entry/compare/v2.0.0...HEAD
11
+
12
+ ## [2.0.0] - 2023-10-25
13
+
14
+ ### Fixed
15
+
16
+ - Ensure LineCheck and AccountFixer can work correctly with unscoped accounts ([#207]).
17
+ - Fixes for running on Ruby 3 ([#212]).
18
+
19
+ ### Changed
20
+
21
+ - Return `[credit, debit]` from `DoubleEntry.transfer` ([#190]).
22
+ - Run the test suite against Rails 6.1, 7.0, 7.1, and Ruby 3.1, 3.2 ([#203], [#214], [#217]).
23
+ - Migrate CI to run on GitHub Actions ([#205])
24
+
25
+ ### Removed
26
+
27
+ - Removed support for Rails < 6.1, and Ruby < 3.0 ([#215], [#217]).
28
+
29
+ ### Added
30
+
31
+ - Add `credit` and `debit` scopes to the `Line` model ([#192]).
32
+
33
+ [2.0.0]: https://github.com/envato/double_entry/compare/v2.0.0.beta5...v2.0.0
34
+ [#190]: https://github.com/envato/double_entry/pull/190
35
+ [#192]: https://github.com/envato/double_entry/pull/192
36
+ [#203]: https://github.com/envato/double_entry/pull/203
37
+ [#205]: https://github.com/envato/double_entry/pull/205
38
+ [#207]: https://github.com/envato/double_entry/pull/207
39
+ [#212]: https://github.com/envato/double_entry/pull/212
40
+ [#214]: https://github.com/envato/double_entry/pull/214
41
+ [#215]: https://github.com/envato/double_entry/pull/215
42
+ [#217]: https://github.com/envato/double_entry/pull/217
43
+
44
+ ## [2.0.0.beta5] - 2021-02-24
45
+
46
+ ### Changed
47
+
48
+ - Use the Ruby 1.9 hash syntax ([#182]).
49
+ - Make the Line detail association optional ([#184]).
50
+ - Support Ruby 3 ([#196]).
51
+
52
+ [#182]: https://github.com/envato/double_entry/pull/182
53
+ [#184]: https://github.com/envato/double_entry/pull/184
54
+ [#196]: https://github.com/envato/double_entry/pull/196
11
55
 
12
56
  ## [2.0.0.beta4] - 2020-01-25
13
57
 
@@ -25,7 +69,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
25
69
 
26
70
  - Prevent using Ruby 2.2 via restrictions in Gemfile and Gemspec ([#175]).
27
71
 
28
- [2.0.0.beta4]: https://github.com/envato/double_entry/compare/v2.0.0.beta3...v2.0.0.beta4
29
72
  [#175]: https://github.com/envato/double_entry/pull/175
30
73
  [#176]: https://github.com/envato/double_entry/pull/176
31
74
  [#178]: https://github.com/envato/double_entry/pull/178
@@ -462,6 +505,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
462
505
 
463
506
  - Library released as Open Source!
464
507
 
508
+ [2.0.0.beta5]: https://github.com/envato/double_entry/compare/v2.0.0.beta4...v2.0.0.beta5
509
+ [2.0.0.beta4]: https://github.com/envato/double_entry/compare/v2.0.0.beta3...v2.0.0.beta4
465
510
  [2.0.0.beta3]: https://github.com/envato/double_entry/compare/v2.0.0.beta2...v2.0.0.beta3
466
511
  [2.0.0.beta2]: https://github.com/envato/double_entry/compare/v2.0.0.beta1...v2.0.0.beta2
467
512
  [2.0.0.beta1]: https://github.com/envato/double_entry/compare/v1.0.1...v2.0.0.beta1
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  [![License MIT](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://github.com/envato/double_entry/blob/master/LICENSE.md)
5
5
  [![Gem Version](https://badge.fury.io/rb/double_entry.svg)](http://badge.fury.io/rb/double_entry)
6
- [![Build Status](https://travis-ci.org/envato/double_entry.svg?branch=master)](https://travis-ci.org/envato/double_entry)
6
+ [![Build Status](https://github.com/envato/double_entry/actions/workflows/ci-workflow.yml/badge.svg)](https://github.com/envato/double_entry/actions/workflows/ci-workflow.yml)
7
7
  [![Code Climate](https://codeclimate.com/github/envato/double_entry/badges/gpa.svg)](https://codeclimate.com/github/envato/double_entry)
8
8
 
9
9
  ![Show me the Money](http://24.media.tumblr.com/tumblr_m3bwbqNJIG1rrgbmqo1_500.gif)
@@ -13,7 +13,7 @@ Keep track of all the monies!
13
13
  DoubleEntry is an accounting system based on the principles of a
14
14
  [Double-entry Bookkeeping](http://en.wikipedia.org/wiki/Double-entry_bookkeeping_system)
15
15
  system. While this gem acts like a double-entry bookkeeping system, as it creates
16
- two entries in the database for each transfer, it does *not* enforce accounting rules.
16
+ two entries in the database for each transfer, it does *not* enforce accounting rules, other than optionally ensuring a balance is positive, and through an allowlist of approved transfers.
17
17
 
18
18
  DoubleEntry uses the [Money gem](https://github.com/RubyMoney/money) to encapsulate operations on currency values.
19
19
 
@@ -22,18 +22,15 @@ DoubleEntry uses the [Money gem](https://github.com/RubyMoney/money) to encapsul
22
22
  DoubleEntry is tested against:
23
23
 
24
24
  Ruby
25
- * 2.3.x
26
- * 2.4.x
27
- * 2.5.x
28
- * 2.6.x
29
25
  * 2.7.x
26
+ * 3.0.x
27
+ * 3.1.x
28
+ * 3.2.x
30
29
 
31
30
  Rails
32
- * 4.2.x
33
- * 5.0.x
34
- * 5.1.x
35
- * 5.2.x
36
31
  * 6.0.x
32
+ * 6.1.x
33
+ * 7.0.x
37
34
 
38
35
  Databases
39
36
  * MySQL
@@ -72,7 +69,7 @@ rake db:migrate
72
69
  ## Interface
73
70
 
74
71
  The entire API for recording financial transactions is available through a few
75
- methods in the **DoubleEntry** module. For full details on
72
+ methods in the [DoubleEntry](lib/double_entry.rb) module. For full details on
76
73
  what the API provides, please view the documentation on these methods.
77
74
 
78
75
  A configuration file should be used to define a set of accounts, and potential
@@ -93,12 +90,12 @@ than scoped accounts due to lock contention.
93
90
  To get a particular account:
94
91
 
95
92
  ```ruby
96
- account = DoubleEntry.account(:spending, :scope => user)
93
+ account = DoubleEntry.account(:spending, scope: user)
97
94
  ```
98
95
 
99
96
  (This actually returns an Account::Instance object.)
100
97
 
101
- See **DoubleEntry::Account** for more info.
98
+ See [DoubleEntry::Account](lib/double_entry/account.rb) for more info.
102
99
 
103
100
 
104
101
  ### Balances
@@ -119,15 +116,15 @@ To transfer money between accounts:
119
116
  ```ruby
120
117
  DoubleEntry.transfer(
121
118
  Money.new(20_00),
122
- :from => one_account,
123
- :to => another_account,
124
- :code => :a_business_code_for_this_type_of_transfer,
119
+ from: one_account,
120
+ to: another_account,
121
+ code: :a_business_code_for_this_type_of_transfer,
125
122
  )
126
123
  ```
127
124
 
128
125
  The possible transfers, and their codes, should be defined in the configuration.
129
126
 
130
- See **DoubleEntry::Transfer** for more info.
127
+ See [DoubleEntry::Transfer](lib/double_entry/transfer.rb) for more info.
131
128
 
132
129
  ### Metadata
133
130
 
@@ -136,10 +133,10 @@ You may associate arbitrary metadata with transfers, for example:
136
133
  ```ruby
137
134
  DoubleEntry.transfer(
138
135
  Money.new(20_00),
139
- :from => one_account,
140
- :to => another_account,
141
- :code => :a_business_code_for_this_type_of_transfer,
142
- :metadata => {:key1 => ['value 1', 'value 2'], :key2 => 'value 3'},
136
+ from: one_account,
137
+ to: another_account,
138
+ code: :a_business_code_for_this_type_of_transfer,
139
+ metadata: {key1: ['value 1', 'value 2'], key2: 'value 3'},
143
140
  )
144
141
  ```
145
142
 
@@ -152,7 +149,7 @@ manually lock the accounts you're using:
152
149
  ```ruby
153
150
  DoubleEntry.lock_accounts(account_a, account_b) do
154
151
  # Perhaps transfer some money
155
- DoubleEntry.transfer(Money.new(20_00), :from => account_a, :to => account_b, :code => :purchase)
152
+ DoubleEntry.transfer(Money.new(20_00), from: account_a, to: account_b, code: :purchase)
156
153
  # Perform other tasks that should be commited atomically with the transfer of funds...
157
154
  end
158
155
  ```
@@ -160,8 +157,31 @@ end
160
157
  The lock_accounts call generates a database transaction, which must be the
161
158
  outermost transaction.
162
159
 
163
- See **DoubleEntry::Locking** for more info.
160
+ See [DoubleEntry::Locking](lib/double_entry/locking.rb) for more info.
164
161
 
162
+ ### Account Checker/Fixer
163
+
164
+ DoubleEntry tries really hard to make sure that stored account balances reflect the running balances from the `double_entry_lines` table, but there is always the unlikely possibility that something will go wrong and the calculated balance might get out of sync with the actual running balance of the lines.
165
+
166
+ DoubleEntry therefore provides a couple of tools to give you some confidence that things are working as expected.
167
+
168
+ The `DoubleEntry::Validation::LineCheck` will check the `double_entry_lines` table to make sure that the `balance` column correctly reflects the calculated running balance, and that the `double_entry_account_balances` table has the correct value in the `balance` column. If either one of these turn out to be incorrect then it will write an entry into the `double_entry_line_checks` table reporting on the differences.
169
+
170
+ You can alternatively pass a `fixer` to the `DoubleEntry::Validation::LineCheck.perform` method which will try and correct the balances. This gem provides the `DoubleEntry::Validation::AccountFixer` class which will correct the balance if it's out of sync.
171
+
172
+ Using these classes is optional and both are provided for additional safety checks. If you want to make use of them then it's recommended to run them in a scheduled job, somewhere on the order of hourly to daily, depending on transaction volume. Keep in mind that this process locks accounts as it inspects their balances, so it will prevent new transactions from being written for a short time.
173
+
174
+ Here are examples that could go in your scheduled job, depending on your needs:
175
+
176
+ ```ruby
177
+ # Check all accounts & write the results to the double_entry_line_checks table
178
+ DoubleEntry::Validation::LineCheck.perform!
179
+
180
+ # Check & fix accounts (results will also be written to the table)
181
+ DoubleEntry::Validation::LineCheck.perform!(fixer: DoubleEntry::Validation::AccountFixer.new)
182
+ ```
183
+
184
+ See [DoubleEntry::Validation](lib/double_entry/validation) for more info.
165
185
 
166
186
  ## Implementation
167
187
 
@@ -173,7 +193,7 @@ Lines table entries also store the running balance for the account. To retrieve
173
193
  the current balance for an account, we find the most recent lines table entry
174
194
  for it.
175
195
 
176
- See **DoubleEntry::Line** for more info.
196
+ See [DoubleEntry::Line](lib/double_entry/line.rb) for more info.
177
197
 
178
198
  AccountBalance records cache the current balance for each Account, and are used
179
199
  to perform database level locking.
@@ -206,13 +226,13 @@ DoubleEntry.configure do |config|
206
226
  raise 'not a User' unless user.class.name == 'User'
207
227
  user.id
208
228
  end
209
- accounts.define(:identifier => :savings, :scope_identifier => user_scope, :positive_only => true)
210
- accounts.define(:identifier => :checking, :scope_identifier => user_scope)
229
+ accounts.define(identifier: :savings, scope_identifier: user_scope, positive_only: true)
230
+ accounts.define(identifier: :checking, scope_identifier: user_scope)
211
231
  end
212
232
 
213
233
  config.define_transfers do |transfers|
214
- transfers.define(:from => :checking, :to => :savings, :code => :deposit)
215
- transfers.define(:from => :savings, :to => :checking, :code => :withdraw)
234
+ transfers.define(from: :checking, to: :savings, code: :deposit)
235
+ transfers.define(from: :savings, to: :checking, code: :withdraw)
216
236
  end
217
237
  end
218
238
  ```
@@ -225,11 +245,25 @@ Transfers between accounts of different currencies are not allowed.
225
245
  ```ruby
226
246
  DoubleEntry.configure do |config|
227
247
  config.define_accounts do |accounts|
228
- accounts.define(:identifier => :savings, :scope_identifier => user_scope, :currency => 'AUD')
248
+ accounts.define(identifier: :savings, scope_identifier: user_scope, currency: 'AUD')
229
249
  end
230
250
  end
231
251
  ```
232
252
 
253
+ ## Testing with RSpec
254
+
255
+ Transfering money needs to be run as a top level transaction. This conflicts with RSpec's default behavior of creating a new transaction for every test, causing an exception of type `DoubleEntry::Locking::LockMustBeOutermostTransaction` to be raised. This behavior may be disabled by adding the following lines into your `rails_helper.rb`.
256
+
257
+ ```ruby
258
+ RSpec.configure do |config|
259
+ # ...
260
+ # This first line should already be there. You will need to add the second one
261
+ config.use_transactional_fixtures = true
262
+ DoubleEntry::Locking.configuration.running_inside_transactional_fixtures = true
263
+ # ...
264
+ end
265
+ ```
266
+
233
267
  ## Jackhammer
234
268
 
235
269
  Run a concurrency test on the code.
@@ -277,42 +311,56 @@ See the Github project [issues](https://github.com/envato/double_entry/issues).
277
311
 
278
312
  ## Development Environment Setup
279
313
 
280
- 1. Clone this repo.
314
+ We're using Docker to provide a convenient and consistent environment for
315
+ executing tests during development. This allows engineers to quickly set up
316
+ a productive development environment.
281
317
 
282
- ```sh
283
- git clone git@github.com:envato/double_entry.git && cd double_entry
284
- ```
318
+ Note: Most development files are mounted in the Docker container. This
319
+ enables engineers to edit files in their favourite editor (on the host
320
+ OS) and have the changes immediately available in the Docker container
321
+ to be exercised.
322
+
323
+ One exception to this is the RSpec configuration. Changes to these files will
324
+ require a rebuild of the Docker image (step 2).
325
+
326
+ Prerequisites:
285
327
 
286
- 2. Run the included setup script to install the gem dependencies.
328
+ * Docker
329
+ * Docker Compose
330
+ * Git
331
+
332
+ 1. Clone this repo.
287
333
 
288
334
  ```sh
289
- ./script/setup.sh
335
+ git clone git@github.com:envato/double_entry.git && cd double_entry
290
336
  ```
291
337
 
292
- 3. Install MySQL, PostgreSQL and SQLite. We run tests against all three databases.
293
- 4. Create a database in MySQL.
338
+ 2. Build the Docker image we'll use to run tests
294
339
 
295
340
  ```sh
296
- mysql -u root -e 'create database double_entry_test;'
341
+ docker-compose build --pull double_entry
297
342
  ```
298
343
 
299
- 5. Create a database in PostgreSQL.
344
+ 3. Startup a container and attach a terminal. This will also start up a
345
+ MySQL and Postgres database.
300
346
 
301
347
  ```sh
302
- psql -c 'create database double_entry_test;' -U postgres
348
+ docker-compose run --rm double_entry ash
303
349
  ```
304
350
 
305
- 6. Specify how the tests should connect to the database
351
+ 4. Run the tests
306
352
 
307
353
  ```sh
308
- cp spec/support/{database.example.yml,database.yml}
309
- vim spec/support/database.yml
354
+ DB=mysql bundle exec rspec
355
+ DB=postgres bundle exec rspec
356
+ DB=sqlite bundle exec rspec
310
357
  ```
311
358
 
312
- 7. Run the tests
359
+ 5. When finished, exit the container terminal and shut down the databases.
313
360
 
314
361
  ```sh
315
- bundle exec rake
362
+ exit
363
+ docker-compose down
316
364
  ```
317
365
 
318
366
  ## Contributors
data/double_entry.gemspec CHANGED
@@ -24,12 +24,12 @@ Gem::Specification.new do |gem|
24
24
  f.match(%r{^(?:double_entry.gemspec|README|LICENSE|CHANGELOG|lib/)})
25
25
  end
26
26
  gem.require_paths = ['lib']
27
- gem.required_ruby_version = '>= 2.3.0'
27
+ gem.required_ruby_version = '>= 3'
28
28
 
29
- gem.add_dependency 'activerecord', '>= 3.2.0'
30
- gem.add_dependency 'activesupport', '>= 3.2.0'
29
+ gem.add_dependency 'activerecord', '>= 6.1.0'
30
+ gem.add_dependency 'activesupport', '>= 6.1.0'
31
31
  gem.add_dependency 'money', '>= 6.0.0'
32
- gem.add_dependency 'railties', '>= 3.2.0'
32
+ gem.add_dependency 'railties', '>= 6.1.0'
33
33
 
34
34
  gem.add_development_dependency 'mysql2'
35
35
  gem.add_development_dependency 'pg'
@@ -18,7 +18,7 @@ module ActiveRecord
18
18
  yield
19
19
  rescue ActiveRecord::StatementInvalid => exception
20
20
  if exception.message =~ /deadlock/i || exception.message =~ /database is locked/i
21
- ActiveSupport::Notifications.publish('deadlock_restart.double_entry', :exception => exception)
21
+ ActiveSupport::Notifications.publish('deadlock_restart.double_entry', exception: exception)
22
22
 
23
23
  raise ActiveRecord::RestartTransaction
24
24
  else
@@ -46,7 +46,7 @@ module ActiveRecord
46
46
  yield
47
47
  rescue ActiveRecord::StatementInvalid, ActiveRecord::RecordNotUnique => exception
48
48
  if exception.message =~ /duplicate/i || exception.message =~ /ConstraintException/
49
- ActiveSupport::Notifications.publish('duplicate_ignore.double_entry', :exception => exception)
49
+ ActiveSupport::Notifications.publish('duplicate_ignore.double_entry', exception: exception)
50
50
 
51
51
  # Just ignore it...someone else has already created the record.
52
52
  else
@@ -63,7 +63,7 @@ module ActiveRecord
63
63
  if exception.message =~ /deadlock/i || exception.message =~ /database is locked/i
64
64
  # Somebody else is in the midst of creating the record. We'd better
65
65
  # retry, so we ensure they're done before we move on.
66
- ActiveSupport::Notifications.publish('deadlock_retry.double_entry', :exception => exception)
66
+ ActiveSupport::Notifications.publish('deadlock_retry.double_entry', exception: exception)
67
67
 
68
68
  retry
69
69
  else
@@ -15,7 +15,7 @@ module DoubleEntry
15
15
  # @api private
16
16
  def account(identifier, options = {})
17
17
  account = accounts.find(identifier, (options[:scope].present? || options[:scope_identity].present?))
18
- Instance.new(:account => account, :scope => options[:scope], :scope_identity => options[:scope_identity])
18
+ Instance.new(account: account, scope: options[:scope], scope_identity: options[:scope_identity])
19
19
  end
20
20
 
21
21
  # @api private
@@ -67,7 +67,7 @@ module DoubleEntry
67
67
 
68
68
  class Instance
69
69
  attr_reader :account, :scope
70
- delegate :identifier, :scope_identifier, :scoped?, :positive_only, :negative_only, :currency, :to => :account
70
+ delegate :identifier, :scope_identifier, :scoped?, :positive_only, :negative_only, :currency, to: :account
71
71
 
72
72
  def initialize(args)
73
73
  @account = args[:account]
@@ -8,7 +8,7 @@ module DoubleEntry
8
8
  #
9
9
  # Account balances are created on demand when transfers occur.
10
10
  class AccountBalance < ActiveRecord::Base
11
- delegate :currency, :to => :account
11
+ delegate :currency, to: :account
12
12
 
13
13
  def balance
14
14
  self[:balance] && Money.new(self[:balance], currency)
@@ -25,11 +25,11 @@ module DoubleEntry
25
25
  end
26
26
 
27
27
  def account
28
- DoubleEntry.account(self[:account].to_sym, :scope_identity => self[:scope])
28
+ DoubleEntry.account(self[:account].to_sym, scope_identity: self[:scope])
29
29
  end
30
30
 
31
31
  def self.find_by_account(account, options = {})
32
- scope = where(:scope => account.scope_identity, :account => account.identifier.to_s)
32
+ scope = where(scope: account.scope_identity, account: account.identifier.to_s)
33
33
  scope = scope.lock(true) if options[:lock]
34
34
  scope.first
35
35
  end
@@ -79,18 +79,18 @@ module DoubleEntry
79
79
  # @api private
80
80
  class RelationBuilder
81
81
  attr_reader :options
82
- delegate :account, :scope, :scope?, :from, :to, :between?, :at, :at?, :codes, :code?, :to => :options
82
+ delegate :account, :scope, :scope?, :from, :to, :between?, :at, :at?, :codes, :code?, to: :options
83
83
 
84
84
  def initialize(options)
85
85
  @options = options
86
86
  end
87
87
 
88
88
  def build
89
- lines = Line.where(:account => account)
89
+ lines = Line.where(account: account)
90
90
  lines = lines.where('created_at <= ?', at) if at?
91
- lines = lines.where(:created_at => from..to) if between?
92
- lines = lines.where(:code => codes) if code?
93
- lines = lines.where(:scope => scope) if scope?
91
+ lines = lines.where(created_at: from..to) if between?
92
+ lines = lines.where(code: codes) if code?
93
+ lines = lines.where(scope: scope) if scope?
94
94
  lines
95
95
  end
96
96
  end
@@ -16,7 +16,7 @@ module DoubleEntry
16
16
  :scope_identifier_max_length=,
17
17
  :account_identifier_max_length,
18
18
  :account_identifier_max_length=,
19
- :to => 'DoubleEntry::Account',
19
+ to: 'DoubleEntry::Account',
20
20
  )
21
21
 
22
22
  delegate(
@@ -24,7 +24,7 @@ module DoubleEntry
24
24
  :transfers=,
25
25
  :code_max_length,
26
26
  :code_max_length=,
27
- :to => 'DoubleEntry::Transfer',
27
+ to: 'DoubleEntry::Transfer',
28
28
  )
29
29
 
30
30
  def define_accounts
@@ -55,8 +55,10 @@ module DoubleEntry
55
55
  # by account, or account and code, over a particular period.
56
56
  #
57
57
  class Line < ActiveRecord::Base
58
- belongs_to :detail, :polymorphic => true
59
- has_many :metadata, :class_name => 'DoubleEntry::LineMetadata' unless -> { DoubleEntry.config.json_metadata }
58
+ belongs_to :detail, polymorphic: true, required: false
59
+ has_many :metadata, class_name: 'DoubleEntry::LineMetadata' unless -> { DoubleEntry.config.json_metadata }
60
+ scope :credits, -> { where('amount > 0') }
61
+ scope :debits, -> { where('amount < 0') }
60
62
  scope :with_id_greater_than, ->(id) { where('id > ?', id) }
61
63
 
62
64
  def amount
@@ -75,12 +77,12 @@ module DoubleEntry
75
77
  self[:balance] = (money && money.fractional)
76
78
  end
77
79
 
78
- def save(*)
80
+ def save(**)
79
81
  check_balance_will_remain_valid
80
82
  super
81
83
  end
82
84
 
83
- def save!(*)
85
+ def save!(**)
84
86
  check_balance_will_remain_valid
85
87
  super
86
88
  end
@@ -102,7 +104,7 @@ module DoubleEntry
102
104
  end
103
105
 
104
106
  def account
105
- DoubleEntry.account(self[:account].to_sym, :scope_identity => scope)
107
+ DoubleEntry.account(self[:account].to_sym, scope_identity: scope)
106
108
  end
107
109
 
108
110
  def currency
@@ -117,7 +119,7 @@ module DoubleEntry
117
119
  end
118
120
 
119
121
  def partner_account
120
- DoubleEntry.account(self[:partner_account].to_sym, :scope_identity => partner_scope)
122
+ DoubleEntry.account(self[:partner_account].to_sym, scope_identity: partner_scope)
121
123
  end
122
124
 
123
125
  def partner
@@ -149,7 +149,7 @@ module DoubleEntry
149
149
  # If one or more account balance records don't exist, set
150
150
  # accounts_with_balances to the corresponding accounts, and return false.
151
151
  def grab_locks
152
- account_balances = @accounts.map { |account| AccountBalance.find_by_account(account, :lock => true) }
152
+ account_balances = @accounts.map { |account| AccountBalance.find_by_account(account, lock: true) }
153
153
 
154
154
  if account_balances.any?(&:nil?)
155
155
  @accounts_without_balances = @accounts.zip(account_balances).
@@ -168,7 +168,7 @@ module DoubleEntry
168
168
  # Get the initial balance from the lines table.
169
169
  balance = account.balance
170
170
  # Try to create the balance record, but ignore it if someone else has done it in the meantime.
171
- AccountBalance.create_ignoring_duplicates!(:account => account, :balance => balance)
171
+ AccountBalance.create_ignoring_duplicates!(account: account, balance: balance)
172
172
  end
173
173
  end
174
174
  end
@@ -76,6 +76,7 @@ module DoubleEntry
76
76
  end
77
77
 
78
78
  def process(amount, options)
79
+ credit = debit = nil
79
80
  from_account = options[:from]
80
81
  to_account = options[:to]
81
82
  code = options[:code]
@@ -91,6 +92,7 @@ module DoubleEntry
91
92
  credit, debit = create_lines(amount, code, detail, from_account, to_account, metadata)
92
93
  create_line_metadata(credit, debit, metadata) if metadata && !DoubleEntry.config.json_metadata
93
94
  end
95
+ [credit, debit]
94
96
  end
95
97
 
96
98
  def create_lines(amount, code, detail, from_account, to_account, metadata)
@@ -121,8 +123,8 @@ module DoubleEntry
121
123
  def create_line_metadata(credit, debit, metadata)
122
124
  metadata.each_pair do |key, value|
123
125
  Array(value).each do |each_value|
124
- LineMetadata.create!(:line => credit, :key => key, :value => each_value)
125
- LineMetadata.create!(:line => debit, :key => key, :value => each_value)
126
+ LineMetadata.create!(line: credit, key: key, value: each_value)
127
+ LineMetadata.create!(line: debit, key: key, value: each_value)
126
128
  end
127
129
  end
128
130
  end
@@ -23,7 +23,7 @@ module DoubleEntry
23
23
  def lines_for_account(account)
24
24
  Line.where(
25
25
  account: account.identifier.to_s,
26
- scope: account.scope_identity.to_s
26
+ scope: account.scope_identity&.to_s
27
27
  ).order(:id)
28
28
  end
29
29
 
@@ -34,9 +34,9 @@ module DoubleEntry
34
34
 
35
35
  unless active_accounts.empty?
36
36
  LineCheck.create!(
37
- :errors_found => incorrect_accounts.any?,
38
- :last_line_id => current_line_id,
39
- :log => log,
37
+ errors_found: incorrect_accounts.any?,
38
+ last_line_id: current_line_id,
39
+ log: log,
40
40
  )
41
41
  end
42
42
  end
@@ -48,34 +48,50 @@ module DoubleEntry
48
48
  end
49
49
 
50
50
  def running_balance_correct?(line, log)
51
- # Another work around for the MySQL 5.1 query optimiser bug that causes the ORDER BY
52
- # on the query to fail in some circumstances, resulting in an old balance being
53
- # returned. This was biting us intermittently in spec runs.
54
- # See http://bugs.mysql.com/bug.php?id=51431
55
- force_index = if Line.connection.adapter_name.match(/mysql/i)
56
- 'FORCE INDEX (lines_scope_account_id_idx)'
57
- else
58
- ''
59
- end
51
+ previous_line = find_previous_line(line.account.identifier.to_s, line.scope, line.id)
52
+
53
+ previous_balance = previous_line.length == 1 ? previous_line[0].balance : Money.zero(line.account.currency)
54
+
55
+ if line.balance != (line.amount + previous_balance)
56
+ log << line_error_message(line, previous_line, previous_balance)
57
+ end
58
+
59
+ line.balance == previous_balance + line.amount
60
+ end
60
61
 
62
+ def find_previous_line(identifier, scope, id)
61
63
  # yes, it needs to be find_by_sql, because any other find will be affected
62
64
  # by the find_each call in perform!
63
- previous_line = Line.find_by_sql([<<-SQL, line.account.identifier.to_s, line.scope, line.id])
65
+
66
+ if scope.nil?
67
+ Line.find_by_sql([<<-SQL, identifier, id])
68
+ SELECT * FROM #{Line.quoted_table_name} #{force_index}
69
+ WHERE account = ?
70
+ AND scope IS NULL
71
+ AND id < ?
72
+ ORDER BY id DESC
73
+ LIMIT 1
74
+ SQL
75
+ else
76
+ Line.find_by_sql([<<-SQL, identifier, scope, id])
64
77
  SELECT * FROM #{Line.quoted_table_name} #{force_index}
65
78
  WHERE account = ?
66
79
  AND scope = ?
67
80
  AND id < ?
68
81
  ORDER BY id DESC
69
82
  LIMIT 1
70
- SQL
71
-
72
- previous_balance = previous_line.length == 1 ? previous_line[0].balance : Money.zero(line.account.currency)
73
-
74
- if line.balance != (line.amount + previous_balance)
75
- log << line_error_message(line, previous_line, previous_balance)
83
+ SQL
76
84
  end
85
+ end
77
86
 
78
- line.balance == previous_balance + line.amount
87
+ def force_index
88
+ # Another work around for the MySQL 5.1 query optimiser bug that causes the ORDER BY
89
+ # on the query to fail in some circumstances, resulting in an old balance being
90
+ # returned. This was biting us intermittently in spec runs.
91
+ # See http://bugs.mysql.com/bug.php?id=51431
92
+ return '' unless Line.connection.adapter_name.match(/mysql/i)
93
+
94
+ 'FORCE INDEX (lines_scope_account_id_idx)'
79
95
  end
80
96
 
81
97
  def line_error_message(line, previous_line, previous_balance)
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module DoubleEntry
4
- VERSION = '2.0.0.beta4'
4
+ VERSION = '2.0.0'
5
5
  end
data/lib/double_entry.rb CHANGED
@@ -64,11 +64,11 @@ module DoubleEntry
64
64
  # @example Transfer $20 from a user's checking to savings account
65
65
  # checking_account = DoubleEntry.account(:checking, scope: user)
66
66
  # savings_account = DoubleEntry.account(:savings, scope: user)
67
- # DoubleEntry.transfer(
67
+ # credit, debit = DoubleEntry.transfer(
68
68
  # 20.dollars,
69
- # :from => checking_account,
70
- # :to => savings_account,
71
- # :code => :save,
69
+ # from: checking_account,
70
+ # to: savings_account,
71
+ # code: :save,
72
72
  # )
73
73
  # @param [Money] amount The quantity of money to transfer from one account
74
74
  # to the other.
@@ -80,6 +80,7 @@ module DoubleEntry
80
80
  # type of transfer. As specified in the transfer configuration.
81
81
  # @option options :detail [ActiveRecord::Base] ActiveRecord model
82
82
  # associated (via a polymorphic association) with the transfer.
83
+ # @return [[Line, Line]] The credit & debit (in that order) created by the transfer
83
84
  # @raise [DoubleEntry::TransferIsNegative] The amount is less than zero.
84
85
  # @raise [DoubleEntry::TransferNotAllowed] A transfer between these
85
86
  # accounts with the provided code is not allowed. Check configuration.
@@ -91,35 +92,35 @@ module DoubleEntry
91
92
  # Get the current or historic balance of an account.
92
93
  #
93
94
  # @example Obtain the current balance of my checking account
94
- # checking_account = DoubleEntry.account(:checking, :scope => user)
95
+ # checking_account = DoubleEntry.account(:checking, scope: user)
95
96
  # DoubleEntry.balance(checking_account)
96
97
  # @example Obtain the current balance of my checking account (without account or user model)
97
- # DoubleEntry.balance(:checking, :scope => user_id)
98
+ # DoubleEntry.balance(:checking, scope: user_id)
98
99
  # @example Obtain a historic balance of my checking account
99
- # checking_account = DoubleEntry.account(:checking, :scope => user)
100
- # DoubleEntry.balance(checking_account, :at => Time.new(2012, 1, 1))
100
+ # checking_account = DoubleEntry.account(:checking, scope: user)
101
+ # DoubleEntry.balance(checking_account, at: Time.new(2012, 1, 1))
101
102
  # @example Obtain the net balance of my checking account during may
102
- # checking_account = DoubleEntry.account(:checking, :scope => user)
103
+ # checking_account = DoubleEntry.account(:checking, scope: user)
103
104
  # DoubleEntry.balance(
104
105
  # checking_account,
105
- # :from => Time.new(2012, 5, 1, 0, 0, 0),
106
- # :to => Time.new(2012, 5, 31, 23, 59, 59),
106
+ # from: Time.new(2012, 5, 1, 0, 0, 0),
107
+ # to: Time.new(2012, 5, 31, 23, 59, 59),
107
108
  # )
108
109
  # @example Obtain the balance of salary deposits made to my checking account during may
109
- # checking_account = DoubleEntry.account(:checking, :scope => user)
110
+ # checking_account = DoubleEntry.account(:checking, scope: user)
110
111
  # DoubleEntry.balance(
111
112
  # checking_account,
112
- # :code => :salary,
113
- # :from => Time.new(2012, 5, 1, 0, 0, 0),
114
- # :to => Time.new(2012, 5, 31, 23, 59, 59),
113
+ # code: :salary,
114
+ # from: Time.new(2012, 5, 1, 0, 0, 0),
115
+ # to: Time.new(2012, 5, 31, 23, 59, 59),
115
116
  # )
116
117
  # @example Obtain the balance of salary & lottery deposits made to my checking account during may
117
- # checking_account = DoubleEntry.account(:checking, :scope => user)
118
+ # checking_account = DoubleEntry.account(:checking, scope: user)
118
119
  # DoubleEntry.balance(
119
120
  # checking_account,
120
- # :codes => [ :salary, :lottery ],
121
- # :from => Time.new(2012, 5, 1, 0, 0, 0),
122
- # :to => Time.new(2012, 5, 31, 23, 59, 59),
121
+ # codes: [ :salary, :lottery ],
122
+ # from: Time.new(2012, 5, 1, 0, 0, 0),
123
+ # to: Time.new(2012, 5, 31, 23, 59, 59),
123
124
  # )
124
125
  # @param [DoubleEntry::Account:Instance, Symbol] account Find the balance
125
126
  # for this account
@@ -9,12 +9,12 @@ DoubleEntry.configure do |config|
9
9
  # raise 'not a User' unless user.class.name == 'User'
10
10
  # user.id
11
11
  # end
12
- # accounts.define(:identifier => :savings, :scope_identifier => user_scope, :positive_only => true)
13
- # accounts.define(:identifier => :checking, :scope_identifier => user_scope)
12
+ # accounts.define(identifier: :savings, scope_identifier: user_scope, positive_only: true)
13
+ # accounts.define(identifier: :checking, scope_identifier: user_scope)
14
14
  # end
15
15
  #
16
16
  # config.define_transfers do |transfers|
17
- # transfers.define(:from => :checking, :to => :savings, :code => :deposit)
18
- # transfers.define(:from => :savings, :to => :checking, :code => :withdraw)
17
+ # transfers.define(from: :checking, to: :savings, code: :deposit)
18
+ # transfers.define(from: :savings, to: :checking, code: :withdraw)
19
19
  # end
20
20
  end
@@ -1,25 +1,25 @@
1
1
  class CreateDoubleEntryTables < ActiveRecord::Migration<%= migration_version %>
2
2
  def self.up
3
3
  create_table "double_entry_account_balances" do |t|
4
- t.string "account", :null => false
4
+ t.string "account", null: false
5
5
  t.string "scope"
6
- t.bigint "balance", :null => false
7
- t.timestamps :null => false
6
+ t.bigint "balance", null: false
7
+ t.timestamps null: false
8
8
  end
9
9
 
10
- add_index "double_entry_account_balances", ["account"], :name => "index_account_balances_on_account"
11
- add_index "double_entry_account_balances", ["scope", "account"], :name => "index_account_balances_on_scope_and_account", :unique => true
10
+ add_index "double_entry_account_balances", ["account"], name: "index_account_balances_on_account"
11
+ add_index "double_entry_account_balances", ["scope", "account"], name: "index_account_balances_on_scope_and_account", unique: true
12
12
 
13
13
  create_table "double_entry_lines" do |t|
14
- t.string "account", :null => false
14
+ t.string "account", null: false
15
15
  t.string "scope"
16
- t.string "code", :null => false
17
- t.bigint "amount", :null => false
18
- t.bigint "balance", :null => false
19
- t.references "partner", :index => false
20
- t.string "partner_account", :null => false
16
+ t.string "code", null: false
17
+ t.bigint "amount", null: false
18
+ t.bigint "balance", null: false
19
+ t.references "partner", index: false
20
+ t.string "partner_account", null: false
21
21
  t.string "partner_scope"
22
- t.references "detail", :index => false, :polymorphic => true
22
+ t.references "detail", index: false, polymorphic: true
23
23
  <%- if json_metadata -%>
24
24
  if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
25
25
  t.jsonb "metadata"
@@ -27,32 +27,32 @@ class CreateDoubleEntryTables < ActiveRecord::Migration<%= migration_version %>
27
27
  t.json "metadata"
28
28
  end
29
29
  <%- end -%>
30
- t.timestamps :null => false
30
+ t.timestamps null: false
31
31
  end
32
32
 
33
- add_index "double_entry_lines", ["account", "code", "created_at"], :name => "lines_account_code_created_at_idx"
34
- add_index "double_entry_lines", ["account", "created_at"], :name => "lines_account_created_at_idx"
35
- add_index "double_entry_lines", ["scope", "account", "created_at"], :name => "lines_scope_account_created_at_idx"
36
- add_index "double_entry_lines", ["scope", "account", "id"], :name => "lines_scope_account_id_idx"
33
+ add_index "double_entry_lines", ["account", "code", "created_at"], name: "lines_account_code_created_at_idx"
34
+ add_index "double_entry_lines", ["account", "created_at"], name: "lines_account_created_at_idx"
35
+ add_index "double_entry_lines", ["scope", "account", "created_at"], name: "lines_scope_account_created_at_idx"
36
+ add_index "double_entry_lines", ["scope", "account", "id"], name: "lines_scope_account_id_idx"
37
37
 
38
38
  create_table "double_entry_line_checks" do |t|
39
- t.references "last_line", :null => false, :index => false
40
- t.boolean "errors_found", :null => false
39
+ t.references "last_line", null: false, index: false
40
+ t.boolean "errors_found", null: false
41
41
  t.text "log"
42
- t.timestamps :null => false
42
+ t.timestamps null: false
43
43
  end
44
44
 
45
- add_index "double_entry_line_checks", ["created_at", "last_line_id"], :name => "line_checks_created_at_last_line_id_idx"
45
+ add_index "double_entry_line_checks", ["created_at", "last_line_id"], name: "line_checks_created_at_last_line_id_idx"
46
46
  <%- unless json_metadata -%>
47
47
 
48
48
  create_table "double_entry_line_metadata" do |t|
49
- t.references "line", :null => false, :index => false
50
- t.string "key", :null => false
51
- t.string "value", :null => false
52
- t.timestamps :null => false
49
+ t.references "line", null: false, index: false
50
+ t.string "key", null: false
51
+ t.string "value", null: false
52
+ t.timestamps null: false
53
53
  end
54
54
 
55
- add_index "double_entry_line_metadata", ["line_id", "key", "value"], :name => "lines_meta_line_id_key_value_idx"
55
+ add_index "double_entry_line_metadata", ["line_id", "key", "value"], name: "lines_meta_line_id_key_value_idx"
56
56
  <%- end -%>
57
57
  end
58
58
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: double_entry
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0.beta4
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Envato
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-01-25 00:00:00.000000000 Z
11
+ date: 2023-10-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -16,28 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 3.2.0
19
+ version: 6.1.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 3.2.0
26
+ version: 6.1.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: activesupport
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 3.2.0
33
+ version: 6.1.0
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: 3.2.0
40
+ version: 6.1.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: money
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -58,14 +58,14 @@ dependencies:
58
58
  requirements:
59
59
  - - ">="
60
60
  - !ruby/object:Gem::Version
61
- version: 3.2.0
61
+ version: 6.1.0
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
- version: 3.2.0
68
+ version: 6.1.0
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: mysql2
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -220,7 +220,7 @@ dependencies:
220
220
  - - ">="
221
221
  - !ruby/object:Gem::Version
222
222
  version: '0'
223
- description:
223
+ description:
224
224
  email:
225
225
  - rubygems@envato.com
226
226
  executables: []
@@ -256,10 +256,10 @@ licenses:
256
256
  - MIT
257
257
  metadata:
258
258
  bug_tracker_uri: https://github.com/envato/double_entry/issues
259
- changelog_uri: https://github.com/envato/double_entry/blob/v2.0.0.beta4/CHANGELOG.md
260
- documentation_uri: https://www.rubydoc.info/gems/double_entry/2.0.0.beta4
261
- source_code_uri: https://github.com/envato/double_entry/tree/v2.0.0.beta4
262
- post_install_message:
259
+ changelog_uri: https://github.com/envato/double_entry/blob/v2.0.0/CHANGELOG.md
260
+ documentation_uri: https://www.rubydoc.info/gems/double_entry/2.0.0
261
+ source_code_uri: https://github.com/envato/double_entry/tree/v2.0.0
262
+ post_install_message:
263
263
  rdoc_options: []
264
264
  require_paths:
265
265
  - lib
@@ -267,15 +267,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
267
267
  requirements:
268
268
  - - ">="
269
269
  - !ruby/object:Gem::Version
270
- version: 2.3.0
270
+ version: '3'
271
271
  required_rubygems_version: !ruby/object:Gem::Requirement
272
272
  requirements:
273
- - - ">"
273
+ - - ">="
274
274
  - !ruby/object:Gem::Version
275
- version: 1.3.1
275
+ version: '0'
276
276
  requirements: []
277
- rubygems_version: 3.1.2
278
- signing_key:
277
+ rubygems_version: 3.4.21
278
+ signing_key:
279
279
  specification_version: 4
280
280
  summary: Tools to build your double entry financial ledger
281
281
  test_files: []