dscf-credit 0.1.4 → 0.1.5

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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/dscf/core/copilot-instructions.md +683 -0
  3. data/app/controllers/concerns/dscf/core/reviewable_controller.rb +347 -0
  4. data/app/controllers/dscf/credit/categories_controller.rb +3 -3
  5. data/app/controllers/dscf/credit/credit_lines_controller.rb +21 -13
  6. data/app/controllers/dscf/credit/eligible_credit_lines_controller.rb +50 -8
  7. data/app/controllers/dscf/credit/facilitator_applications_controller.rb +35 -0
  8. data/app/controllers/dscf/credit/facilitators_controller.rb +8 -96
  9. data/app/controllers/dscf/credit/loan_applications_controller.rb +252 -0
  10. data/app/controllers/dscf/credit/loan_profiles_controller.rb +61 -68
  11. data/app/controllers/dscf/credit/payment_requests_controller.rb +3 -5
  12. data/app/controllers/dscf/credit/scoring_parameters_controller.rb +59 -13
  13. data/app/controllers/dscf/credit/system_configs_controller.rb +30 -12
  14. data/app/models/concerns/core/reviewable_model.rb +31 -0
  15. data/app/models/dscf/credit/bank.rb +3 -3
  16. data/app/models/dscf/credit/bank_branch.rb +1 -1
  17. data/app/models/dscf/credit/category.rb +1 -2
  18. data/app/models/dscf/credit/credit_line.rb +4 -10
  19. data/app/models/dscf/credit/eligible_credit_line.rb +2 -2
  20. data/app/models/dscf/credit/facilitator.rb +6 -17
  21. data/app/models/dscf/credit/facilitator_application.rb +20 -0
  22. data/app/models/dscf/credit/loan_application.rb +30 -0
  23. data/app/models/dscf/credit/loan_profile.rb +10 -30
  24. data/app/models/dscf/credit/parameter_normalizer.rb +1 -1
  25. data/app/models/dscf/credit/scoring_parameter.rb +5 -7
  26. data/app/models/dscf/credit/system_config.rb +4 -9
  27. data/app/serializers/dscf/credit/category_serializer.rb +0 -1
  28. data/app/serializers/dscf/credit/credit_line_serializer.rb +2 -2
  29. data/app/serializers/dscf/credit/facilitator_application_serializer.rb +7 -0
  30. data/app/serializers/dscf/credit/facilitator_serializer.rb +3 -6
  31. data/app/serializers/dscf/credit/loan_application_serializer.rb +12 -0
  32. data/app/serializers/dscf/credit/loan_profile_serializer.rb +3 -6
  33. data/app/serializers/dscf/credit/scoring_parameter_serializer.rb +3 -4
  34. data/app/serializers/dscf/credit/system_config_serializer.rb +2 -2
  35. data/app/services/dscf/credit/credit_scoring_engine.rb +258 -0
  36. data/app/services/dscf/credit/facility_limit_calculation_engine.rb +159 -0
  37. data/app/services/dscf/credit/loan_profile_creation_service.rb +91 -0
  38. data/app/services/dscf/credit/risk_application_service.rb +61 -11
  39. data/config/locales/en.yml +63 -48
  40. data/config/routes.rb +31 -17
  41. data/db/migrate/20250822091131_create_dscf_credit_credit_lines.rb +1 -8
  42. data/db/migrate/20250822091820_create_dscf_credit_system_configs.rb +0 -7
  43. data/db/migrate/20250822092050_create_dscf_credit_scoring_parameters.rb +2 -6
  44. data/db/migrate/20250822092225_create_dscf_credit_parameter_normalizers.rb +1 -1
  45. data/db/migrate/20250822092236_create_dscf_credit_loan_applications.rb +20 -0
  46. data/db/migrate/20250822092246_create_dscf_credit_loan_profiles.rb +7 -19
  47. data/db/migrate/20250822092426_create_dscf_credit_facilitator_applications.rb +10 -0
  48. data/db/migrate/20250822092436_create_dscf_credit_facilitators.rb +1 -16
  49. data/db/seeds.rb +316 -203
  50. data/lib/dscf/credit/version.rb +1 -1
  51. data/spec/factories/dscf/credit/banks.rb +1 -1
  52. data/spec/factories/dscf/credit/credit_lines.rb +0 -23
  53. data/spec/factories/dscf/credit/facilitator_applications.rb +37 -0
  54. data/spec/factories/dscf/credit/facilitators.rb +8 -30
  55. data/spec/factories/dscf/credit/loan_applications.rb +42 -0
  56. data/spec/factories/dscf/credit/loan_profiles.rb +20 -34
  57. data/spec/factories/dscf/credit/parameter_normalizers.rb +4 -4
  58. data/spec/factories/dscf/credit/scoring_parameters.rb +14 -11
  59. data/spec/factories/dscf/credit/system_configs.rb +21 -5
  60. metadata +20 -10
  61. data/app/controllers/concerns/dscf/credit/reviewable.rb +0 -112
  62. data/app/controllers/dscf/credit/scoring_tables_controller.rb +0 -63
  63. data/app/models/dscf/credit/scoring_table.rb +0 -24
  64. data/app/serializers/dscf/credit/scoring_table_serializer.rb +0 -9
  65. data/db/migrate/20250901172842_create_dscf_credit_scoring_tables.rb +0 -18
  66. data/spec/factories/dscf/credit/scoring_tables.rb +0 -25
@@ -0,0 +1,258 @@
1
+ module Dscf::Credit
2
+ class CreditScoringEngine
3
+ attr_reader :loan_application, :category_id, :errors
4
+
5
+ def initialize(loan_application_id, category_id)
6
+ @loan_application = find_loan_application(loan_application_id)
7
+ @category_id = category_id
8
+ @errors = []
9
+ end
10
+
11
+ def calculate_score
12
+ return error_result("Loan application not found") unless loan_application
13
+ return error_result("Category ID is required") unless category_id
14
+
15
+ begin
16
+ scoring_parameters = get_active_scoring_parameters
17
+
18
+ if scoring_parameters.empty?
19
+ return error_result("No active scoring parameters found for this bank for category ID #{category_id}")
20
+ end
21
+
22
+ # Calculate weighted score using the formula
23
+ score_result = calculate_weighted_score(scoring_parameters)
24
+
25
+ success_result(score_result)
26
+ rescue StandardError => e
27
+ Rails.logger.error "Credit scoring failed: #{e.message}"
28
+ Rails.logger.error e.backtrace.join("\n")
29
+ error_result("Score calculation failed: #{e.message}")
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def find_loan_application(loan_application_id)
36
+ Dscf::Credit::LoanApplication.includes(
37
+ :bank,
38
+ bank: :scoring_parameters
39
+ ).find_by(id: loan_application_id)
40
+ end
41
+
42
+ def get_active_scoring_parameters
43
+ return [] unless loan_application&.bank
44
+
45
+ loan_application.bank
46
+ .scoring_parameters
47
+ .active
48
+ .where(category_id: category_id)
49
+ .includes(:scoring_param_type, :parameter_normalizers, :category)
50
+ end
51
+
52
+ def calculate_weighted_score(scoring_parameters)
53
+ total_weighted_score = 0.0
54
+ total_weight = 0.0
55
+ score_breakdown = []
56
+ parameters_processed = 0
57
+ parameters_skipped = 0
58
+
59
+ scoring_parameters.each do |parameter|
60
+ raw_value = extract_parameter_value(parameter)
61
+
62
+ if raw_value.nil?
63
+ parameters_skipped += 1
64
+ score_breakdown << {
65
+ parameter_id: parameter.id,
66
+ parameter_name: parameter.name,
67
+ parameter_type: parameter.scoring_param_type.name,
68
+ raw_value: nil,
69
+ normalized_value: nil,
70
+ weight: parameter.weight,
71
+ weighted_score: 0.0,
72
+ status: "missing"
73
+ }
74
+ next
75
+ end
76
+
77
+ normalized_value = normalize_parameter_value(parameter, raw_value)
78
+ weighted_score = normalized_value * parameter.weight
79
+
80
+ total_weighted_score += weighted_score
81
+ total_weight += parameter.weight
82
+ parameters_processed += 1
83
+
84
+ score_breakdown << {
85
+ parameter_id: parameter.id,
86
+ parameter_name: parameter.name,
87
+ parameter_type: parameter.scoring_param_type.name,
88
+ raw_value: raw_value,
89
+ normalized_value: normalized_value,
90
+ weight: parameter.weight,
91
+ weighted_score: weighted_score,
92
+ status: "processed"
93
+ }
94
+ end
95
+
96
+ # Calculate final score using the formula: (sum of normalized_value × weight) / total_weight × 100
97
+ final_score = total_weight > 0 ? (total_weighted_score / total_weight * 100) : 0.0
98
+
99
+ {
100
+ score: final_score.round(2),
101
+ total_weighted_score: total_weighted_score.round(4),
102
+ total_weight: total_weight.round(4),
103
+ parameters_processed: parameters_processed,
104
+ parameters_skipped: parameters_skipped,
105
+ breakdown: score_breakdown
106
+ }
107
+ end
108
+
109
+ def extract_parameter_value(parameter)
110
+ param_type_name = parameter.scoring_param_type.name
111
+ parameter_id = parameter.id.to_s
112
+
113
+ # Get the appropriate JSON field based on parameter type
114
+ json_field = get_json_field_for_param_type(param_type_name)
115
+ return nil unless json_field
116
+
117
+ # Extract value using parameter ID as key
118
+ param_data = json_field[parameter_id]
119
+ return nil unless param_data.is_a?(Hash)
120
+
121
+ # Validate that the param_name matches (optional safety check)
122
+ expected_param_name = param_data["param_name"]
123
+ if expected_param_name && expected_param_name != parameter.name
124
+ Rails.logger.warn "Parameter name mismatch for ID #{parameter_id}: expected '#{parameter.name}', got '#{expected_param_name}'"
125
+ end
126
+
127
+ param_data["value"]
128
+ end
129
+
130
+ def get_json_field_for_param_type(param_type_name)
131
+ case param_type_name.downcase
132
+ when "user_info"
133
+ loan_application.user_info || {}
134
+ when "bank_info"
135
+ loan_application.bank_info || {}
136
+ when "facilitator_info"
137
+ loan_application.facilitator_info || {}
138
+ when "field_assessment"
139
+ loan_application.field_assessment || {}
140
+ else
141
+ Rails.logger.warn "Unknown parameter type: #{param_type_name}"
142
+ nil
143
+ end
144
+ end
145
+
146
+ def normalize_parameter_value(parameter, raw_value)
147
+ return 0.0 if raw_value.nil?
148
+
149
+ normalizer = find_normalizer(parameter, raw_value)
150
+ if normalizer
151
+ normalized_val = normalizer.normalized_value.to_f
152
+
153
+ # Ensure normalized value is between 0 and 1
154
+ return [ [ normalized_val, 0.0 ].max, 1.0 ].min
155
+ end
156
+
157
+ Rails.logger.warn "No normalizer found for parameter '#{parameter.name}' with value '#{raw_value}'"
158
+ 0.0
159
+ rescue StandardError => e
160
+ Rails.logger.error "Failed to normalize value '#{raw_value}' for parameter '#{parameter.name}': #{e.message}"
161
+ 0.0 # Return 0 for any normalization errors
162
+ end
163
+
164
+ def find_normalizer(parameter, raw_value)
165
+ exact_match = parameter.parameter_normalizers.find { |normalizer| normalizer.raw_value == raw_value.to_s }
166
+ return exact_match if exact_match
167
+
168
+ if parameter.data_type.in?([ "integer", "decimal" ])
169
+ numeric_value = parse_numeric_value(raw_value)
170
+ return nil unless numeric_value
171
+
172
+ threshold_normalizer = parameter.parameter_normalizers.find do |normalizer|
173
+ evaluate_numeric_threshold(normalizer.raw_value, numeric_value)
174
+ end
175
+
176
+ return threshold_normalizer if threshold_normalizer
177
+ end
178
+
179
+ nil
180
+ end
181
+
182
+ def parse_numeric_value(raw_value)
183
+ return raw_value if raw_value.is_a?(Numeric)
184
+
185
+ begin
186
+ Float(raw_value.to_s)
187
+ rescue ArgumentError, TypeError
188
+ nil
189
+ end
190
+ end
191
+
192
+ def evaluate_numeric_threshold(threshold_expression, numeric_value)
193
+ # Handle threshold expressions like ">=10000", "<5000", etc.
194
+ case threshold_expression
195
+ when /^>=(.+)$/
196
+ numeric_value >= Float($1)
197
+ when /^>(.+)$/
198
+ numeric_value > Float($1)
199
+ when /^<=(.+)$/
200
+ numeric_value <= Float($1)
201
+ when /^<(.+)$/
202
+ numeric_value < Float($1)
203
+ when /^=(.+)$/, /^(.+)$/ # Exact match or just the number
204
+ numeric_value == Float($1)
205
+ else
206
+ false
207
+ end
208
+ rescue ArgumentError, TypeError
209
+ false
210
+ end
211
+
212
+ def success_result(score_result)
213
+ final_score = score_result[:score]
214
+ review_status = determine_review_status(final_score)
215
+
216
+ {
217
+ success: true,
218
+ loan_application_id: loan_application.id,
219
+ bank_id: loan_application.bank_id,
220
+ category_id: category_id,
221
+ calculated_at: Time.current,
222
+ score: final_score,
223
+ status: review_status,
224
+ total_weighted_score: score_result[:total_weighted_score],
225
+ total_weight: score_result[:total_weight],
226
+ parameters_processed: score_result[:parameters_processed],
227
+ parameters_skipped: score_result[:parameters_skipped],
228
+ breakdown: score_result[:breakdown],
229
+ message: "Credit score calculated successfully"
230
+ }
231
+ end
232
+
233
+ def error_result(message)
234
+ @errors << message
235
+ {
236
+ success: false,
237
+ loan_application_id: loan_application&.id,
238
+ category_id: category_id,
239
+ error: message,
240
+ errors: @errors,
241
+ score: 0.0,
242
+ status: "rejected", # Errors result in rejection
243
+ calculated_at: Time.current
244
+ }
245
+ end
246
+
247
+ def determine_review_status(score)
248
+ case score
249
+ when 0...50
250
+ "rejected"
251
+ when 50..60
252
+ "pending_review"
253
+ else # score > 60
254
+ "approved"
255
+ end
256
+ end
257
+ end
258
+ end
@@ -0,0 +1,159 @@
1
+ module Dscf::Credit
2
+ class FacilityLimitCalculationEngine
3
+ attr_reader :loan_profile, :category_id, :errors
4
+
5
+ def initialize(loan_profile_id, category_id)
6
+ @errors = []
7
+ @loan_profile = LoanProfile.find(loan_profile_id)
8
+ @category_id = category_id
9
+ rescue ActiveRecord::RecordNotFound => e
10
+ @errors << "Loan profile not found: #{e.message}"
11
+ end
12
+
13
+ # Calculate facility limits for all credit lines in the category
14
+ def calculate_facility_limits
15
+ return error_result("Loan profile is required") unless loan_profile
16
+ return error_result("Category ID is required") unless category_id
17
+
18
+ begin
19
+ ActiveRecord::Base.transaction do
20
+ # Get all credit lines for the category and bank
21
+ credit_lines = get_credit_lines_for_calculation
22
+
23
+ if credit_lines.empty?
24
+ return error_result("No credit lines found for category #{category_id} and bank #{loan_profile.loan_application.bank_id}")
25
+ end
26
+
27
+ # Calculate base metric from facilitator info
28
+ base_metric = calculate_base_metric
29
+
30
+ if base_metric <= 0
31
+ return error_result("Invalid base metric calculated: #{base_metric}. Check facilitator_info avg_monthly_purchase.")
32
+ end
33
+
34
+ # Calculate facility limits for each credit line
35
+ eligible_credit_lines = []
36
+
37
+ credit_lines.each do |credit_line|
38
+ limit_result = calculate_credit_line_limit(credit_line, base_metric)
39
+
40
+ if limit_result[:success] && limit_result[:limit] >= 0
41
+ eligible_credit_line = create_eligible_credit_line(credit_line, limit_result[:limit])
42
+ eligible_credit_lines << eligible_credit_line
43
+ end
44
+ end
45
+
46
+ # Update loan profile total limit
47
+ update_loan_profile_total_limit
48
+
49
+ success_result(eligible_credit_lines, "Facility limits calculated successfully")
50
+ end
51
+ rescue StandardError => e
52
+ Rails.logger.error "Facility limit calculation failed: #{e.message}"
53
+ Rails.logger.error e.backtrace.join("\n")
54
+ error_result("Failed to calculate facility limits: #{e.message}")
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ # Get credit lines for the specified category and bank
61
+ def get_credit_lines_for_calculation
62
+ CreditLine.joins(:category, :credit_line_specs)
63
+ .where(
64
+ category_id: category_id,
65
+ bank_id: loan_profile.loan_application.bank_id,
66
+ dscf_credit_credit_line_specs: { active: true }
67
+ )
68
+ .includes(:credit_line_specs)
69
+ .distinct
70
+ end
71
+
72
+ # Calculate base metric from facilitator info
73
+ def calculate_base_metric
74
+ facilitator_info = loan_profile.loan_application.facilitator_info || {}
75
+
76
+ # Find avg_monthly_purchase from the nested JSON structure
77
+ # facilitator_info structure: { "301": { "param_name": "avg_monthly_purchase", "value": 30000 } }
78
+ avg_monthly_purchase = nil
79
+
80
+ facilitator_info.each do |param_id, param_data|
81
+ if param_data.is_a?(Hash) &&
82
+ (param_data["param_name"] == "avg_monthly_purchase" || param_data[:param_name] == "avg_monthly_purchase")
83
+ avg_monthly_purchase = param_data["value"] || param_data[:value]
84
+ break
85
+ end
86
+ end
87
+
88
+ # Handle string and numeric values
89
+ if avg_monthly_purchase.is_a?(String)
90
+ avg_monthly_purchase = avg_monthly_purchase.to_f
91
+ else
92
+ avg_monthly_purchase = avg_monthly_purchase.to_f if avg_monthly_purchase
93
+ end
94
+
95
+ return 0.0 if avg_monthly_purchase.nil? || avg_monthly_purchase <= 0
96
+
97
+ avg_monthly_purchase / 30.0
98
+ end
99
+
100
+ # Calculate facility limit for a specific credit line
101
+ def calculate_credit_line_limit(credit_line, base_metric)
102
+ credit_line_spec = credit_line.credit_line_specs.active.order(created_at: :desc).first
103
+
104
+ unless credit_line_spec
105
+ return { success: false, error: "No active credit line spec found for credit line #{credit_line.id}" }
106
+ end
107
+
108
+ score_percentage = loan_profile.score / 100.0
109
+ calculated_limit = base_metric * credit_line_spec.credit_line_multiplier * score_percentage
110
+
111
+ final_limit = apply_min_max_constraints(calculated_limit, credit_line_spec)
112
+
113
+ { success: true, limit: final_limit, credit_line_spec: credit_line_spec }
114
+ end
115
+
116
+ def apply_min_max_constraints(calculated_limit, credit_line_spec)
117
+ if calculated_limit > credit_line_spec.max_amount
118
+ return credit_line_spec.max_amount
119
+ end
120
+
121
+ if calculated_limit < credit_line_spec.min_amount
122
+ return 0.0
123
+ end
124
+
125
+ calculated_limit
126
+ end
127
+
128
+ def create_eligible_credit_line(credit_line, limit)
129
+ loan_profile.eligible_credit_lines.create!(
130
+ credit_line: credit_line,
131
+ credit_limit: limit,
132
+ available_limit: limit,
133
+ risk: nil
134
+ )
135
+ end
136
+
137
+ def update_loan_profile_total_limit
138
+ total_limit = loan_profile.eligible_credit_lines.sum(:available_limit)
139
+ loan_profile.update!(total_limit: total_limit)
140
+ end
141
+
142
+ def success_result(data, message)
143
+ {
144
+ success: true,
145
+ data: data,
146
+ message: message
147
+ }
148
+ end
149
+
150
+ def error_result(error_message)
151
+ @errors << error_message unless @errors.include?(error_message)
152
+ {
153
+ success: false,
154
+ error: error_message,
155
+ errors: @errors
156
+ }
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,91 @@
1
+ module Dscf::Credit
2
+ class LoanProfileCreationService
3
+ attr_reader :loan_application, :score, :errors
4
+
5
+ def initialize(loan_application, score)
6
+ @loan_application = loan_application
7
+ @score = score
8
+ @errors = []
9
+ end
10
+
11
+ def create_loan_profile
12
+ return error_result("Loan application is required") unless loan_application
13
+ return error_result("Score is required") unless score
14
+
15
+ if loan_application.loan_profile.present?
16
+ return success_result(loan_application.loan_profile, "Loan profile already exists")
17
+ end
18
+
19
+ begin
20
+ ActiveRecord::Base.transaction do
21
+ loan_profile = build_loan_profile
22
+ loan_profile.save!
23
+
24
+ create_initial_review(loan_profile)
25
+
26
+ success_result(loan_profile, "Loan profile created successfully")
27
+ end
28
+ rescue StandardError => e
29
+ Rails.logger.error "Loan profile creation failed: #{e.message}"
30
+ Rails.logger.error e.backtrace.join("\n")
31
+ error_result("Failed to create loan profile: #{e.message}")
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def build_loan_profile
38
+ loan_application.build_loan_profile(
39
+ code: generate_loan_profile_code,
40
+ score: score,
41
+ total_limit: calculate_total_limit
42
+ )
43
+ end
44
+
45
+ def generate_loan_profile_code
46
+ # Generate unique code starting with "BB" followed by up to 6 digits
47
+ loop do
48
+ # Generate a random 6-digit number
49
+ random_digits = sprintf("%06d", rand(1000000))
50
+ code = "BB#{random_digits}"
51
+
52
+ unless Dscf::Credit::LoanProfile.exists?(code: code)
53
+ return code
54
+ end
55
+ end
56
+ end
57
+
58
+ def calculate_total_limit
59
+ 0.0
60
+ end
61
+
62
+ def create_initial_review(loan_profile)
63
+ loan_profile.reviews.create!(
64
+ context: "default",
65
+ status: "approved",
66
+ reviewed_by: nil, # System created
67
+ reviewed_at: Time.current,
68
+ feedback: {
69
+ message: "Loan profile automatically created for approved loan application"
70
+ }
71
+ )
72
+ end
73
+
74
+ def success_result(loan_profile, message)
75
+ {
76
+ success: true,
77
+ loan_profile: loan_profile,
78
+ message: message
79
+ }
80
+ end
81
+
82
+ def error_result(message)
83
+ @errors << message
84
+ {
85
+ success: false,
86
+ error: message,
87
+ errors: @errors
88
+ }
89
+ end
90
+ end
91
+ end
@@ -1,27 +1,77 @@
1
1
  module Dscf::Credit
2
2
  class RiskApplicationService
3
- def initialize(eligible_credit_line, risk_value)
3
+ attr_reader :eligible_credit_line, :risk_factor, :errors
4
+
5
+ def initialize(eligible_credit_line, risk_factor)
4
6
  @eligible_credit_line = eligible_credit_line
5
- @risk_value = risk_value.to_f
7
+ @risk_factor = risk_factor.nil? ? nil : risk_factor.to_f
8
+ @errors = []
6
9
  end
7
10
 
8
11
  def apply_risk
9
- return { success: false, errors: @eligible_credit_line.errors.full_messages } unless @eligible_credit_line.valid?
12
+ return error_result("Eligible credit line is required") unless eligible_credit_line
13
+ return error_result("Risk factor is required") if risk_factor.nil?
14
+ return error_result("Risk factor must be between 0.0 and 0.9") unless valid_risk_factor?
15
+
16
+ begin
17
+ ActiveRecord::Base.transaction do
18
+ eligible_credit_line.risk = risk_factor
19
+
20
+ new_available_limit = calculate_available_limit_with_risk
21
+ eligible_credit_line.available_limit = new_available_limit
22
+
23
+ eligible_credit_line.save!
10
24
 
11
- @eligible_credit_line.risk = @risk_value
12
- @eligible_credit_line.available_limit = calculate_available_limit
25
+ update_loan_profile_total_limit
13
26
 
14
- if @eligible_credit_line.save
15
- { success: true, data: @eligible_credit_line }
16
- else
17
- { success: false, errors: @eligible_credit_line.errors.full_messages }
27
+ success_result(eligible_credit_line, "Risk factor applied successfully")
28
+ end
29
+ rescue StandardError => e
30
+ Rails.logger.error "Risk application failed: #{e.message}"
31
+ Rails.logger.error e.backtrace.join("\n")
32
+ error_result("Failed to apply risk factor: #{e.message}")
18
33
  end
19
34
  end
20
35
 
21
36
  private
22
37
 
23
- def calculate_available_limit
24
- @eligible_credit_line.credit_limit * (1 - @risk_value)
38
+ def valid_risk_factor?
39
+ risk_factor >= 0.0 && risk_factor <= 0.9
40
+ end
41
+
42
+ def calculate_available_limit_with_risk
43
+ # available_limit = credit_limit × (1 - risk_factor)
44
+ # If risk_factor is 0.0 (0%), available_limit = credit_limit
45
+ # If risk_factor is 0.9 (90%), available_limit = credit_limit × 0.1
46
+ result = eligible_credit_line.credit_limit * (1 - risk_factor)
47
+ result.round(2)
48
+ end
49
+
50
+ def update_loan_profile_total_limit
51
+ loan_profile = eligible_credit_line.loan_profile
52
+ return unless loan_profile
53
+
54
+ total_available_limit = loan_profile.eligible_credit_lines.sum(:available_limit)
55
+
56
+ loan_profile.update!(total_limit: total_available_limit)
57
+
58
+ Rails.logger.info "Updated loan profile #{loan_profile.id} total limit to #{total_available_limit}"
59
+ end
60
+
61
+ def success_result(data, message)
62
+ {
63
+ success: true,
64
+ data: data,
65
+ message: message
66
+ }
67
+ end
68
+
69
+ def error_result(message)
70
+ {
71
+ success: false,
72
+ error: message,
73
+ errors: [ message ]
74
+ }
25
75
  end
26
76
  end
27
77
  end