loan_creator 0.8.0 → 0.10.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 +22 -3
- data/README.md +14 -1
- data/lib/loan_creator.rb +11 -10
- data/lib/loan_creator/borrower_timetable.rb +1 -2
- data/lib/loan_creator/bullet.rb +15 -9
- data/lib/loan_creator/common.rb +126 -17
- data/lib/loan_creator/linear.rb +38 -14
- data/lib/loan_creator/standard.rb +19 -21
- data/lib/loan_creator/term_dates_validator.rb +75 -0
- data/lib/loan_creator/timetable.rb +11 -38
- data/lib/loan_creator/uncapitalized_bullet.rb +15 -9
- data/lib/loan_creator/version.rb +1 -1
- data/loan_creator.gemspec +2 -0
- data/scripts/convert_export_to_spec.rb +51 -0
- metadata +32 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 838d0d280c585125b013b8ea6d7e8b58e94c2234fffe3d49d1c97f39a67b254f
|
4
|
+
data.tar.gz: 7a607035130b53af6678bdec2a91b9d99fcace0a1710db1ef83333a4b15133b0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 06c8e0dfa135c8a80f9969f60db83a8bc3630161fea4f208dff6480ab9142635d196aaa729fdb32fd89e38e87804cbc76fcc1f10ec464c680c10219ef7aec23c
|
7
|
+
data.tar.gz: c710e04d68debd3d7f63569586aed46ac1426aabf9ed995fa762a18745916d1f31339f93b173159ddd61dcf23b145527471dbc7ff873a1bdfc46169e310f4322
|
data/CHANGELOG.md
CHANGED
@@ -1,14 +1,33 @@
|
|
1
|
+
v0.9.1
|
2
|
+
-------------------------
|
3
|
+
- add spec importer `scripts/convert_export_to_spec.rb {csv_url}`
|
4
|
+
- fix period interests rate for realistic duration period overlapping leap & non leap years
|
5
|
+
|
6
|
+
v0.9.0
|
7
|
+
-------------------------
|
8
|
+
- add possibility to pass the argument `term_dates` for `LoanCreator::Bullet`, `LoanCreator::InFine` and `LoanCreator::Linear`
|
9
|
+
- `term_dates` option is based on `realistic_durations` and allow to compute terms custom date to custom date
|
10
|
+
|
11
|
+
v0.8.2
|
12
|
+
-------------------------
|
13
|
+
|
14
|
+
- manage initial value `due_interests` for Linear and InFine loan types
|
15
|
+
|
16
|
+
v0.8.1
|
17
|
+
-------------------------
|
18
|
+
- add `realistic_durations` option
|
19
|
+
|
1
20
|
v0.8.0
|
2
21
|
-------------------------
|
3
22
|
|
4
|
-
|
23
|
+
#### Feature
|
5
24
|
- due interests to date are now store into `due_interests_` columns for `LoanCreator::UncapitalizedBullet`
|
6
25
|
|
7
|
-
|
26
|
+
#### Bugfix
|
8
27
|
- fix `initial_values` not taking `paid_capital` and `paid_interests` into account for `LoanCreator::UncapitalizedBullet`
|
9
28
|
and `LoanCreator::Bullet`
|
10
29
|
|
11
|
-
|
30
|
+
#### Breaking changes
|
12
31
|
- rename columns `capitalized_interests_beginning_of_period` and `capitalized_interests_end_of_period` for
|
13
32
|
`due_interests_beginning_of_period` and `due_interests_end_of_period`
|
14
33
|
- rename `capitalized_interests` option in `:inital_values` into `due_interests`
|
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:
|
@@ -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`.
|
@@ -176,6 +180,15 @@ additional term with only interests for the time difference.
|
|
176
180
|
For example, with a `start_at` in january 2020 and a `interests_start_date` in october 2019, the timetable will include a
|
177
181
|
first term corresponding to 3 months of interests.
|
178
182
|
|
183
|
+
`realistic_durations`: Optional. A boolean that specifies whether or not to use the real number of days in each month when calculating the periodic interests rate (ie: for a period between January 1st 2021 and February 1st 2021 the period rate is annual_interests_rate * 31/355).
|
184
|
+
Note that leap years are taken into account (ie: for a period between January 1st 2020 and February 1st 2020 the period rate is annual_interests_rate * 31/356). Also, an overlaping period
|
185
|
+
take each parts into account (ie: for a period between December 1st 2020 and February 1st 2021, the period rate is (annual_interests_rate * (31/355) + annual_interests_rate * (31/356)))
|
186
|
+
Default: `false`.
|
187
|
+
The default behaviour is to use the `period` in relation to the number of months in a year (ie: for a monthly timetable annual_interests_rate * 1/12, for quarter annual_interests_rate * 3/12, etc.)
|
188
|
+
|
189
|
+
|
190
|
+
`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 computed from date to date. Must be an array with following dates. Must contain duration + 1 dates. The first element of the array is the first period start_date and last is the last period pay day.
|
191
|
+
|
179
192
|
## Calculation
|
180
193
|
|
181
194
|
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
|
@@ -21,8 +21,7 @@ module LoanCreator
|
|
21
21
|
|
22
22
|
borrower_timetable = LoanCreator::Timetable.new(
|
23
23
|
starting_index: lenders_timetables.first.starting_index,
|
24
|
-
|
25
|
-
period: lenders_timetables.first.period
|
24
|
+
loan: lenders_timetables.first.loan
|
26
25
|
)
|
27
26
|
|
28
27
|
# Borrower timetable is not concerned with computation-related value (delta, etc.),
|
data/lib/loan_creator/bullet.rb
CHANGED
@@ -7,18 +7,24 @@ 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
|
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)
|
19
25
|
@crd_end_of_period = bigd('0')
|
20
26
|
@due_interests_beginning_of_period = @due_interests_end_of_period
|
21
|
-
@period_interests = @due_interests_end_of_period + compute_capitalized_interests
|
27
|
+
@period_interests = @due_interests_end_of_period + compute_capitalized_interests(@due_on, timetable)
|
22
28
|
@due_interests_end_of_period = 0
|
23
29
|
@period_capital = @crd_beginning_of_period
|
24
30
|
@total_paid_capital_end_of_period += @period_capital
|
@@ -26,14 +32,14 @@ module LoanCreator
|
|
26
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
41
|
@due_interests_beginning_of_period = @due_interests_end_of_period
|
35
|
-
@due_interests_end_of_period += compute_capitalized_interests
|
36
|
-
timetable << current_term
|
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,7 +139,8 @@ 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
|
|
@@ -152,8 +189,7 @@ module LoanCreator
|
|
152
189
|
|
153
190
|
def new_timetable
|
154
191
|
LoanCreator::Timetable.new(
|
155
|
-
|
156
|
-
period: period,
|
192
|
+
loan: self,
|
157
193
|
interests_start_date: interests_start_date,
|
158
194
|
starting_index: @starting_index
|
159
195
|
)
|
@@ -163,6 +199,10 @@ module LoanCreator
|
|
163
199
|
@index ? (@starting_index + @index - 1) : nil
|
164
200
|
end
|
165
201
|
|
202
|
+
def last_period?(idx)
|
203
|
+
idx == (duration_in_periods - 1)
|
204
|
+
end
|
205
|
+
|
166
206
|
def compute_term_zero
|
167
207
|
@crd_beginning_of_period = @crd_end_of_period
|
168
208
|
@period_theoric_interests = term_zero_interests
|
@@ -172,6 +212,7 @@ module LoanCreator
|
|
172
212
|
@total_paid_interests_end_of_period += @period_interests
|
173
213
|
@period_amount_to_pay = @period_interests
|
174
214
|
@index = 0
|
215
|
+
@due_on = timetable_term_dates[0]
|
175
216
|
end
|
176
217
|
|
177
218
|
def term_zero_interests
|
@@ -192,7 +233,75 @@ module LoanCreator
|
|
192
233
|
end
|
193
234
|
|
194
235
|
def term_zero?
|
195
|
-
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 leap_days_count(date, relative_to_date:)
|
240
|
+
start_year = relative_to_date.year
|
241
|
+
end_year = date.year
|
242
|
+
|
243
|
+
(start_year..end_year).sum do |year|
|
244
|
+
next 0 unless Date.gregorian_leap?(year)
|
245
|
+
|
246
|
+
start_date =
|
247
|
+
if start_year == year
|
248
|
+
relative_to_date
|
249
|
+
else
|
250
|
+
Date.new(year - 1, 12, 31)
|
251
|
+
end
|
252
|
+
|
253
|
+
end_date =
|
254
|
+
if end_year == year
|
255
|
+
date
|
256
|
+
else
|
257
|
+
Date.new(year, 12, 31)
|
258
|
+
end
|
259
|
+
|
260
|
+
end_date - start_date
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def compute_realistic_periodic_interests_rate_percentage_for(date, relative_to_date:)
|
265
|
+
total_days = date - relative_to_date
|
266
|
+
leap_days = bigd(leap_days_count(date, relative_to_date: relative_to_date))
|
267
|
+
non_leap_days = bigd(total_days - leap_days)
|
268
|
+
|
269
|
+
annual_interests_rate.mult(
|
270
|
+
leap_days.div(366, BIG_DECIMAL_DIGITS) +
|
271
|
+
non_leap_days.div(365, BIG_DECIMAL_DIGITS),
|
272
|
+
BIG_DECIMAL_DIGITS
|
273
|
+
)
|
274
|
+
end
|
275
|
+
|
276
|
+
def realistic_durations?
|
277
|
+
term_dates? || @realistic_durations.present?
|
278
|
+
end
|
279
|
+
|
280
|
+
def required_attributes
|
281
|
+
if term_dates?
|
282
|
+
REQUIRED_ATTRIBUTES_TERM_DATES
|
283
|
+
else
|
284
|
+
REQUIRED_ATTRIBUTES
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
def term_dates?
|
289
|
+
@options[:term_dates].present?
|
290
|
+
end
|
291
|
+
|
292
|
+
def prepare_custom_term_dates
|
293
|
+
term_dates = @options[:term_dates].each_with_index.with_object({}) do |(term_date, index), obj|
|
294
|
+
obj[index + @starting_index - 1] = term_date
|
295
|
+
end
|
296
|
+
|
297
|
+
# if starting_index > 1 term_dates[0] is not set
|
298
|
+
term_dates[0] ||= starts_on
|
299
|
+
@_timetable_term_dates = term_dates
|
300
|
+
@realistic_durations = true
|
301
|
+
end
|
302
|
+
|
303
|
+
def compute_period_generated_interests(interests_rate)
|
304
|
+
(@crd_beginning_of_period + @due_interests_beginning_of_period).mult(interests_rate, BIG_DECIMAL_DIGITS)
|
196
305
|
end
|
197
306
|
end
|
198
307
|
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)
|
@@ -0,0 +1,75 @@
|
|
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 + 1
|
21
|
+
error_message = "the size of :term_dates (#{term_dates.size}) do not match the :duration_in_periods (#{duration_in_periods})."
|
22
|
+
advice = "You must pass the previous term date (or start_on if starting_index == 1) as the first term date"
|
23
|
+
raise ArgumentError, "#{error_message} #{advice}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.interests_start_date_present(interests_start_date)
|
28
|
+
if interests_start_date.present?
|
29
|
+
raise ArgumentError, ":interests_start_date is no compatible with :term_dates"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.coherent_dates_for_non_bullet(term_dates)
|
34
|
+
term_dates.each_with_index do |term_date, index|
|
35
|
+
next if index.zero?
|
36
|
+
|
37
|
+
previous_term_date = term_dates[index - 1]
|
38
|
+
|
39
|
+
unless term_date > previous_term_date
|
40
|
+
previous_term_date_description =
|
41
|
+
":term_dates[#{index - 1}] (#{term_dates[index - 1].strftime('%Y-%m-%d')})"
|
42
|
+
|
43
|
+
error_message = "#{previous_term_date_description} must be before :term_dates[#{index}] (#{term_date.strftime('%Y-%m-%d')})"
|
44
|
+
|
45
|
+
raise ArgumentError, error_message
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.coherent_dates_for_bullet(term_dates)
|
51
|
+
term_dates.each_with_index do |term_date, index|
|
52
|
+
next if index.zero?
|
53
|
+
|
54
|
+
days_in_year = 365
|
55
|
+
days_in_year += 1 if term_date.leap?
|
56
|
+
|
57
|
+
previous_term_date = term_dates[index - 1]
|
58
|
+
days_between = (term_date - previous_term_date).to_i.abs
|
59
|
+
|
60
|
+
if days_between > days_in_year
|
61
|
+
previous_term_date_description =
|
62
|
+
":term_dates[#{index - 1}] (#{term_dates[index - 1].strftime('%Y-%m-%d')})"
|
63
|
+
|
64
|
+
error_description = "There are #{days_between} days between #{previous_term_date_description} and :term_dates[#{index}]"
|
65
|
+
|
66
|
+
raise ArgumentError, "term dates can't be more than 1 year apart. #{error_description}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.bullet?(loan_class)
|
72
|
+
loan_class == "LoanCreator::Bullet"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
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,18 +7,24 @@ 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
|
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)
|
19
25
|
@crd_end_of_period = bigd('0')
|
20
26
|
@due_interests_beginning_of_period = @due_interests_end_of_period
|
21
|
-
@period_interests = @due_interests_end_of_period + compute_interests
|
27
|
+
@period_interests = @due_interests_end_of_period + compute_interests(@due_on, timetable)
|
22
28
|
@due_interests_end_of_period = 0
|
23
29
|
@period_capital = @crd_beginning_of_period
|
24
30
|
@total_paid_capital_end_of_period += @period_capital
|
@@ -26,14 +32,14 @@ module LoanCreator
|
|
26
32
|
@period_amount_to_pay = @period_capital + @period_interests
|
27
33
|
end
|
28
34
|
|
29
|
-
def compute_interests
|
30
|
-
|
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)
|
31
38
|
end
|
32
39
|
|
33
40
|
def compute_term(timetable)
|
34
41
|
@due_interests_beginning_of_period = @due_interests_end_of_period
|
35
|
-
@due_interests_end_of_period += compute_interests
|
36
|
-
timetable << current_term
|
42
|
+
@due_interests_end_of_period += compute_interests(@due_on, timetable)
|
37
43
|
end
|
38
44
|
end
|
39
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'
|
@@ -0,0 +1,51 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'csv'
|
3
|
+
require 'open-uri'
|
4
|
+
require 'pry'
|
5
|
+
require 'securerandom'
|
6
|
+
|
7
|
+
csv_url = ARGV[0]
|
8
|
+
|
9
|
+
spec_order = %i[
|
10
|
+
index
|
11
|
+
due_on
|
12
|
+
crd_beginning_of_period
|
13
|
+
crd_end_of_period
|
14
|
+
period_theoric_interests
|
15
|
+
delta_interests
|
16
|
+
accrued_delta_interests
|
17
|
+
amount_to_add
|
18
|
+
period_interests
|
19
|
+
period_capital
|
20
|
+
total_paid_capital_end_of_period
|
21
|
+
total_paid_interests_end_of_period
|
22
|
+
period_amount_to_pay
|
23
|
+
due_interests_beginning_of_period
|
24
|
+
due_interests_end_of_period
|
25
|
+
]
|
26
|
+
|
27
|
+
column_getter = {
|
28
|
+
index: proc { |r| r['t'].to_i },
|
29
|
+
due_on: proc { |r| r['date'] },
|
30
|
+
crd_beginning_of_period: proc { |r| r['crd_start'].gsub(',', '.').to_f },
|
31
|
+
crd_end_of_period: proc { |r| r['crd_end'].gsub(',', '.').to_f },
|
32
|
+
period_theoric_interests: proc { |r| r['period_theoric_interests'].gsub(',', '.').to_f },
|
33
|
+
delta_interests: proc { |r| r['delta_interests'].gsub(',', '.').to_f },
|
34
|
+
accrued_delta_interests: proc { |r| r['cumulated_delta_interests'].gsub(',', '.').to_f },
|
35
|
+
amount_to_add: proc { |r| r['amount_to_add'].gsub(',', '.').to_f },
|
36
|
+
period_interests: proc { |r| r['period_interests'].gsub(',', '.').to_f },
|
37
|
+
period_capital: proc { |r| r['period_capital'].gsub(',', '.').to_f },
|
38
|
+
total_paid_capital_end_of_period: proc { |r| r['total_capital_paid'].gsub(',', '.').to_f },
|
39
|
+
total_paid_interests_end_of_period: proc { |r| r['total_interests_paid'].gsub(',', '.').to_f },
|
40
|
+
period_amount_to_pay: proc { |r| r['period_total'].gsub(',', '.').to_f },
|
41
|
+
due_interests_beginning_of_period: proc { |r| r['due_interests_beginning_of_period'].gsub(',', '.').to_f },
|
42
|
+
due_interests_end_of_period: proc { |r| r['due_interests_end_of_period'].gsub(',', '.').to_f }
|
43
|
+
}
|
44
|
+
|
45
|
+
data = CSV.parse(URI.open(csv_url), headers: true).map(&:to_hash)
|
46
|
+
|
47
|
+
CSV.open("./spec/fixtures/new/#{SecureRandom.alphanumeric}.csv", 'w') do |csv|
|
48
|
+
data.each do |row|
|
49
|
+
csv << spec_order.map { |column| column_getter[column].call(row) }
|
50
|
+
end
|
51
|
+
end
|
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.10.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- thibaulth
|
@@ -12,7 +12,7 @@ authors:
|
|
12
12
|
autorequire:
|
13
13
|
bindir: exe
|
14
14
|
cert_chain: []
|
15
|
-
date: 2021-
|
15
|
+
date: 2021-05-26 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
|
@@ -149,10 +177,12 @@ 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
|
155
184
|
- loan_creator.gemspec
|
185
|
+
- scripts/convert_export_to_spec.rb
|
156
186
|
homepage: https://github.com/CapSens/loan-creator
|
157
187
|
licenses:
|
158
188
|
- MIT
|