loan_creator 0.10.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []