loan_creator 0.10.0 → 0.12.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/lib/loan_creator/bullet.rb +16 -5
- data/lib/loan_creator/common.rb +66 -57
- data/lib/loan_creator/linear.rb +13 -13
- data/lib/loan_creator/standard.rb +3 -15
- data/lib/loan_creator/time_helper.rb +62 -0
- data/lib/loan_creator/timetable.rb +4 -0
- data/lib/loan_creator/uncapitalized_bullet.rb +3 -2
- data/lib/loan_creator/version.rb +1 -1
- data/lib/loan_creator.rb +1 -0
- data/loan_creator.gemspec +1 -0
- metadata +7 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2e7d8a7b39adf42e2c93e977c82ecf6c53f5695716cc5ef291a6b5a0963e0dd5
|
4
|
+
data.tar.gz: a4ed3fcf50360222a3795846d0d1ec3367126b851e6381a8ffbfc98ba972a709
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e6e6465cc26a14667894139cc4f57e1bab128ebd77a01559d2b04d1ba77b3aecc9c5ec457fe5ba7a1c1cea4ac9f7021fa6f26d16646f6241167ade85b9251d98
|
7
|
+
data.tar.gz: 144466496ce6a6b8895fca5657cf07db090e0ca8834b04ac84631a0dbfe71e89d8dfce2d8697358f905486edae636b1014cc049597c267fa0ec829088966d406
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,20 @@
|
|
1
|
+
v0.12.1
|
2
|
+
-------------------------
|
3
|
+
#### Bugfix
|
4
|
+
- `Bullet` and `Uncapitalized Bullet` now manage roundings as other loan types
|
5
|
+
|
6
|
+
v0.12.0
|
7
|
+
-------------------------
|
8
|
+
- improve argument error management and allow amount equal to zero.
|
9
|
+
|
10
|
+
v0.11.0
|
11
|
+
-------------------------
|
12
|
+
- add `multi_part_interests_calculation` option.
|
13
|
+
|
14
|
+
v0.10.0
|
15
|
+
-------------------------
|
16
|
+
- fix `term_dates` options, notably with `starts_on`.
|
17
|
+
|
1
18
|
v0.9.1
|
2
19
|
-------------------------
|
3
20
|
- add spec importer `scripts/convert_export_to_spec.rb {csv_url}`
|
data/lib/loan_creator/bullet.rb
CHANGED
@@ -24,7 +24,7 @@ module LoanCreator
|
|
24
24
|
def compute_last_term(timetable)
|
25
25
|
@crd_end_of_period = bigd('0')
|
26
26
|
@due_interests_beginning_of_period = @due_interests_end_of_period
|
27
|
-
@period_interests = @due_interests_end_of_period + compute_capitalized_interests(
|
27
|
+
@period_interests = @due_interests_end_of_period + compute_capitalized_interests(timetable)
|
28
28
|
@due_interests_end_of_period = 0
|
29
29
|
@period_capital = @crd_beginning_of_period
|
30
30
|
@total_paid_capital_end_of_period += @period_capital
|
@@ -32,14 +32,25 @@ module LoanCreator
|
|
32
32
|
@period_amount_to_pay = @period_capital + @period_interests
|
33
33
|
end
|
34
34
|
|
35
|
-
def compute_capitalized_interests(
|
36
|
-
|
37
|
-
|
35
|
+
def compute_capitalized_interests(timetable)
|
36
|
+
periodic_theoric_interests =
|
37
|
+
if multi_part_interests_calculation && term_dates? && (timetable_term_dates[timetable.current_index] + 1.year) < @due_on
|
38
|
+
multi_part_interests(
|
39
|
+
timetable_term_dates[timetable.current_index],
|
40
|
+
@due_on,
|
41
|
+
annual_interests_rate,
|
42
|
+
amount_to_capitalize
|
43
|
+
)
|
44
|
+
else
|
45
|
+
compute_period_generated_interests(periodic_interests_rate(timetable_term_dates[timetable.current_index], @due_on))
|
46
|
+
end
|
47
|
+
|
48
|
+
apply_interests_roundings(periodic_theoric_interests)
|
38
49
|
end
|
39
50
|
|
40
51
|
def compute_term(timetable)
|
41
52
|
@due_interests_beginning_of_period = @due_interests_end_of_period
|
42
|
-
@due_interests_end_of_period += compute_capitalized_interests(
|
53
|
+
@due_interests_end_of_period += compute_capitalized_interests(timetable)
|
43
54
|
end
|
44
55
|
end
|
45
56
|
end
|
data/lib/loan_creator/common.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
module LoanCreator
|
2
2
|
class Common
|
3
3
|
extend BorrowerTimetable
|
4
|
+
include TimeHelper
|
4
5
|
|
5
6
|
PERIODS_IN_MONTHS = {
|
6
7
|
month: 1,
|
@@ -30,9 +31,11 @@ module LoanCreator
|
|
30
31
|
deferred_in_periods: 0,
|
31
32
|
interests_start_date: nil,
|
32
33
|
initial_values: {},
|
33
|
-
realistic_durations: false
|
34
|
+
realistic_durations: false,
|
35
|
+
multi_part_interests_calculation: true
|
34
36
|
}.freeze
|
35
37
|
|
38
|
+
|
36
39
|
attr_reader *REQUIRED_ATTRIBUTES
|
37
40
|
attr_reader *OPTIONAL_ATTRIBUTES.keys
|
38
41
|
|
@@ -47,9 +50,9 @@ module LoanCreator
|
|
47
50
|
prepare_custom_term_dates if term_dates?
|
48
51
|
end
|
49
52
|
|
50
|
-
def periodic_interests_rate(
|
53
|
+
def periodic_interests_rate(start_date, end_date)
|
51
54
|
if realistic_durations?
|
52
|
-
|
55
|
+
compute_realistic_periodic_interests_rate(start_date, end_date, annual_interests_rate)
|
53
56
|
else
|
54
57
|
@periodic_interests_rate ||=
|
55
58
|
annual_interests_rate.div(12 / PERIODS_IN_MONTHS[period], BIG_DECIMAL_DIGITS).div(100, BIG_DECIMAL_DIGITS)
|
@@ -101,23 +104,37 @@ module LoanCreator
|
|
101
104
|
|
102
105
|
def set_attributes
|
103
106
|
required_attributes.each { |k| instance_variable_set(:"@#{k}", @options.fetch(k)) }
|
104
|
-
OPTIONAL_ATTRIBUTES.each { |k,v| instance_variable_set(:"@#{k}", @options.fetch(k, v)) }
|
107
|
+
OPTIONAL_ATTRIBUTES.each { |k, v| instance_variable_set(:"@#{k}", @options.fetch(k, v)) }
|
105
108
|
end
|
106
109
|
|
107
|
-
def validate(
|
108
|
-
raise unless block.call
|
109
|
-
rescue => e
|
110
|
-
raise ArgumentError.new([key, e.message].join(': '))
|
110
|
+
def validate(message, &block)
|
111
|
+
raise ArgumentError.new(message) unless block.call
|
111
112
|
end
|
112
113
|
|
113
114
|
def validate_attributes
|
114
|
-
validate(
|
115
|
-
|
116
|
-
|
117
|
-
validate(
|
118
|
-
|
119
|
-
|
120
|
-
|
115
|
+
validate("amount #{@amount} must be positive") do
|
116
|
+
@amount.is_a?(BigDecimal) && @amount >= 0
|
117
|
+
end
|
118
|
+
validate("annual_interests_rate #{@annual_interests_rate} must be positive") do
|
119
|
+
@annual_interests_rate.is_a?(BigDecimal) && @annual_interests_rate >= 0
|
120
|
+
end
|
121
|
+
validate("starts_on #{@starts_on} must be a date but is a #{@starts_on.class}") do
|
122
|
+
@starts_on.is_a?(Date)
|
123
|
+
end
|
124
|
+
validate("duration_in_periods #{@duration_in_periods} must be positive") do
|
125
|
+
@duration_in_periods.is_a?(Integer) && @duration_in_periods > 0
|
126
|
+
end
|
127
|
+
validate("deferred_in_periods #{@deferred_in_periods} must be positive and less than duration_in_periods #{@duration_in_periods}") do
|
128
|
+
@deferred_in_periods.is_a?(Integer) && @deferred_in_periods >= 0 && @deferred_in_periods < duration_in_periods
|
129
|
+
end
|
130
|
+
if term_dates?
|
131
|
+
validate_term_dates
|
132
|
+
else
|
133
|
+
validate("period #{@period} must be in #{PERIODS_IN_MONTHS}") do
|
134
|
+
PERIODS_IN_MONTHS.keys.include?(@period)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
121
138
|
end
|
122
139
|
|
123
140
|
def validate_term_dates
|
@@ -132,10 +149,18 @@ module LoanCreator
|
|
132
149
|
def validate_initial_values
|
133
150
|
return if initial_values.blank?
|
134
151
|
|
135
|
-
validate(
|
136
|
-
|
137
|
-
|
138
|
-
validate(
|
152
|
+
validate("total_paid_capital_end_of_period #{@total_paid_capital_end_of_period} must be positive") do
|
153
|
+
@total_paid_capital_end_of_period.is_a?(BigDecimal) && @total_paid_capital_end_of_period >= 0
|
154
|
+
end
|
155
|
+
validate("total_paid_interests_end_of_period #{@total_paid_interests_end_of_period} must be positive") do
|
156
|
+
@total_paid_interests_end_of_period.is_a?(BigDecimal) && @total_paid_interests_end_of_period >= 0
|
157
|
+
end
|
158
|
+
validate("accrued_delta_interests must be a BigDecimal but is a #{@accrued_delta_interests.class}") do
|
159
|
+
@accrued_delta_interests.is_a?(BigDecimal)
|
160
|
+
end
|
161
|
+
validate("starting_index #{@starting_index} must be positive") do
|
162
|
+
@starting_index.is_a?(Integer) && @starting_index >= 0
|
163
|
+
end
|
139
164
|
end
|
140
165
|
|
141
166
|
def set_initial_values
|
@@ -236,43 +261,6 @@ module LoanCreator
|
|
236
261
|
(interests_start_date && interests_start_date < term_zero_date) && !term_dates?
|
237
262
|
end
|
238
263
|
|
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
264
|
def realistic_durations?
|
277
265
|
term_dates? || @realistic_durations.present?
|
278
266
|
end
|
@@ -301,7 +289,28 @@ module LoanCreator
|
|
301
289
|
end
|
302
290
|
|
303
291
|
def compute_period_generated_interests(interests_rate)
|
304
|
-
|
292
|
+
amount_to_capitalize.mult(interests_rate, BIG_DECIMAL_DIGITS)
|
293
|
+
end
|
294
|
+
|
295
|
+
def amount_to_capitalize
|
296
|
+
@crd_beginning_of_period + @due_interests_beginning_of_period
|
297
|
+
end
|
298
|
+
|
299
|
+
def apply_interests_roundings(periodic_theoric_interests)
|
300
|
+
@period_theoric_interests = periodic_theoric_interests
|
301
|
+
@delta_interests = @period_theoric_interests - @period_theoric_interests.round(2)
|
302
|
+
@accrued_delta_interests += @delta_interests
|
303
|
+
@amount_to_add = bigd(
|
304
|
+
if @accrued_delta_interests >= bigd('0.01')
|
305
|
+
'0.01'
|
306
|
+
elsif @accrued_delta_interests <= bigd('-0.01')
|
307
|
+
'-0.01'
|
308
|
+
else
|
309
|
+
'0'
|
310
|
+
end
|
311
|
+
)
|
312
|
+
@accrued_delta_interests -= @amount_to_add
|
313
|
+
@period_theoric_interests.round(2) + @amount_to_add
|
305
314
|
end
|
306
315
|
end
|
307
316
|
end
|
data/lib/loan_creator/linear.rb
CHANGED
@@ -23,25 +23,25 @@ module LoanCreator
|
|
23
23
|
@last_period = last_period?(idx)
|
24
24
|
@deferred_period = @index <= deferred_in_periods
|
25
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])
|
27
26
|
|
28
27
|
# Reminder: CRD beginning of period = CRD end of period **of previous period**
|
29
28
|
@crd_beginning_of_period = @crd_end_of_period
|
30
29
|
@due_interests_beginning_of_period = @due_interests_end_of_period
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
30
|
+
|
31
|
+
period_theoric_interests =
|
32
|
+
# if period is more than a year
|
33
|
+
if multi_part_interests_calculation && term_dates? && (timetable_term_dates[timetable.current_index] + 1.year) < @due_on
|
34
|
+
multi_part_interests(
|
35
|
+
timetable_term_dates[timetable.current_index],
|
36
|
+
@due_on,
|
37
|
+
annual_interests_rate,
|
38
|
+
amount_to_capitalize
|
39
|
+
)
|
39
40
|
else
|
40
|
-
|
41
|
+
period_theoric_interests(periodic_interests_rate(timetable_term_dates[timetable.current_index], @due_on))
|
41
42
|
end
|
42
|
-
|
43
|
-
@
|
44
|
-
@period_interests = @period_theoric_interests.round(2) + @amount_to_add
|
43
|
+
|
44
|
+
@period_interests = apply_interests_roundings(period_theoric_interests)
|
45
45
|
@period_capital = period_capital
|
46
46
|
@total_paid_capital_end_of_period += @period_capital
|
47
47
|
@total_paid_interests_end_of_period += @period_interests
|
@@ -24,23 +24,11 @@ module LoanCreator
|
|
24
24
|
@last_period = last_period?(idx)
|
25
25
|
@deferred_period = @index <= deferred_in_periods
|
26
26
|
@due_on = timetable_term_dates[timetable.next_index]
|
27
|
-
computed_periodic_interests_rate = periodic_interests_rate(
|
27
|
+
computed_periodic_interests_rate = periodic_interests_rate(timetable_term_dates[timetable.current_index], @due_on)
|
28
28
|
|
29
29
|
@crd_beginning_of_period = @crd_end_of_period
|
30
|
-
|
31
|
-
@
|
32
|
-
@accrued_delta_interests += @delta_interests
|
33
|
-
@amount_to_add = bigd(
|
34
|
-
if @accrued_delta_interests >= bigd('0.01')
|
35
|
-
'0.01'
|
36
|
-
elsif @accrued_delta_interests <= bigd('-0.01')
|
37
|
-
'-0.01'
|
38
|
-
else
|
39
|
-
'0'
|
40
|
-
end
|
41
|
-
)
|
42
|
-
@accrued_delta_interests -= @amount_to_add
|
43
|
-
@period_interests = @period_theoric_interests.round(2) + @amount_to_add
|
30
|
+
|
31
|
+
@period_interests = apply_interests_roundings(period_theoric_interests(@index, computed_periodic_interests_rate))
|
44
32
|
@period_capital = period_capital(@index, computed_periodic_interests_rate)
|
45
33
|
@total_paid_capital_end_of_period += @period_capital
|
46
34
|
@total_paid_interests_end_of_period += @period_interests
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module LoanCreator
|
2
|
+
module TimeHelper
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
def leap_days_count(start_date, end_date)
|
7
|
+
start_year = start_date.year
|
8
|
+
# mostly no op but allows to skip one iteration if end date is january 1st
|
9
|
+
end_year = (end_date - 1.day).year
|
10
|
+
|
11
|
+
(start_year..end_year).sum do |year|
|
12
|
+
next 0 unless Date.gregorian_leap?(year)
|
13
|
+
|
14
|
+
current_start_date =
|
15
|
+
if start_year == year
|
16
|
+
start_date
|
17
|
+
else
|
18
|
+
Date.new(year, 1, 1)
|
19
|
+
end
|
20
|
+
|
21
|
+
current_end_date =
|
22
|
+
if end_year == year
|
23
|
+
end_date
|
24
|
+
else
|
25
|
+
Date.new(year + 1, 1, 1)
|
26
|
+
end
|
27
|
+
|
28
|
+
current_end_date - current_start_date
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def compute_realistic_periodic_interests_rate(start_date, end_date, annual_interests_rate)
|
33
|
+
total_days = end_date - start_date
|
34
|
+
leap_days = bigd(leap_days_count(start_date, end_date))
|
35
|
+
non_leap_days = bigd(total_days - leap_days)
|
36
|
+
|
37
|
+
annual_interests_rate.mult(
|
38
|
+
leap_days.div(366, BIG_DECIMAL_DIGITS) +
|
39
|
+
non_leap_days.div(365, BIG_DECIMAL_DIGITS),
|
40
|
+
BIG_DECIMAL_DIGITS
|
41
|
+
).div(100, BIG_DECIMAL_DIGITS)
|
42
|
+
end
|
43
|
+
|
44
|
+
# for terms spanning more than a year,
|
45
|
+
# we capitalize each years until the last one which behaves normally
|
46
|
+
def multi_part_interests(start_date, end_date, annual_interests_rate, amount_to_capitalize)
|
47
|
+
duration_in_days = end_date - start_date
|
48
|
+
leap_days = bigd(leap_days_count(start_date, end_date))
|
49
|
+
non_leap_days = bigd(duration_in_days - leap_days)
|
50
|
+
|
51
|
+
ratio = non_leap_days.div(365, BIG_DECIMAL_DIGITS) + leap_days.div(366, BIG_DECIMAL_DIGITS)
|
52
|
+
full_years, year_part = ratio.divmod(1)
|
53
|
+
rate = annual_interests_rate.div(100, BIG_DECIMAL_DIGITS)
|
54
|
+
|
55
|
+
total = amount_to_capitalize.mult((1 + rate)**full_years, BIG_DECIMAL_DIGITS)
|
56
|
+
.mult(1 + rate * year_part, BIG_DECIMAL_DIGITS)
|
57
|
+
|
58
|
+
total - amount_to_capitalize
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -33,8 +33,9 @@ module LoanCreator
|
|
33
33
|
end
|
34
34
|
|
35
35
|
def compute_interests(due_date, timetable)
|
36
|
-
computed_periodic_interests_rate = periodic_interests_rate(
|
37
|
-
|
36
|
+
computed_periodic_interests_rate = periodic_interests_rate(timetable_term_dates[timetable.current_index], due_date)
|
37
|
+
|
38
|
+
apply_interests_roundings(amount.mult(bigd(computed_periodic_interests_rate), BIG_DECIMAL_DIGITS))
|
38
39
|
end
|
39
40
|
|
40
41
|
def compute_term(timetable)
|
data/lib/loan_creator/version.rb
CHANGED
data/lib/loan_creator.rb
CHANGED
@@ -8,6 +8,7 @@ module LoanCreator
|
|
8
8
|
BIG_DECIMAL_DIGITS = 14
|
9
9
|
|
10
10
|
autoload :ExcelFormulas, 'loan_creator/excel_formulas'
|
11
|
+
autoload :TimeHelper, 'loan_creator/time_helper'
|
11
12
|
autoload :BorrowerTimetable, 'loan_creator/borrower_timetable'
|
12
13
|
autoload :Common, 'loan_creator/common'
|
13
14
|
autoload :Standard, 'loan_creator/standard'
|
data/loan_creator.gemspec
CHANGED
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.12.1
|
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:
|
15
|
+
date: 2022-08-04 00:00:00.000000000 Z
|
16
16
|
dependencies:
|
17
17
|
- !ruby/object:Gem::Dependency
|
18
18
|
name: bundler
|
@@ -140,7 +140,7 @@ dependencies:
|
|
140
140
|
- - ">="
|
141
141
|
- !ruby/object:Gem::Version
|
142
142
|
version: '0'
|
143
|
-
description:
|
143
|
+
description:
|
144
144
|
email:
|
145
145
|
- thibault@capsens.eu
|
146
146
|
- nicolas.besnard@capsens.eu
|
@@ -178,6 +178,7 @@ files:
|
|
178
178
|
- lib/loan_creator/standard.rb
|
179
179
|
- lib/loan_creator/term.rb
|
180
180
|
- lib/loan_creator/term_dates_validator.rb
|
181
|
+
- lib/loan_creator/time_helper.rb
|
181
182
|
- lib/loan_creator/timetable.rb
|
182
183
|
- lib/loan_creator/uncapitalized_bullet.rb
|
183
184
|
- lib/loan_creator/version.rb
|
@@ -187,7 +188,7 @@ homepage: https://github.com/CapSens/loan-creator
|
|
187
188
|
licenses:
|
188
189
|
- MIT
|
189
190
|
metadata: {}
|
190
|
-
post_install_message:
|
191
|
+
post_install_message:
|
191
192
|
rdoc_options: []
|
192
193
|
require_paths:
|
193
194
|
- lib
|
@@ -203,7 +204,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
203
204
|
version: '0'
|
204
205
|
requirements: []
|
205
206
|
rubygems_version: 3.1.2
|
206
|
-
signing_key:
|
207
|
+
signing_key:
|
207
208
|
specification_version: 4
|
208
209
|
summary: Create and update timetables from input data
|
209
210
|
test_files: []
|