loan_creator 0.9.1 → 0.12.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 +12 -0
- data/README.md +1 -1
- data/lib/loan_creator/bullet.rb +13 -5
- data/lib/loan_creator/common.rb +52 -59
- data/lib/loan_creator/linear.rb +15 -2
- data/lib/loan_creator/standard.rb +1 -1
- data/lib/loan_creator/term_dates_validator.rb +5 -3
- data/lib/loan_creator/time_helper.rb +62 -0
- data/lib/loan_creator/timetable.rb +4 -0
- data/lib/loan_creator/uncapitalized_bullet.rb +1 -1
- data/lib/loan_creator/version.rb +1 -1
- data/lib/loan_creator.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9a85f9d61c45df0f972e329a33a1beedbedc1e0090d481613e93ad90d66dea98
|
4
|
+
data.tar.gz: 7dab891b9a5d3acefb8d6cd43eef0bdb5cfe511631f5674ae655046cda11ca25
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f92cd167e57a06b93169011fe8c178e7e3f434dc4c99c4a9458fb29501dd643d6011da2b8af7adfd94dd7412e7e0cd9566f281c5ff3331815daa2aa39bc27d58
|
7
|
+
data.tar.gz: c21997ee861f81b7ad4a28b334f2f1d4258e2df54eb01865de79e0b30dae5abba1d4a6771055564a6e349d16ad07ea82e6aac57a3f35ef70fee07c9ba8f94f6a
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
v0.12.0
|
2
|
+
-------------------------
|
3
|
+
- improve argument error management and allow amount equal to zero.
|
4
|
+
|
5
|
+
v0.11.0
|
6
|
+
-------------------------
|
7
|
+
- add `multi_part_interests_calculation` option.
|
8
|
+
|
9
|
+
v0.10.0
|
10
|
+
-------------------------
|
11
|
+
- fix `term_dates` options, notably with `starts_on`.
|
12
|
+
|
1
13
|
v0.9.1
|
2
14
|
-------------------------
|
3
15
|
- add spec importer `scripts/convert_export_to_spec.rb {csv_url}`
|
data/README.md
CHANGED
@@ -187,7 +187,7 @@ Default: `false`.
|
|
187
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
188
|
|
189
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
|
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
191
|
|
192
192
|
## Calculation
|
193
193
|
|
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,22 @@ 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
|
+
if multi_part_interests_calculation && term_dates? && (timetable_term_dates[timetable.current_index] + 1.year) < @due_on
|
37
|
+
multi_part_interests(
|
38
|
+
timetable_term_dates[timetable.current_index],
|
39
|
+
@due_on,
|
40
|
+
annual_interests_rate,
|
41
|
+
amount_to_capitalize
|
42
|
+
)
|
43
|
+
else
|
44
|
+
compute_period_generated_interests(periodic_interests_rate(timetable_term_dates[timetable.current_index], @due_on))
|
45
|
+
end
|
38
46
|
end
|
39
47
|
|
40
48
|
def compute_term(timetable)
|
41
49
|
@due_interests_beginning_of_period = @due_interests_end_of_period
|
42
|
-
@due_interests_end_of_period += compute_capitalized_interests(
|
50
|
+
@due_interests_end_of_period += compute_capitalized_interests(timetable)
|
43
51
|
end
|
44
52
|
end
|
45
53
|
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
|
@@ -291,16 +279,21 @@ module LoanCreator
|
|
291
279
|
|
292
280
|
def prepare_custom_term_dates
|
293
281
|
term_dates = @options[:term_dates].each_with_index.with_object({}) do |(term_date, index), obj|
|
294
|
-
obj[index + 1] = term_date
|
282
|
+
obj[index + @starting_index - 1] = term_date
|
295
283
|
end
|
296
284
|
|
297
|
-
term_dates[0]
|
285
|
+
# if starting_index > 1 term_dates[0] is not set
|
286
|
+
term_dates[0] ||= starts_on
|
298
287
|
@_timetable_term_dates = term_dates
|
299
288
|
@realistic_durations = true
|
300
289
|
end
|
301
290
|
|
302
291
|
def compute_period_generated_interests(interests_rate)
|
303
|
-
|
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
|
304
297
|
end
|
305
298
|
end
|
306
299
|
end
|
data/lib/loan_creator/linear.rb
CHANGED
@@ -23,12 +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
|
-
|
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
|
+
)
|
40
|
+
else
|
41
|
+
period_theoric_interests(periodic_interests_rate(timetable_term_dates[timetable.current_index], @due_on))
|
42
|
+
end
|
43
|
+
)
|
44
|
+
|
32
45
|
@delta_interests = @period_theoric_interests - @period_theoric_interests.round(2)
|
33
46
|
@accrued_delta_interests += @delta_interests
|
34
47
|
@amount_to_add = bigd(
|
@@ -24,7 +24,7 @@ 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
30
|
@period_theoric_interests = period_theoric_interests(@index, computed_periodic_interests_rate)
|
@@ -17,8 +17,10 @@ module LoanCreator
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def self.matches_duration(term_dates, duration_in_periods)
|
20
|
-
unless term_dates.size == duration_in_periods
|
21
|
-
|
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}"
|
22
24
|
end
|
23
25
|
end
|
24
26
|
|
@@ -64,7 +66,7 @@ module LoanCreator
|
|
64
66
|
raise ArgumentError, "term dates can't be more than 1 year apart. #{error_description}"
|
65
67
|
end
|
66
68
|
end
|
67
|
-
end
|
69
|
+
end
|
68
70
|
|
69
71
|
def self.bullet?(loan_class)
|
70
72
|
loan_class == "LoanCreator::Bullet"
|
@@ -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,7 +33,7 @@ module LoanCreator
|
|
33
33
|
end
|
34
34
|
|
35
35
|
def compute_interests(due_date, timetable)
|
36
|
-
computed_periodic_interests_rate = periodic_interests_rate(
|
36
|
+
computed_periodic_interests_rate = periodic_interests_rate(timetable_term_dates[timetable.current_index], due_date)
|
37
37
|
amount.mult(bigd(computed_periodic_interests_rate), BIG_DECIMAL_DIGITS)
|
38
38
|
end
|
39
39
|
|
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'
|
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.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:
|
15
|
+
date: 2022-07-12 00:00:00.000000000 Z
|
16
16
|
dependencies:
|
17
17
|
- !ruby/object:Gem::Dependency
|
18
18
|
name: bundler
|
@@ -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
|