blend_spreadsheet_loan_generator 0.1.0

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.
@@ -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