loan_creator 0.7.0 → 0.9.0

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: 2cca09d7b818ee423c142ca692301a01963982ff2b003c65b8097f2972648fa5
4
- data.tar.gz: ba3169e453b3dcd7dbab5f5ce543fbc03c6744e87425adfdb12ad8bfdbf102b7
3
+ metadata.gz: fb6c8e7e02bc1349e47dcb07602828d051cfa63ac7fc3e16394604f8dda05e27
4
+ data.tar.gz: d103c1e0524641e73b307702eec5bd1cf43029236a3d791805e4d62e7e0732d3
5
5
  SHA512:
6
- metadata.gz: a40a97895eacab077be8a4a8ca414fda3a026bb6aac721ca35e03891d24ea6c58f44cd6a68e875860ff660a8b418692ad88e00d89b0e6c16842006f99330cae7
7
- data.tar.gz: 833292dfdfabe53174a6c65b9509df02060c72c09bfa67c08e8e73535ed1e413e335269d5a073ad5951a76cc310b5eb2f53e138053312d6be8622ad096e3309e
6
+ metadata.gz: c201e2a5fb2a5aaa54b1870b635e11c08b90d35f45258852f4cf61e5faa99756272b54db5a85c470123b5baaf5dedd5a9be79af8577796595997540cad4662b9
7
+ data.tar.gz: 00556c176b2e27765ca5112878495c7982cdaeb75abfe3b3956b57c6be57dfcf6c8cf89136bbfa0c94c40e7d43927ff2eafa8c0518f99d0be914e1bd7df1b70d
data/CHANGELOG.md CHANGED
@@ -1,3 +1,38 @@
1
+ v0.9.0
2
+ -------------------------
3
+ - add possibility to pass the argument `term_dates` for `LoanCreator::Bullet`, `LoanCreator::InFine` and `LoanCreator::Linear`
4
+ - `term_dates` option is based on `realistic_durations` and allow to compute terms custom date to custom date
5
+
6
+ v0.8.2
7
+ -------------------------
8
+
9
+ - manage initial value `due_interests` for Linear and InFine loan types
10
+
11
+ v0.8.1
12
+ -------------------------
13
+ - add `realistic_durations` option
14
+
15
+ v0.8.0
16
+ -------------------------
17
+
18
+ #### Feature
19
+ - due interests to date are now store into `due_interests_` columns for `LoanCreator::UncapitalizedBullet`
20
+
21
+ #### Bugfix
22
+ - fix `initial_values` not taking `paid_capital` and `paid_interests` into account for `LoanCreator::UncapitalizedBullet`
23
+ and `LoanCreator::Bullet`
24
+
25
+ #### Breaking changes
26
+ - rename columns `capitalized_interests_beginning_of_period` and `capitalized_interests_end_of_period` for
27
+ `due_interests_beginning_of_period` and `due_interests_end_of_period`
28
+ - rename `capitalized_interests` option in `:inital_values` into `due_interests`
29
+
30
+
31
+ v0.7.1
32
+ -------------------------
33
+
34
+ - fix `index` for `borrower_timetable` method that did not take into account starting index of `lender_timetables`
35
+
1
36
  v0.7.0
2
37
  -------------------------
3
38
 
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 (default to zero)
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:
@@ -57,7 +58,7 @@ Initial values must be a hash with specific keys, like so:
57
58
  paid_interests: 11000.0,
58
59
  accrued_delta_interests: 0,
59
60
  starting_index: 2,
60
- capitalized_interests: 0
61
+ due_interests: 0
61
62
  }
62
63
  ```
63
64
 
@@ -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`.
@@ -148,9 +152,11 @@ Capital share shall be repaid in full at loan's end.
148
152
  `Bullet` loan generates terms where terms' payments are zero. \
149
153
  Interests are capitalized, i.e. added to the borrowed capital on each term.\
150
154
  Capital share shall be repaid in full and all interests paid at loan's end.
155
+ N.b.: Due capitalized interests to date are stored into terms under `due_intesrests_` columns
151
156
 
152
157
  `UncapitalizedBullet` same as bullet, the only difference is the interests\
153
158
  are NOT capitalized.
159
+ N.b.: Due interests to date are stored into terms under `due_intesrests_` columns
154
160
 
155
161
  There is no deferred time for `InFine` and `Bullet` loans as it would be equivalent to increasing loan's duration.
156
162
 
@@ -174,6 +180,11 @@ additional term with only interests for the time difference.
174
180
  For example, with a `start_at` in january 2020 and a `interests_start_date` in october 2019, the timetable will include a
175
181
  first term corresponding to 3 months of interests.
176
182
 
183
+ `realistic_durations`: Optional. A boolean tha specifies whether or not to use the real number of days in each month when calculating the periodic interests rate. Note that leap years are taken into account. Default: `false`.
184
+ The default behaviour is to use the `period` in relation to the number of months in a year (ie: 12)
185
+
186
+ `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 compute from date to date. Must be an array with following dates.
187
+
177
188
  ## Calculation
178
189
 
179
190
  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, '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'
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
@@ -7,6 +7,8 @@ module LoanCreator
7
7
  :period_capital,
8
8
  :total_paid_capital_end_of_period,
9
9
  :total_paid_interests_end_of_period,
10
+ :due_interests_beginning_of_period,
11
+ :due_interests_end_of_period,
10
12
  :period_amount_to_pay
11
13
  ].freeze
12
14
 
@@ -18,8 +20,8 @@ module LoanCreator
18
20
  end
19
21
 
20
22
  borrower_timetable = LoanCreator::Timetable.new(
21
- starts_on: lenders_timetables.first.starts_on,
22
- period: lenders_timetables.first.period
23
+ starting_index: lenders_timetables.first.starting_index,
24
+ loan: lenders_timetables.first.loan
23
25
  )
24
26
 
25
27
  # Borrower timetable is not concerned with computation-related value (delta, etc.),
@@ -7,33 +7,39 @@ module LoanCreator
7
7
  reset_current_term
8
8
  @crd_beginning_of_period = amount
9
9
  @crd_end_of_period = amount
10
- (duration_in_periods - 1).times { |period| compute_term(timetable) }
11
- compute_last_term
12
- timetable << current_term
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 compute_last_term
19
- @crd_end_of_period = bigd('0')
20
- @capitalized_interests_beginning_of_period = @capitalized_interests_end_of_period
21
- @period_interests = @capitalized_interests_end_of_period + compute_capitalized_interests
22
- @capitalized_interests_end_of_period = 0
23
- @period_capital = @crd_beginning_of_period
24
- @total_paid_capital_end_of_period = @period_capital
25
- @total_paid_interests_end_of_period = @period_interests
26
- @period_amount_to_pay = @period_capital + @period_interests
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)
25
+ @crd_end_of_period = bigd('0')
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)
28
+ @due_interests_end_of_period = 0
29
+ @period_capital = @crd_beginning_of_period
30
+ @total_paid_capital_end_of_period += @period_capital
31
+ @total_paid_interests_end_of_period += @period_interests
32
+ @period_amount_to_pay = @period_capital + @period_interests
27
33
  end
28
34
 
29
- def compute_capitalized_interests
30
- (amount + @capitalized_interests_beginning_of_period).mult(periodic_interests_rate, BIG_DECIMAL_DIGITS)
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
- @capitalized_interests_beginning_of_period = @capitalized_interests_end_of_period
35
- @capitalized_interests_end_of_period += compute_capitalized_interests
36
- timetable << current_term
41
+ @due_interests_beginning_of_period = @due_interests_end_of_period
42
+ @due_interests_end_of_period += compute_capitalized_interests(@due_on, timetable)
37
43
  end
38
44
  end
39
45
  end
@@ -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 periodic_interests_rate_percentage
41
- @periodic_interests_rate_percentage ||=
42
- annual_interests_rate.div(12 / PERIODS_IN_MONTHS[period], BIG_DECIMAL_DIGITS)
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 periodic_interests_rate
46
- @periodic_interests_rate ||=
47
- periodic_interests_rate_percentage.div(100, BIG_DECIMAL_DIGITS)
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
- REQUIRED_ATTRIBUTES.each { |k| raise ArgumentError.new(k) unless @options.fetch(k, nil) }
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
- REQUIRED_ATTRIBUTES.each { |k| instance_variable_set(:"@#{k}", @options.fetch(k)) }
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,59 +139,57 @@ module LoanCreator
103
139
  end
104
140
 
105
141
  def set_initial_values
106
- @starting_index = initial_values[:starting_index] || 1
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
 
110
147
  (@total_paid_capital_end_of_period = bigd(initial_values[:paid_capital]))
111
148
  (@total_paid_interests_end_of_period = bigd(initial_values[:paid_interests]))
112
149
  (@accrued_delta_interests = bigd(initial_values[:accrued_delta_interests]))
113
- if self.is_a?(LoanCreator::Bullet)
114
- (@capitalized_interests_beginning_of_period = bigd(initial_values[:capitalized_interests] || 0))
115
- end
150
+ (@due_interests_beginning_of_period = bigd(initial_values[:due_interests] || 0))
116
151
  end
117
152
 
118
153
  def reset_current_term
119
- @accrued_delta_interests ||= bigd('0')
120
- @total_paid_capital_end_of_period ||= bigd('0')
154
+ @accrued_delta_interests ||= bigd('0')
155
+ @total_paid_capital_end_of_period ||= bigd('0')
121
156
  @total_paid_interests_end_of_period ||= bigd('0')
122
- @capitalized_interests_beginning_of_period ||= bigd('0')
123
- @crd_beginning_of_period = bigd('0')
124
- @crd_end_of_period = bigd('0')
125
- @period_theoric_interests = bigd('0')
126
- @capitalized_interests_end_of_period = @capitalized_interests_beginning_of_period
127
- @delta_interests = bigd('0')
128
- @amount_to_add = bigd('0')
129
- @period_interests = bigd('0')
130
- @period_capital = bigd('0')
131
- @period_amount_to_pay = bigd('0')
132
- @due_on = nil
157
+ @due_interests_beginning_of_period ||= bigd('0')
158
+ @crd_beginning_of_period = bigd('0')
159
+ @crd_end_of_period = bigd('0')
160
+ @period_theoric_interests = bigd('0')
161
+ @due_interests_end_of_period = @due_interests_beginning_of_period
162
+ @delta_interests = bigd('0')
163
+ @amount_to_add = bigd('0')
164
+ @period_interests = bigd('0')
165
+ @period_capital = bigd('0')
166
+ @period_amount_to_pay = bigd('0')
167
+ @due_on = nil
133
168
  end
134
169
 
135
170
  def current_term
136
171
  LoanCreator::Term.new(
137
- crd_beginning_of_period: @crd_beginning_of_period,
138
- crd_end_of_period: @crd_end_of_period,
139
- period_theoric_interests: @period_theoric_interests,
140
- delta_interests: @delta_interests,
141
- accrued_delta_interests: @accrued_delta_interests,
142
- capitalized_interests_beginning_of_period: @capitalized_interests_beginning_of_period,
143
- capitalized_interests_end_of_period: @capitalized_interests_end_of_period,
144
- amount_to_add: @amount_to_add,
145
- period_interests: @period_interests,
146
- period_capital: @period_capital,
147
- total_paid_capital_end_of_period: @total_paid_capital_end_of_period,
148
- total_paid_interests_end_of_period: @total_paid_interests_end_of_period,
149
- period_amount_to_pay: @period_amount_to_pay,
150
- due_on: @due_on,
151
- index: compute_index
172
+ crd_beginning_of_period: @crd_beginning_of_period,
173
+ crd_end_of_period: @crd_end_of_period,
174
+ period_theoric_interests: @period_theoric_interests,
175
+ delta_interests: @delta_interests,
176
+ accrued_delta_interests: @accrued_delta_interests,
177
+ due_interests_beginning_of_period: @due_interests_beginning_of_period,
178
+ due_interests_end_of_period: @due_interests_end_of_period,
179
+ amount_to_add: @amount_to_add,
180
+ period_interests: @period_interests,
181
+ period_capital: @period_capital,
182
+ total_paid_capital_end_of_period: @total_paid_capital_end_of_period,
183
+ total_paid_interests_end_of_period: @total_paid_interests_end_of_period,
184
+ period_amount_to_pay: @period_amount_to_pay,
185
+ due_on: @due_on,
186
+ index: compute_index
152
187
  )
153
188
  end
154
189
 
155
190
  def new_timetable
156
191
  LoanCreator::Timetable.new(
157
- starts_on: starts_on,
158
- period: period,
192
+ loan: self,
159
193
  interests_start_date: interests_start_date,
160
194
  starting_index: @starting_index
161
195
  )
@@ -165,6 +199,10 @@ module LoanCreator
165
199
  @index ? (@starting_index + @index - 1) : nil
166
200
  end
167
201
 
202
+ def last_period?(idx)
203
+ idx == (duration_in_periods - 1)
204
+ end
205
+
168
206
  def compute_term_zero
169
207
  @crd_beginning_of_period = @crd_end_of_period
170
208
  @period_theoric_interests = term_zero_interests
@@ -174,6 +212,7 @@ module LoanCreator
174
212
  @total_paid_interests_end_of_period += @period_interests
175
213
  @period_amount_to_pay = @period_interests
176
214
  @index = 0
215
+ @due_on = timetable_term_dates[0]
177
216
  end
178
217
 
179
218
  def term_zero_interests
@@ -194,7 +233,45 @@ module LoanCreator
194
233
  end
195
234
 
196
235
  def term_zero?
197
- 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 compute_realistic_periodic_interests_rate_percentage_for(date, relative_to_date:)
240
+ realistic_days = 365
241
+ realistic_days += 1 if date.leap?
242
+ realistic_days_in_period = (date - relative_to_date).to_i
243
+
244
+ annual_interests_rate.div(bigd(realistic_days) / bigd(realistic_days_in_period), BIG_DECIMAL_DIGITS)
245
+ end
246
+
247
+ def realistic_durations?
248
+ term_dates? || @realistic_durations.present?
249
+ end
250
+
251
+ def required_attributes
252
+ if term_dates?
253
+ REQUIRED_ATTRIBUTES_TERM_DATES
254
+ else
255
+ REQUIRED_ATTRIBUTES
256
+ end
257
+ end
258
+
259
+ def term_dates?
260
+ @options[:term_dates].present?
261
+ end
262
+
263
+ def prepare_custom_term_dates
264
+ term_dates = @options[:term_dates].each_with_index.with_object({}) do |(term_date, index), obj|
265
+ obj[index + 1] = term_date
266
+ end
267
+
268
+ term_dates[0] = starts_on
269
+ @_timetable_term_dates = term_dates
270
+ @realistic_durations = true
271
+ end
272
+
273
+ def compute_period_generated_interests(interests_rate)
274
+ (@crd_beginning_of_period + @due_interests_beginning_of_period).mult(interests_rate, BIG_DECIMAL_DIGITS)
198
275
  end
199
276
  end
200
277
  end
@@ -11,26 +11,24 @@ module LoanCreator
11
11
  timetable << current_term
12
12
  end
13
13
 
14
- duration_in_periods.times do |idx|
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 last_period?(idx)
27
- idx == (duration_in_periods - 1)
28
- end
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
- @period_theoric_interests = @crd_beginning_of_period * periodic_interests_rate
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
- @due_on = nil
53
- @index = idx + 1
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
- (amount / (duration_in_periods - deferred_in_periods)).round(2)
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 do |idx|
16
- @last_period = last_period?(idx)
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 last_period?(idx)
27
- idx == (duration_in_periods - 1)
28
- end
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(idx)
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(idx)
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
- @due_on = nil
52
- @index = idx + 1
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 * periodic_interests_rate
55
+ @crd_beginning_of_period * computed_periodic_interests_rate
58
56
  else
59
57
  -ipmt(
60
- periodic_interests_rate,
61
- (idx + 1) - deferred_in_periods,
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
- periodic_interests_rate,
76
- (idx + 1) - deferred_in_periods,
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)
@@ -17,11 +17,11 @@ module LoanCreator
17
17
  # Accrued interests' delta
18
18
  :accrued_delta_interests,
19
19
 
20
- # Capitalized interests at the beginning of the term (Bullet only)
21
- :capitalized_interests_beginning_of_period,
20
+ # Due interests at the beginning of the term (Bullet and UncapitalizedBullet only)
21
+ :due_interests_beginning_of_period,
22
22
 
23
- # Capitalized interests at the end of the term (Bullet only)
24
- :capitalized_interests_end_of_period,
23
+ # Due interests at the end of the term (Bullet and UncapitalizedBullet only)
24
+ :due_interests_end_of_period,
25
25
 
26
26
  # Adjustment of -0.01, 0 or +0.01 cent depending on accrued_delta_interests
27
27
  :amount_to_add,
@@ -0,0 +1,73 @@
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
21
+ raise ArgumentError, "the size of :term_dates (#{term_dates.size}) do not match the :duration_in_periods (#{duration_in_periods})"
22
+ end
23
+ end
24
+
25
+ def self.interests_start_date_present(interests_start_date)
26
+ if interests_start_date.present?
27
+ raise ArgumentError, ":interests_start_date is no compatible with :term_dates"
28
+ end
29
+ end
30
+
31
+ def self.coherent_dates_for_non_bullet(term_dates)
32
+ term_dates.each_with_index do |term_date, index|
33
+ next if index.zero?
34
+
35
+ previous_term_date = term_dates[index - 1]
36
+
37
+ unless term_date > previous_term_date
38
+ previous_term_date_description =
39
+ ":term_dates[#{index - 1}] (#{term_dates[index - 1].strftime('%Y-%m-%d')})"
40
+
41
+ error_message = "#{previous_term_date_description} must be before :term_dates[#{index}] (#{term_date.strftime('%Y-%m-%d')})"
42
+
43
+ raise ArgumentError, error_message
44
+ end
45
+ end
46
+ end
47
+
48
+ def self.coherent_dates_for_bullet(term_dates)
49
+ term_dates.each_with_index do |term_date, index|
50
+ next if index.zero?
51
+
52
+ days_in_year = 365
53
+ days_in_year += 1 if term_date.leap?
54
+
55
+ previous_term_date = term_dates[index - 1]
56
+ days_between = (term_date - previous_term_date).to_i.abs
57
+
58
+ if days_between > days_in_year
59
+ previous_term_date_description =
60
+ ":term_dates[#{index - 1}] (#{term_dates[index - 1].strftime('%Y-%m-%d')})"
61
+
62
+ error_description = "There are #{days_between} days between #{previous_term_date_description} and :term_dates[#{index}]"
63
+
64
+ raise ArgumentError, "term dates can't be more than 1 year apart. #{error_description}"
65
+ end
66
+ end
67
+ end
68
+
69
+ def self.bullet?(loan_class)
70
+ loan_class == "LoanCreator::Bullet"
71
+ end
72
+ end
73
+ end
@@ -1,22 +1,13 @@
1
1
  # coding: utf-8
2
2
  module LoanCreator
3
3
  class Timetable
4
- # Used to calculate next term's date (see ActiveSupport#advance)
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
- attr_reader :terms, :starts_on, :period #, :interests_start_date
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
- @starts_on = (starts_on.is_a?(Date) ? starts_on : Date.parse(starts_on))
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
- term.index ||= autoincrement_index
30
- term.due_on ||= date_for(term.index)
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
- private
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,28 +7,39 @@ module LoanCreator
7
7
  reset_current_term
8
8
  @crd_beginning_of_period = amount
9
9
  @crd_end_of_period = amount
10
- (duration_in_periods - 1).times { timetable << current_term }
11
- compute_last_term
12
- timetable << current_term
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 compute_last_term
19
- @crd_end_of_period = bigd('0')
20
- @period_interests = total_interests
21
- @period_capital = @crd_beginning_of_period
22
- @total_paid_capital_end_of_period = @period_capital
23
- @total_paid_interests_end_of_period = @period_interests
24
- @period_amount_to_pay = @period_capital + @period_interests
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)
25
+ @crd_end_of_period = bigd('0')
26
+ @due_interests_beginning_of_period = @due_interests_end_of_period
27
+ @period_interests = @due_interests_end_of_period + compute_interests(@due_on, timetable)
28
+ @due_interests_end_of_period = 0
29
+ @period_capital = @crd_beginning_of_period
30
+ @total_paid_capital_end_of_period += @period_capital
31
+ @total_paid_interests_end_of_period += @period_interests
32
+ @period_amount_to_pay = @period_capital + @period_interests
33
+ end
34
+
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)
25
38
  end
26
39
 
27
- def total_interests
28
- amount.mult(
29
- bigd(periodic_interests_rate) * bigd(duration_in_periods),
30
- BIG_DECIMAL_DIGITS
31
- )
40
+ def compute_term(timetable)
41
+ @due_interests_beginning_of_period = @due_interests_end_of_period
42
+ @due_interests_end_of_period += compute_interests(@due_on, timetable)
32
43
  end
33
44
  end
34
45
  end
@@ -1,3 +1,3 @@
1
1
  module LoanCreator
2
- VERSION = '0.7.0'.freeze
2
+ VERSION = '0.9.0'.freeze
3
3
  end
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'
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.7.0
4
+ version: 0.9.0
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-01-06 00:00:00.000000000 Z
15
+ date: 2021-05-07 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
@@ -112,7 +140,7 @@ dependencies:
112
140
  - - ">="
113
141
  - !ruby/object:Gem::Version
114
142
  version: '0'
115
- description:
143
+ description:
116
144
  email:
117
145
  - thibault@capsens.eu
118
146
  - nicolas.besnard@capsens.eu
@@ -149,6 +177,7 @@ 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
@@ -157,7 +186,7 @@ homepage: https://github.com/CapSens/loan-creator
157
186
  licenses:
158
187
  - MIT
159
188
  metadata: {}
160
- post_install_message:
189
+ post_install_message:
161
190
  rdoc_options: []
162
191
  require_paths:
163
192
  - lib
@@ -173,7 +202,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
173
202
  version: '0'
174
203
  requirements: []
175
204
  rubygems_version: 3.1.2
176
- signing_key:
205
+ signing_key:
177
206
  specification_version: 4
178
207
  summary: Create and update timetables from input data
179
208
  test_files: []