dscf-credit 0.4.1 → 0.4.2

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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/dscf/credit/categories_controller.rb +29 -25
  3. data/app/controllers/dscf/credit/credit_limit_calculations_controller.rb +4 -3
  4. data/app/controllers/dscf/credit/credit_line_specs_controller.rb +2 -1
  5. data/app/controllers/dscf/credit/information_sources_controller.rb +93 -0
  6. data/app/controllers/dscf/credit/loan_application_data_controller.rb +35 -0
  7. data/app/controllers/dscf/credit/loan_applications_controller.rb +186 -125
  8. data/app/controllers/dscf/credit/loan_profiles_controller.rb +4 -4
  9. data/app/controllers/dscf/credit/scoring_parameters_controller.rb +8 -45
  10. data/app/controllers/dscf/credit/scoring_table_normalizers_controller.rb +38 -0
  11. data/app/controllers/dscf/credit/scoring_table_parameters_controller.rb +45 -0
  12. data/app/controllers/dscf/credit/scoring_tables_controller.rb +98 -0
  13. data/app/models/dscf/credit/category.rb +21 -13
  14. data/app/models/dscf/credit/credit_line_spec.rb +56 -36
  15. data/app/models/dscf/credit/information_source.rb +24 -0
  16. data/app/models/dscf/credit/loan_application.rb +3 -2
  17. data/app/models/dscf/credit/loan_application_datum.rb +20 -0
  18. data/app/models/dscf/credit/loan_profile.rb +2 -2
  19. data/app/models/dscf/credit/scoring_parameter.rb +24 -21
  20. data/app/models/dscf/credit/scoring_result.rb +22 -0
  21. data/app/models/dscf/credit/scoring_table.rb +47 -0
  22. data/app/models/dscf/credit/scoring_table_normalizer.rb +53 -0
  23. data/app/models/dscf/credit/scoring_table_parameter.rb +46 -0
  24. data/app/serializers/dscf/credit/category_serializer.rb +9 -5
  25. data/app/serializers/dscf/credit/credit_line_spec_serializer.rb +13 -10
  26. data/app/serializers/dscf/credit/information_source_serializer.rb +10 -0
  27. data/app/serializers/dscf/credit/loan_application_datum_serializer.rb +8 -0
  28. data/app/serializers/dscf/credit/loan_application_serializer.rb +3 -2
  29. data/app/serializers/dscf/credit/loan_profile_serializer.rb +1 -1
  30. data/app/serializers/dscf/credit/scoring_parameter_serializer.rb +10 -12
  31. data/app/serializers/dscf/credit/scoring_result_serializer.rb +7 -0
  32. data/app/serializers/dscf/credit/scoring_table_normalizer_serializer.rb +17 -0
  33. data/app/serializers/dscf/credit/scoring_table_parameter_serializer.rb +12 -0
  34. data/app/serializers/dscf/credit/scoring_table_serializer.rb +15 -0
  35. data/app/services/dscf/credit/credit_scoring_engine.rb +98 -240
  36. data/app/services/dscf/credit/facility_limit_calculation_engine.rb +69 -252
  37. data/app/services/dscf/credit/loan_profile_creation_service.rb +31 -9
  38. data/app/services/dscf/credit/repayment_service.rb +19 -2
  39. data/config/locales/en.yml +100 -34
  40. data/config/routes.rb +27 -6
  41. data/db/dev_seeds.rb +1235 -0
  42. data/db/migrate/20250822091009_create_dscf_credit_scoring_tables.rb +27 -0
  43. data/db/migrate/20250822091011_create_dscf_credit_categories.rb +6 -0
  44. data/db/migrate/20250822091500_create_dscf_credit_information_sources.rb +19 -0
  45. data/db/migrate/20250822091526_create_dscf_credit_scoring_parameters.rb +4 -8
  46. data/db/migrate/20250822091527_create_dscf_credit_credit_line_specs.rb +4 -0
  47. data/db/migrate/20250822091528_create_dscf_credit_scoring_table_parameters.rb +29 -0
  48. data/db/migrate/20250822091529_create_dscf_credit_scoring_table_normalizers.rb +28 -0
  49. data/db/migrate/20250822092236_create_dscf_credit_loan_applications.rb +1 -4
  50. data/db/migrate/20250822092417_create_dscf_credit_scoring_results.rb +19 -0
  51. data/db/migrate/20260218100000_create_dscf_credit_loan_application_data.rb +17 -0
  52. data/db/seeds.rb +438 -55
  53. data/lib/dscf/credit/version.rb +1 -1
  54. data/spec/factories/dscf/credit/categories.rb +13 -8
  55. data/spec/factories/dscf/credit/credit_line_specs.rb +25 -16
  56. data/spec/factories/dscf/credit/information_sources.rb +50 -0
  57. data/spec/factories/dscf/credit/loan_application_data.rb +16 -0
  58. data/spec/factories/dscf/credit/loan_application_datum.rb +16 -0
  59. data/spec/factories/dscf/credit/loan_applications.rb +5 -8
  60. data/spec/factories/dscf/credit/scoring_parameters.rb +9 -21
  61. data/spec/factories/dscf/credit/scoring_results.rb +44 -0
  62. data/spec/factories/dscf/credit/scoring_table_normalizers.rb +34 -0
  63. data/spec/factories/dscf/credit/scoring_table_parameters.rb +30 -0
  64. data/spec/factories/dscf/credit/scoring_tables.rb +33 -0
  65. metadata +34 -18
  66. data/app/controllers/dscf/credit/parameter_normalizers_controller.rb +0 -31
  67. data/app/controllers/dscf/credit/scoring_param_types_controller.rb +0 -31
  68. data/app/models/dscf/credit/loan_profile_scoring_spec.rb +0 -21
  69. data/app/models/dscf/credit/parameter_normalizer.rb +0 -11
  70. data/app/models/dscf/credit/scoring_param_type.rb +0 -17
  71. data/app/serializers/dscf/credit/loan_profile_scoring_spec_serializer.rb +0 -8
  72. data/app/serializers/dscf/credit/parameter_normalizer_serializer.rb +0 -8
  73. data/app/serializers/dscf/credit/scoring_param_type_serializer.rb +0 -7
  74. data/app/services/dscf/credit/credit_limit_calculation_service.rb +0 -153
  75. data/db/migrate/20250822091525_create_dscf_credit_scoring_param_types.rb +0 -12
  76. data/db/migrate/20250822092225_create_dscf_credit_parameter_normalizers.rb +0 -18
  77. data/db/migrate/20250822092417_create_dscf_credit_loan_profile_scoring_specs.rb +0 -21
  78. data/spec/factories/dscf/credit/loan_profile_scoring_specs.rb +0 -25
  79. data/spec/factories/dscf/credit/parameter_normalizers.rb +0 -24
  80. data/spec/factories/dscf/credit/scoring_param_types.rb +0 -31
  81. /data/db/migrate/{20250822091015_create_dscf_credit_banks.rb → 20250822091008_create_dscf_credit_banks.rb} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0be229f7298a4e4f905fec15eae5ede2fcd755f6f56510f73f12b32e6d5b3453
4
- data.tar.gz: 7de36c9ecca064d851e13b7ceb8ed7aea01cc7c6d8794fef6e203d5af63400a8
3
+ metadata.gz: 6588b8a4895fec7dc6cbbbaafb11bbfd06a2ccf1342c8edf6f0467b8e322ff89
4
+ data.tar.gz: 598e9e952de5f1c510fd95efa3a7b6686f34f29b1dd3c5cee5f24e2857e1b340
5
5
  SHA512:
6
- metadata.gz: eb8742b82a81552545e811623eb176fca2a12f06fbf301aa90d060a64293ef2f77c5ad5f3e7db59a77b08e0659e81f0ee84956e0ba0d7b012cbc1cf1873de5cf
7
- data.tar.gz: e8350b9ce353bf7c88992e00ca287fe7791be962449b10feefb4afbcc8b4af89157b5677957f2ec2cbccc64398fb4b58d40cf54a0896561ab58a33316ab87d54
6
+ metadata.gz: 9468e717d617bab5fedad24fc41429d3cae8d9eb3c66c94515ce94ce0d01a9ade439d7412fe33a9c9f6c10f19c48733b7deefda7eafa66bf17bb9969a51d6410
7
+ data.tar.gz: '0316249a1df4a801ab43c25dda370650516877651a5a2dd818223f24d828f7dbd9eb648ce9b77786e3326eb44cd0209c67bf945045fecb1684bdbe843cc76e31'
@@ -1,33 +1,37 @@
1
- module Dscf::Credit
2
- class CategoriesController < ApplicationController
3
- include Dscf::Core::Common
1
+ module Dscf
2
+ module Credit
3
+ class CategoriesController < ApplicationController
4
+ include Dscf::Core::Common
4
5
 
5
- private
6
+ private
6
7
 
7
- def model_params
8
- params.require(:category).permit(
9
- :category_type,
10
- :name,
11
- :description,
12
- :document_reference
13
- )
14
- end
8
+ def model_params
9
+ params.require(:category).permit(
10
+ :bank_id,
11
+ :category_type,
12
+ :name,
13
+ :description,
14
+ :document_reference,
15
+ :scoring_table_id
16
+ )
17
+ end
15
18
 
16
- def eager_loaded_associations
17
- [ :credit_lines, :scoring_parameters ]
18
- end
19
+ def eager_loaded_associations
20
+ [ :bank, :scoring_table, :credit_lines, :scoring_parameters ]
21
+ end
19
22
 
20
- def allowed_order_columns
21
- %w[id type name description document_reference created_at updated_at]
22
- end
23
+ def allowed_order_columns
24
+ %w[id category_type name description document_reference created_at updated_at]
25
+ end
23
26
 
24
- def default_serializer_includes
25
- {
26
- index: [],
27
- show: [ :credit_lines, :scoring_parameters ],
28
- create: [],
29
- update: [ :credit_lines, :scoring_parameters ]
30
- }
27
+ def default_serializer_includes
28
+ {
29
+ index: [],
30
+ show: [ :bank, :scoring_table, :credit_lines, :scoring_parameters ],
31
+ create: [ :bank, :scoring_table ],
32
+ update: [ :bank, :scoring_table, :credit_lines, :scoring_parameters ]
33
+ }
34
+ end
31
35
  end
32
36
  end
33
37
  end
@@ -4,10 +4,11 @@ module Dscf::Credit
4
4
  loan_profile = Dscf::Credit::LoanProfile.find(params[:loan_profile_id])
5
5
  category = Dscf::Credit::Category.find(params[:category_id])
6
6
 
7
- service = Dscf::Credit::CreditLimitCalculationService.new(loan_profile, category)
8
- result = service.calculate_credit_limits
7
+ engine = Dscf::Credit::FacilityLimitCalculationEngine.new(loan_profile.id, category.id)
8
+ result = engine.calculate_facility_limits
9
9
 
10
10
  if result[:success]
11
+ loan_profile.reload
11
12
  render_success(
12
13
  "credit_limit_calculation.success.create",
13
14
  data: {
@@ -15,7 +16,7 @@ module Dscf::Credit
15
16
  category: category,
16
17
  eligible_credit_lines: result[:data],
17
18
  calculation_summary: {
18
- total_credit_lines_processed: result[:count],
19
+ total_credit_lines_processed: result[:data]&.size || 0,
19
20
  calculated_at: Time.current
20
21
  }
21
22
  },
@@ -49,6 +49,7 @@ module Dscf::Credit
49
49
  :max_interest_calculation_days,
50
50
  :penalty_frequency,
51
51
  :penalty_income_tax,
52
+ :minimum_score,
52
53
  :active,
53
54
  :base_scoring_parameter_id,
54
55
  :credit_line_divider
@@ -60,7 +61,7 @@ module Dscf::Credit
60
61
  end
61
62
 
62
63
  def allowed_order_columns
63
- %w[id min_amount max_amount interest_rate penalty_rate facilitation_fee_rate tax_rate credit_line_multiplier max_penalty_days loan_duration interest_frequency interest_income_tax vat max_interest_calculation_days penalty_frequency penalty_income_tax active created_at updated_at]
64
+ %w[id min_amount max_amount interest_rate penalty_rate facilitation_fee_rate tax_rate credit_line_multiplier max_penalty_days loan_duration interest_frequency interest_income_tax vat max_interest_calculation_days penalty_frequency penalty_income_tax minimum_score active created_at updated_at]
64
65
  end
65
66
 
66
67
  def default_serializer_includes
@@ -0,0 +1,93 @@
1
+ module Dscf::Credit
2
+ class InformationSourcesController < ApplicationController
3
+ include Dscf::Core::Common
4
+ include Dscf::Core::ReviewableController
5
+ include Dscf::Core::AuditableController
6
+
7
+ before_action :set_object, only: %i[ show update activate deactivate ]
8
+
9
+ auditable associated: [ :reviews ],
10
+ on: %i[approve submit reject request_modification resubmit]
11
+
12
+ auditable only: [ :active ],
13
+ on: %i[activate deactivate]
14
+
15
+ auditable on: %i[create],
16
+ associated: { reviews: { only: [ :status ] } }
17
+
18
+ def create
19
+ super do
20
+ information_source = @clazz.new(model_params)
21
+ information_source.created_by = current_user
22
+ information_source.reviews.build(context: "default", status: "draft")
23
+ information_source
24
+ end
25
+ end
26
+
27
+ def update
28
+ unless @obj.editable?
29
+ return render_error(
30
+ errors: [ "Cannot update information source after submission. Use modification request workflow instead." ],
31
+ status: :unprocessable_entity
32
+ )
33
+ end
34
+ super
35
+ end
36
+
37
+ def activate
38
+ if @obj.update(active: true)
39
+ render_success(data: @obj)
40
+ else
41
+ render_error(errors: @obj.errors.full_messages, status: :unprocessable_entity)
42
+ end
43
+ end
44
+
45
+ def deactivate
46
+ if @obj.update(active: false)
47
+ render_success(data: @obj)
48
+ else
49
+ render_error(errors: @obj.errors.full_messages, status: :unprocessable_entity)
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def model_params
56
+ params.require(:information_source).permit(
57
+ :bank_id,
58
+ :code,
59
+ :name,
60
+ :description,
61
+ :active
62
+ )
63
+ end
64
+
65
+ def eager_loaded_associations
66
+ [
67
+ :bank, :created_by,
68
+ reviews: { reviewed_by: :user_profile },
69
+ audit_logs: :actor
70
+ ]
71
+ end
72
+
73
+ def allowed_order_columns
74
+ %w[id code name active created_at updated_at]
75
+ end
76
+
77
+ def default_serializer_includes
78
+ {
79
+ index: [ :bank, reviews: { reviewed_by: :user_profile } ],
80
+ show: [
81
+ :bank, :created_by,
82
+ reviews: { reviewed_by: :user_profile },
83
+ audit_logs: :actor
84
+ ],
85
+ create: [ :bank, :created_by, :reviews ],
86
+ update: [
87
+ :bank, :created_by,
88
+ reviews: { reviewed_by: :user_profile }
89
+ ]
90
+ }
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,35 @@
1
+ module Dscf::Credit
2
+ class LoanApplicationDataController < ApplicationController
3
+ include Dscf::Core::Common
4
+
5
+ private
6
+
7
+ def model_params
8
+ params.require(:loan_application_datum).permit(
9
+ :loan_application_id,
10
+ :information_source_id,
11
+ :submitted_by_type,
12
+ :submitted_by_id,
13
+ :submitted_at,
14
+ data: {}
15
+ )
16
+ end
17
+
18
+ def eager_loaded_associations
19
+ [ :loan_application, :information_source, :submitted_by ]
20
+ end
21
+
22
+ def allowed_order_columns
23
+ %w[id loan_application_id information_source_id submitted_at created_at updated_at]
24
+ end
25
+
26
+ def default_serializer_includes
27
+ {
28
+ index: [ :loan_application, :information_source, :submitted_by ],
29
+ show: [ :loan_application, :information_source, :submitted_by ],
30
+ create: [ :loan_application, :information_source, :submitted_by ],
31
+ update: [ :loan_application, :information_source, :submitted_by ]
32
+ }
33
+ end
34
+ end
35
+ end
@@ -23,7 +23,26 @@ module Dscf::Credit
23
23
  after: ->(review) {
24
24
  loan_application = review.reviewable
25
25
  score = loan_application.score
26
- LoanApplicationsController.new.create_loan_profile_for_approved_application(loan_application, score)
26
+ return unless score
27
+
28
+ # Build scoring input snapshot from loan application data
29
+ input_snapshot = {}
30
+ loan_application.loan_application_data.includes(:information_source).each do |datum|
31
+ input_snapshot[datum.information_source.code] = datum.data
32
+ end
33
+
34
+ scoring_result_data = {
35
+ scoring_table_id: loan_application.category&.scoring_table&.id,
36
+ scoring_input_data: input_snapshot,
37
+ breakdown: {} # Breakdown unavailable for manually-approved pending applications
38
+ }
39
+
40
+ service = LoanProfileCreationService.new(loan_application, score, scoring_result_data)
41
+ result = service.create_loan_profile
42
+
43
+ unless result[:success]
44
+ raise StandardError, "Failed to create loan profile: #{result[:error]}"
45
+ end
27
46
  }
28
47
  },
29
48
  reject: { status: "rejected", require_feedback: true },
@@ -51,90 +70,123 @@ module Dscf::Credit
51
70
  super
52
71
  end
53
72
 
54
- def update_bank_info
55
- loan_application = @clazz.find(params[:id])
56
- if loan_application.update(bank_info: bank_info_params)
57
- render_success("loan_application.success.update_bank_info", data: loan_application, serializer_options: { include: [ :bank, :user ] })
58
- else
59
- render_error("loan_application.errors.update_bank_info", errors: loan_application.errors.full_messages[0], status: :unprocessable_entity)
60
- end
61
- rescue => e
62
- if Rails.env.development? || Rails.env.test?
63
- render_error(errors: e.message, status: :unprocessable_entity)
64
- else
65
- render_error(status: :unprocessable_entity)
66
- end
67
- Rails.logger.error("Unexpected error: #{e.class} - #{e.message}")
68
- end
73
+ def submit_source_data
74
+ set_object
75
+ source_code = params[:source_code]
76
+ information_source = Dscf::Credit::InformationSource.find_by(code: source_code)
69
77
 
70
- def update_facilitator_info
71
- loan_application = @clazz.find(params[:id])
72
- if loan_application.update(facilitator_info: facilitator_info_params)
73
- render_success("loan_application.success.update_facilitator_info", data: loan_application, serializer_options: { include: [ :bank, :user ] })
74
- else
75
- render_error("loan_application.errors.update_facilitator_info", errors: loan_application.errors.full_messages[0], status: :unprocessable_entity)
78
+ unless information_source
79
+ return render_error(errors: [ "Information source '#{source_code}' not found" ], status: :not_found)
76
80
  end
77
- rescue => e
78
- if Rails.env.development? || Rails.env.test?
79
- render_error(errors: e.message, status: :unprocessable_entity)
81
+
82
+ datum = @obj.loan_application_data.find_or_initialize_by(information_source: information_source)
83
+ datum.data = params.dig(:loan_application_datum, :data) || params[:data] || {}
84
+ datum.submitted_by = current_user
85
+ datum.submitted_at = Time.current
86
+
87
+ if datum.save
88
+ render_success(data: datum)
80
89
  else
81
- render_error(status: :unprocessable_entity)
90
+ render_error(errors: datum.errors.full_messages, status: :unprocessable_entity)
82
91
  end
83
- Rails.logger.error("Unexpected error: #{e.class} - #{e.message}")
84
92
  end
85
93
 
86
- def update_field_assessment
87
- loan_application = @clazz.find(params[:id])
88
- if loan_application.update(field_assessment: field_assessment_params)
89
- render_success("loan_application.success.update_field_assessment", data: loan_application, serializer_options: { include: [ :bank, :user ] })
90
- else
91
- render_error("loan_application.errors.update_field_assessment", errors: loan_application.errors.full_messages[0], status: :unprocessable_entity)
94
+ def scoring_form
95
+ set_object
96
+ source_code = params[:source_code]
97
+ information_source = Dscf::Credit::InformationSource.find_by(code: source_code)
98
+
99
+ unless information_source
100
+ return render_error(errors: [ "Information source '#{source_code}' not found" ], status: :not_found)
92
101
  end
93
- rescue => e
94
- if Rails.env.development? || Rails.env.test?
95
- render_error(errors: e.message, status: :unprocessable_entity)
96
- else
97
- render_error(status: :unprocessable_entity)
102
+
103
+ category = @obj.category
104
+ scoring_table = category&.scoring_table
105
+
106
+ unless scoring_table
107
+ return render_error(errors: [ "No scoring table configured for this loan application's category" ], status: :unprocessable_entity)
98
108
  end
109
+
110
+ # Load parameters for this source with normalizers
111
+ parameters = scoring_table.scoring_table_parameters
112
+ .where(information_source: information_source)
113
+ .includes(:scoring_parameter, :scoring_table_normalizers)
114
+ .order(:order)
115
+
116
+ # Load existing submitted data for this source
117
+ existing_datum = @obj.loan_application_data.find_by(information_source: information_source)
118
+ existing_data = existing_datum&.data || {}
119
+
120
+ # Build response
121
+ form_data = {
122
+ source: {
123
+ code: information_source.code,
124
+ name: information_source.name
125
+ },
126
+ scoring_table: {
127
+ id: scoring_table.id,
128
+ name: scoring_table.name,
129
+ code: scoring_table.code
130
+ },
131
+ submitted: existing_datum&.submitted_at.present? || false,
132
+ submitted_at: existing_datum&.submitted_at,
133
+ fields: parameters.map do |stp|
134
+ param = stp.scoring_parameter
135
+ {
136
+ parameter_id: param.id,
137
+ code: param.code,
138
+ name: param.name,
139
+ data_type: param.data_type,
140
+ required: stp.required,
141
+ min_value: stp.min_value,
142
+ max_value: stp.max_value,
143
+ current_value: existing_data[param.id.to_s],
144
+ options: extract_options(stp, param.data_type)
145
+ }
146
+ end
147
+ }
148
+
149
+ render_success(data: form_data)
99
150
  end
100
151
 
101
152
  def calculate_credit_score
102
153
  loan_application = @clazz.find(params[:id])
103
- scoring_param_type_id = score_params[:scoring_param_type_id]
104
154
 
105
- unless scoring_param_type_id
106
- return render_error(
107
- "loan_application.errors.calculate_credit_score",
108
- errors: [ "Scoring parameter type ID is required for credit scoring" ],
109
- status: :unprocessable_entity
110
- )
111
- end
112
-
113
- scoring_engine = CreditScoringEngine.new(loan_application.id, scoring_param_type_id)
114
- result = scoring_engine.calculate_score
155
+ result = CreditScoringEngine.new(loan_application).calculate_score
115
156
 
116
157
  if result[:success]
158
+ scoring_result_data = {
159
+ scoring_table_id: loan_application.category&.scoring_table&.id,
160
+ breakdown: result[:breakdown],
161
+ scoring_input_data: build_scoring_input_snapshot(loan_application)
162
+ }
163
+
117
164
  ActiveRecord::Base.transaction do
118
165
  loan_application.update!(score: result[:score])
119
166
 
120
167
  update_review_status(loan_application, result[:status])
121
168
 
122
169
  if result[:status] == "approved"
123
- create_loan_profile_for_approved_application(loan_application, result[:score])
170
+ create_loan_profile_for_approved_application(loan_application, result[:score], scoring_result_data)
124
171
  end
125
172
 
126
173
  loan_application.reload
127
174
  end
128
175
 
176
+ eligibility = build_eligibility_response(loan_application)
177
+
129
178
  render_success(
130
179
  "loan_application.success.calculate_credit_score",
131
- data: result.merge(loan_application: loan_application),
180
+ data: result.merge(
181
+ loan_application: loan_application,
182
+ eligibility: eligibility
183
+ ),
132
184
  status: :ok
133
185
  )
134
186
  else
135
187
  render_error(
136
188
  "loan_application.errors.calculate_credit_score",
137
- errors: result[:errors] || [ result[:error] ],
189
+ errors: result[:errors],
138
190
  status: :unprocessable_entity
139
191
  )
140
192
  end
@@ -148,29 +200,20 @@ module Dscf::Credit
148
200
  Rails.logger.error("Credit scoring error: #{e.class} - #{e.message}")
149
201
  Rails.logger.error(e.backtrace.join("\n"))
150
202
 
151
- if Rails.env.development? || Rails.env.test?
152
- render_error(
153
- "loan_application.errors.calculate_credit_score",
154
- errors: [ e.message ],
155
- status: :unprocessable_entity
156
- )
157
- else
158
- render_error(
159
- "loan_application.errors.calculate_credit_score",
160
- errors: [ "An error occurred while calculating credit score" ],
161
- status: :unprocessable_entity
162
- )
163
- end
203
+ render_error(
204
+ "loan_application.errors.calculate_credit_score",
205
+ errors: [ "An error occurred while calculating credit score" ],
206
+ status: :unprocessable_entity
207
+ )
164
208
  end
165
209
 
166
- def create_loan_profile_for_approved_application(loan_application, score)
167
- profile_service = LoanProfileCreationService.new(loan_application, score)
210
+ def create_loan_profile_for_approved_application(loan_application, score, scoring_result_data = {})
211
+ profile_service = LoanProfileCreationService.new(loan_application, score, scoring_result_data)
168
212
  profile_result = profile_service.create_loan_profile
169
213
 
170
214
  unless profile_result[:success]
171
215
  error_message = "Failed to create loan profile for approved application #{loan_application.id}: #{profile_result[:error]}"
172
216
  Rails.logger.error error_message
173
- # Raise error to rollback the entire transaction including score and review status
174
217
  raise StandardError, error_message
175
218
  else
176
219
  Rails.logger.info "Loan profile created successfully for approved application #{loan_application.id}"
@@ -191,7 +234,7 @@ module Dscf::Credit
191
234
  status: status,
192
235
  reviewed_by: current_user,
193
236
  reviewed_at: Time.current,
194
- feedback: build_review_feedback(status, loan_application.score)
237
+ feedback: build_review_feedback(status, loan_application.score, loan_application)
195
238
  )
196
239
  else
197
240
  loan_application.reviews.create!(
@@ -199,89 +242,103 @@ module Dscf::Credit
199
242
  status: status,
200
243
  reviewed_by: current_user,
201
244
  reviewed_at: Time.current,
202
- feedback: build_review_feedback(status, loan_application.score)
245
+ feedback: build_review_feedback(status, loan_application.score, loan_application)
203
246
  )
204
247
  end
205
248
  end
206
249
 
207
250
  # Build feedback message based on status and score
208
- def build_review_feedback(status, score)
251
+ def build_review_feedback(status, score, loan_application = nil)
252
+ scoring_table = loan_application&.category&.scoring_table || @obj&.category&.scoring_table
253
+ passing = scoring_table&.passing_score || 60.0
254
+ pending_thresh = scoring_table&.pending_threshold || 50.0
255
+
209
256
  case status
210
257
  when "approved"
211
- {
212
- message: "Automatically approved based on credit score: #{score}% (>60%)"
213
- }
258
+ { message: "Automatically approved based on credit score: #{score}% (>=#{passing}%)" }
214
259
  when "rejected"
215
- {
216
- message: "Automatically rejected based on credit score: #{score}% (<50%)"
217
- }
260
+ { message: "Automatically rejected based on credit score: #{score}% (<#{pending_thresh}%)" }
218
261
  when "pending"
219
- {
220
- message: "Manual review required based on credit score: #{score}% (50%-60%)"
221
- }
262
+ { message: "Manual review required based on credit score: #{score}% (#{pending_thresh}%-#{passing}%)" }
222
263
  else
223
- {
224
- message: "Status determined by credit scoring system"
225
- }
264
+ { message: "Status determined by credit scoring system" }
226
265
  end
227
266
  end
228
267
 
229
- # Create loan profile for approved applications
230
- def create_loan_profile_from_review(review)
231
- loan_application = review.reviewable # Assumes Review belongs_to :reviewable (polymorphic or direct association to LoanApplication)
232
- score = loan_application.score # Assumes score is already set on the loan application
233
-
234
- unless score
235
- Rails.logger.warn "No score available for loan application #{loan_application.id} during approve callback"
236
- return
268
+ def build_scoring_input_snapshot(loan_application)
269
+ snapshot = {}
270
+ loan_application.loan_application_data.includes(:information_source).each do |datum|
271
+ source_code = datum.information_source.code
272
+ snapshot[source_code] = datum.data
237
273
  end
274
+ snapshot
275
+ end
238
276
 
239
- profile_service = LoanProfileCreationService.new(loan_application, score)
240
- profile_result = profile_service.create_loan_profile
277
+ def build_eligibility_response(loan_application)
278
+ category = loan_application.category
279
+ return [] unless category
280
+
281
+ score = loan_application.score.to_f
282
+ loan_profile = loan_application.loan_profile
283
+
284
+ category.credit_lines.includes(credit_line_specs: :base_scoring_parameter).map do |credit_line|
285
+ spec = credit_line.credit_line_specs.active.order(created_at: :desc).first
286
+ next unless spec
287
+
288
+ # If a loan profile exists, check the actual EligibleCreditLine record
289
+ if loan_profile
290
+ ecl = loan_profile.eligible_credit_lines.find_by(credit_line: credit_line)
291
+ eligible = ecl.present?
292
+ credit_limit = ecl&.credit_limit
293
+ reason = eligible ? nil : "Score #{score} below minimum #{spec.minimum_score}"
294
+ else
295
+ # No loan profile yet (pending/rejected) — evaluate purely on score vs minimum_score
296
+ eligible = false
297
+ credit_limit = nil
298
+ reason = if score < spec.minimum_score.to_f
299
+ "Score #{score} below minimum #{spec.minimum_score}"
300
+ else
301
+ "Not yet evaluated (application not approved)"
302
+ end
303
+ end
241
304
 
242
- unless profile_result[:success]
243
- error_message = "Failed to create loan profile for approved application #{loan_application.id}: #{profile_result[:error]}"
244
- Rails.logger.error error_message
245
- raise StandardError, error_message # This will rollback the transaction in perform_review_action
246
- else
247
- Rails.logger.info "Loan profile created successfully for approved application #{loan_application.id} via review callback"
248
- end
305
+ {
306
+ credit_line_id: credit_line.id,
307
+ credit_line_name: credit_line.name,
308
+ eligible: eligible,
309
+ credit_limit: credit_limit,
310
+ minimum_score: spec.minimum_score,
311
+ reason: reason
312
+ }
313
+ end.compact
314
+ end
315
+
316
+ def extract_options(scoring_table_parameter, data_type)
317
+ return nil unless %w[string boolean].include?(data_type)
318
+
319
+ text_normalizers = scoring_table_parameter.scoring_table_normalizers
320
+ .where.not(text_value: nil)
321
+ .pluck(:text_value)
322
+
323
+ text_normalizers.presence
249
324
  end
250
325
 
251
326
  def model_params
252
327
  params.require(:loan_application).permit(
253
328
  :bank_id,
329
+ :category_id,
254
330
  :backer_type,
255
331
  :backer_id,
256
332
  :review_branch_id,
257
333
  :bank_statement_source,
258
- user_info: {},
259
- facilitator_info: {},
260
- bank_info: {},
261
- field_assessment: {},
262
334
  bank_statement_attachments: []
263
335
  )
264
336
  end
265
337
 
266
- def bank_info_params
267
- params.require(:loan_application).require(:bank_info)
268
- end
269
-
270
- def facilitator_info_params
271
- params.require(:loan_application).require(:facilitator_info)
272
- end
273
-
274
- def field_assessment_params
275
- params.require(:loan_application).require(:field_assessment)
276
- end
277
-
278
- def score_params
279
- params.permit(:scoring_param_type_id)
280
- end
281
-
282
338
  def eager_loaded_associations
283
339
  [
284
- :bank, :user, :backer, :review_branch, :loan_profile,
340
+ :bank, :user, :backer, :review_branch, :loan_profile, :category,
341
+ loan_application_data: [ :information_source ],
285
342
  reviews: { reviewed_by: :user_profile },
286
343
  audit_logs: :actor
287
344
  ]
@@ -294,19 +351,23 @@ module Dscf::Credit
294
351
  def default_serializer_includes
295
352
  {
296
353
  index: [
297
- :bank, :user, :backer, :review_branch, :loan_profile,
354
+ :bank, :user, :backer, :review_branch, :loan_profile, :category,
355
+ loan_application_data: [ :information_source ],
298
356
  reviews: { reviewed_by: :user_profile }
299
357
  ],
300
358
  show: [
301
- :bank, :user, :backer, :review_branch, :loan_profile,
359
+ :bank, :user, :backer, :review_branch, :loan_profile, :category,
360
+ loan_application_data: [ :information_source ],
302
361
  reviews: { reviewed_by: :user_profile },
303
362
  audit_logs: :actor
304
363
  ],
305
364
  create: [
306
- :bank, :user, :backer, :review_branch, :loan_profile, :reviews
365
+ :bank, :user, :backer, :review_branch, :loan_profile, :category, :reviews,
366
+ { loan_application_data: [ :information_source ] }
307
367
  ],
308
368
  update: [
309
- :bank, :user, :backer, :review_branch, :loan_profile,
369
+ :bank, :user, :backer, :review_branch, :loan_profile, :category,
370
+ loan_application_data: [ :information_source ],
310
371
  reviews: { reviewed_by: :user_profile }
311
372
  ]
312
373
  }