loan_creator 0.2.1 → 0.6.2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +45 -0
- data/CODE_OF_CONDUCT.md +73 -0
- data/CapSens_Loan.xlsx +0 -0
- data/README.md +65 -4
- data/lib/loan_creator.rb +10 -9
- data/lib/loan_creator/borrower_timetable.rb +1 -1
- data/lib/loan_creator/bullet.rb +13 -15
- data/lib/loan_creator/common.rb +97 -26
- data/lib/loan_creator/excel_formulas.rb +4 -0
- data/lib/loan_creator/in_fine.rb +1 -3
- data/lib/loan_creator/linear.rb +11 -2
- data/lib/loan_creator/standard.rb +8 -0
- data/lib/loan_creator/term.rb +8 -5
- data/lib/loan_creator/timetable.rb +40 -31
- data/lib/loan_creator/uncapitalized_bullet.rb +34 -0
- data/lib/loan_creator/version.rb +1 -1
- data/loan_creator.gemspec +3 -3
- metadata +11 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8dff90f00c1b47c09fd2ac97d4a0e3a28e12fbd5
|
4
|
+
data.tar.gz: '078e7ad135c712ef0fb767208cc99557718b99b1'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e318e46a05d49caa450705cb5a6e234c7b3e497dad501961c1339638c0641cc76f88dafb00f07af80b9b9cdc6d5ba1d0b5e761133f863f7a6034d479ddb0f012
|
7
|
+
data.tar.gz: bc258a7bd58a783c033c7c81857b467966e2e4ee25b091cc73cf1de1f5ec94a94f889fb2c32f5be072668503295564a841beb0573585c509b867dc9b0025cbe1
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
v0.6.2
|
2
|
+
-------------------------
|
3
|
+
|
4
|
+
- add `:initial_values` for loans initialization
|
5
|
+
- add and compute `capitalized_interests` for `LoanCreator::Bullet` terms
|
6
|
+
|
7
|
+
v0.6.1
|
8
|
+
-------------------------
|
9
|
+
|
10
|
+
- fix homepage url
|
11
|
+
|
12
|
+
v0.6.0
|
13
|
+
-------------------------
|
14
|
+
|
15
|
+
- add `LoanCreator::UncapitalizedBullet`
|
16
|
+
|
17
|
+
v0.5.0
|
18
|
+
-------------------------
|
19
|
+
|
20
|
+
- add `interests_start_date` in `LoanCreator::Common` attributes, replacing `first_term_date`
|
21
|
+
|
22
|
+
v0.3.0
|
23
|
+
-------------------------
|
24
|
+
|
25
|
+
- add `first_term_date` in `LoanCreator::Common` attributes
|
26
|
+
|
27
|
+
v0.2.3
|
28
|
+
-------------------------
|
29
|
+
|
30
|
+
- rename `starts_at` -> `starts_on`
|
31
|
+
|
32
|
+
v0.2.2
|
33
|
+
-------------------------
|
34
|
+
|
35
|
+
- rename `date` -> `due_on`
|
36
|
+
|
37
|
+
v0.2.1
|
38
|
+
-------------------------
|
39
|
+
|
40
|
+
- convert some options to their expected type by default
|
41
|
+
|
42
|
+
v0.2.0
|
43
|
+
-------------------------
|
44
|
+
|
45
|
+
- Huge rework : add `period`, rename `amount_in_cents` to `amount` and other breaking changes.
|
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
2
|
+
|
3
|
+
## Our Pledge
|
4
|
+
|
5
|
+
In the interest of fostering an open and welcoming environment, we as
|
6
|
+
contributors and maintainers pledge to making participation in our project and
|
7
|
+
our community a harassment-free experience for everyone, regardless of age, body
|
8
|
+
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
9
|
+
level of experience, education, socio-economic status, nationality, personal
|
10
|
+
appearance, race, religion, or sexual identity and orientation.
|
11
|
+
|
12
|
+
## Our Standards
|
13
|
+
|
14
|
+
Examples of behavior that contributes to creating a positive environment
|
15
|
+
include:
|
16
|
+
|
17
|
+
* Using welcoming and inclusive language
|
18
|
+
* Being respectful of differing viewpoints and experiences
|
19
|
+
* Gracefully accepting constructive criticism
|
20
|
+
* Focusing on what is best for the community
|
21
|
+
* Showing empathy towards other community members
|
22
|
+
|
23
|
+
Examples of unacceptable behavior by participants include:
|
24
|
+
|
25
|
+
* The use of sexualized language or imagery and unwelcome sexual attention or
|
26
|
+
advances
|
27
|
+
* Trolling, insulting/derogatory comments, and personal or political attacks
|
28
|
+
* Public or private harassment
|
29
|
+
* Publishing others' private information, such as a physical or electronic
|
30
|
+
address, without explicit permission
|
31
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
32
|
+
professional setting
|
33
|
+
|
34
|
+
## Our Responsibilities
|
35
|
+
|
36
|
+
Project maintainers are responsible for clarifying the standards of acceptable
|
37
|
+
behavior and are expected to take appropriate and fair corrective action in
|
38
|
+
response to any instances of unacceptable behavior.
|
39
|
+
|
40
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
41
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
42
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
43
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
44
|
+
threatening, offensive, or harmful.
|
45
|
+
|
46
|
+
## Scope
|
47
|
+
|
48
|
+
This Code of Conduct applies both within project spaces and in public spaces
|
49
|
+
when an individual is representing the project or its community. Examples of
|
50
|
+
representing a project or community include using an official project e-mail
|
51
|
+
address, posting via an official social media account, or acting as an appointed
|
52
|
+
representative at an online or offline event. Representation of a project may be
|
53
|
+
further defined and clarified by project maintainers.
|
54
|
+
|
55
|
+
## Enforcement
|
56
|
+
|
57
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
58
|
+
reported by contacting the project team at development@capsens.eu. All
|
59
|
+
complaints will be reviewed and investigated and will result in a response that
|
60
|
+
is deemed necessary and appropriate to the circumstances. The project team is
|
61
|
+
obligated to maintain confidentiality with regard to the reporter of an incident.
|
62
|
+
Further details of specific enforcement policies may be posted separately.
|
63
|
+
|
64
|
+
Project maintainers who do not follow or enforce the Code of Conduct in good
|
65
|
+
faith may face temporary or permanent repercussions as determined by other
|
66
|
+
members of the project's leadership.
|
67
|
+
|
68
|
+
## Attribution
|
69
|
+
|
70
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
71
|
+
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
72
|
+
|
73
|
+
[homepage]: https://www.contributor-covenant.org
|
data/CapSens_Loan.xlsx
ADDED
Binary file
|
data/README.md
CHANGED
@@ -2,8 +2,6 @@
|
|
2
2
|
|
3
3
|
`loan_creator` gem intends to provide a set of methods to allow automatic generation of loan timetables, for simulation, from a lender point of view and from a borrower point of view, regarding financial rounding differences. As of today, the gem makes the borrower support any rounding issue. In a later work, an option should be provided to decide who supports such issues.
|
4
4
|
|
5
|
-
Link to Loan_Creator excel simulator [Click here] (Excel)
|
6
|
-
|
7
5
|
## Installation
|
8
6
|
|
9
7
|
Add this line to your application's Gemfile:
|
@@ -35,6 +33,7 @@ There are four types of loans. All inherit from a `LoanCreator::Common` class.
|
|
35
33
|
LoanCreator::Linear
|
36
34
|
LoanCreator::InFine
|
37
35
|
LoanCreator::Bullet
|
36
|
+
LoanCreator::UncapitalizedBullet
|
38
37
|
```
|
39
38
|
|
40
39
|
Each instance of one of the previous classes has the following attributes:
|
@@ -46,6 +45,20 @@ Each instance of one of the previous classes has the following attributes:
|
|
46
45
|
:starts_at
|
47
46
|
:duration_in_periods
|
48
47
|
:deferred_in_periods (default to zero)
|
48
|
+
:interests_start_date (optional)
|
49
|
+
:initial_values (to generate a timetable from a previous term or at a given state)
|
50
|
+
```
|
51
|
+
|
52
|
+
Initial values must be a hash with specific keys, like so:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
{
|
56
|
+
paid_capital: 0,
|
57
|
+
paid_interests: 11000.0,
|
58
|
+
accrued_delta_interests: 0,
|
59
|
+
starting_index: 2,
|
60
|
+
capitalized_interests: 0
|
61
|
+
}
|
49
62
|
```
|
50
63
|
|
51
64
|
There is also a `LoanCreator::Timetable` class dedicated to record the data of the loans' terms. Each instance of `LoanCreator::Timetable` represents an array of `LoanCreator::Term` records, each having the following attributes:
|
@@ -55,7 +68,7 @@ There is also a `LoanCreator::Timetable` class dedicated to record the data of t
|
|
55
68
|
:index
|
56
69
|
|
57
70
|
# Term date
|
58
|
-
:
|
71
|
+
:due_on
|
59
72
|
|
60
73
|
# Remaining due capital at the beginning of the term
|
61
74
|
:crd_beginning_of_period
|
@@ -99,8 +112,29 @@ support all those differences.
|
|
99
112
|
`.borrower_timetable(*lenders_timetables)` (class method) intends to sum each attribute of each provided `lender_timetable` on each term and thus to provide an ascending order array of `LoanCreator::Term`. It should be used for the borrower of a loan, once all lenders and their lending amounts
|
100
113
|
are known. It makes the borrower support all financial rounding differences.
|
101
114
|
|
115
|
+
## Example
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
loan_creator = LoanCreator::Standard.new(
|
119
|
+
period: :year,
|
120
|
+
amount: 42_000,
|
121
|
+
annual_interests_rate: 4,
|
122
|
+
starts_on: '2019-03-01',
|
123
|
+
duration_in_periods: 5,
|
124
|
+
deferred_in_periods: 1,
|
125
|
+
interests_start_date: '2019-02-10'
|
126
|
+
)
|
127
|
+
loan_creator.lender_timetable
|
128
|
+
# => #<LoanCreator::Timetable:0x0000000003198bd0 @terms=[...] ...>
|
129
|
+
loan_creator.lender_timetable.terms.first
|
130
|
+
# => #<LoanCreator::Term:0x00000000030f1a88 @crd_beginning_of_period=0.42e5,
|
131
|
+
# [...] @period_amount_to_pay=0.8745e2, @index=0, @due_on=Sun, 10 Feb 2019>
|
132
|
+
````
|
133
|
+
|
102
134
|
## Explanation
|
103
135
|
|
136
|
+
### Classes
|
137
|
+
|
104
138
|
`Standard` loan generates terms with constant payments.
|
105
139
|
|
106
140
|
`Linear` loan generates terms with constant capital share payment.
|
@@ -115,8 +149,35 @@ Capital share shall be repaid in full at loan's end.
|
|
115
149
|
Interests are capitalized, i.e. added to the borrowed capital on each term.\
|
116
150
|
Capital share shall be repaid in full and all interests paid at loan's end.
|
117
151
|
|
152
|
+
`UncapitalizedBullet` same as bullet, the only difference is the interests\
|
153
|
+
are NOT capitalized.
|
154
|
+
|
118
155
|
There is no deferred time for `InFine` and `Bullet` loans as it would be equivalent to increasing loan's duration.
|
119
156
|
|
157
|
+
### Attributes
|
158
|
+
|
159
|
+
`period`: A `Symbol`. `:month`, `:quarter`, `:semester` or `:year`.
|
160
|
+
|
161
|
+
`duration_in_periods`: An `Integer`.
|
162
|
+
|
163
|
+
`amount`: Any number that can be converted to `BigDecimal`.
|
164
|
+
|
165
|
+
`annual_interests_rate`: In percentage. Any number that can be converted to `BigDecimal`.
|
166
|
+
|
167
|
+
`starts_at`: A `Date`, or a `String` that can be parsed.
|
168
|
+
|
169
|
+
`deferred_in_periods`: Optional. An `Integer`, smaller than `duration_in_periods`. Number of periods during which no
|
170
|
+
capital is refunded, only interest. Only relevant for `Standard` and `Linear` loans.
|
171
|
+
|
172
|
+
`interests_start_date`: Optional. To be used when the loan starts before the first full term date. This then compute an
|
173
|
+
additional term with only interests for the time difference.
|
174
|
+
For example, with a `start_at` in january 2020 and a `interests_start_date` in october 2019, the timetable will include a
|
175
|
+
first term corresponding to 3 months of interests.
|
176
|
+
|
177
|
+
## Calculation
|
178
|
+
|
179
|
+
An excel simulator for standard case can be found [here](CapSens_Loan.xlsx).
|
180
|
+
|
120
181
|
## Development
|
121
182
|
|
122
183
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
@@ -125,7 +186,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
125
186
|
|
126
187
|
## Contributing
|
127
188
|
|
128
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
189
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/CapSens/loan_creator.
|
129
190
|
|
130
191
|
|
131
192
|
## License
|
data/lib/loan_creator.rb
CHANGED
@@ -7,13 +7,14 @@ require 'loan_creator/version'
|
|
7
7
|
module LoanCreator
|
8
8
|
BIG_DECIMAL_DIGITS = 14
|
9
9
|
|
10
|
-
autoload :ExcelFormulas,
|
11
|
-
autoload :BorrowerTimetable,
|
12
|
-
autoload :Common,
|
13
|
-
autoload :Standard,
|
14
|
-
autoload :Linear,
|
15
|
-
autoload :InFine,
|
16
|
-
autoload :Bullet,
|
17
|
-
autoload :Timetable,
|
18
|
-
autoload :Term,
|
10
|
+
autoload :ExcelFormulas, 'loan_creator/excel_formulas'
|
11
|
+
autoload :BorrowerTimetable, 'loan_creator/borrower_timetable'
|
12
|
+
autoload :Common, 'loan_creator/common'
|
13
|
+
autoload :Standard, 'loan_creator/standard'
|
14
|
+
autoload :Linear, 'loan_creator/linear'
|
15
|
+
autoload :InFine, 'loan_creator/in_fine'
|
16
|
+
autoload :Bullet, 'loan_creator/bullet'
|
17
|
+
autoload :Timetable, 'loan_creator/timetable'
|
18
|
+
autoload :Term, 'loan_creator/term'
|
19
|
+
autoload :UncapitalizedBullet, 'loan_creator/uncapitalized_bullet'
|
19
20
|
end
|
data/lib/loan_creator/bullet.rb
CHANGED
@@ -2,11 +2,12 @@ module LoanCreator
|
|
2
2
|
class Bullet < LoanCreator::Common
|
3
3
|
def lender_timetable
|
4
4
|
raise ArgumentError.new(:deferred_in_periods) unless deferred_in_periods == 0
|
5
|
+
raise ArgumentError.new(:interests_start_date) unless interests_start_date.nil?
|
5
6
|
timetable = new_timetable
|
6
7
|
reset_current_term
|
7
8
|
@crd_beginning_of_period = amount
|
8
9
|
@crd_end_of_period = amount
|
9
|
-
(duration_in_periods - 1).times { timetable
|
10
|
+
(duration_in_periods - 1).times { |period| compute_term(timetable, period + 1) }
|
10
11
|
compute_last_term
|
11
12
|
timetable << current_term
|
12
13
|
timetable
|
@@ -15,25 +16,22 @@ module LoanCreator
|
|
15
16
|
private
|
16
17
|
|
17
18
|
def compute_last_term
|
18
|
-
@crd_end_of_period
|
19
|
-
@period_interests
|
20
|
-
@period_capital
|
21
|
-
@total_paid_capital_end_of_period
|
19
|
+
@crd_end_of_period = bigd('0')
|
20
|
+
@period_interests = compute_capitalized_interests(duration_in_periods)
|
21
|
+
@period_capital = @crd_beginning_of_period
|
22
|
+
@total_paid_capital_end_of_period = @period_capital
|
22
23
|
@total_paid_interests_end_of_period = @period_interests
|
23
|
-
@period_amount_to_pay
|
24
|
+
@period_amount_to_pay = @period_capital + @period_interests
|
25
|
+
@capitalized_interests = compute_capitalized_interests(duration_in_periods)
|
24
26
|
end
|
25
27
|
|
26
|
-
|
27
|
-
|
28
|
-
def total_payment
|
29
|
-
amount.mult(
|
30
|
-
(bigd(1) + periodic_interests_rate) ** bigd(duration_in_periods),
|
31
|
-
BIG_DECIMAL_DIGITS
|
32
|
-
)
|
28
|
+
def compute_capitalized_interests(period)
|
29
|
+
amount.mult((bigd(1) + periodic_interests_rate) ** period, BIG_DECIMAL_DIGITS) - amount
|
33
30
|
end
|
34
31
|
|
35
|
-
def
|
36
|
-
|
32
|
+
def compute_term(timetable, period)
|
33
|
+
@capitalized_interests = compute_capitalized_interests(period)
|
34
|
+
timetable << current_term
|
37
35
|
end
|
38
36
|
end
|
39
37
|
end
|
data/lib/loan_creator/common.rb
CHANGED
@@ -13,13 +13,15 @@ module LoanCreator
|
|
13
13
|
:period,
|
14
14
|
:amount,
|
15
15
|
:annual_interests_rate,
|
16
|
-
:
|
16
|
+
:starts_on,
|
17
17
|
:duration_in_periods
|
18
18
|
].freeze
|
19
19
|
|
20
20
|
OPTIONAL_ATTRIBUTES = {
|
21
21
|
# attribute: default_value
|
22
|
-
deferred_in_periods: 0
|
22
|
+
deferred_in_periods: 0,
|
23
|
+
interests_start_date: nil,
|
24
|
+
initial_values: {}
|
23
25
|
}.freeze
|
24
26
|
|
25
27
|
attr_reader *REQUIRED_ATTRIBUTES
|
@@ -31,6 +33,8 @@ module LoanCreator
|
|
31
33
|
reinterpret_attributes
|
32
34
|
set_attributes
|
33
35
|
validate_attributes
|
36
|
+
set_initial_values
|
37
|
+
validate_initial_values
|
34
38
|
end
|
35
39
|
|
36
40
|
def periodic_interests_rate_percentage
|
@@ -48,7 +52,7 @@ module LoanCreator
|
|
48
52
|
end
|
49
53
|
|
50
54
|
def self.bigd(value)
|
51
|
-
BigDecimal
|
55
|
+
BigDecimal(value, BIG_DECIMAL_DIGITS)
|
52
56
|
end
|
53
57
|
|
54
58
|
def bigd(value)
|
@@ -65,6 +69,8 @@ module LoanCreator
|
|
65
69
|
@options[:period] = @options[:period].to_sym
|
66
70
|
@options[:amount] = bigd(@options[:amount])
|
67
71
|
@options[:annual_interests_rate] = bigd(@options[:annual_interests_rate])
|
72
|
+
@options[:starts_on] = Date.parse(@options[:starts_on]) if @options[:starts_on].is_a?(String)
|
73
|
+
@options[:interests_start_date] = Date.parse(@options[:interests_start_date]) if @options[:interests_start_date].is_a?(String)
|
68
74
|
end
|
69
75
|
|
70
76
|
def set_attributes
|
@@ -82,43 +88,108 @@ module LoanCreator
|
|
82
88
|
validate(:period) { |v| PERIODS_IN_MONTHS.keys.include?(v) }
|
83
89
|
validate(:amount) { |v| v.is_a?(BigDecimal) && v > 0 }
|
84
90
|
validate(:annual_interests_rate) { |v| v.is_a?(BigDecimal) && v >= 0 }
|
85
|
-
validate(:
|
91
|
+
validate(:starts_on) { |v| v.is_a?(Date) }
|
86
92
|
validate(:duration_in_periods) { |v| v.is_a?(Integer) && v > 0 }
|
87
93
|
validate(:deferred_in_periods) { |v| v.is_a?(Integer) && v >= 0 && v < duration_in_periods }
|
88
94
|
end
|
89
95
|
|
96
|
+
def validate_initial_values
|
97
|
+
return if initial_values.blank?
|
98
|
+
|
99
|
+
validate(:total_paid_capital_end_of_period) { |v| v.is_a?(BigDecimal) && v >= 0 }
|
100
|
+
validate(:total_paid_interests_end_of_period) { |v| v.is_a?(BigDecimal) && v >= 0 }
|
101
|
+
validate(:accrued_delta_interests) { |v| v.is_a?(BigDecimal) }
|
102
|
+
validate(:starting_index) { |v| v.is_a?(Integer) && v >= 0 }
|
103
|
+
end
|
104
|
+
|
105
|
+
def set_initial_values
|
106
|
+
@starting_index = initial_values[:starting_index] || 1
|
107
|
+
|
108
|
+
return if initial_values.blank?
|
109
|
+
|
110
|
+
(@total_paid_capital_end_of_period = bigd(initial_values[:paid_capital]))
|
111
|
+
(@total_paid_interests_end_of_period = bigd(initial_values[:paid_interests]))
|
112
|
+
(@accrued_delta_interests = bigd(initial_values[:accrued_delta_interests]))
|
113
|
+
end
|
114
|
+
|
90
115
|
def reset_current_term
|
91
|
-
@crd_beginning_of_period
|
92
|
-
@crd_end_of_period
|
93
|
-
@period_theoric_interests
|
94
|
-
@
|
95
|
-
@
|
96
|
-
@
|
97
|
-
@
|
98
|
-
@
|
99
|
-
@
|
100
|
-
@
|
101
|
-
@
|
116
|
+
@crd_beginning_of_period = bigd('0')
|
117
|
+
@crd_end_of_period = bigd('0')
|
118
|
+
@period_theoric_interests = bigd('0')
|
119
|
+
@capitalized_interests = bigd('0')
|
120
|
+
@delta_interests = bigd('0')
|
121
|
+
@accrued_delta_interests = @accrued_delta_interests || bigd('0')
|
122
|
+
@amount_to_add = bigd('0')
|
123
|
+
@period_interests = bigd('0')
|
124
|
+
@period_capital = bigd('0')
|
125
|
+
@total_paid_capital_end_of_period = @total_paid_capital_end_of_period || bigd('0')
|
126
|
+
@total_paid_interests_end_of_period = @total_paid_interests_end_of_period || bigd('0')
|
127
|
+
@period_amount_to_pay = bigd('0')
|
128
|
+
@due_on = nil
|
102
129
|
end
|
103
130
|
|
104
131
|
def current_term
|
105
132
|
LoanCreator::Term.new(
|
106
|
-
crd_beginning_of_period:
|
107
|
-
crd_end_of_period:
|
108
|
-
period_theoric_interests:
|
109
|
-
delta_interests:
|
110
|
-
accrued_delta_interests:
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
133
|
+
crd_beginning_of_period: @crd_beginning_of_period,
|
134
|
+
crd_end_of_period: @crd_end_of_period,
|
135
|
+
period_theoric_interests: @period_theoric_interests,
|
136
|
+
delta_interests: @delta_interests,
|
137
|
+
accrued_delta_interests: @accrued_delta_interests,
|
138
|
+
capitalized_interests: @capitalized_interests,
|
139
|
+
amount_to_add: @amount_to_add,
|
140
|
+
period_interests: @period_interests,
|
141
|
+
period_capital: @period_capital,
|
142
|
+
total_paid_capital_end_of_period: @total_paid_capital_end_of_period,
|
115
143
|
total_paid_interests_end_of_period: @total_paid_interests_end_of_period,
|
116
|
-
period_amount_to_pay:
|
144
|
+
period_amount_to_pay: @period_amount_to_pay,
|
145
|
+
due_on: @due_on,
|
146
|
+
index: compute_index
|
117
147
|
)
|
118
148
|
end
|
119
149
|
|
120
150
|
def new_timetable
|
121
|
-
LoanCreator::Timetable.new(
|
151
|
+
LoanCreator::Timetable.new(
|
152
|
+
starts_on: starts_on,
|
153
|
+
period: period,
|
154
|
+
interests_start_date: interests_start_date,
|
155
|
+
starting_index: @starting_index
|
156
|
+
)
|
157
|
+
end
|
158
|
+
|
159
|
+
def compute_index
|
160
|
+
@index ? (@starting_index + @index - 1) : nil
|
161
|
+
end
|
162
|
+
|
163
|
+
def compute_term_zero
|
164
|
+
@crd_beginning_of_period = @crd_end_of_period
|
165
|
+
@period_theoric_interests = term_zero_interests
|
166
|
+
@delta_interests = @period_theoric_interests - @period_theoric_interests.round(2)
|
167
|
+
@accrued_delta_interests += @delta_interests
|
168
|
+
@period_interests = @period_theoric_interests.round(2)
|
169
|
+
@total_paid_interests_end_of_period += @period_interests
|
170
|
+
@period_amount_to_pay = @period_interests
|
171
|
+
@index = 0
|
172
|
+
end
|
173
|
+
|
174
|
+
def term_zero_interests
|
175
|
+
@crd_beginning_of_period * term_zero_interests_rate
|
176
|
+
end
|
177
|
+
|
178
|
+
def term_zero_interests_rate
|
179
|
+
term_zero_interests_rate_percentage = (annual_interests_rate * term_zero_duration).div(365, BIG_DECIMAL_DIGITS)
|
180
|
+
term_zero_interests_rate_percentage.div(100, BIG_DECIMAL_DIGITS)
|
181
|
+
end
|
182
|
+
|
183
|
+
def term_zero_duration
|
184
|
+
(term_zero_date - interests_start_date).to_i
|
185
|
+
end
|
186
|
+
|
187
|
+
def term_zero_date
|
188
|
+
starts_on.advance(months: -PERIODS_IN_MONTHS.fetch(@period))
|
189
|
+
end
|
190
|
+
|
191
|
+
def term_zero?
|
192
|
+
interests_start_date && interests_start_date < term_zero_date
|
122
193
|
end
|
123
194
|
end
|
124
195
|
end
|
@@ -1,12 +1,16 @@
|
|
1
1
|
# Source: https://gist.github.com/mattetti/1015948
|
2
2
|
|
3
3
|
module LoanCreator::ExcelFormulas
|
4
|
+
# Returns the interest payment
|
5
|
+
# for a given period for an investment based on periodic, constant payments and a constant interest rate.
|
4
6
|
def ipmt(rate, per, nper, pv, fv=0, type=0)
|
5
7
|
p = _pmt(rate, nper, pv, fv, 0);
|
6
8
|
ip = -(pv * _pow1p(rate, per - 1) * rate + p * _pow1pm1(rate, per - 1))
|
7
9
|
(type == 0) ? ip : ip / (1 + rate)
|
8
10
|
end
|
9
11
|
|
12
|
+
# Returns the payment on the principal
|
13
|
+
# for a given period for an investment based on periodic, constant payments and a constant interest rate.
|
10
14
|
def ppmt(rate, per, nper, pv, fv=0, type=0)
|
11
15
|
p = _pmt(rate, nper, pv, fv, type)
|
12
16
|
ip = ipmt(rate, per, nper, pv, fv, type)
|
data/lib/loan_creator/in_fine.rb
CHANGED
@@ -3,9 +3,7 @@ module LoanCreator
|
|
3
3
|
# InFine is the same as a Linear loan with (duration - 1) deferred periods.
|
4
4
|
# Thus we're generating a Linear loan instead of rewriting already existing code.
|
5
5
|
def lender_timetable
|
6
|
-
|
7
|
-
options = { deferred_in_periods: duration_in_periods - 1 }
|
8
|
-
options = REQUIRED_ATTRIBUTES.each_with_object(options) { |k,h| h[k] = send(k) }
|
6
|
+
options = @options.merge(deferred_in_periods: duration_in_periods - 1)
|
9
7
|
LoanCreator::Linear.new(options).lender_timetable
|
10
8
|
end
|
11
9
|
end
|
data/lib/loan_creator/linear.rb
CHANGED
@@ -5,12 +5,19 @@ module LoanCreator
|
|
5
5
|
timetable = new_timetable
|
6
6
|
reset_current_term
|
7
7
|
@crd_end_of_period = amount
|
8
|
+
|
9
|
+
if term_zero?
|
10
|
+
compute_term_zero
|
11
|
+
timetable << current_term
|
12
|
+
end
|
13
|
+
|
8
14
|
duration_in_periods.times do |idx|
|
9
15
|
@last_period = last_period?(idx)
|
10
16
|
@deferred_period = idx < deferred_in_periods
|
11
|
-
compute_current_term
|
17
|
+
compute_current_term(idx)
|
12
18
|
timetable << current_term
|
13
19
|
end
|
20
|
+
|
14
21
|
timetable
|
15
22
|
end
|
16
23
|
|
@@ -20,7 +27,7 @@ module LoanCreator
|
|
20
27
|
idx == (duration_in_periods - 1)
|
21
28
|
end
|
22
29
|
|
23
|
-
def compute_current_term
|
30
|
+
def compute_current_term(idx)
|
24
31
|
# Reminder: CRD beginning of period = CRD end of period **of previous period**
|
25
32
|
@crd_beginning_of_period = @crd_end_of_period
|
26
33
|
@period_theoric_interests = @crd_beginning_of_period * periodic_interests_rate
|
@@ -42,6 +49,8 @@ module LoanCreator
|
|
42
49
|
@total_paid_interests_end_of_period += @period_interests
|
43
50
|
@period_amount_to_pay = @period_interests + @period_capital
|
44
51
|
@crd_end_of_period -= @period_capital
|
52
|
+
@due_on = nil
|
53
|
+
@index = idx + 1
|
45
54
|
end
|
46
55
|
|
47
56
|
def period_capital
|
@@ -6,6 +6,12 @@ module LoanCreator
|
|
6
6
|
timetable = new_timetable
|
7
7
|
reset_current_term
|
8
8
|
@crd_end_of_period = amount
|
9
|
+
|
10
|
+
if term_zero?
|
11
|
+
compute_term_zero
|
12
|
+
timetable << current_term
|
13
|
+
end
|
14
|
+
|
9
15
|
duration_in_periods.times do |idx|
|
10
16
|
@last_period = last_period?(idx)
|
11
17
|
@deferred_period = idx < deferred_in_periods
|
@@ -42,6 +48,8 @@ module LoanCreator
|
|
42
48
|
@total_paid_interests_end_of_period += @period_interests
|
43
49
|
@period_amount_to_pay = @period_interests + @period_capital
|
44
50
|
@crd_end_of_period -= @period_capital
|
51
|
+
@due_on = nil
|
52
|
+
@index = idx + 1
|
45
53
|
end
|
46
54
|
|
47
55
|
def period_theoric_interests(idx)
|
data/lib/loan_creator/term.rb
CHANGED
@@ -17,6 +17,9 @@ module LoanCreator
|
|
17
17
|
# Accrued interests' delta
|
18
18
|
:accrued_delta_interests,
|
19
19
|
|
20
|
+
# Capitalized interests (Bullet only)
|
21
|
+
:capitalized_interests,
|
22
|
+
|
20
23
|
# Adjustment of -0.01, 0 or +0.01 cent depending on accrued_delta_interests
|
21
24
|
:amount_to_add,
|
22
25
|
|
@@ -36,23 +39,23 @@ module LoanCreator
|
|
36
39
|
:period_amount_to_pay
|
37
40
|
].freeze
|
38
41
|
|
39
|
-
|
42
|
+
OPTIONAL_ARGUMENTS = [
|
40
43
|
# Term number (starts at 1)
|
41
44
|
# This value is to be set by Timetable
|
42
45
|
:index,
|
43
46
|
|
44
47
|
# Term date
|
45
48
|
# This value is to be set by Timetable
|
46
|
-
:
|
49
|
+
:due_on,
|
50
|
+
]
|
47
51
|
|
48
|
-
|
49
|
-
*ARGUMENTS
|
50
|
-
].freeze
|
52
|
+
ATTRIBUTES = (ARGUMENTS + OPTIONAL_ARGUMENTS).freeze
|
51
53
|
|
52
54
|
attr_accessor *ATTRIBUTES
|
53
55
|
|
54
56
|
def initialize(**options)
|
55
57
|
ARGUMENTS.each { |k| instance_variable_set(:"@#{k}", options.fetch(k)) }
|
58
|
+
OPTIONAL_ARGUMENTS.each { |k| instance_variable_set(:"@#{k}", options.fetch(k, nil)) }
|
56
59
|
end
|
57
60
|
|
58
61
|
def to_csv
|
@@ -3,39 +3,35 @@ module LoanCreator
|
|
3
3
|
class Timetable
|
4
4
|
# Used to calculate next term's date (see ActiveSupport#advance)
|
5
5
|
PERIODS = {
|
6
|
-
month:
|
7
|
-
quarter:
|
8
|
-
semester: {
|
9
|
-
year:
|
6
|
+
month: {months: 1},
|
7
|
+
quarter: {months: 3},
|
8
|
+
semester: {months: 6},
|
9
|
+
year: {years: 1}
|
10
10
|
}
|
11
11
|
|
12
|
-
attr_reader :terms, :
|
12
|
+
attr_reader :terms, :starts_on, :period #, :interests_start_date
|
13
13
|
|
14
|
-
def initialize(
|
15
|
-
@terms = []
|
16
|
-
@starts_at = (Date === starts_at ? starts_at : Date.parse(starts_at))
|
14
|
+
def initialize(starts_on:, period:, interests_start_date: nil, starting_index: 1)
|
17
15
|
raise ArgumentError.new(:period) unless PERIODS.keys.include?(period)
|
18
|
-
|
16
|
+
|
17
|
+
@terms = []
|
18
|
+
@starts_on = (starts_on.is_a?(Date) ? starts_on : Date.parse(starts_on))
|
19
|
+
@period = period
|
20
|
+
@starting_index = starting_index
|
21
|
+
|
22
|
+
if interests_start_date
|
23
|
+
@interests_start_date = (interests_start_date.is_a?(Date) ? interests_start_date : Date.parse(interests_start_date))
|
24
|
+
end
|
19
25
|
end
|
20
26
|
|
21
27
|
def <<(term)
|
22
|
-
raise ArgumentError.new('LoanCreator::Term expected') unless LoanCreator::Term
|
23
|
-
term.index
|
24
|
-
term.
|
28
|
+
raise ArgumentError.new('LoanCreator::Term expected') unless term.is_a?(LoanCreator::Term)
|
29
|
+
term.index ||= autoincrement_index
|
30
|
+
term.due_on ||= date_for(term.index)
|
25
31
|
@terms << term
|
26
32
|
self
|
27
33
|
end
|
28
34
|
|
29
|
-
def reset_indexes_and_dates
|
30
|
-
@autoincrement_index = 0
|
31
|
-
@autoincrement_date = @starts_at
|
32
|
-
@terms.each do |term|
|
33
|
-
term[:index] = autoincrement_index
|
34
|
-
term[:date] = autoincrement_date
|
35
|
-
end
|
36
|
-
self
|
37
|
-
end
|
38
|
-
|
39
35
|
def to_csv(header: true)
|
40
36
|
output = []
|
41
37
|
output << terms.first.to_h.keys.join(',') if header
|
@@ -43,20 +39,33 @@ module LoanCreator
|
|
43
39
|
output
|
44
40
|
end
|
45
41
|
|
42
|
+
def term(index)
|
43
|
+
@terms.find { |term| term.index == index }
|
44
|
+
end
|
45
|
+
|
46
46
|
private
|
47
47
|
|
48
|
-
# First term index of a timetable term is 1
|
49
48
|
def autoincrement_index
|
50
|
-
@
|
51
|
-
|
49
|
+
@current_index = (@current_index.nil? ? @starting_index : @current_index + 1)
|
50
|
+
end
|
51
|
+
|
52
|
+
def date_for(index)
|
53
|
+
@_dates ||= Hash.new do |dates, index|
|
54
|
+
dates[index] =
|
55
|
+
if index < 1
|
56
|
+
dates[index + 1].advance(PERIODS.fetch(period).transform_values {|n| -n})
|
57
|
+
elsif index == 1
|
58
|
+
starts_on
|
59
|
+
else
|
60
|
+
dates[index - 1].advance(PERIODS.fetch(period))
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
@_dates[index]
|
52
65
|
end
|
53
66
|
|
54
|
-
|
55
|
-
|
56
|
-
@autoincrement_date ||= @starts_at
|
57
|
-
date = @autoincrement_date
|
58
|
-
@autoincrement_date = @autoincrement_date.advance(PERIODS.fetch(@period))
|
59
|
-
date
|
67
|
+
def reset_dates
|
68
|
+
@_dates = nil
|
60
69
|
end
|
61
70
|
end
|
62
71
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module LoanCreator
|
2
|
+
class UncapitalizedBullet < LoanCreator::Common
|
3
|
+
def lender_timetable
|
4
|
+
raise ArgumentError.new(:deferred_in_periods) unless deferred_in_periods == 0
|
5
|
+
raise ArgumentError.new(:interests_start_date) unless interests_start_date.nil?
|
6
|
+
timetable = new_timetable
|
7
|
+
reset_current_term
|
8
|
+
@crd_beginning_of_period = amount
|
9
|
+
@crd_end_of_period = amount
|
10
|
+
(duration_in_periods - 1).times { timetable << current_term }
|
11
|
+
compute_last_term
|
12
|
+
timetable << current_term
|
13
|
+
timetable
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def compute_last_term
|
19
|
+
@crd_end_of_period = bigd('0')
|
20
|
+
@period_interests = total_interests
|
21
|
+
@period_capital = @crd_beginning_of_period
|
22
|
+
@total_paid_capital_end_of_period = @period_capital
|
23
|
+
@total_paid_interests_end_of_period = @period_interests
|
24
|
+
@period_amount_to_pay = @period_capital + @period_interests
|
25
|
+
end
|
26
|
+
|
27
|
+
def total_interests
|
28
|
+
amount.mult(
|
29
|
+
bigd(periodic_interests_rate) * bigd(duration_in_periods),
|
30
|
+
BIG_DECIMAL_DIGITS
|
31
|
+
)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/loan_creator/version.rb
CHANGED
data/loan_creator.gemspec
CHANGED
@@ -5,11 +5,11 @@ require 'loan_creator/version'
|
|
5
5
|
Gem::Specification.new do |spec|
|
6
6
|
spec.name = 'loan_creator'
|
7
7
|
spec.version = LoanCreator::VERSION
|
8
|
-
spec.authors =
|
9
|
-
spec.email = ['thibault@capsens.eu', 'nicolas.besnard@capsens.eu', 'younes.serraj@gmail.com']
|
8
|
+
spec.authors = ["thibaulth", "nicob", "younes.serraj", "Antoine Becquet", "Jerome Drevet"]
|
9
|
+
spec.email = ['thibault@capsens.eu', 'nicolas.besnard@capsens.eu', 'younes.serraj@gmail.com', "antoine@capsens.eu", "jerome@capsens.eu"]
|
10
10
|
|
11
11
|
spec.summary = 'Create and update timetables from input data'
|
12
|
-
spec.homepage = 'https://
|
12
|
+
spec.homepage = 'https://github.com/CapSens/loan-creator'
|
13
13
|
spec.license = 'MIT'
|
14
14
|
|
15
15
|
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
metadata
CHANGED
@@ -1,16 +1,18 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: loan_creator
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2
|
4
|
+
version: 0.6.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- thibaulth
|
8
8
|
- nicob
|
9
9
|
- younes.serraj
|
10
|
+
- Antoine Becquet
|
11
|
+
- Jerome Drevet
|
10
12
|
autorequire:
|
11
13
|
bindir: exe
|
12
14
|
cert_chain: []
|
13
|
-
date: 2020-
|
15
|
+
date: 2020-12-08 00:00:00.000000000 Z
|
14
16
|
dependencies:
|
15
17
|
- !ruby/object:Gem::Dependency
|
16
18
|
name: bundler
|
@@ -115,6 +117,8 @@ email:
|
|
115
117
|
- thibault@capsens.eu
|
116
118
|
- nicolas.besnard@capsens.eu
|
117
119
|
- younes.serraj@gmail.com
|
120
|
+
- antoine@capsens.eu
|
121
|
+
- jerome@capsens.eu
|
118
122
|
executables: []
|
119
123
|
extensions: []
|
120
124
|
extra_rdoc_files: []
|
@@ -126,6 +130,9 @@ files:
|
|
126
130
|
- ".ruby-gemset"
|
127
131
|
- ".ruby-version"
|
128
132
|
- ".travis.yml"
|
133
|
+
- CHANGELOG.md
|
134
|
+
- CODE_OF_CONDUCT.md
|
135
|
+
- CapSens_Loan.xlsx
|
129
136
|
- Gemfile
|
130
137
|
- LICENSE.txt
|
131
138
|
- README.md
|
@@ -143,9 +150,10 @@ files:
|
|
143
150
|
- lib/loan_creator/standard.rb
|
144
151
|
- lib/loan_creator/term.rb
|
145
152
|
- lib/loan_creator/timetable.rb
|
153
|
+
- lib/loan_creator/uncapitalized_bullet.rb
|
146
154
|
- lib/loan_creator/version.rb
|
147
155
|
- loan_creator.gemspec
|
148
|
-
homepage: https://
|
156
|
+
homepage: https://github.com/CapSens/loan-creator
|
149
157
|
licenses:
|
150
158
|
- MIT
|
151
159
|
metadata: {}
|