blend_spreadsheet_loan_generator 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,211 @@
1
+ module BlendSpreadsheetLoanGenerator
2
+ class Formula
3
+ include SpreadsheetConcern
4
+
5
+ attr_accessor :loan
6
+
7
+ def initialize(loan:)
8
+ @loan = loan
9
+ @interests_formula = loan.interests_formula
10
+ @loan_type_formula = loan.loan_type_formula
11
+ end
12
+
13
+ def index_formula(line:)
14
+ line - 1
15
+ end
16
+
17
+ def due_on_formula(line:)
18
+ term = line - 1
19
+ loan.due_on + ((term - 1) * loan.period_duration).months
20
+ end
21
+
22
+ def period_capital_formula(line:)
23
+ if line == loan.duration + 1
24
+ "=ARRONDI(#{excel_float(loan.amount)} - #{total_paid_capital_end_of_period(line - 1)}; 2)"
25
+ else
26
+ "=ARRONDI(#{period_calculated_capital(line)} - #{period_reimbursed_capitalized_interests(line)} - #{period_reimbursed_capitalized_fees(line)}; 2)"
27
+ end
28
+ end
29
+
30
+ def period_interests_formula(line:)
31
+ term = line - 1
32
+ if term <= loan.deferred_and_capitalized
33
+ excel_float(0.0)
34
+ else
35
+ "=ARRONDI(#{period_calculated_interests(line)}; 2)"
36
+ end
37
+ end
38
+
39
+ def period_total_formula(line:)
40
+ total = [
41
+ period_capital(line),
42
+ '+',
43
+ period_interests(line),
44
+ '+',
45
+ period_reimbursed_capitalized_interests(line)
46
+ ].join(' ')
47
+ "=ARRONDI(#{total}; 2)"
48
+ end
49
+
50
+ def period_calculated_capital_formula(line:)
51
+ term = line - 1
52
+ if term <= loan.total_deferred_duration
53
+ excel_float(0.0)
54
+ else
55
+ @loan_type_formula.period_calculated_capital_formula(line: line)
56
+ end
57
+ end
58
+
59
+ def period_theoric_interests_formula(line:)
60
+ term = line - 1
61
+ if term <= loan.deferred_and_capitalized
62
+ excel_float(0.0)
63
+ else
64
+ "=#{period_calculated_interests(line)}"
65
+ end
66
+ end
67
+
68
+ def period_calculated_interests_formula(line:)
69
+ amount_to_capitalize = [
70
+ '(',
71
+ remaining_capital_start(line),
72
+ '+',
73
+ capitalized_interests_start(line),
74
+ ')'
75
+ ].join(' ')
76
+
77
+ "=#{amount_to_capitalize} * #{period_rate(line)}"
78
+ end
79
+
80
+ def remaining_capital_start_formula(line:)
81
+ return excel_float(loan.amount) if line == 2
82
+
83
+ "=#{excel_float(loan.amount)} - #{total_paid_capital_end_of_period(line - 1)}"
84
+ end
85
+
86
+ def remaining_capital_end_formula(line:)
87
+ "=#{excel_float(loan.amount)} - #{total_paid_capital_end_of_period(line)}"
88
+ end
89
+
90
+ def total_paid_capital_end_of_period_formula(line:)
91
+ return "=#{period_capital(line)}" if line == 2
92
+
93
+ "=ARRONDI(SOMME(#{column_range(column: period_capital, upto: line)}); 2)"
94
+ end
95
+
96
+ def total_paid_interests_end_of_period_formula(line:)
97
+ return "=#{period_interests(line)}" if line == 2
98
+
99
+ "=ARRONDI(SOMME(#{column_range(column: period_interests, upto: line)}); 2)"
100
+ end
101
+
102
+ def capitalized_interests_start_formula(line:)
103
+ return excel_float(loan.starting_capitalized_interests) if line == 2
104
+
105
+ "=ARRONDI(#{capitalized_interests_end(line - 1)}; 2)"
106
+ end
107
+
108
+ def capitalized_interests_end_formula(line:)
109
+ term = line - 1
110
+ if term <= loan.deferred_and_capitalized
111
+ "=ARRONDI(#{capitalized_interests_start(line)} + #{period_calculated_interests(line)}; 2)"
112
+ else
113
+ "=ARRONDI(#{capitalized_interests_start(line)} - #{period_reimbursed_capitalized_interests(line)}; 2)"
114
+ end
115
+ end
116
+
117
+ def period_reimbursed_capitalized_interests_formula(line:)
118
+ term = line - 1
119
+ if term <= loan.total_deferred_duration
120
+ excel_float(0.0)
121
+ else
122
+ "=ARRONDI(MIN(#{period_calculated_capital(line)} - #{period_reimbursed_capitalized_fees(line)}; #{capitalized_interests_start(line)}); 2)"
123
+ end
124
+ end
125
+
126
+ def period_rate_formula(line:)
127
+ @interests_formula.period_rate_formula(line: line)
128
+ end
129
+
130
+ def delta_formula(line:)
131
+ "=#{period_theoric_interests(line)} - #{period_interests(line)}"
132
+ end
133
+
134
+ def accrued_delta_formula(line:)
135
+ return excel_float(0.0) if line == 2
136
+
137
+ "=#{accrued_delta(line - 1)} + #{delta(line)} - #{amount_to_add(line - 1)}"
138
+ end
139
+
140
+ def amount_to_add_formula(line:)
141
+ "=TRONQUE(#{accrued_delta(line)}; 2)"
142
+ end
143
+
144
+ def period_leap_days_formula(line:)
145
+ term = line - 1
146
+ from = loan.due_on + ((term - 2) * loan.period_duration).months
147
+ to = loan.due_on + ((term - 1) * loan.period_duration).months
148
+
149
+ (from...to).sum { |d| d.leap? ? 1 : 0 }
150
+ end
151
+
152
+ def period_non_leap_days_formula(line:)
153
+ term = line - 1
154
+ from = loan.due_on + ((term - 2) * loan.period_duration).months
155
+ to = loan.due_on + ((term - 1) * loan.period_duration).months
156
+
157
+ (from...to).sum { |d| d.leap? ? 0 : 1 }
158
+ end
159
+
160
+ def period_fees_formula(line:)
161
+ term = line - 1
162
+ if term <= loan.deferred_and_capitalized
163
+ excel_float(0.0)
164
+ else
165
+ "=ARRONDI(#{period_calculated_fees(line)}; 2)"
166
+ end
167
+ end
168
+
169
+ def period_calculated_fees_formula(line:)
170
+ amount_to_capitalize = [
171
+ '(',
172
+ remaining_capital_start(line),
173
+ '+',
174
+ capitalized_interests_start(line),
175
+ '+',
176
+ capitalized_fees_start(line),
177
+ ')'
178
+ ].join(' ')
179
+
180
+ "=#{amount_to_capitalize} * #{period_fees_rate(line)}"
181
+ end
182
+
183
+ def capitalized_fees_start_formula(line:)
184
+ return excel_float(loan.starting_capitalized_fees) if line == 2
185
+
186
+ "=ARRONDI(#{capitalized_fees_end(line - 1)}; 2)"
187
+ end
188
+
189
+ def capitalized_fees_end_formula(line:)
190
+ term = line - 1
191
+ if term <= loan.deferred_and_capitalized
192
+ "=ARRONDI(#{capitalized_fees_start(line)} + #{period_calculated_fees(line)}; 2)"
193
+ else
194
+ "=ARRONDI(#{capitalized_fees_start(line)} - #{period_reimbursed_capitalized_fees(line)}; 2)"
195
+ end
196
+ end
197
+
198
+ def period_reimbursed_capitalized_fees_formula(line:)
199
+ term = line - 1
200
+ if term <= loan.total_deferred_duration
201
+ excel_float(0.0)
202
+ else
203
+ "=ARRONDI(MIN(#{period_calculated_capital(line)}; #{capitalized_fees_start(line)}); 2)"
204
+ end
205
+ end
206
+
207
+ def period_fees_rate_formula(line:)
208
+ @interests_formula.period_fees_rate_formula(line: line)
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,73 @@
1
+ module BlendSpreadsheetLoanGenerator
2
+ class Generate < Dry::CLI::Command
3
+ include SpreadsheetConcern
4
+ include CsvConcern
5
+
6
+ attr_accessor :loan
7
+
8
+ desc "Generate spreadsheet"
9
+
10
+ argument :amount, type: :float, required: true, desc: 'amount borrowed'
11
+ argument :duration, type: :integer, required: true, desc: 'number of reimbursements'
12
+ argument :rate, type: :float, required: true, desc: 'year rate'
13
+
14
+ option :period_duration, type: :integer, default: 1, desc: 'duration of a period in months'
15
+ option :due_on, type: :date, default: Date.today, desc: 'date of the pay day of the first period DD/MM/YYYY'
16
+ option :deferred_and_capitalized, type: :integer, default: 0, desc: 'periods with no capital or interests paid'
17
+ option :deferred, type: :integer, default: 0, desc: 'periods with only interests paid'
18
+ option :type, type: :string, default: 'standard', values: %w[standard linear], desc: 'type of amortization'
19
+ option :interests_type, type: :string, default: 'simple', values: %w[simple realistic normal], desc: 'type of interests calculations'
20
+ option :fees_rate, type: :float, default: 0.0, required: true, desc: 'year fees rate'
21
+ option :starting_capitalized_interests, type: :float, default: 0.0, desc: 'starting capitalized interests (if ongoing loan)'
22
+ option :starting_capitalized_fees, type: :float, default: 0.0, desc: 'starting capitalized fees (if ongoing loan)'
23
+ option :target_path, type: :string, default: './', desc: 'where to put the generated csv'
24
+
25
+ def call(amount:, duration:, rate:, **options)
26
+ begin
27
+ session = GoogleDrive::Session.from_config(
28
+ File.join(ENV['SPREADSHEET_LOAN_GENERATOR_DIR'], 'config.json')
29
+ )
30
+ rescue StandardError => e
31
+ if ENV['SPREADSHEET_LOAN_GENERATOR_DIR'].blank?
32
+ puts 'please set SPREADSHEET_LOAN_GENERATOR_DIR'
33
+ else
34
+ puts 'Cannot connect to google drive. Did you run slg init CLIENT_ID CLIENT_SECRET ?'
35
+ end
36
+ return
37
+ end
38
+
39
+ @loan = Loan.new(
40
+ amount: amount,
41
+ duration: duration,
42
+ period_duration: options.fetch(:period_duration),
43
+ rate: rate,
44
+ due_on: options.fetch(:due_on),
45
+ deferred_and_capitalized: options.fetch(:deferred_and_capitalized),
46
+ deferred: options.fetch(:deferred),
47
+ type: options.fetch(:type),
48
+ interests_type: options.fetch(:interests_type),
49
+ starting_capitalized_interests: options.fetch(:starting_capitalized_interests),
50
+ starting_capitalized_fees: options.fetch(:starting_capitalized_fees),
51
+ fees_rate: options.fetch(:fees_rate)
52
+ )
53
+
54
+ spreadsheet = session.create_spreadsheet(loan.name)
55
+ worksheet = spreadsheet.add_worksheet(loan.type, loan.duration + 2, columns.count + 1, index: 0)
56
+
57
+ @formula = Formula.new(loan: loan)
58
+
59
+ apply_formulas(worksheet: worksheet)
60
+ apply_formats(worksheet: worksheet)
61
+
62
+ worksheet.save
63
+ worksheet.reload
64
+
65
+ generate_csv(worksheet: worksheet, target_path: options.fetch(:target_path))
66
+
67
+ puts worksheet.human_url
68
+ rescue => e
69
+ require 'pry'
70
+ binding.pry
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,36 @@
1
+ module BlendSpreadsheetLoanGenerator
2
+ class Init < Dry::CLI::Command
3
+
4
+ argument :client_id, type: :string, required: true, desc: 'GCP client_id'
5
+ argument :client_secret, type: :string, required: true, desc: 'GCP client_secret'
6
+
7
+ def call(client_id:, client_secret:)
8
+ if !ENV.key?('SPREADSHEET_LOAN_GENERATOR_DIR')
9
+ puts "Please set up the environment variable SPREADSHEET_LOAN_GENERATOR_DIR to a dir that will store this gem's configuration"
10
+ return
11
+ end
12
+
13
+ create_config_dir
14
+ create_config_file(client_id: client_id, client_secret: client_secret)
15
+ end
16
+
17
+ def config_path
18
+ File.join(ENV['SPREADSHEET_LOAN_GENERATOR_DIR'], 'config.json')
19
+ end
20
+
21
+ def create_config_dir
22
+ if !File.exists?(ENV['SPREADSHEET_LOAN_GENERATOR_DIR'])
23
+ FileUtils.mkdir_p(ENV['SPREADSHEET_LOAN_GENERATOR_DIR'])
24
+ end
25
+ end
26
+
27
+ def create_config_file(client_id:, client_secret:)
28
+ f = File.new(config_path, 'w')
29
+ f.write({
30
+ client_id: client_id,
31
+ client_secret: client_secret
32
+ }.to_json)
33
+ f.close
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,20 @@
1
+ module BlendSpreadsheetLoanGenerator
2
+ class Linear
3
+ include SpreadsheetConcern
4
+
5
+ attr_accessor :loan
6
+ def initialize(loan:)
7
+ @loan = loan
8
+ end
9
+
10
+ def period_calculated_capital_formula(*)
11
+ amount =
12
+ if loan.deferred_and_capitalized.zero?
13
+ excel_float(loan.amount)
14
+ else
15
+ "(#{capitalized_interests_end(loan.deferred_and_capitalized + 1)} + #{capitalized_fees_end(loan.deferred_and_capitalized + 1)} + #{excel_float(loan.amount)})"
16
+ end
17
+ "=#{amount} / #{loan.non_deferred_duration}"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,127 @@
1
+ module BlendSpreadsheetLoanGenerator
2
+ class Loan
3
+ attr_accessor :amount,
4
+ :duration,
5
+ :period_duration,
6
+ :rate,
7
+ :due_on,
8
+ :deferred_and_capitalized,
9
+ :deferred,
10
+ :type,
11
+ :interests_type,
12
+ :starting_capitalized_interests,
13
+ :fees_rate,
14
+ :starting_capitalized_fees
15
+
16
+ def initialize(
17
+ amount:,
18
+ duration:,
19
+ period_duration:,
20
+ rate:,
21
+ due_on:,
22
+ deferred_and_capitalized:,
23
+ deferred:,
24
+ type:,
25
+ interests_type:,
26
+ starting_capitalized_interests:,
27
+ fees_rate:,
28
+ starting_capitalized_fees:)
29
+ @amount = amount.to_f
30
+ @duration = duration.to_i
31
+ @period_duration = period_duration.to_i
32
+ @rate = rate.to_f
33
+ @due_on = due_on.is_a?(Date) ? due_on : Date.parse(due_on)
34
+ @deferred_and_capitalized = deferred_and_capitalized.to_i
35
+ @deferred = deferred.to_i
36
+ @type = type
37
+ @interests_type = interests_type
38
+ @fees_rate = fees_rate.to_f
39
+ @starting_capitalized_interests = starting_capitalized_interests.to_f
40
+ @starting_capitalized_fees = starting_capitalized_fees.to_f
41
+
42
+ print_validation_errors
43
+ end
44
+
45
+ def print_validation_errors
46
+ puts 'amount < 0' if amount < 0
47
+ puts 'deferred & deferred_and_capitalized >= duration' if total_deferred_duration >= duration
48
+ if type == 'standard' && interests_type == 'realistic' && !fully_deferred?
49
+ puts 'standard & realistic interests do not work together'
50
+ end
51
+ end
52
+
53
+ def loan_type_formula
54
+ "BlendSpreadsheetLoanGenerator::#{type.classify}".constantize.new(loan: self)
55
+ end
56
+
57
+ def interests_formula
58
+ "BlendSpreadsheetLoanGenerator::#{interests_type.classify}Interests".constantize.new(loan: self)
59
+ end
60
+
61
+ def name_type
62
+ return 'bullet' if bullet?
63
+ return 'in_fine' if in_fine?
64
+
65
+ type
66
+ end
67
+
68
+ def name_period_duration
69
+ if period_duration.in?([1, 3, 6, 12])
70
+ {
71
+ '1' => 'month',
72
+ '3' => 'quarter',
73
+ '6' => 'semester',
74
+ '12' => 'year'
75
+ }[period_duration.to_s]
76
+ else
77
+ period_duration.to_s
78
+ end
79
+ end
80
+
81
+ def name_deferred
82
+ return '0' if fully_deferred?
83
+
84
+ total_deferred_duration
85
+ end
86
+
87
+ def name_due_on
88
+ due_on.strftime('%Y%m%d')
89
+ end
90
+
91
+ def name
92
+ args = []
93
+ args += ['realistic'] if interests_type == 'realistic'
94
+ args += [
95
+ name_type,
96
+ name_period_duration,
97
+ amount,
98
+ (rate * 100).to_s,
99
+ duration.to_s,
100
+ name_deferred,
101
+ name_due_on
102
+ ]
103
+
104
+ args.join('_')
105
+ end
106
+
107
+ def fully_deferred?
108
+ duration > 1 && non_deferred_duration == 1
109
+ end
110
+
111
+ def bullet?
112
+ fully_deferred? && deferred_and_capitalized == total_deferred_duration
113
+ end
114
+
115
+ def in_fine?
116
+ fully_deferred? && deferred == total_deferred_duration
117
+ end
118
+
119
+ def non_deferred_duration
120
+ duration - total_deferred_duration
121
+ end
122
+
123
+ def total_deferred_duration
124
+ deferred_and_capitalized + deferred
125
+ end
126
+ end
127
+ end