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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 838d0d280c585125b013b8ea6d7e8b58e94c2234fffe3d49d1c97f39a67b254f
4
- data.tar.gz: 7a607035130b53af6678bdec2a91b9d99fcace0a1710db1ef83333a4b15133b0
3
+ metadata.gz: 2e7d8a7b39adf42e2c93e977c82ecf6c53f5695716cc5ef291a6b5a0963e0dd5
4
+ data.tar.gz: a4ed3fcf50360222a3795846d0d1ec3367126b851e6381a8ffbfc98ba972a709
5
5
  SHA512:
6
- metadata.gz: 06c8e0dfa135c8a80f9969f60db83a8bc3630161fea4f208dff6480ab9142635d196aaa729fdb32fd89e38e87804cbc76fcc1f10ec464c680c10219ef7aec23c
7
- data.tar.gz: c710e04d68debd3d7f63569586aed46ac1426aabf9ed995fa762a18745916d1f31339f93b173159ddd61dcf23b145527471dbc7ff873a1bdfc46169e310f4322
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}`
@@ -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(@due_on, timetable)
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(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)
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(@due_on, timetable)
53
+ @due_interests_end_of_period += compute_capitalized_interests(timetable)
43
54
  end
44
55
  end
45
56
  end
@@ -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(date = nil, relative_to_date: nil)
53
+ def periodic_interests_rate(start_date, end_date)
51
54
  if realistic_durations?
52
- compute_realistic_periodic_interests_rate_percentage_for(date, relative_to_date: relative_to_date).div(100, BIG_DECIMAL_DIGITS)
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(key, &block)
108
- raise unless block.call(instance_variable_get(:"@#{key}"))
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(:period) { |v| PERIODS_IN_MONTHS.keys.include?(v) } unless term_dates?
115
- validate(:amount) { |v| v.is_a?(BigDecimal) && v > 0 }
116
- validate(:annual_interests_rate) { |v| v.is_a?(BigDecimal) && v >= 0 }
117
- validate(:starts_on) { |v| v.is_a?(Date) }
118
- validate(:duration_in_periods) { |v| v.is_a?(Integer) && v > 0 }
119
- validate(:deferred_in_periods) { |v| v.is_a?(Integer) && v >= 0 && v < duration_in_periods }
120
- validate_term_dates if term_dates?
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(:total_paid_capital_end_of_period) { |v| v.is_a?(BigDecimal) && v >= 0 }
136
- validate(:total_paid_interests_end_of_period) { |v| v.is_a?(BigDecimal) && v >= 0 }
137
- validate(:accrued_delta_interests) { |v| v.is_a?(BigDecimal) }
138
- validate(:starting_index) { |v| v.is_a?(Integer) && v >= 0 }
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
- (@crd_beginning_of_period + @due_interests_beginning_of_period).mult(interests_rate, BIG_DECIMAL_DIGITS)
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
@@ -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
- @period_theoric_interests = period_theoric_interests(computed_periodic_interests_rate)
32
- @delta_interests = @period_theoric_interests - @period_theoric_interests.round(2)
33
- @accrued_delta_interests += @delta_interests
34
- @amount_to_add = bigd(
35
- if @accrued_delta_interests >= bigd('0.01')
36
- '0.01'
37
- elsif @accrued_delta_interests <= bigd('-0.01')
38
- '-0.01'
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
- '0'
41
+ period_theoric_interests(periodic_interests_rate(timetable_term_dates[timetable.current_index], @due_on))
41
42
  end
42
- )
43
- @accrued_delta_interests -= @amount_to_add
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(@due_on, relative_to_date: timetable_term_dates[timetable.next_index - 1])
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
- @period_theoric_interests = period_theoric_interests(@index, computed_periodic_interests_rate)
31
- @delta_interests = @period_theoric_interests - @period_theoric_interests.round(2)
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
@@ -40,5 +40,9 @@ module LoanCreator
40
40
  def next_index
41
41
  @current_index.nil? ? @starting_index : @current_index + 1
42
42
  end
43
+
44
+ def current_index
45
+ @current_index.nil? ? @starting_index - 1 : @current_index
46
+ end
43
47
  end
44
48
  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(due_date, relative_to_date: timetable_term_dates[timetable.next_index - 1])
37
- amount.mult(bigd(computed_periodic_interests_rate), BIG_DECIMAL_DIGITS)
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)
@@ -1,3 +1,3 @@
1
1
  module LoanCreator
2
- VERSION = '0.10.0'.freeze
2
+ VERSION = '0.12.1'.freeze
3
3
  end
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
@@ -27,6 +27,7 @@ Gem::Specification.new do |spec|
27
27
  spec.add_development_dependency 'pry', '~> 0.13.1'
28
28
  spec.add_development_dependency 'table_print', '~> 1.5'
29
29
 
30
+
30
31
  spec.add_runtime_dependency 'bigdecimal'
31
32
  spec.add_runtime_dependency 'activesupport'
32
33
  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.10.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: 2021-05-26 00:00:00.000000000 Z
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: []