morty 0.0.1 → 0.1.0

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.
Files changed (134) hide show
  1. checksums.yaml +5 -5
  2. data/.github/dependabot.yml +12 -0
  3. data/.github/workflows/ci.yml +107 -0
  4. data/.gitignore +17 -3
  5. data/.rubocop.yml +20 -0
  6. data/Appraisals +24 -0
  7. data/Gemfile +28 -1
  8. data/LICENSE +21 -0
  9. data/README.md +37 -7
  10. data/Rakefile +37 -0
  11. data/app/models/morty/account.rb +37 -0
  12. data/app/models/morty/account_type.rb +7 -0
  13. data/app/models/morty/activity.rb +147 -0
  14. data/app/models/morty/activity_type.rb +7 -0
  15. data/app/models/morty/application_record.rb +6 -0
  16. data/app/models/morty/entry.rb +24 -0
  17. data/app/models/morty/entry_type.rb +23 -0
  18. data/app/models/morty/ledger.rb +8 -0
  19. data/config/routes.rb +2 -0
  20. data/config.ru +7 -0
  21. data/cucumber.yml +2 -0
  22. data/db/migrate/20260224063053_create_morty_schema.rb +17 -0
  23. data/db/seeds.rb +18 -0
  24. data/db/sql/create_morty_schema.sql +479 -0
  25. data/features/accountant.feature +47 -0
  26. data/features/adjustment.feature +79 -0
  27. data/features/cancel.feature +130 -0
  28. data/features/daily.feature +42 -0
  29. data/features/default.feature +33 -0
  30. data/features/ledger.feature +57 -0
  31. data/features/retroactive.feature +92 -0
  32. data/features/return.feature +112 -0
  33. data/features/reversal.feature +57 -0
  34. data/features/simulation.feature +128 -0
  35. data/features/support/accountants/adjusting_accountant.rb +34 -0
  36. data/features/support/accountants/daily_accountant.rb +13 -0
  37. data/features/support/accountants/default_accountant.rb +2 -0
  38. data/features/support/accountants/defaulting_accountant.rb +32 -0
  39. data/features/support/accountants/multiple_ledgers_accountant.rb +51 -0
  40. data/features/support/accountants/simulating_accountant.rb +36 -0
  41. data/features/support/accountants/sourceless_accountant.rb +2 -0
  42. data/features/support/accountants/waterfalling_accountant.rb +15 -0
  43. data/features/support/env.rb +17 -0
  44. data/features/waterfall.feature +34 -0
  45. data/gemfiles/rails_7.0.gemfile +30 -0
  46. data/gemfiles/rails_7.0.gemfile.lock +494 -0
  47. data/gemfiles/rails_7.1.gemfile +30 -0
  48. data/gemfiles/rails_7.1.gemfile.lock +543 -0
  49. data/gemfiles/rails_7.2.gemfile +30 -0
  50. data/gemfiles/rails_7.2.gemfile.lock +539 -0
  51. data/gemfiles/rails_8.0.gemfile +30 -0
  52. data/gemfiles/rails_8.0.gemfile.lock +536 -0
  53. data/gemfiles/rails_8.1.gemfile +30 -0
  54. data/gemfiles/rails_8.1.gemfile.lock +538 -0
  55. data/lib/morty/accountant.rb +332 -0
  56. data/lib/morty/adjustment.rb +64 -0
  57. data/lib/morty/book.rb +54 -0
  58. data/lib/morty/context/activity.rb +52 -0
  59. data/lib/morty/context/daily.rb +23 -0
  60. data/lib/morty/context/simulation.rb +26 -0
  61. data/lib/morty/cucumber/helpers.rb +27 -0
  62. data/lib/morty/cucumber/steps.rb +191 -0
  63. data/lib/morty/diff.rb +71 -0
  64. data/lib/morty/dsl.rb +86 -0
  65. data/lib/morty/engine.rb +21 -0
  66. data/lib/morty/error.rb +3 -0
  67. data/lib/morty/event.rb +27 -0
  68. data/lib/morty/list/activity.rb +57 -0
  69. data/lib/morty/rate.rb +59 -0
  70. data/lib/morty/schedule.rb +36 -0
  71. data/lib/morty/seed.rb +60 -0
  72. data/lib/morty/source.rb +19 -0
  73. data/lib/morty/tasks/morty_tasks.rake +4 -0
  74. data/lib/morty/version.rb +1 -1
  75. data/lib/morty.rb +27 -1
  76. data/morty.gemspec +22 -19
  77. data/spec/dummy/Rakefile +6 -0
  78. data/spec/dummy/app/assets/images/.keep +0 -0
  79. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  80. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  81. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  82. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  83. data/spec/dummy/app/jobs/application_job.rb +7 -0
  84. data/spec/dummy/app/models/application_record.rb +3 -0
  85. data/spec/dummy/app/models/concerns/.keep +0 -0
  86. data/spec/dummy/app/views/layouts/application.html.erb +28 -0
  87. data/spec/dummy/app/views/pwa/manifest.json.erb +22 -0
  88. data/spec/dummy/app/views/pwa/service-worker.js +26 -0
  89. data/spec/dummy/bin/ci +6 -0
  90. data/spec/dummy/bin/dev +2 -0
  91. data/spec/dummy/bin/rails +4 -0
  92. data/spec/dummy/bin/rake +4 -0
  93. data/spec/dummy/bin/setup +35 -0
  94. data/spec/dummy/config/application.rb +48 -0
  95. data/spec/dummy/config/boot.rb +5 -0
  96. data/spec/dummy/config/cable.yml +10 -0
  97. data/spec/dummy/config/ci.rb +15 -0
  98. data/spec/dummy/config/database.yml +15 -0
  99. data/spec/dummy/config/environment.rb +5 -0
  100. data/spec/dummy/config/environments/development.rb +47 -0
  101. data/spec/dummy/config/environments/test.rb +53 -0
  102. data/spec/dummy/config/initializers/content_security_policy.rb +29 -0
  103. data/spec/dummy/config/initializers/filter_parameter_logging.rb +8 -0
  104. data/spec/dummy/config/initializers/inflections.rb +16 -0
  105. data/spec/dummy/config/locales/en.yml +31 -0
  106. data/spec/dummy/config/puma.rb +39 -0
  107. data/spec/dummy/config/routes.rb +3 -0
  108. data/spec/dummy/config/storage.yml +27 -0
  109. data/spec/dummy/config.ru +6 -0
  110. data/spec/dummy/db/seeds.rb +52 -0
  111. data/spec/dummy/log/.keep +0 -0
  112. data/spec/dummy/public/400.html +135 -0
  113. data/spec/dummy/public/404.html +135 -0
  114. data/spec/dummy/public/406-unsupported-browser.html +135 -0
  115. data/spec/dummy/public/422.html +135 -0
  116. data/spec/dummy/public/500.html +135 -0
  117. data/spec/dummy/public/icon.png +0 -0
  118. data/spec/dummy/public/icon.svg +3 -0
  119. data/spec/lib/accountant_spec.rb +236 -0
  120. data/spec/lib/book_spec.rb +91 -0
  121. data/spec/lib/diff_spec.rb +102 -0
  122. data/spec/lib/event_spec.rb +53 -0
  123. data/spec/lib/list/activity_spec.rb +117 -0
  124. data/spec/lib/schedule_spec.rb +106 -0
  125. data/spec/lib/source_spec.rb +31 -0
  126. data/spec/models/account_spec.rb +48 -0
  127. data/spec/models/activity_spec.rb +139 -0
  128. data/spec/models/entry_spec.rb +41 -0
  129. data/spec/models/entry_type_spec.rb +43 -0
  130. data/spec/rate_spec.rb +83 -0
  131. data/spec/spec_helper.rb +36 -0
  132. data/spec/support/test_helpers.rb +25 -0
  133. metadata +193 -16
  134. data/LICENSE.txt +0 -22
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: c0eccb25c8002383a5645689dd7514e8281142e9
4
- data.tar.gz: 4eb37d866ecde867de7867a6bdeeaf8382a3379d
2
+ SHA256:
3
+ metadata.gz: ca8e5765f21e862e08f9fb6cadf68a873670152ce5b6379580280553f0502948
4
+ data.tar.gz: 0103d960dce53fba686749e8d8de673ecdf5e026491891d9424ecb8f272edc27
5
5
  SHA512:
6
- metadata.gz: 4fa0eceb9117ad0d991b80d5b65aab06b5608a922da5e12f0b45a738cbdd0ac521f73407de02f03cfd198916a94b7da13ab261f13431167335cad430e6c207eb
7
- data.tar.gz: 9dcb0516a5a529bd31d1bd65babbb81b82dfbeeea6f20eb044a870e14bd1c52156059ee3e55cbf0de3a2d0dfbbbf79f53a7201a275644e291ebbe61eabccb9dc
6
+ metadata.gz: 292c7454d6b6367b0b567dfb492b322dc89254628250f0bda98f5b8e2628d207f8f786e72c52c60ac159400c938a0e982b6903510247cd8044e02f024ac2ee9f
7
+ data.tar.gz: 3ddf80e12ca731a7a46c1ca4682cd246f03c40cfbbeb288b93ea6fcb807d2c00db13ebd7aa8906dff856867b5f9838bbf6a131fd867c6284c64db8f32ee57ce7
@@ -0,0 +1,12 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: bundler
4
+ directory: "/"
5
+ schedule:
6
+ interval: weekly
7
+ open-pull-requests-limit: 10
8
+ - package-ecosystem: github-actions
9
+ directory: "/"
10
+ schedule:
11
+ interval: weekly
12
+ open-pull-requests-limit: 10
@@ -0,0 +1,107 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [ main ]
7
+
8
+ jobs:
9
+ lint:
10
+ runs-on: ubuntu-latest
11
+ env:
12
+ RUBY_VERSION: ruby-4.0
13
+ steps:
14
+ - name: Checkout code
15
+ uses: actions/checkout@v6
16
+
17
+ - name: Set up Ruby
18
+ uses: ruby/setup-ruby@v1
19
+ with:
20
+ ruby-version: ${{ env.RUBY_VERSION }}
21
+ bundler-cache: true
22
+
23
+ - name: Lint code for consistent style
24
+ run: bin/rubocop -f github
25
+
26
+ test:
27
+ runs-on: ubuntu-latest
28
+
29
+ services:
30
+ postgres:
31
+ image: postgres:16
32
+ env:
33
+ POSTGRES_PASSWORD: postgres
34
+ ports:
35
+ - 5432:5432
36
+ options: >-
37
+ --health-cmd pg_isready
38
+ --health-interval 10s
39
+ --health-timeout 5s
40
+ --health-retries 5
41
+
42
+ strategy:
43
+ fail-fast: false
44
+ matrix:
45
+ include:
46
+ # Rails 7.0
47
+ - { ruby: '3.2', rails: 'rails_7.0' }
48
+ - { ruby: '3.3', rails: 'rails_7.0' }
49
+ - { ruby: '3.4', rails: 'rails_7.0' }
50
+ - { ruby: '4.0', rails: 'rails_7.0' }
51
+
52
+ # Rails 7.1
53
+ - { ruby: '3.2', rails: 'rails_7.1' }
54
+ - { ruby: '3.3', rails: 'rails_7.1' }
55
+ - { ruby: '3.4', rails: 'rails_7.1' }
56
+ - { ruby: '4.0', rails: 'rails_7.1' }
57
+
58
+ # Rails 7.2
59
+ - { ruby: '3.2', rails: 'rails_7.2' }
60
+ - { ruby: '3.3', rails: 'rails_7.2' }
61
+ - { ruby: '3.4', rails: 'rails_7.2' }
62
+ - { ruby: '4.0', rails: 'rails_7.2' }
63
+
64
+ # Rails 8.0
65
+ - { ruby: '3.2', rails: 'rails_8.0' }
66
+ - { ruby: '3.3', rails: 'rails_8.0' }
67
+ - { ruby: '3.4', rails: 'rails_8.0' }
68
+ - { ruby: '4.0', rails: 'rails_8.0' }
69
+
70
+ # Rails 8.1
71
+ - { ruby: '3.2', rails: 'rails_8.1' }
72
+ - { ruby: '3.3', rails: 'rails_8.1' }
73
+ - { ruby: '3.4', rails: 'rails_8.1' }
74
+ - { ruby: '4.0', rails: 'rails_8.1' }
75
+
76
+ env:
77
+ BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile
78
+ PGHOST: localhost
79
+ PGUSER: postgres
80
+ PGPASSWORD: postgres
81
+
82
+ steps:
83
+ - uses: actions/checkout@v6
84
+
85
+ - name: Set up Ruby ${{ matrix.ruby }}
86
+ uses: ruby/setup-ruby@v1
87
+ with:
88
+ ruby-version: ${{ matrix.ruby }}
89
+ bundler-cache: true
90
+
91
+ - name: Create database
92
+ run: createdb morty_test -h localhost -U postgres
93
+
94
+ - name: Run tests
95
+ run: bundle exec rake
96
+ env:
97
+ CI: true
98
+ CUCUMBER_PROFILE: ci
99
+
100
+ - name: Upload test results
101
+ uses: actions/upload-artifact@v7
102
+ if: always()
103
+ with:
104
+ name: test-results-ruby${{ matrix.ruby }}-${{ matrix.rails }}
105
+ path: |
106
+ tmp/rspec.xml
107
+ tmp/cucumber.xml/
data/.gitignore CHANGED
@@ -1,14 +1,28 @@
1
1
  /.bundle/
2
- /.yardoc
3
2
  /Gemfile.lock
3
+
4
+ /.yardoc
4
5
  /_yardoc/
5
- /coverage/
6
6
  /doc/
7
+
8
+ /log/*.log
9
+
7
10
  /pkg/
8
- /spec/reports/
9
11
  /tmp/
12
+
10
13
  *.bundle
11
14
  *.so
12
15
  *.o
13
16
  *.a
14
17
  mkmf.log
18
+
19
+ /coverage/
20
+ /spec/reports/
21
+
22
+ /spec/dummy/db/*.sqlite3
23
+ /spec/dummy/db/*.sqlite3-*
24
+ /spec/dummy/db/structure.sql
25
+ /spec/dummy/db/schema.rb
26
+ /spec/dummy/log/*.log
27
+ /spec/dummy/storage/
28
+ /spec/dummy/tmp/
data/.rubocop.yml ADDED
@@ -0,0 +1,20 @@
1
+ # Omakase Ruby styling for Rails
2
+ inherit_gem: { rubocop-rails-omakase: rubocop.yml }
3
+
4
+ AllCops:
5
+ TargetRubyVersion: 4.0
6
+
7
+ Layout/SpaceInsideArrayLiteralBrackets:
8
+ Enabled: false
9
+
10
+ Layout/SpaceInsideArrayPercentLiteral:
11
+ Enabled: false
12
+
13
+ Layout/SpaceBeforeComma:
14
+ Enabled: false
15
+
16
+ Style/PercentLiteralDelimiters:
17
+ Enabled: false
18
+
19
+ Layout/EndAlignment:
20
+ Enabled: false
data/Appraisals ADDED
@@ -0,0 +1,24 @@
1
+ appraise "rails-7.0" do
2
+ gem "activerecord", "~> 7.0.0"
3
+ gem "railties", "~> 7.0.0"
4
+ end
5
+
6
+ appraise "rails-7.1" do
7
+ gem "activerecord", "~> 7.1.0"
8
+ gem "railties", "~> 7.1.0"
9
+ end
10
+
11
+ appraise "rails-7.2" do
12
+ gem "activerecord", "~> 7.2.0"
13
+ gem "railties", "~> 7.2.0"
14
+ end
15
+
16
+ appraise "rails-8.0" do
17
+ gem "activerecord", "~> 8.0.0"
18
+ gem "railties", "~> 8.0.0"
19
+ end
20
+
21
+ appraise "rails-8.1" do
22
+ gem "activerecord", "~> 8.1.0"
23
+ gem "railties", "~> 8.1.0"
24
+ end
data/Gemfile CHANGED
@@ -1,4 +1,31 @@
1
- source 'https://rubygems.org'
1
+ source "https://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in morty.gemspec
4
4
  gemspec
5
+
6
+ gem "rails"
7
+ gem "lookup_by", github: "companygardener/lookup_by"
8
+
9
+ # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/]
10
+ gem "rubocop-rails-omakase", require: false
11
+
12
+ gem "appraisal", "~> 2.5.0"
13
+
14
+ group :development, :test do
15
+ gem "rspec-rails"
16
+
17
+ gem "pg"
18
+ gem "puma"
19
+ gem "propshaft"
20
+ end
21
+
22
+ group :test do
23
+ gem "rspec-its"
24
+ gem "factory_bot_rails"
25
+ gem "shoulda-matchers"
26
+ gem "cucumber"
27
+ gem "simplecov", require: false
28
+ gem "rspec_junit_formatter"
29
+ gem "timecop"
30
+ gem "chronic"
31
+ end
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019-2026 Erik Peterson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -1,26 +1,46 @@
1
1
  # Morty
2
2
 
3
- TODO: Write a gem description
3
+ Morty is an accountant.
4
+
5
+ **Depends on:**
6
+
7
+ * Rails 8.1.2
8
+ * Ruby 4.0.1
9
+ * PostgreSQL 17
10
+
11
+ Source code
12
+
13
+ * [github.com/companygardener/morty][source]
4
14
 
5
15
  ## Installation
6
16
 
7
17
  Add this line to your application's Gemfile:
8
18
 
9
- ```ruby
10
- gem 'morty'
11
- ```
19
+ gem 'morty'
12
20
 
13
21
  And then execute:
14
22
 
15
- $ bundle
23
+ $ bundle install
16
24
 
17
25
  Or install it yourself as:
18
26
 
19
27
  $ gem install morty
20
28
 
21
- ## Usage
29
+ Install migrations (Morty is a Rails engine):
30
+
31
+ $ rake morty:install:migrations
32
+ $ rake db:migrate
33
+
34
+ ## Testing
35
+
36
+ Morty uses rspec and cucumber. Install them:
22
37
 
23
- TODO: Write usage instructions here
38
+ $ bundle install
39
+
40
+ Run the test suite:
41
+
42
+ $ rake db:reset
43
+ $ rake
24
44
 
25
45
  ## Contributing
26
46
 
@@ -29,3 +49,13 @@ TODO: Write usage instructions here
29
49
  3. Commit your changes (`git commit -am 'Add some feature'`)
30
50
  4. Push to the branch (`git push origin my-new-feature`)
31
51
  5. Create a new Pull Request
52
+
53
+ ## License
54
+
55
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
56
+
57
+ ## Copyright
58
+
59
+ Copyright © 2025-2026 Erik Peterson. Licensed under the MIT License.
60
+
61
+ [source]: https://github.com/companygardener/morty "Morty source"
data/Rakefile CHANGED
@@ -1,2 +1,39 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
1
6
  require "bundler/gem_tasks"
2
7
 
8
+ require "rspec/core/rake_task"
9
+ RSpec::Core::RakeTask.new(:spec) do |t|
10
+ if ENV["CI"]
11
+ t.rspec_opts = "--format progress --format RspecJunitFormatter --out tmp/rspec.xml"
12
+ end
13
+ end
14
+
15
+ require "cucumber/rake/task"
16
+ Cucumber::Rake::Task.new do |t|
17
+ t.profile = ENV["CUCUMBER_PROFILE"] if ENV["CUCUMBER_PROFILE"]
18
+ end
19
+
20
+ task :db_setup do
21
+ print "Preparing database..."
22
+ ActiveRecord::Migration.verbose = false
23
+ original_stdout = $stdout
24
+ $stdout = File.open(File::NULL, "w")
25
+ Rake::Task["app:db:prepare"].invoke
26
+ ensure
27
+ $stdout = original_stdout
28
+ puts " done"
29
+ end
30
+
31
+ task spec: :db_setup
32
+ task cucumber: :db_setup
33
+ task default: [:spec, :cucumber]
34
+
35
+ desc "Start a console with Morty loaded in the dummy app context"
36
+ task :console do
37
+ ENV["APP_RAKEFILE"] = APP_RAKEFILE
38
+ exec "irb -r #{File.expand_path("spec/dummy/config/environment", __dir__)}"
39
+ end
@@ -0,0 +1,37 @@
1
+ module Morty
2
+ class Account < ApplicationRecord
3
+ lookup_by :account, cache: true
4
+
5
+ lookup_for :account_type, class_name: AccountType
6
+
7
+ def self.sum_over_activities(activity_ids)
8
+ return {} if activity_ids.empty?
9
+
10
+ rows = connection.select_all(sanitize_sql_array([
11
+ "SELECT account, SUM(amount) AS balance FROM morty.details WHERE activity_id IN (?) GROUP BY account",
12
+ activity_ids
13
+ ]))
14
+
15
+ rows.each_with_object({}) do |row, hash|
16
+ hash[row["account"].to_sym] = row["balance"].to_d
17
+ end
18
+ end
19
+
20
+ def self.sum_by_source(source, effective_date: nil, accounting_date: nil)
21
+ raise "pick one: effective_date or accounting_date" unless effective_date || accounting_date
22
+ raise "pick one: effective_date or accounting_date" if effective_date && accounting_date
23
+
24
+ date_col = effective_date ? "effective_date" : "accounting_date"
25
+ date_val = effective_date || accounting_date
26
+
27
+ rows = connection.select_all(sanitize_sql_array([
28
+ "SELECT ledger, account, SUM(amount) AS balance FROM morty.details WHERE source_id = ? AND #{date_col} <= ? GROUP BY ledger, account",
29
+ source.id, date_val
30
+ ]))
31
+
32
+ rows.each_with_object(Hash.new { |h, k| h[k] = {} }) do |row, hash|
33
+ hash[row["ledger"].to_sym][row["account"].to_sym] = row["balance"].to_d
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,7 @@
1
+ module Morty
2
+ class AccountType < ApplicationRecord
3
+ lookup_by :account_type, cache: true
4
+
5
+ has_many :accounts
6
+ end
7
+ end
@@ -0,0 +1,147 @@
1
+ require "colorize"
2
+
3
+ module Morty
4
+ class Activity < ApplicationRecord
5
+ lookup_for :activity_type, symbolize: true
6
+
7
+ alias_attribute :amount, :activity_amount
8
+
9
+ # AR doesn't support alias_attribute for associations, so we have to do it manually
10
+ alias_method :type, :activity_type
11
+ alias_method :type=, :activity_type=
12
+ alias_method :type?, :activity_type?
13
+
14
+ belongs_to :cancels, class_name: "Activity", optional: true
15
+
16
+ has_one :cancelled_by, class_name: "Activity", foreign_key: :cancels_id
17
+
18
+ has_many :entries
19
+
20
+ default_scope -> { includes(:cancelled_by, :entries) }
21
+
22
+ scope :until, ->(date) { where("effective_date <= ?", date) }
23
+ scope :with_source, ->(source) { where(source_id: source.id) }
24
+ scope :with_type, ->(type) { where(activity_type: type) }
25
+
26
+ after_initialize do
27
+ self.effective_date ||= accounting_date
28
+ end
29
+
30
+ def ==(other)
31
+ other.class == self.class &&
32
+ (id.nil? || other.id.nil? || other.id == id) &&
33
+ other.source_id == source_id &&
34
+
35
+ other.type == type &&
36
+ other.effective_date == effective_date &&
37
+ other.amount == amount
38
+ end
39
+
40
+ def cancel(date, type)
41
+ build_cancelled_by(type: type) do |a|
42
+ a.source_id = source_id
43
+ a.accounting_date = date
44
+ a.effective_date = effective_date
45
+ a.amount = amount
46
+ a.entries = entries.map(&:inverse)
47
+ a.cancels = self
48
+ end
49
+ end
50
+
51
+ def reverse(date, type)
52
+ self.class.new(type: type) do |a|
53
+ a.source_id = source_id
54
+ a.accounting_date = date
55
+ a.entries = entries.map(&:inverse)
56
+ end
57
+ end
58
+
59
+ def cancelled?
60
+ !! cancelled_by
61
+ end
62
+
63
+ def cancelling?
64
+ cancels? || cancelled?
65
+ end
66
+
67
+ def cancels?
68
+ !! cancels_id || cancels
69
+ end
70
+
71
+ def retroactive?
72
+ effective_date < accounting_date
73
+ end
74
+
75
+ # belongs_to lite
76
+ def source=(obj)
77
+ self.source_id = obj.id
78
+ end
79
+
80
+ def to_event
81
+ { amount: amount, date: effective_date, type: type }
82
+ end
83
+
84
+ def debug(ledger = :default)
85
+ result = "\n"
86
+
87
+ header = type.to_s.humanize
88
+ header << " $%.2f" % amount if amount
89
+ header << " %s\n" % accounting_date
90
+
91
+ result << header.blue
92
+
93
+ result << "\nEffective on %s" % effective_date unless accounting_date == effective_date
94
+
95
+ # entries.with_ledger(ledger)
96
+ list = entries.select { |e| e.ledger == ledger }
97
+
98
+ if list.any?
99
+ max = Account.pluck(:account).map(&:length).max
100
+
101
+ result << "\n#{ledger.to_s.humanize.yellow} ledger entries\n"
102
+ result << "\n"
103
+ result << " " * max + " | DR | CR \n"
104
+ result << "-" * max + "--|-----------|----------\n"
105
+
106
+ amounts = Hash.new(0)
107
+
108
+ list.each do |entry|
109
+ amounts[entry.type.dr] += entry.amount
110
+ amounts[entry.type.cr] -= entry.amount
111
+ end
112
+
113
+ amounts.each do |account, amount|
114
+ if amount > 0
115
+ result << " %#{max}s | %9.2f |\n" % [account, amount]
116
+ else
117
+ result << " %#{max}s | | %9.2f\n" % [account, -amount]
118
+ end
119
+ end
120
+ else
121
+ result << "\n#{ledger.to_s.humanize} ledger has no entries\n".yellow
122
+ end
123
+
124
+ result << "\n"
125
+
126
+ puts result
127
+ end
128
+
129
+ def inspect
130
+ result = "#<Activity%-10s " % "[#{id || "new"}]"
131
+
132
+ result << (amount ? "$%8.2f" % amount : " %8s" % "")
133
+
134
+ result << " #{accounting_date}" if accounting_date
135
+
136
+ if effective_date != accounting_date
137
+ result << " #{effective_date}"
138
+ else
139
+ result << " "
140
+ end
141
+
142
+ result << " #{type}" if type
143
+ result << ">"
144
+ result
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,7 @@
1
+ module Morty
2
+ class ActivityType < ApplicationRecord
3
+ lookup_by :activity_type, cache: true
4
+
5
+ has_many :activities
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ module Morty
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ self.table_name_prefix = "morty."
5
+ end
6
+ end
@@ -0,0 +1,24 @@
1
+ module Morty
2
+ class Entry < ApplicationRecord
3
+ belongs_to :activity
4
+ belongs_to :entry_type
5
+
6
+ delegate :ledger, :dr, :cr, to: :type
7
+
8
+ def inspect
9
+ if type
10
+ "#<Entry[%s] $%.2f %s DR[%s] CR[%s]>" % [id || "new", amount, ledger, dr, cr]
11
+ else
12
+ "#<Entry[new]>"
13
+ end
14
+ end
15
+
16
+ def inverse
17
+ self.class.new(amount:, entry_type: type.inverse)
18
+ end
19
+
20
+ def type
21
+ entry_type
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,23 @@
1
+ module Morty
2
+ class EntryType < ApplicationRecord
3
+ lookup_for :dr, class_name: Account, symbolize: true
4
+ lookup_for :cr, class_name: Account, symbolize: true
5
+ lookup_for :ledger, class_name: Ledger, symbolize: true
6
+
7
+ has_many :entries
8
+
9
+ def self.find_by_accounts(dr, cr, ledger = :default)
10
+ all.detect { |obj| obj.dr == dr &&
11
+ obj.cr == cr &&
12
+ obj.ledger == ledger }
13
+ end
14
+
15
+ def inverse
16
+ self.class.find_by_accounts(cr, dr, ledger)
17
+ end
18
+
19
+ def inspect
20
+ "#<EntryType[%2s] %s ledger DR[%-20s] CR[%-20s]>" % [id, ledger, dr, cr]
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,8 @@
1
+ module Morty
2
+ class Ledger < ApplicationRecord
3
+ lookup_by :ledger, cache: true
4
+
5
+ has_many :entry_types
6
+ has_many :entries, through: :entry_types
7
+ end
8
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,2 @@
1
+ Morty::Engine.routes.draw do
2
+ end
data/config.ru ADDED
@@ -0,0 +1,7 @@
1
+ require "rubygems"
2
+ require "bundler"
3
+
4
+ Bundler.require :default, :development
5
+
6
+ Combustion.initialize! :all
7
+ run Combustion::Application
data/cucumber.yml ADDED
@@ -0,0 +1,2 @@
1
+ default: --publish-quiet
2
+ ci: --publish-quiet --format progress --format junit --out tmp/cucumber.xml
@@ -0,0 +1,17 @@
1
+ class CreateMortySchema < ActiveRecord::Migration[7.0]
2
+ def up
3
+ original_search_path = execute("SHOW search_path").first['search_path']
4
+
5
+ begin
6
+ execute Morty::Engine.root.join("db/sql/create_morty_schema.sql").read
7
+ ensure
8
+ connection.execute("SET search_path TO #{original_search_path}")
9
+ end
10
+ end
11
+
12
+ def down
13
+ raise "this will drop the morty schema, run with UNSAFE_MIGRATION=true to execute" unless ENV["UNSAFE_MIGRATION"]
14
+
15
+ drop_schema :morty
16
+ end
17
+ end