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
@@ -0,0 +1,130 @@
1
+ Feature: Cancellations
2
+
3
+ Create an adjustment when an activity is cancelled.
4
+
5
+ Background:
6
+ Given a simulating accountant
7
+
8
+ And a start date of 2026-01-01
9
+ And an interest rate of 36.5%
10
+ And the schedule:
11
+ | payment | 2026-01-06 | 1005.00 |
12
+
13
+ And I simulate these activities:
14
+ | issue | 2026-01-01 | 1000.00 |
15
+
16
+ Then I have these balances:
17
+ | cash | -1000.00 |
18
+ | principal | 1000.00 |
19
+
20
+ And I have 1 issue activity
21
+
22
+ Scenario: Cancel an activity without adjustment
23
+ When I save and reload the accountant
24
+ And I cancel the 2026-01-01 issue on 2026-01-01
25
+
26
+ Then I have these balances:
27
+ | cash | 0.00 |
28
+ | principal | 0.00 |
29
+
30
+ And these activity counts:
31
+ | issue | 1 |
32
+ | cancel | 1 |
33
+
34
+ Scenario: Cancel an activity with adjustment
35
+ Given I simulate until 2026-01-02
36
+
37
+ When I save and reload the accountant
38
+ And I cancel the 2026-01-01 issue on 2026-01-02
39
+
40
+ Then I have these balances:
41
+ | cash | 0.00 |
42
+ | principal | 0.00 |
43
+ | interest | 0.00 |
44
+ | revenue | 0.00 |
45
+
46
+ And these activity counts:
47
+ | issue | 1 |
48
+ | cancel | 1 |
49
+ | interest | 1 |
50
+ | adjustment | 1 |
51
+
52
+ Scenario: Cancel an activity in the past
53
+ When I simulate until 2026-01-08
54
+ And I save and reload the accountant
55
+ And I cancel the 2026-01-06 payment on 2026-01-08
56
+
57
+ Then I have these balances:
58
+ | cash | -1000.00 |
59
+ | principal | 1000.00 |
60
+ | interest | 7.00 |
61
+ | revenue | -7.00 |
62
+
63
+ And these activity counts:
64
+ | issue | 1 |
65
+ | payment | 1 |
66
+ | interest | 5 |
67
+ | cancel | 1 |
68
+ | adjustment | 1 |
69
+
70
+ Scenario: Cancel an activity entered today
71
+ When I simulate until 2026-01-06
72
+
73
+ Then I have these balances:
74
+ | cash | 5.00 |
75
+ | principal | 0.00 |
76
+ | interest | 0.00 |
77
+ | revenue | -5.00 |
78
+
79
+ And these activity counts:
80
+ | issue | 1 |
81
+ | payment | 1 |
82
+ | interest | 5 |
83
+
84
+ When I save and reload the accountant
85
+ And I cancel the 2026-01-06 payment on 2026-01-06
86
+
87
+ Then I have these balances:
88
+ | cash | -1000.00 |
89
+ | principal | 1000.00 |
90
+ | interest | 5.00 |
91
+ | revenue | -5.00 |
92
+
93
+ And these activity counts:
94
+ | issue | 1 |
95
+ | payment | 1 |
96
+ | interest | 5 |
97
+ | cancel | 1 |
98
+
99
+ Scenario: Cancel an interleaved activity
100
+ When I simulate these activities:
101
+ | payment | 2026-01-01 | 500.00 |
102
+ | finish | 2026-01-06 | |
103
+
104
+ Then I have these balances:
105
+ | cash | 505.00 |
106
+ | principal | 0.00 |
107
+ | interest | 0.00 |
108
+ | revenue | -2.50 |
109
+ | payable | -502.50 |
110
+
111
+ And I have these activity counts:
112
+ | issue | 1 |
113
+ | payment | 2 |
114
+ | interest | 5 |
115
+
116
+ When I cancel the 2026-01-01 payment
117
+
118
+ Then I have these balances:
119
+ | cash | 5.00 |
120
+ | principal | 0.00 |
121
+ | interest | 0.00 |
122
+ | revenue | -5.00 |
123
+ | payable | 0.00 |
124
+
125
+ And these activity counts:
126
+ | issue | 1 |
127
+ | payment | 2 |
128
+ | interest | 5 |
129
+ | cancel | 1 |
130
+ | adjustment | 1 |
@@ -0,0 +1,42 @@
1
+ Feature: Daily
2
+
3
+ An accountant can define a daily process to run during a simulation.
4
+ It may accrue daily interest, assess late fees, default principal balances, etc.
5
+
6
+ In addition, it could be the actual process used to perform these activities on a daily basis.
7
+
8
+ Background:
9
+ Given a daily accountant
10
+ And a start date of today
11
+ And an interest rate of 36.5%
12
+
13
+ Scenario: Daily guard passes
14
+ When I run the daily for today
15
+ Then I have an interest activity
16
+
17
+ Scenario: Daily guard fails
18
+ When I run the daily for today
19
+ Then I have 1 activity
20
+
21
+ When I run the daily for tomorrow
22
+ Then I still have 1 activity
23
+
24
+ When I run the daily for 2 days from now
25
+ Then I have 2 activities
26
+
27
+ And I have these balances:
28
+ | interest | 2.00 |
29
+ | revenue | -2.00 |
30
+
31
+ Scenario: Daily runs only once
32
+ Given a simulating accountant
33
+ And a start date of today
34
+ And an interest rate of 36.5%
35
+ And I simulate these activities:
36
+ | issue | 1000.00 |
37
+
38
+ When I run the daily for tomorrow
39
+ Then I have 1 interest activity
40
+
41
+ When I run the daily for tomorrow
42
+ Then I have 1 interest activity
@@ -0,0 +1,33 @@
1
+ Feature: Default
2
+
3
+ Scenario: Late principal and interest
4
+ Given a defaulting accountant
5
+ And a start date of 2026-01-01
6
+ And an interest rate of 36.5%
7
+
8
+ And the schedule:
9
+ | issue | 2026-01-01 | 1000.00 |
10
+ | default | 2026-02-01 | 100.00 |
11
+ | default | 2026-03-01 | 100.00 |
12
+
13
+ When I simulate until 2026-02-01
14
+
15
+ Then I save and reload the accountant
16
+ And I have these balances:
17
+ | cash | -1000.00 |
18
+ | principal | 931.00 |
19
+ | principal_late | 69.00 |
20
+ | interest | 0.00 |
21
+ | interest_late | 31.00 |
22
+ | revenue | -31.00 |
23
+
24
+ When I simulate until 2026-03-01
25
+
26
+ Then I save and reload the accountant
27
+ And I have these balances:
28
+ | cash | -1000.00 |
29
+ | principal | 859.00 |
30
+ | principal_late | 141.00 |
31
+ | interest | 0.00 |
32
+ | interest_late | 59.00 |
33
+ | revenue | -59.00 |
@@ -0,0 +1,57 @@
1
+ Feature: Ledgers
2
+
3
+ Background:
4
+ Given a multiple ledgers accountant
5
+ And a start date of 2026-01-01
6
+ And an interest rate of 36.5%
7
+ And the schedule:
8
+ | payment | 2026-01-06 | 1005.00 |
9
+
10
+ Scenario: Different dailies
11
+ When I simulate these activities:
12
+ | issue | 2026-01-01 | 1000.00 |
13
+ | finish | 2026-01-06 | |
14
+
15
+ Then the default ledger has these balances:
16
+ | cash | 5.00 |
17
+ | principal | 0.00 |
18
+ | interest | 0.00 |
19
+ | revenue | -5.00 |
20
+
21
+ And the aggressive ledger has these balances:
22
+ | cash | 5.00 |
23
+ | principal | 45.00 |
24
+ | interest | 0.00 |
25
+ | revenue | -50.00 |
26
+
27
+ And I have these activity counts:
28
+ | issue | 1 |
29
+ | payment | 1 |
30
+ | interest | 5 |
31
+
32
+ When I save and reload the accountant
33
+
34
+ Then I have these activity counts:
35
+ | issue | 1 |
36
+ | payment | 1 |
37
+ | interest | 5 |
38
+
39
+ And the default ledger has these balances:
40
+ | cash | 5.00 |
41
+ | principal | 0.00 |
42
+ | interest | 0.00 |
43
+ | revenue | -5.00 |
44
+
45
+ And the default ledger has these aggregated balances:
46
+ | accruing | 0.00 |
47
+ | varying | 0.00 |
48
+
49
+ And the aggressive ledger has these balances:
50
+ | cash | 5.00 |
51
+ | principal | 45.00 |
52
+ | interest | 0.00 |
53
+ | revenue | -50.00 |
54
+
55
+ And the aggressive ledger has these aggregated balances:
56
+ | accruing | 45.00 |
57
+ | varying | 50.00 |
@@ -0,0 +1,92 @@
1
+ Feature: Retroactive Activities
2
+
3
+ Create activities that are accounted for in present, but effective in the past.
4
+
5
+ Background:
6
+ Given a simulating accountant
7
+ And a start date of 2026-01-01
8
+ And an interest rate of 36.5%
9
+
10
+ Scenario: Retroactive activity
11
+
12
+ When I simulate to 2026-01-03
13
+
14
+ Then I have all zero balances
15
+
16
+ When I apply an issue activity effective 2026-01-01 for $1000.00
17
+
18
+ Then I have these balances:
19
+ | cash | -1000.00 |
20
+ | principal | 1000.00 |
21
+ | interest | 2.00 |
22
+ | revenue | -2.00 |
23
+
24
+ And I have these activity counts:
25
+ | issue | 1 |
26
+ | adjustment | 1 |
27
+
28
+ Scenario: Retroactive activity with adjustment
29
+
30
+ When I simulate these activities:
31
+ | issue | 2026-01-01 | 1000.00 |
32
+ | finish | 2026-01-02 | |
33
+
34
+ Then I have these balances:
35
+ | cash | -1000.00 |
36
+ | principal | 1000.00 |
37
+ | interest | 1.00 |
38
+ | revenue | -1.00 |
39
+
40
+ When I apply a payment activity effective 2026-01-01 for $1000.00
41
+
42
+ Then I have all zero balances
43
+ And I have these activity counts:
44
+ | issue | 1 |
45
+ | payment | 1 |
46
+ | interest | 1 |
47
+ | adjustment | 1 |
48
+
49
+ Scenario: Cancelling a retroactive activity
50
+
51
+ When I simulate these activities:
52
+ | issue | 2026-01-01 | 1000.00 |
53
+ | finish | 2026-01-02 | |
54
+
55
+ And I apply a payment activity effective 2026-01-01 for $500.00
56
+
57
+ Then I have these balances:
58
+ | cash | -500.00 |
59
+ | principal | 500.00 |
60
+ | interest | 0.50 |
61
+ | revenue | -0.50 |
62
+
63
+ When I simulate to 2026-01-03
64
+
65
+ Then I have these balances:
66
+ | cash | -500.00 |
67
+ | principal | 500.00 |
68
+ | interest | 1.00 |
69
+ | revenue | -1.00 |
70
+
71
+ When I cancel the 2026-01-01 payment
72
+
73
+ Then I have these balances:
74
+ | cash | -1000.00 |
75
+ | principal | 1000.00 |
76
+ | interest | 2.00 |
77
+ | revenue | -2.00 |
78
+
79
+ When I save and reload the accountant
80
+
81
+ Then I have these balances:
82
+ | cash | -1000.00 |
83
+ | principal | 1000.00 |
84
+ | interest | 2.00 |
85
+ | revenue | -2.00 |
86
+
87
+ And I have these activity counts:
88
+ | issue | 1 |
89
+ | interest | 2 |
90
+ | payment | 1 |
91
+ | cancel | 1 |
92
+ | adjustment | 2 |
@@ -0,0 +1,112 @@
1
+ Feature: Returns
2
+
3
+ Create an adjustment activity when an activity is returned.
4
+
5
+ Background:
6
+ Given a simulating accountant
7
+
8
+ And a start date of 2016-01-01
9
+ And an interest rate of 36.5%
10
+ And the schedule:
11
+ | payment | 2026-01-06 | 1005.00 |
12
+
13
+ And I simulate these activities:
14
+ | issue | 2026-01-01 | 1000.00 |
15
+
16
+ Then I have these balances:
17
+ | cash | -1000.00 |
18
+ | principal | 1000.00 |
19
+
20
+ And I have 1 issue activity
21
+
22
+ Scenario: Return an activity without adjustment
23
+ When I save and reload the accountant
24
+ And I return the 2026-01-01 issue
25
+
26
+ Then I have these balances:
27
+ | cash | 0.00 |
28
+ | principal | 0.00 |
29
+
30
+ And these activity counts:
31
+ | issue | 1 |
32
+ | return | 1 |
33
+
34
+ Scenario: Return an activity with adjustment
35
+ When I simulate until 2026-01-02
36
+
37
+ And I save and reload the accountant
38
+ And I return the 2026-01-01 issue
39
+
40
+ Then I have these balances:
41
+ | cash | 0.00 |
42
+ | principal | 0.00 |
43
+ | interest | 0.00 |
44
+ | revenue | 0.00 |
45
+
46
+ And these activity counts:
47
+ | issue | 1 |
48
+ | return | 1 |
49
+ | interest | 1 |
50
+ | adjustment | 1 |
51
+
52
+ Scenario: Return an activity entered today
53
+ When I simulate until 2026-01-06
54
+
55
+ Then I have these balances:
56
+ | cash | 5.00 |
57
+ | principal | 0.00 |
58
+ | interest | 0.00 |
59
+ | revenue | -5.00 |
60
+
61
+ And these activity counts:
62
+ | issue | 1 |
63
+ | payment | 1 |
64
+ | interest | 5 |
65
+
66
+ When I save and reload the accountant
67
+ And I return the 2026-01-06 payment
68
+
69
+ Then I have these balances:
70
+ | cash | -1000.00 |
71
+ | principal | 1000.00 |
72
+ | interest | 5.00 |
73
+ | revenue | -5.00 |
74
+
75
+ And these activity counts:
76
+ | issue | 1 |
77
+ | payment | 1 |
78
+ | interest | 5 |
79
+ | return | 1 |
80
+
81
+ Scenario: Return an interleaved activity
82
+ When I simulate these activities:
83
+ | payment | 2026-01-01 | 500.00 |
84
+ | finish | 2026-01-06 | |
85
+
86
+ Then I have these balances:
87
+ | cash | 505.00 |
88
+ | principal | 0.00 |
89
+ | interest | 0.00 |
90
+ | revenue | -2.50 |
91
+ | payable | -502.50 |
92
+
93
+ And these activity counts:
94
+ | issue | 1 |
95
+ | payment | 2 |
96
+ | interest | 5 |
97
+
98
+ When I return the 2026-01-01 payment
99
+
100
+ Then I have these balances:
101
+ | cash | 5.00 |
102
+ | principal | 0.00 |
103
+ | interest | 0.00 |
104
+ | revenue | -5.00 |
105
+ | payable | 0.00 |
106
+
107
+ And these activity counts:
108
+ | issue | 1 |
109
+ | payment | 2 |
110
+ | interest | 5 |
111
+ | return | 1 |
112
+ | adjustment | 1 |
@@ -0,0 +1,57 @@
1
+ Feature: Reversal
2
+
3
+ Reverse an activity.
4
+
5
+ Background:
6
+ Given a simulating accountant
7
+
8
+ And a start date of 2026-01-01
9
+ And an interest rate of 0.00%
10
+
11
+ And I simulate these activities:
12
+ | issue | 2026-01-01 | 1000.00 |
13
+ | late_fee | 2026-01-02 | 10.00 |
14
+
15
+ Then I have these balances:
16
+ | principal | 1000.00 |
17
+ | cash | -1000.00 |
18
+ | late_fee | 10.00 |
19
+ | late_fee_revenue | -10.00 |
20
+
21
+ And I have a late_fee activity
22
+ And I save and reload the accountant
23
+
24
+ Scenario: Reverse an activity
25
+ When I reverse the 2026-01-02 late_fee
26
+
27
+ Then I have these balances:
28
+ | principal | 1000.00 |
29
+ | cash | -1000.00 |
30
+ | late_fee | 0.00 |
31
+ | late_fee_revenue | 0.00 |
32
+
33
+ And these activity counts:
34
+ | issue | 1 |
35
+ | late_fee | 1 |
36
+ | reversal | 1 |
37
+
38
+ # Cancelling the late fee would make a lot more sense,
39
+ # as it will create the adjustment and zero out the late_fee
40
+ # receivable
41
+ Scenario: Reverse an activity, retroactively
42
+ When I apply a payment for $10
43
+ And I simulate until 2026-01-03
44
+
45
+ And I reverse the 2026-01-02 late_fee
46
+
47
+ Then I have these balances:
48
+ | principal | 1000.00 |
49
+ | cash | -990.00 |
50
+ | late_fee | -10.00 |
51
+ | late_fee_revenue | 0.00 |
52
+
53
+ And these activity counts:
54
+ | issue | 1 |
55
+ | late_fee | 1 |
56
+ | payment | 1 |
57
+ | reversal | 1 |
@@ -0,0 +1,128 @@
1
+ Feature: Simulation
2
+
3
+ Background:
4
+ Given a simulating accountant
5
+ And a start date of 2026-01-01
6
+ And an interest rate of 36.5%
7
+ And the schedule:
8
+ | payment | 2026-01-11 | 1010.00 |
9
+
10
+ Scenario: No start date
11
+ Given the accountant:
12
+ """
13
+ @accountant = SimulatingAccountant.new
14
+ """
15
+ Then the accountant is valid
16
+ But I cannot simulate
17
+
18
+ Scenario: Same day payoff
19
+ When I simulate these activities:
20
+ | issue | 2026-01-01 | 100.00 |
21
+ | payment | 2026-01-01 | 100.00 |
22
+
23
+ Then I have these balances:
24
+ | cash | 0.00 |
25
+ | principal | 0.00 |
26
+
27
+ Scenario: Next-Day Payment
28
+ When I simulate these activities:
29
+ | issue | 2026-01-01 | 1000.00 |
30
+ | payment | 2026-01-02 | 1000.00 |
31
+
32
+ Then I have these balances:
33
+ | cash | 0.00 |
34
+ | principal | 1.00 |
35
+ | interest | 0.00 |
36
+ | revenue | -1.00 |
37
+
38
+ And I have these activity counts:
39
+ | issue | 1 |
40
+ | payment | 1 |
41
+ | interest | 1 |
42
+
43
+ Scenario: Simulation with scheduled activity
44
+ When I simulate these activities:
45
+ | issue | 2026-01-01 | 1000.00 |
46
+ | finish | 2026-01-11 | |
47
+
48
+ Then I have these balances:
49
+ | cash | 10.00 |
50
+ | principal | 0.00 |
51
+ | interest | 0.00 |
52
+ | revenue | -10.00 |
53
+
54
+ And I have these activity counts:
55
+ | issue | 1 |
56
+ | payment | 1 |
57
+ | interest | 10 |
58
+
59
+ And I save the accountant
60
+
61
+ When I reload the accountant with a start date of 2026-01-02
62
+ Then I have these balances:
63
+ | cash | -1000.00 |
64
+ | principal | 1000.00 |
65
+ | interest | 1.00 |
66
+ | revenue | -1.00 |
67
+
68
+ And I have these activity counts:
69
+ | issue | 1 |
70
+ | interest | 1 |
71
+
72
+ When I reload the accountant with a start_date of 2026-01-11
73
+ Then I have these balances:
74
+ | cash | 10.00 |
75
+ | principal | 0.00 |
76
+ | interest | 0.00 |
77
+ | revenue | -10.00 |
78
+
79
+ And I have these activity counts:
80
+ | issue | 1 |
81
+ | payment | 1 |
82
+ | interest | 10 |
83
+
84
+ Scenario: Multiple interest rates
85
+ Given a simulating accountant
86
+ And a start date of 2026-01-01
87
+ And the interest rates:
88
+ | effective_date | rate |
89
+ | 2026-01-01 | 36.5 |
90
+ | 2026-01-02 | 0 |
91
+ | 2026-01-03 | 36.5 |
92
+
93
+ When I simulate these activities:
94
+ | issue | 2026-01-01 | 1000.0 |
95
+ | finish | 2026-01-04 | |
96
+
97
+ Then I have these balances:
98
+ | cash | -1000.00 |
99
+ | principal | 1000.00 |
100
+ | interest | 2.00 |
101
+ | revenue | -2.00 |
102
+
103
+ Scenario: Daily interest rate, payment on start date
104
+ Given a simulating accountant
105
+ And a start date of 2026-01-01
106
+ And a daily interest rate of 1.00%
107
+ And the schedule:
108
+ | issue | 2026-01-01 | 1000.00|
109
+ | payment | 2026-01-01 | 500.00 |
110
+
111
+ When I simulate until 2026-01-01
112
+ Then I have these balances:
113
+ | cash | -500.00 |
114
+ | principal | 500.00 |
115
+
116
+ When I simulate until 2026-01-01
117
+ Then I have these balances:
118
+ | cash | -500.00 |
119
+ | principal | 500.00 |
120
+
121
+ When I save the accountant
122
+ And I reload the accountant with a start date of 2026-01-03
123
+ And I simulate until 2026-01-03
124
+ Then I have these balances:
125
+ | cash | -500.00 |
126
+ | principal | 500.00 |
127
+ | interest | 5.00 |
128
+ | revenue | -5.00 |
@@ -0,0 +1,34 @@
1
+ class AdjustingAccountant < Morty::Accountant
2
+ balance :accruing, %w[principal]
3
+
4
+ activity :issue do
5
+ entry :principal, :cash, amount
6
+ end
7
+
8
+ activity :interest do
9
+ if rate = accountant.rate_for(accountant.date.yesterday)
10
+ amount ||= (rate.daily_for(accountant.date.yesterday) * balances[:accruing]).floor(2)
11
+
12
+ entry :interest, :revenue, amount
13
+ end
14
+ end
15
+
16
+ activity :late_fee do
17
+ entry :late_fee, :late_fee_revenue, amount
18
+ end
19
+
20
+ waterfall :payment, limit: :cr, complete: true, entries: <<~END
21
+ cash late_fee
22
+ cash interest
23
+ cash principal
24
+ cash payable
25
+ END
26
+
27
+ daily do
28
+ activity :interest, accountant.date.yesterday
29
+ end
30
+
31
+ daily_guard do
32
+ accountant.activities.with_type(:interest).none? { |a| a.effective_date == accountant.date.yesterday }
33
+ end
34
+ end
@@ -0,0 +1,13 @@
1
+ class DailyAccountant < Morty::Accountant
2
+ daily do
3
+ activity :interest, today, "1.00"
4
+ end
5
+
6
+ daily_guard do
7
+ today != Date.today + 1
8
+ end
9
+
10
+ activity :interest do
11
+ entry :interest, :revenue, amount
12
+ end
13
+ end
@@ -0,0 +1,2 @@
1
+ class DefaultAccountant < Morty::Accountant
2
+ end