loan_creator 0.7.0 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -0
- data/README.md +13 -2
- data/lib/loan_creator.rb +11 -10
- data/lib/loan_creator/borrower_timetable.rb +4 -2
- data/lib/loan_creator/bullet.rb +23 -17
- data/lib/loan_creator/common.rb +125 -48
- data/lib/loan_creator/linear.rb +38 -14
- data/lib/loan_creator/standard.rb +19 -21
- data/lib/loan_creator/term.rb +4 -4
- data/lib/loan_creator/term_dates_validator.rb +73 -0
- data/lib/loan_creator/timetable.rb +11 -38
- data/lib/loan_creator/uncapitalized_bullet.rb +26 -15
- data/lib/loan_creator/version.rb +1 -1
- data/loan_creator.gemspec +2 -0
- metadata +35 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fb6c8e7e02bc1349e47dcb07602828d051cfa63ac7fc3e16394604f8dda05e27
|
4
|
+
data.tar.gz: d103c1e0524641e73b307702eec5bd1cf43029236a3d791805e4d62e7e0732d3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c201e2a5fb2a5aaa54b1870b635e11c08b90d35f45258852f4cf61e5faa99756272b54db5a85c470123b5baaf5dedd5a9be79af8577796595997540cad4662b9
|
7
|
+
data.tar.gz: 00556c176b2e27765ca5112878495c7982cdaeb75abfe3b3956b57c6be57dfcf6c8cf89136bbfa0c94c40e7d43927ff2eafa8c0518f99d0be914e1bd7df1b70d
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,38 @@
|
|
1
|
+
v0.9.0
|
2
|
+
-------------------------
|
3
|
+
- add possibility to pass the argument `term_dates` for `LoanCreator::Bullet`, `LoanCreator::InFine` and `LoanCreator::Linear`
|
4
|
+
- `term_dates` option is based on `realistic_durations` and allow to compute terms custom date to custom date
|
5
|
+
|
6
|
+
v0.8.2
|
7
|
+
-------------------------
|
8
|
+
|
9
|
+
- manage initial value `due_interests` for Linear and InFine loan types
|
10
|
+
|
11
|
+
v0.8.1
|
12
|
+
-------------------------
|
13
|
+
- add `realistic_durations` option
|
14
|
+
|
15
|
+
v0.8.0
|
16
|
+
-------------------------
|
17
|
+
|
18
|
+
#### Feature
|
19
|
+
- due interests to date are now store into `due_interests_` columns for `LoanCreator::UncapitalizedBullet`
|
20
|
+
|
21
|
+
#### Bugfix
|
22
|
+
- fix `initial_values` not taking `paid_capital` and `paid_interests` into account for `LoanCreator::UncapitalizedBullet`
|
23
|
+
and `LoanCreator::Bullet`
|
24
|
+
|
25
|
+
#### Breaking changes
|
26
|
+
- rename columns `capitalized_interests_beginning_of_period` and `capitalized_interests_end_of_period` for
|
27
|
+
`due_interests_beginning_of_period` and `due_interests_end_of_period`
|
28
|
+
- rename `capitalized_interests` option in `:inital_values` into `due_interests`
|
29
|
+
|
30
|
+
|
31
|
+
v0.7.1
|
32
|
+
-------------------------
|
33
|
+
|
34
|
+
- fix `index` for `borrower_timetable` method that did not take into account starting index of `lender_timetables`
|
35
|
+
|
1
36
|
v0.7.0
|
2
37
|
-------------------------
|
3
38
|
|
data/README.md
CHANGED
@@ -44,9 +44,10 @@ Each instance of one of the previous classes has the following attributes:
|
|
44
44
|
:annual_interests_rate
|
45
45
|
:starts_at
|
46
46
|
:duration_in_periods
|
47
|
-
:deferred_in_periods (
|
47
|
+
:deferred_in_periods (defaults to zero)
|
48
48
|
:interests_start_date (optional)
|
49
49
|
:initial_values (to generate a timetable from a previous term or at a given state)
|
50
|
+
:realistic_durations (optional, defaults to false)
|
50
51
|
```
|
51
52
|
|
52
53
|
Initial values must be a hash with specific keys, like so:
|
@@ -57,7 +58,7 @@ Initial values must be a hash with specific keys, like so:
|
|
57
58
|
paid_interests: 11000.0,
|
58
59
|
accrued_delta_interests: 0,
|
59
60
|
starting_index: 2,
|
60
|
-
|
61
|
+
due_interests: 0
|
61
62
|
}
|
62
63
|
```
|
63
64
|
|
@@ -102,6 +103,9 @@ There is also a `LoanCreator::Timetable` class dedicated to record the data of t
|
|
102
103
|
|
103
104
|
# Amount to pay this term
|
104
105
|
:period_amount_to_pay
|
106
|
+
|
107
|
+
# Whether or not to use the real number of days in each month
|
108
|
+
:realistic_durations
|
105
109
|
```
|
106
110
|
|
107
111
|
`#periodic_interests_rate` renders a precise calculation of the loan's periodic interests rate based on two inputs: `#annual_interests_rate` and `#period`.
|
@@ -148,9 +152,11 @@ Capital share shall be repaid in full at loan's end.
|
|
148
152
|
`Bullet` loan generates terms where terms' payments are zero. \
|
149
153
|
Interests are capitalized, i.e. added to the borrowed capital on each term.\
|
150
154
|
Capital share shall be repaid in full and all interests paid at loan's end.
|
155
|
+
N.b.: Due capitalized interests to date are stored into terms under `due_intesrests_` columns
|
151
156
|
|
152
157
|
`UncapitalizedBullet` same as bullet, the only difference is the interests\
|
153
158
|
are NOT capitalized.
|
159
|
+
N.b.: Due interests to date are stored into terms under `due_intesrests_` columns
|
154
160
|
|
155
161
|
There is no deferred time for `InFine` and `Bullet` loans as it would be equivalent to increasing loan's duration.
|
156
162
|
|
@@ -174,6 +180,11 @@ additional term with only interests for the time difference.
|
|
174
180
|
For example, with a `start_at` in january 2020 and a `interests_start_date` in october 2019, the timetable will include a
|
175
181
|
first term corresponding to 3 months of interests.
|
176
182
|
|
183
|
+
`realistic_durations`: Optional. A boolean tha specifies whether or not to use the real number of days in each month when calculating the periodic interests rate. Note that leap years are taken into account. Default: `false`.
|
184
|
+
The default behaviour is to use the `period` in relation to the number of months in a year (ie: 12)
|
185
|
+
|
186
|
+
`term_dates`: Optional. Implemented for `LoanCreator::Bullet`, `LoanCreator::InFine` and `LoanCreator::Linear`. Can be used if you want to implement custom due dates for terms. Terms will be compute from date to date. Must be an array with following dates.
|
187
|
+
|
177
188
|
## Calculation
|
178
189
|
|
179
190
|
An excel simulator for standard case can be found [here](CapSens_Loan.xlsx).
|
data/lib/loan_creator.rb
CHANGED
@@ -7,14 +7,15 @@ 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,
|
19
|
-
autoload :UncapitalizedBullet,
|
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'
|
20
|
+
autoload :TermDatesValidator, 'loan_creator/term_dates_validator'
|
20
21
|
end
|
@@ -7,6 +7,8 @@ module LoanCreator
|
|
7
7
|
:period_capital,
|
8
8
|
:total_paid_capital_end_of_period,
|
9
9
|
:total_paid_interests_end_of_period,
|
10
|
+
:due_interests_beginning_of_period,
|
11
|
+
:due_interests_end_of_period,
|
10
12
|
:period_amount_to_pay
|
11
13
|
].freeze
|
12
14
|
|
@@ -18,8 +20,8 @@ module LoanCreator
|
|
18
20
|
end
|
19
21
|
|
20
22
|
borrower_timetable = LoanCreator::Timetable.new(
|
21
|
-
|
22
|
-
|
23
|
+
starting_index: lenders_timetables.first.starting_index,
|
24
|
+
loan: lenders_timetables.first.loan
|
23
25
|
)
|
24
26
|
|
25
27
|
# Borrower timetable is not concerned with computation-related value (delta, etc.),
|
data/lib/loan_creator/bullet.rb
CHANGED
@@ -7,33 +7,39 @@ module LoanCreator
|
|
7
7
|
reset_current_term
|
8
8
|
@crd_beginning_of_period = amount
|
9
9
|
@crd_end_of_period = amount
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
|
11
|
+
duration_in_periods.times { |idx| timetable << compute_current_term(idx, timetable) }
|
12
|
+
|
13
13
|
timetable
|
14
14
|
end
|
15
15
|
|
16
16
|
private
|
17
17
|
|
18
|
-
def
|
19
|
-
@
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
@
|
26
|
-
@
|
18
|
+
def compute_current_term(idx, timetable)
|
19
|
+
@due_on = timetable_term_dates[timetable.next_index]
|
20
|
+
last_period?(idx) ? compute_last_term(timetable) : compute_term(timetable)
|
21
|
+
current_term
|
22
|
+
end
|
23
|
+
|
24
|
+
def compute_last_term(timetable)
|
25
|
+
@crd_end_of_period = bigd('0')
|
26
|
+
@due_interests_beginning_of_period = @due_interests_end_of_period
|
27
|
+
@period_interests = @due_interests_end_of_period + compute_capitalized_interests(@due_on, timetable)
|
28
|
+
@due_interests_end_of_period = 0
|
29
|
+
@period_capital = @crd_beginning_of_period
|
30
|
+
@total_paid_capital_end_of_period += @period_capital
|
31
|
+
@total_paid_interests_end_of_period += @period_interests
|
32
|
+
@period_amount_to_pay = @period_capital + @period_interests
|
27
33
|
end
|
28
34
|
|
29
|
-
def compute_capitalized_interests
|
30
|
-
|
35
|
+
def compute_capitalized_interests(due_date, timetable)
|
36
|
+
computed_periodic_interests_rate = periodic_interests_rate(due_date, relative_to_date: timetable_term_dates[timetable.next_index - 1])
|
37
|
+
(amount + @due_interests_beginning_of_period).mult(computed_periodic_interests_rate, BIG_DECIMAL_DIGITS)
|
31
38
|
end
|
32
39
|
|
33
40
|
def compute_term(timetable)
|
34
|
-
@
|
35
|
-
@
|
36
|
-
timetable << current_term
|
41
|
+
@due_interests_beginning_of_period = @due_interests_end_of_period
|
42
|
+
@due_interests_end_of_period += compute_capitalized_interests(@due_on, timetable)
|
37
43
|
end
|
38
44
|
end
|
39
45
|
end
|
data/lib/loan_creator/common.rb
CHANGED
@@ -17,11 +17,20 @@ module LoanCreator
|
|
17
17
|
:duration_in_periods
|
18
18
|
].freeze
|
19
19
|
|
20
|
+
REQUIRED_ATTRIBUTES_TERM_DATES = [
|
21
|
+
:amount,
|
22
|
+
:annual_interests_rate,
|
23
|
+
:starts_on,
|
24
|
+
:duration_in_periods,
|
25
|
+
:term_dates
|
26
|
+
].freeze
|
27
|
+
|
20
28
|
OPTIONAL_ATTRIBUTES = {
|
21
29
|
# attribute: default_value
|
22
30
|
deferred_in_periods: 0,
|
23
31
|
interests_start_date: nil,
|
24
|
-
initial_values: {}
|
32
|
+
initial_values: {},
|
33
|
+
realistic_durations: false
|
25
34
|
}.freeze
|
26
35
|
|
27
36
|
attr_reader *REQUIRED_ATTRIBUTES
|
@@ -35,16 +44,29 @@ module LoanCreator
|
|
35
44
|
validate_attributes
|
36
45
|
set_initial_values
|
37
46
|
validate_initial_values
|
47
|
+
prepare_custom_term_dates if term_dates?
|
38
48
|
end
|
39
49
|
|
40
|
-
def
|
41
|
-
|
42
|
-
|
50
|
+
def periodic_interests_rate(date = nil, relative_to_date: nil)
|
51
|
+
if realistic_durations?
|
52
|
+
compute_realistic_periodic_interests_rate_percentage_for(date, relative_to_date: relative_to_date).div(100, BIG_DECIMAL_DIGITS)
|
53
|
+
else
|
54
|
+
@periodic_interests_rate ||=
|
55
|
+
annual_interests_rate.div(12 / PERIODS_IN_MONTHS[period], BIG_DECIMAL_DIGITS).div(100, BIG_DECIMAL_DIGITS)
|
56
|
+
end
|
43
57
|
end
|
44
58
|
|
45
|
-
def
|
46
|
-
@
|
47
|
-
|
59
|
+
def timetable_term_dates
|
60
|
+
@_timetable_term_dates ||= Hash.new do |dates, index|
|
61
|
+
dates[index] =
|
62
|
+
if index < 1
|
63
|
+
dates[index + 1].advance(months: -PERIODS_IN_MONTHS.fetch(period))
|
64
|
+
elsif index == 1
|
65
|
+
starts_on
|
66
|
+
else
|
67
|
+
starts_on.advance(months: PERIODS_IN_MONTHS.fetch(period) * (index - 1))
|
68
|
+
end
|
69
|
+
end
|
48
70
|
end
|
49
71
|
|
50
72
|
def lender_timetable
|
@@ -62,35 +84,49 @@ module LoanCreator
|
|
62
84
|
private
|
63
85
|
|
64
86
|
def require_attributes
|
65
|
-
|
87
|
+
required_attributes.each { |k| raise ArgumentError.new(k) unless @options.fetch(k, nil) }
|
66
88
|
end
|
67
89
|
|
68
90
|
def reinterpret_attributes
|
69
|
-
@options[:period] = @options[:period].to_sym
|
91
|
+
@options[:period] = @options[:period].to_sym unless term_dates?
|
70
92
|
@options[:amount] = bigd(@options[:amount])
|
71
93
|
@options[:annual_interests_rate] = bigd(@options[:annual_interests_rate])
|
72
94
|
@options[:starts_on] = Date.parse(@options[:starts_on]) if @options[:starts_on].is_a?(String)
|
73
95
|
@options[:interests_start_date] = Date.parse(@options[:interests_start_date]) if @options[:interests_start_date].is_a?(String)
|
96
|
+
|
97
|
+
if term_dates?
|
98
|
+
@options[:term_dates].map!{ |term_date| Date.parse(term_date.to_s) }
|
99
|
+
end
|
74
100
|
end
|
75
101
|
|
76
102
|
def set_attributes
|
77
|
-
|
103
|
+
required_attributes.each { |k| instance_variable_set(:"@#{k}", @options.fetch(k)) }
|
78
104
|
OPTIONAL_ATTRIBUTES.each { |k,v| instance_variable_set(:"@#{k}", @options.fetch(k, v)) }
|
79
105
|
end
|
80
106
|
|
81
107
|
def validate(key, &block)
|
82
108
|
raise unless block.call(instance_variable_get(:"@#{key}"))
|
83
|
-
rescue
|
84
|
-
raise ArgumentError.new(key)
|
109
|
+
rescue => e
|
110
|
+
raise ArgumentError.new([key, e.message].join(': '))
|
85
111
|
end
|
86
112
|
|
87
113
|
def validate_attributes
|
88
|
-
validate(:period) { |v| PERIODS_IN_MONTHS.keys.include?(v) }
|
114
|
+
validate(:period) { |v| PERIODS_IN_MONTHS.keys.include?(v) } unless term_dates?
|
89
115
|
validate(:amount) { |v| v.is_a?(BigDecimal) && v > 0 }
|
90
116
|
validate(:annual_interests_rate) { |v| v.is_a?(BigDecimal) && v >= 0 }
|
91
117
|
validate(:starts_on) { |v| v.is_a?(Date) }
|
92
118
|
validate(:duration_in_periods) { |v| v.is_a?(Integer) && v > 0 }
|
93
119
|
validate(:deferred_in_periods) { |v| v.is_a?(Integer) && v >= 0 && v < duration_in_periods }
|
120
|
+
validate_term_dates if term_dates?
|
121
|
+
end
|
122
|
+
|
123
|
+
def validate_term_dates
|
124
|
+
TermDatesValidator.call(
|
125
|
+
term_dates: @options[:term_dates],
|
126
|
+
duration_in_periods: @options[:duration_in_periods],
|
127
|
+
interests_start_date: @options[:interests_start_date],
|
128
|
+
loan_class: self.class.name
|
129
|
+
)
|
94
130
|
end
|
95
131
|
|
96
132
|
def validate_initial_values
|
@@ -103,59 +139,57 @@ module LoanCreator
|
|
103
139
|
end
|
104
140
|
|
105
141
|
def set_initial_values
|
106
|
-
@starting_index
|
142
|
+
@starting_index = initial_values[:starting_index] || 1
|
143
|
+
@initial_due_interests = bigd(initial_values[:due_interests] || 0)
|
107
144
|
|
108
145
|
return if initial_values.blank?
|
109
146
|
|
110
147
|
(@total_paid_capital_end_of_period = bigd(initial_values[:paid_capital]))
|
111
148
|
(@total_paid_interests_end_of_period = bigd(initial_values[:paid_interests]))
|
112
149
|
(@accrued_delta_interests = bigd(initial_values[:accrued_delta_interests]))
|
113
|
-
|
114
|
-
(@capitalized_interests_beginning_of_period = bigd(initial_values[:capitalized_interests] || 0))
|
115
|
-
end
|
150
|
+
(@due_interests_beginning_of_period = bigd(initial_values[:due_interests] || 0))
|
116
151
|
end
|
117
152
|
|
118
153
|
def reset_current_term
|
119
|
-
@accrued_delta_interests
|
120
|
-
@total_paid_capital_end_of_period
|
154
|
+
@accrued_delta_interests ||= bigd('0')
|
155
|
+
@total_paid_capital_end_of_period ||= bigd('0')
|
121
156
|
@total_paid_interests_end_of_period ||= bigd('0')
|
122
|
-
@
|
123
|
-
@crd_beginning_of_period
|
124
|
-
@crd_end_of_period
|
125
|
-
@period_theoric_interests
|
126
|
-
@
|
127
|
-
@delta_interests
|
128
|
-
@amount_to_add
|
129
|
-
@period_interests
|
130
|
-
@period_capital
|
131
|
-
@period_amount_to_pay
|
132
|
-
@due_on
|
157
|
+
@due_interests_beginning_of_period ||= bigd('0')
|
158
|
+
@crd_beginning_of_period = bigd('0')
|
159
|
+
@crd_end_of_period = bigd('0')
|
160
|
+
@period_theoric_interests = bigd('0')
|
161
|
+
@due_interests_end_of_period = @due_interests_beginning_of_period
|
162
|
+
@delta_interests = bigd('0')
|
163
|
+
@amount_to_add = bigd('0')
|
164
|
+
@period_interests = bigd('0')
|
165
|
+
@period_capital = bigd('0')
|
166
|
+
@period_amount_to_pay = bigd('0')
|
167
|
+
@due_on = nil
|
133
168
|
end
|
134
169
|
|
135
170
|
def current_term
|
136
171
|
LoanCreator::Term.new(
|
137
|
-
crd_beginning_of_period:
|
138
|
-
crd_end_of_period:
|
139
|
-
period_theoric_interests:
|
140
|
-
delta_interests:
|
141
|
-
accrued_delta_interests:
|
142
|
-
|
143
|
-
|
144
|
-
amount_to_add:
|
145
|
-
period_interests:
|
146
|
-
period_capital:
|
147
|
-
total_paid_capital_end_of_period:
|
148
|
-
total_paid_interests_end_of_period:
|
149
|
-
period_amount_to_pay:
|
150
|
-
due_on:
|
151
|
-
index:
|
172
|
+
crd_beginning_of_period: @crd_beginning_of_period,
|
173
|
+
crd_end_of_period: @crd_end_of_period,
|
174
|
+
period_theoric_interests: @period_theoric_interests,
|
175
|
+
delta_interests: @delta_interests,
|
176
|
+
accrued_delta_interests: @accrued_delta_interests,
|
177
|
+
due_interests_beginning_of_period: @due_interests_beginning_of_period,
|
178
|
+
due_interests_end_of_period: @due_interests_end_of_period,
|
179
|
+
amount_to_add: @amount_to_add,
|
180
|
+
period_interests: @period_interests,
|
181
|
+
period_capital: @period_capital,
|
182
|
+
total_paid_capital_end_of_period: @total_paid_capital_end_of_period,
|
183
|
+
total_paid_interests_end_of_period: @total_paid_interests_end_of_period,
|
184
|
+
period_amount_to_pay: @period_amount_to_pay,
|
185
|
+
due_on: @due_on,
|
186
|
+
index: compute_index
|
152
187
|
)
|
153
188
|
end
|
154
189
|
|
155
190
|
def new_timetable
|
156
191
|
LoanCreator::Timetable.new(
|
157
|
-
|
158
|
-
period: period,
|
192
|
+
loan: self,
|
159
193
|
interests_start_date: interests_start_date,
|
160
194
|
starting_index: @starting_index
|
161
195
|
)
|
@@ -165,6 +199,10 @@ module LoanCreator
|
|
165
199
|
@index ? (@starting_index + @index - 1) : nil
|
166
200
|
end
|
167
201
|
|
202
|
+
def last_period?(idx)
|
203
|
+
idx == (duration_in_periods - 1)
|
204
|
+
end
|
205
|
+
|
168
206
|
def compute_term_zero
|
169
207
|
@crd_beginning_of_period = @crd_end_of_period
|
170
208
|
@period_theoric_interests = term_zero_interests
|
@@ -174,6 +212,7 @@ module LoanCreator
|
|
174
212
|
@total_paid_interests_end_of_period += @period_interests
|
175
213
|
@period_amount_to_pay = @period_interests
|
176
214
|
@index = 0
|
215
|
+
@due_on = timetable_term_dates[0]
|
177
216
|
end
|
178
217
|
|
179
218
|
def term_zero_interests
|
@@ -194,7 +233,45 @@ module LoanCreator
|
|
194
233
|
end
|
195
234
|
|
196
235
|
def term_zero?
|
197
|
-
interests_start_date && interests_start_date < term_zero_date
|
236
|
+
(interests_start_date && interests_start_date < term_zero_date) && !term_dates?
|
237
|
+
end
|
238
|
+
|
239
|
+
def compute_realistic_periodic_interests_rate_percentage_for(date, relative_to_date:)
|
240
|
+
realistic_days = 365
|
241
|
+
realistic_days += 1 if date.leap?
|
242
|
+
realistic_days_in_period = (date - relative_to_date).to_i
|
243
|
+
|
244
|
+
annual_interests_rate.div(bigd(realistic_days) / bigd(realistic_days_in_period), BIG_DECIMAL_DIGITS)
|
245
|
+
end
|
246
|
+
|
247
|
+
def realistic_durations?
|
248
|
+
term_dates? || @realistic_durations.present?
|
249
|
+
end
|
250
|
+
|
251
|
+
def required_attributes
|
252
|
+
if term_dates?
|
253
|
+
REQUIRED_ATTRIBUTES_TERM_DATES
|
254
|
+
else
|
255
|
+
REQUIRED_ATTRIBUTES
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
def term_dates?
|
260
|
+
@options[:term_dates].present?
|
261
|
+
end
|
262
|
+
|
263
|
+
def prepare_custom_term_dates
|
264
|
+
term_dates = @options[:term_dates].each_with_index.with_object({}) do |(term_date, index), obj|
|
265
|
+
obj[index + 1] = term_date
|
266
|
+
end
|
267
|
+
|
268
|
+
term_dates[0] = starts_on
|
269
|
+
@_timetable_term_dates = term_dates
|
270
|
+
@realistic_durations = true
|
271
|
+
end
|
272
|
+
|
273
|
+
def compute_period_generated_interests(interests_rate)
|
274
|
+
(@crd_beginning_of_period + @due_interests_beginning_of_period).mult(interests_rate, BIG_DECIMAL_DIGITS)
|
198
275
|
end
|
199
276
|
end
|
200
277
|
end
|
data/lib/loan_creator/linear.rb
CHANGED
@@ -11,26 +11,24 @@ module LoanCreator
|
|
11
11
|
timetable << current_term
|
12
12
|
end
|
13
13
|
|
14
|
-
duration_in_periods.times
|
15
|
-
@last_period = last_period?(idx)
|
16
|
-
@deferred_period = idx < deferred_in_periods
|
17
|
-
compute_current_term(idx)
|
18
|
-
timetable << current_term
|
19
|
-
end
|
14
|
+
duration_in_periods.times { |idx| timetable << compute_current_term(idx, timetable) }
|
20
15
|
|
21
16
|
timetable
|
22
17
|
end
|
23
18
|
|
24
19
|
private
|
25
20
|
|
26
|
-
def
|
27
|
-
|
28
|
-
|
21
|
+
def compute_current_term(idx, timetable)
|
22
|
+
@index = idx + 1
|
23
|
+
@last_period = last_period?(idx)
|
24
|
+
@deferred_period = @index <= deferred_in_periods
|
25
|
+
@due_on = timetable_term_dates[timetable.next_index]
|
26
|
+
computed_periodic_interests_rate = periodic_interests_rate(@due_on, relative_to_date: timetable_term_dates[timetable.next_index - 1])
|
29
27
|
|
30
|
-
def compute_current_term(idx)
|
31
28
|
# Reminder: CRD beginning of period = CRD end of period **of previous period**
|
32
29
|
@crd_beginning_of_period = @crd_end_of_period
|
33
|
-
@
|
30
|
+
@due_interests_beginning_of_period = @due_interests_end_of_period
|
31
|
+
@period_theoric_interests = period_theoric_interests(computed_periodic_interests_rate)
|
34
32
|
@delta_interests = @period_theoric_interests - @period_theoric_interests.round(2)
|
35
33
|
@accrued_delta_interests += @delta_interests
|
36
34
|
@amount_to_add = bigd(
|
@@ -49,8 +47,17 @@ module LoanCreator
|
|
49
47
|
@total_paid_interests_end_of_period += @period_interests
|
50
48
|
@period_amount_to_pay = @period_interests + @period_capital
|
51
49
|
@crd_end_of_period -= @period_capital
|
52
|
-
@
|
53
|
-
|
50
|
+
@due_interests_end_of_period -= reimbursed_due_interests
|
51
|
+
|
52
|
+
current_term
|
53
|
+
end
|
54
|
+
|
55
|
+
def period_theoric_interests(computed_periodic_interests_rate)
|
56
|
+
if @due_interests_beginning_of_period > 0
|
57
|
+
reimbursed_due_interests + compute_period_generated_interests(computed_periodic_interests_rate)
|
58
|
+
else
|
59
|
+
compute_period_generated_interests(computed_periodic_interests_rate)
|
60
|
+
end
|
54
61
|
end
|
55
62
|
|
56
63
|
def period_capital
|
@@ -58,9 +65,26 @@ module LoanCreator
|
|
58
65
|
@crd_beginning_of_period
|
59
66
|
elsif @deferred_period
|
60
67
|
bigd(0)
|
68
|
+
elsif @due_interests_beginning_of_period > 0
|
69
|
+
compute_period_capital - reimbursed_due_interests
|
70
|
+
else
|
71
|
+
compute_period_capital
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def reimbursed_due_interests
|
76
|
+
if @deferred_period
|
77
|
+
bigd(0)
|
61
78
|
else
|
62
|
-
|
79
|
+
[
|
80
|
+
@due_interests_beginning_of_period,
|
81
|
+
compute_period_capital
|
82
|
+
].min
|
63
83
|
end
|
64
84
|
end
|
85
|
+
|
86
|
+
def compute_period_capital
|
87
|
+
((amount + @initial_due_interests) / (duration_in_periods - deferred_in_periods)).round(2)
|
88
|
+
end
|
65
89
|
end
|
66
90
|
end
|
@@ -12,24 +12,22 @@ module LoanCreator
|
|
12
12
|
timetable << current_term
|
13
13
|
end
|
14
14
|
|
15
|
-
duration_in_periods.times
|
16
|
-
|
17
|
-
@deferred_period = idx < deferred_in_periods
|
18
|
-
compute_current_term(idx)
|
19
|
-
timetable << current_term
|
20
|
-
end
|
15
|
+
duration_in_periods.times { |idx| timetable << compute_current_term(idx, timetable) }
|
16
|
+
|
21
17
|
timetable
|
22
18
|
end
|
23
19
|
|
24
20
|
private
|
25
21
|
|
26
|
-
def
|
27
|
-
|
28
|
-
|
22
|
+
def compute_current_term(idx, timetable)
|
23
|
+
@index = idx + 1
|
24
|
+
@last_period = last_period?(idx)
|
25
|
+
@deferred_period = @index <= deferred_in_periods
|
26
|
+
@due_on = timetable_term_dates[timetable.next_index]
|
27
|
+
computed_periodic_interests_rate = periodic_interests_rate(@due_on, relative_to_date: timetable_term_dates[timetable.next_index - 1])
|
29
28
|
|
30
|
-
def compute_current_term(idx)
|
31
29
|
@crd_beginning_of_period = @crd_end_of_period
|
32
|
-
@period_theoric_interests = period_theoric_interests(
|
30
|
+
@period_theoric_interests = period_theoric_interests(@index, computed_periodic_interests_rate)
|
33
31
|
@delta_interests = @period_theoric_interests - @period_theoric_interests.round(2)
|
34
32
|
@accrued_delta_interests += @delta_interests
|
35
33
|
@amount_to_add = bigd(
|
@@ -43,37 +41,37 @@ module LoanCreator
|
|
43
41
|
)
|
44
42
|
@accrued_delta_interests -= @amount_to_add
|
45
43
|
@period_interests = @period_theoric_interests.round(2) + @amount_to_add
|
46
|
-
@period_capital = period_capital(
|
44
|
+
@period_capital = period_capital(@index, computed_periodic_interests_rate)
|
47
45
|
@total_paid_capital_end_of_period += @period_capital
|
48
46
|
@total_paid_interests_end_of_period += @period_interests
|
49
47
|
@period_amount_to_pay = @period_interests + @period_capital
|
50
48
|
@crd_end_of_period -= @period_capital
|
51
|
-
|
52
|
-
|
49
|
+
|
50
|
+
current_term
|
53
51
|
end
|
54
52
|
|
55
|
-
def period_theoric_interests(idx)
|
53
|
+
def period_theoric_interests(idx, computed_periodic_interests_rate)
|
56
54
|
if @deferred_period
|
57
|
-
@crd_beginning_of_period *
|
55
|
+
@crd_beginning_of_period * computed_periodic_interests_rate
|
58
56
|
else
|
59
57
|
-ipmt(
|
60
|
-
|
61
|
-
|
58
|
+
computed_periodic_interests_rate,
|
59
|
+
idx - deferred_in_periods,
|
62
60
|
duration_in_periods - deferred_in_periods,
|
63
61
|
amount
|
64
62
|
)
|
65
63
|
end
|
66
64
|
end
|
67
65
|
|
68
|
-
def period_capital(idx)
|
66
|
+
def period_capital(idx, computed_periodic_interests_rate)
|
69
67
|
if @last_period
|
70
68
|
@crd_beginning_of_period
|
71
69
|
elsif @deferred_period
|
72
70
|
bigd(0)
|
73
71
|
else
|
74
72
|
-ppmt(
|
75
|
-
|
76
|
-
|
73
|
+
computed_periodic_interests_rate,
|
74
|
+
idx - deferred_in_periods,
|
77
75
|
duration_in_periods - deferred_in_periods,
|
78
76
|
amount
|
79
77
|
).round(2)
|
data/lib/loan_creator/term.rb
CHANGED
@@ -17,11 +17,11 @@ module LoanCreator
|
|
17
17
|
# Accrued interests' delta
|
18
18
|
:accrued_delta_interests,
|
19
19
|
|
20
|
-
#
|
21
|
-
:
|
20
|
+
# Due interests at the beginning of the term (Bullet and UncapitalizedBullet only)
|
21
|
+
:due_interests_beginning_of_period,
|
22
22
|
|
23
|
-
#
|
24
|
-
:
|
23
|
+
# Due interests at the end of the term (Bullet and UncapitalizedBullet only)
|
24
|
+
:due_interests_end_of_period,
|
25
25
|
|
26
26
|
# Adjustment of -0.01, 0 or +0.01 cent depending on accrued_delta_interests
|
27
27
|
:amount_to_add,
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module LoanCreator
|
2
|
+
module TermDatesValidator
|
3
|
+
def self.call(term_dates:, duration_in_periods:, interests_start_date:, loan_class:)
|
4
|
+
is_array(term_dates)
|
5
|
+
matches_duration(term_dates, duration_in_periods)
|
6
|
+
interests_start_date_present(interests_start_date)
|
7
|
+
coherent_dates_for_non_bullet(term_dates)
|
8
|
+
coherent_dates_for_bullet(term_dates) if bullet?(loan_class)
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def self.is_array(term_dates)
|
14
|
+
unless term_dates.is_a?(Array)
|
15
|
+
raise TypeError, 'the :term_dates option must be an Array'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.matches_duration(term_dates, duration_in_periods)
|
20
|
+
unless term_dates.size == duration_in_periods
|
21
|
+
raise ArgumentError, "the size of :term_dates (#{term_dates.size}) do not match the :duration_in_periods (#{duration_in_periods})"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.interests_start_date_present(interests_start_date)
|
26
|
+
if interests_start_date.present?
|
27
|
+
raise ArgumentError, ":interests_start_date is no compatible with :term_dates"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.coherent_dates_for_non_bullet(term_dates)
|
32
|
+
term_dates.each_with_index do |term_date, index|
|
33
|
+
next if index.zero?
|
34
|
+
|
35
|
+
previous_term_date = term_dates[index - 1]
|
36
|
+
|
37
|
+
unless term_date > previous_term_date
|
38
|
+
previous_term_date_description =
|
39
|
+
":term_dates[#{index - 1}] (#{term_dates[index - 1].strftime('%Y-%m-%d')})"
|
40
|
+
|
41
|
+
error_message = "#{previous_term_date_description} must be before :term_dates[#{index}] (#{term_date.strftime('%Y-%m-%d')})"
|
42
|
+
|
43
|
+
raise ArgumentError, error_message
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.coherent_dates_for_bullet(term_dates)
|
49
|
+
term_dates.each_with_index do |term_date, index|
|
50
|
+
next if index.zero?
|
51
|
+
|
52
|
+
days_in_year = 365
|
53
|
+
days_in_year += 1 if term_date.leap?
|
54
|
+
|
55
|
+
previous_term_date = term_dates[index - 1]
|
56
|
+
days_between = (term_date - previous_term_date).to_i.abs
|
57
|
+
|
58
|
+
if days_between > days_in_year
|
59
|
+
previous_term_date_description =
|
60
|
+
":term_dates[#{index - 1}] (#{term_dates[index - 1].strftime('%Y-%m-%d')})"
|
61
|
+
|
62
|
+
error_description = "There are #{days_between} days between #{previous_term_date_description} and :term_dates[#{index}]"
|
63
|
+
|
64
|
+
raise ArgumentError, "term dates can't be more than 1 year apart. #{error_description}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.bullet?(loan_class)
|
70
|
+
loan_class == "LoanCreator::Bullet"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -1,22 +1,13 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
module LoanCreator
|
3
3
|
class Timetable
|
4
|
-
|
5
|
-
PERIODS = {
|
6
|
-
month: {months: 1},
|
7
|
-
quarter: {months: 3},
|
8
|
-
semester: {months: 6},
|
9
|
-
year: {years: 1}
|
10
|
-
}
|
4
|
+
attr_reader :loan, :terms, :starting_index #, :interests_start_date
|
11
5
|
|
12
|
-
|
13
|
-
|
14
|
-
def initialize(starts_on:, period:, interests_start_date: nil, starting_index: 1)
|
15
|
-
raise ArgumentError.new(:period) unless PERIODS.keys.include?(period)
|
6
|
+
delegate :starts_on, :period, to: :loan
|
16
7
|
|
8
|
+
def initialize(loan:, interests_start_date: nil, starting_index: 1)
|
17
9
|
@terms = []
|
18
|
-
@
|
19
|
-
@period = period
|
10
|
+
@loan = loan
|
20
11
|
@starting_index = starting_index
|
21
12
|
|
22
13
|
if interests_start_date
|
@@ -26,8 +17,11 @@ module LoanCreator
|
|
26
17
|
|
27
18
|
def <<(term)
|
28
19
|
raise ArgumentError.new('LoanCreator::Term expected') unless term.is_a?(LoanCreator::Term)
|
29
|
-
|
30
|
-
|
20
|
+
|
21
|
+
@current_index = term.index || next_index
|
22
|
+
|
23
|
+
term.index = @current_index
|
24
|
+
term.due_on ||= loan.timetable_term_dates[term.index]
|
31
25
|
@terms << term
|
32
26
|
self
|
33
27
|
end
|
@@ -43,29 +37,8 @@ module LoanCreator
|
|
43
37
|
@terms.find { |term| term.index == index }
|
44
38
|
end
|
45
39
|
|
46
|
-
|
47
|
-
|
48
|
-
def autoincrement_index
|
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]
|
65
|
-
end
|
66
|
-
|
67
|
-
def reset_dates
|
68
|
-
@_dates = nil
|
40
|
+
def next_index
|
41
|
+
@current_index.nil? ? @starting_index : @current_index + 1
|
69
42
|
end
|
70
43
|
end
|
71
44
|
end
|
@@ -7,28 +7,39 @@ module LoanCreator
|
|
7
7
|
reset_current_term
|
8
8
|
@crd_beginning_of_period = amount
|
9
9
|
@crd_end_of_period = amount
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
|
11
|
+
duration_in_periods.times { |idx| timetable << compute_current_term(idx, timetable) }
|
12
|
+
|
13
13
|
timetable
|
14
14
|
end
|
15
15
|
|
16
16
|
private
|
17
17
|
|
18
|
-
def
|
19
|
-
@
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
18
|
+
def compute_current_term(idx, timetable)
|
19
|
+
@due_on = timetable_term_dates[timetable.next_index]
|
20
|
+
last_period?(idx) ? compute_last_term(timetable) : compute_term(timetable)
|
21
|
+
current_term
|
22
|
+
end
|
23
|
+
|
24
|
+
def compute_last_term(timetable)
|
25
|
+
@crd_end_of_period = bigd('0')
|
26
|
+
@due_interests_beginning_of_period = @due_interests_end_of_period
|
27
|
+
@period_interests = @due_interests_end_of_period + compute_interests(@due_on, timetable)
|
28
|
+
@due_interests_end_of_period = 0
|
29
|
+
@period_capital = @crd_beginning_of_period
|
30
|
+
@total_paid_capital_end_of_period += @period_capital
|
31
|
+
@total_paid_interests_end_of_period += @period_interests
|
32
|
+
@period_amount_to_pay = @period_capital + @period_interests
|
33
|
+
end
|
34
|
+
|
35
|
+
def compute_interests(due_date, timetable)
|
36
|
+
computed_periodic_interests_rate = periodic_interests_rate(due_date, relative_to_date: timetable_term_dates[timetable.next_index - 1])
|
37
|
+
amount.mult(bigd(computed_periodic_interests_rate), BIG_DECIMAL_DIGITS)
|
25
38
|
end
|
26
39
|
|
27
|
-
def
|
28
|
-
|
29
|
-
|
30
|
-
BIG_DECIMAL_DIGITS
|
31
|
-
)
|
40
|
+
def compute_term(timetable)
|
41
|
+
@due_interests_beginning_of_period = @due_interests_end_of_period
|
42
|
+
@due_interests_end_of_period += compute_interests(@due_on, timetable)
|
32
43
|
end
|
33
44
|
end
|
34
45
|
end
|
data/lib/loan_creator/version.rb
CHANGED
data/loan_creator.gemspec
CHANGED
@@ -24,6 +24,8 @@ Gem::Specification.new do |spec|
|
|
24
24
|
spec.add_development_dependency 'rspec', '~> 3.0'
|
25
25
|
spec.add_development_dependency 'simplecov', '~> 0.16'
|
26
26
|
spec.add_development_dependency 'byebug', '~> 11.0'
|
27
|
+
spec.add_development_dependency 'pry', '~> 0.13.1'
|
28
|
+
spec.add_development_dependency 'table_print', '~> 1.5'
|
27
29
|
|
28
30
|
spec.add_runtime_dependency 'bigdecimal'
|
29
31
|
spec.add_runtime_dependency 'activesupport'
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: loan_creator
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- thibaulth
|
@@ -9,10 +9,10 @@ authors:
|
|
9
9
|
- younes.serraj
|
10
10
|
- Antoine Becquet
|
11
11
|
- Jerome Drevet
|
12
|
-
autorequire:
|
12
|
+
autorequire:
|
13
13
|
bindir: exe
|
14
14
|
cert_chain: []
|
15
|
-
date: 2021-
|
15
|
+
date: 2021-05-07 00:00:00.000000000 Z
|
16
16
|
dependencies:
|
17
17
|
- !ruby/object:Gem::Dependency
|
18
18
|
name: bundler
|
@@ -84,6 +84,34 @@ dependencies:
|
|
84
84
|
- - "~>"
|
85
85
|
- !ruby/object:Gem::Version
|
86
86
|
version: '11.0'
|
87
|
+
- !ruby/object:Gem::Dependency
|
88
|
+
name: pry
|
89
|
+
requirement: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - "~>"
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 0.13.1
|
94
|
+
type: :development
|
95
|
+
prerelease: false
|
96
|
+
version_requirements: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - "~>"
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: 0.13.1
|
101
|
+
- !ruby/object:Gem::Dependency
|
102
|
+
name: table_print
|
103
|
+
requirement: !ruby/object:Gem::Requirement
|
104
|
+
requirements:
|
105
|
+
- - "~>"
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '1.5'
|
108
|
+
type: :development
|
109
|
+
prerelease: false
|
110
|
+
version_requirements: !ruby/object:Gem::Requirement
|
111
|
+
requirements:
|
112
|
+
- - "~>"
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: '1.5'
|
87
115
|
- !ruby/object:Gem::Dependency
|
88
116
|
name: bigdecimal
|
89
117
|
requirement: !ruby/object:Gem::Requirement
|
@@ -112,7 +140,7 @@ dependencies:
|
|
112
140
|
- - ">="
|
113
141
|
- !ruby/object:Gem::Version
|
114
142
|
version: '0'
|
115
|
-
description:
|
143
|
+
description:
|
116
144
|
email:
|
117
145
|
- thibault@capsens.eu
|
118
146
|
- nicolas.besnard@capsens.eu
|
@@ -149,6 +177,7 @@ files:
|
|
149
177
|
- lib/loan_creator/linear.rb
|
150
178
|
- lib/loan_creator/standard.rb
|
151
179
|
- lib/loan_creator/term.rb
|
180
|
+
- lib/loan_creator/term_dates_validator.rb
|
152
181
|
- lib/loan_creator/timetable.rb
|
153
182
|
- lib/loan_creator/uncapitalized_bullet.rb
|
154
183
|
- lib/loan_creator/version.rb
|
@@ -157,7 +186,7 @@ homepage: https://github.com/CapSens/loan-creator
|
|
157
186
|
licenses:
|
158
187
|
- MIT
|
159
188
|
metadata: {}
|
160
|
-
post_install_message:
|
189
|
+
post_install_message:
|
161
190
|
rdoc_options: []
|
162
191
|
require_paths:
|
163
192
|
- lib
|
@@ -173,7 +202,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
173
202
|
version: '0'
|
174
203
|
requirements: []
|
175
204
|
rubygems_version: 3.1.2
|
176
|
-
signing_key:
|
205
|
+
signing_key:
|
177
206
|
specification_version: 4
|
178
207
|
summary: Create and update timetables from input data
|
179
208
|
test_files: []
|