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.
- checksums.yaml +4 -4
- data/app/controllers/concerns/dscf/core/copilot-instructions.md +683 -0
- data/app/controllers/concerns/dscf/core/reviewable_controller.rb +347 -0
- data/app/controllers/dscf/credit/categories_controller.rb +3 -3
- data/app/controllers/dscf/credit/credit_lines_controller.rb +21 -13
- data/app/controllers/dscf/credit/eligible_credit_lines_controller.rb +50 -8
- data/app/controllers/dscf/credit/facilitator_applications_controller.rb +35 -0
- data/app/controllers/dscf/credit/facilitators_controller.rb +8 -96
- data/app/controllers/dscf/credit/loan_applications_controller.rb +252 -0
- data/app/controllers/dscf/credit/loan_profiles_controller.rb +61 -68
- data/app/controllers/dscf/credit/payment_requests_controller.rb +3 -5
- data/app/controllers/dscf/credit/scoring_parameters_controller.rb +59 -13
- data/app/controllers/dscf/credit/system_configs_controller.rb +30 -12
- data/app/models/concerns/core/reviewable_model.rb +31 -0
- data/app/models/dscf/credit/bank.rb +3 -3
- data/app/models/dscf/credit/bank_branch.rb +1 -1
- data/app/models/dscf/credit/category.rb +1 -2
- data/app/models/dscf/credit/credit_line.rb +4 -10
- data/app/models/dscf/credit/eligible_credit_line.rb +2 -2
- data/app/models/dscf/credit/facilitator.rb +6 -17
- data/app/models/dscf/credit/facilitator_application.rb +20 -0
- data/app/models/dscf/credit/loan_application.rb +30 -0
- data/app/models/dscf/credit/loan_profile.rb +10 -30
- data/app/models/dscf/credit/parameter_normalizer.rb +1 -1
- data/app/models/dscf/credit/scoring_parameter.rb +5 -7
- data/app/models/dscf/credit/system_config.rb +4 -9
- data/app/serializers/dscf/credit/category_serializer.rb +0 -1
- data/app/serializers/dscf/credit/credit_line_serializer.rb +2 -2
- data/app/serializers/dscf/credit/facilitator_application_serializer.rb +7 -0
- data/app/serializers/dscf/credit/facilitator_serializer.rb +3 -6
- data/app/serializers/dscf/credit/loan_application_serializer.rb +12 -0
- data/app/serializers/dscf/credit/loan_profile_serializer.rb +3 -6
- data/app/serializers/dscf/credit/scoring_parameter_serializer.rb +3 -4
- data/app/serializers/dscf/credit/system_config_serializer.rb +2 -2
- data/app/services/dscf/credit/credit_scoring_engine.rb +258 -0
- data/app/services/dscf/credit/facility_limit_calculation_engine.rb +159 -0
- data/app/services/dscf/credit/loan_profile_creation_service.rb +91 -0
- data/app/services/dscf/credit/risk_application_service.rb +61 -11
- data/config/locales/en.yml +63 -48
- data/config/routes.rb +31 -17
- data/db/migrate/20250822091131_create_dscf_credit_credit_lines.rb +1 -8
- data/db/migrate/20250822091820_create_dscf_credit_system_configs.rb +0 -7
- data/db/migrate/20250822092050_create_dscf_credit_scoring_parameters.rb +2 -6
- data/db/migrate/20250822092225_create_dscf_credit_parameter_normalizers.rb +1 -1
- data/db/migrate/20250822092236_create_dscf_credit_loan_applications.rb +20 -0
- data/db/migrate/20250822092246_create_dscf_credit_loan_profiles.rb +7 -19
- data/db/migrate/20250822092426_create_dscf_credit_facilitator_applications.rb +10 -0
- data/db/migrate/20250822092436_create_dscf_credit_facilitators.rb +1 -16
- data/db/seeds.rb +316 -203
- data/lib/dscf/credit/version.rb +1 -1
- data/spec/factories/dscf/credit/banks.rb +1 -1
- data/spec/factories/dscf/credit/credit_lines.rb +0 -23
- data/spec/factories/dscf/credit/facilitator_applications.rb +37 -0
- data/spec/factories/dscf/credit/facilitators.rb +8 -30
- data/spec/factories/dscf/credit/loan_applications.rb +42 -0
- data/spec/factories/dscf/credit/loan_profiles.rb +20 -34
- data/spec/factories/dscf/credit/parameter_normalizers.rb +4 -4
- data/spec/factories/dscf/credit/scoring_parameters.rb +14 -11
- data/spec/factories/dscf/credit/system_configs.rb +21 -5
- metadata +20 -10
- data/app/controllers/concerns/dscf/credit/reviewable.rb +0 -112
- data/app/controllers/dscf/credit/scoring_tables_controller.rb +0 -63
- data/app/models/dscf/credit/scoring_table.rb +0 -24
- data/app/serializers/dscf/credit/scoring_table_serializer.rb +0 -9
- data/db/migrate/20250901172842_create_dscf_credit_scoring_tables.rb +0 -18
- data/spec/factories/dscf/credit/scoring_tables.rb +0 -25
@@ -0,0 +1,252 @@
|
|
1
|
+
module Dscf::Credit
|
2
|
+
class LoanApplicationsController < ApplicationController
|
3
|
+
include Dscf::Core::Common
|
4
|
+
include Dscf::Core::ReviewableController
|
5
|
+
|
6
|
+
def create
|
7
|
+
super do
|
8
|
+
loan_application = @clazz.new(model_params)
|
9
|
+
loan_application.user = current_user
|
10
|
+
loan_application.reviews.build(context: "default", status: "pending")
|
11
|
+
|
12
|
+
loan_application
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def update_bank_info
|
17
|
+
loan_application = @clazz.find(params[:id])
|
18
|
+
if loan_application.update(bank_info: bank_info_params)
|
19
|
+
render_success("loan_application.success.update_bank_info", data: loan_application, serializer_options: { include: [ :bank, :user ] })
|
20
|
+
else
|
21
|
+
render_error("loan_application.errors.update_bank_info", errors: loan_application.errors.full_messages[0], status: :unprocessable_entity)
|
22
|
+
end
|
23
|
+
rescue => e
|
24
|
+
if Rails.env.development? || Rails.env.test?
|
25
|
+
render_error(errors: e.message, status: :unprocessable_entity)
|
26
|
+
else
|
27
|
+
render_error(status: :unprocessable_entity)
|
28
|
+
end
|
29
|
+
Rails.logger.error("Unexpected error: #{e.class} - #{e.message}")
|
30
|
+
end
|
31
|
+
|
32
|
+
def update_facilitator_info
|
33
|
+
loan_application = @clazz.find(params[:id])
|
34
|
+
if loan_application.update(facilitator_info: facilitator_info_params)
|
35
|
+
render_success("loan_application.success.update_facilitator_info", data: loan_application, serializer_options: { include: [ :bank, :user ] })
|
36
|
+
else
|
37
|
+
render_error("loan_application.errors.update_facilitator_info", errors: loan_application.errors.full_messages[0], status: :unprocessable_entity)
|
38
|
+
end
|
39
|
+
rescue => e
|
40
|
+
if Rails.env.development? || Rails.env.test?
|
41
|
+
render_error(errors: e.message, status: :unprocessable_entity)
|
42
|
+
else
|
43
|
+
render_error(status: :unprocessable_entity)
|
44
|
+
end
|
45
|
+
Rails.logger.error("Unexpected error: #{e.class} - #{e.message}")
|
46
|
+
end
|
47
|
+
|
48
|
+
def update_field_assessment
|
49
|
+
loan_application = @clazz.find(params[:id])
|
50
|
+
if loan_application.update(field_assessment: field_assessment_params)
|
51
|
+
render_success("loan_application.success.update_field_assessment", data: loan_application, serializer_options: { include: [ :bank, :user ] })
|
52
|
+
else
|
53
|
+
render_error("loan_application.errors.update_field_assessment", errors: loan_application.errors.full_messages[0], status: :unprocessable_entity)
|
54
|
+
end
|
55
|
+
rescue => e
|
56
|
+
if Rails.env.development? || Rails.env.test?
|
57
|
+
render_error(errors: e.message, status: :unprocessable_entity)
|
58
|
+
else
|
59
|
+
render_error(status: :unprocessable_entity)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def calculate_credit_score
|
64
|
+
loan_application = @clazz.find(params[:id])
|
65
|
+
category_id = score_params[:category_id]
|
66
|
+
|
67
|
+
unless category_id
|
68
|
+
return render_error(
|
69
|
+
"loan_application.errors.calculate_credit_score",
|
70
|
+
errors: [ "Category ID is required for credit scoring" ],
|
71
|
+
status: :unprocessable_entity
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
75
|
+
scoring_engine = CreditScoringEngine.new(loan_application.id, category_id)
|
76
|
+
result = scoring_engine.calculate_score
|
77
|
+
|
78
|
+
if result[:success]
|
79
|
+
ActiveRecord::Base.transaction do
|
80
|
+
loan_application.update!(score: result[:score])
|
81
|
+
|
82
|
+
update_review_status(loan_application, result[:status])
|
83
|
+
|
84
|
+
if result[:status] == "approved"
|
85
|
+
create_loan_profile_for_approved_application(loan_application, result[:score])
|
86
|
+
end
|
87
|
+
|
88
|
+
loan_application.reload
|
89
|
+
end
|
90
|
+
|
91
|
+
render_success(
|
92
|
+
"loan_application.success.calculate_credit_score",
|
93
|
+
data: result.merge(loan_application: loan_application),
|
94
|
+
status: :ok
|
95
|
+
)
|
96
|
+
else
|
97
|
+
render_error(
|
98
|
+
"loan_application.errors.calculate_credit_score",
|
99
|
+
errors: result[:errors] || [ result[:error] ],
|
100
|
+
status: :unprocessable_entity
|
101
|
+
)
|
102
|
+
end
|
103
|
+
rescue ActiveRecord::RecordNotFound
|
104
|
+
render_error(
|
105
|
+
"loan_application.errors.not_found",
|
106
|
+
errors: [ "Loan application not found" ],
|
107
|
+
status: :not_found
|
108
|
+
)
|
109
|
+
rescue => e
|
110
|
+
Rails.logger.error("Credit scoring error: #{e.class} - #{e.message}")
|
111
|
+
Rails.logger.error(e.backtrace.join("\n"))
|
112
|
+
|
113
|
+
if Rails.env.development? || Rails.env.test?
|
114
|
+
render_error(
|
115
|
+
"loan_application.errors.calculate_credit_score",
|
116
|
+
errors: [ e.message ],
|
117
|
+
status: :unprocessable_entity
|
118
|
+
)
|
119
|
+
else
|
120
|
+
render_error(
|
121
|
+
"loan_application.errors.calculate_credit_score",
|
122
|
+
errors: [ "An error occurred while calculating credit score" ],
|
123
|
+
status: :unprocessable_entity
|
124
|
+
)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
def update_review_status(loan_application, status)
|
131
|
+
current_review = loan_application.current_review_for(:default)
|
132
|
+
|
133
|
+
if current_review
|
134
|
+
current_review.update!(
|
135
|
+
status: status,
|
136
|
+
reviewed_by: current_user,
|
137
|
+
reviewed_at: Time.current,
|
138
|
+
feedback: build_review_feedback(status, loan_application.score)
|
139
|
+
)
|
140
|
+
else
|
141
|
+
loan_application.reviews.create!(
|
142
|
+
context: "default",
|
143
|
+
status: status,
|
144
|
+
reviewed_by: current_user,
|
145
|
+
reviewed_at: Time.current,
|
146
|
+
feedback: build_review_feedback(status, loan_application.score)
|
147
|
+
)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# Build feedback message based on status and score
|
152
|
+
def build_review_feedback(status, score)
|
153
|
+
case status
|
154
|
+
when "approved"
|
155
|
+
{
|
156
|
+
message: "Automatically approved based on credit score: #{score}% (>60%)"
|
157
|
+
}
|
158
|
+
when "rejected"
|
159
|
+
{
|
160
|
+
message: "Automatically rejected based on credit score: #{score}% (<50%)"
|
161
|
+
}
|
162
|
+
when "pending_review"
|
163
|
+
{
|
164
|
+
message: "Manual review required based on credit score: #{score}% (50%-60%)"
|
165
|
+
}
|
166
|
+
else
|
167
|
+
{
|
168
|
+
message: "Status determined by credit scoring system"
|
169
|
+
}
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Create loan profile for approved applications
|
174
|
+
def create_loan_profile_for_approved_application(loan_application, score)
|
175
|
+
profile_service = LoanProfileCreationService.new(loan_application, score)
|
176
|
+
profile_result = profile_service.create_loan_profile
|
177
|
+
|
178
|
+
unless profile_result[:success]
|
179
|
+
error_message = "Failed to create loan profile for approved application #{loan_application.id}: #{profile_result[:error]}"
|
180
|
+
Rails.logger.error error_message
|
181
|
+
# Raise error to rollback the entire transaction including score and review status
|
182
|
+
raise StandardError, error_message
|
183
|
+
else
|
184
|
+
Rails.logger.info "Loan profile created successfully for approved application #{loan_application.id}"
|
185
|
+
end
|
186
|
+
|
187
|
+
profile_result
|
188
|
+
end
|
189
|
+
|
190
|
+
def model_params
|
191
|
+
params.require(:loan_application).permit(
|
192
|
+
:bank_id,
|
193
|
+
:backer_type,
|
194
|
+
:backer_id,
|
195
|
+
:review_branch_id,
|
196
|
+
:bank_statement_source,
|
197
|
+
user_info: {},
|
198
|
+
facilitator_info: {},
|
199
|
+
bank_info: {},
|
200
|
+
field_assessment: {},
|
201
|
+
bank_statement_attachments: []
|
202
|
+
)
|
203
|
+
end
|
204
|
+
|
205
|
+
def bank_info_params
|
206
|
+
params.require(:loan_application).require(:bank_info)
|
207
|
+
end
|
208
|
+
|
209
|
+
def facilitator_info_params
|
210
|
+
params.require(:loan_application).require(:facilitator_info)
|
211
|
+
end
|
212
|
+
|
213
|
+
def field_assessment_params
|
214
|
+
params.require(:loan_application).require(:field_assessment)
|
215
|
+
end
|
216
|
+
|
217
|
+
def score_params
|
218
|
+
params.permit(:category_id)
|
219
|
+
end
|
220
|
+
|
221
|
+
def eager_loaded_associations
|
222
|
+
[
|
223
|
+
:bank, :user, :backer, :review_branch, :loan_profile,
|
224
|
+
reviews: { reviewed_by: :user_profile }
|
225
|
+
]
|
226
|
+
end
|
227
|
+
|
228
|
+
def allowed_order_columns
|
229
|
+
%w[id bank_statement_source created_at updated_at]
|
230
|
+
end
|
231
|
+
|
232
|
+
def default_serializer_includes
|
233
|
+
{
|
234
|
+
index: [
|
235
|
+
:bank, :user, :backer, :review_branch, :loan_profile,
|
236
|
+
reviews: { reviewed_by: :user_profile }
|
237
|
+
],
|
238
|
+
show: [
|
239
|
+
:bank, :user, :backer, :review_branch, :loan_profile,
|
240
|
+
reviews: { reviewed_by: :user_profile }
|
241
|
+
],
|
242
|
+
create: [
|
243
|
+
:bank, :user, :backer, :review_branch, :loan_profile, :reviews
|
244
|
+
],
|
245
|
+
update: [
|
246
|
+
:bank, :user, :backer, :review_branch, :loan_profile,
|
247
|
+
reviews: { reviewed_by: :user_profile }
|
248
|
+
]
|
249
|
+
}
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
@@ -1,78 +1,46 @@
|
|
1
1
|
module Dscf::Credit
|
2
2
|
class LoanProfilesController < ApplicationController
|
3
3
|
include Dscf::Core::Common
|
4
|
-
include Dscf::
|
4
|
+
include Dscf::Core::ReviewableController
|
5
5
|
|
6
6
|
def create
|
7
7
|
super do
|
8
8
|
loan_profile = @clazz.new(model_params)
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
if loan_profile.user.present?
|
14
|
-
loan_profile.loan_profile_scoring_specs.create!(
|
15
|
-
created_by: loan_profile.user
|
16
|
-
)
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
9
|
+
loan_profile.reviews.build(
|
10
|
+
status: "pending",
|
11
|
+
context: "default",
|
12
|
+
)
|
20
13
|
loan_profile
|
21
14
|
end
|
22
15
|
end
|
23
16
|
|
24
|
-
|
17
|
+
|
18
|
+
def calculate_facility_limits
|
25
19
|
loan_profile = @clazz.find(params[:id])
|
26
|
-
|
20
|
+
category_id = facility_params[:category_id]
|
27
21
|
|
28
|
-
|
22
|
+
return render_error(
|
23
|
+
"loan_profile.errors.calculate_facility_limits",
|
24
|
+
errors: [ "Category ID is required" ],
|
25
|
+
status: :unprocessable_entity
|
26
|
+
) unless category_id
|
29
27
|
|
30
|
-
|
28
|
+
facility_engine = FacilityLimitCalculationEngine.new(loan_profile.id, category_id)
|
29
|
+
result = facility_engine.calculate_facility_limits
|
31
30
|
|
32
31
|
if result[:success]
|
33
|
-
ActiveRecord::Base.transaction do
|
34
|
-
scoring_spec = if external_scoring_data.present?
|
35
|
-
loan_profile.loan_profile_scoring_specs.find_or_initialize_by(
|
36
|
-
scoring_input_data: external_scoring_data
|
37
|
-
)
|
38
|
-
else
|
39
|
-
# If no external data, find existing spec with input data or latest one
|
40
|
-
loan_profile.loan_profile_scoring_specs
|
41
|
-
.where.not(scoring_input_data: [ nil, {} ])
|
42
|
-
.order(created_at: :desc)
|
43
|
-
.first ||
|
44
|
-
loan_profile.loan_profile_scoring_specs.order(created_at: :desc).first ||
|
45
|
-
loan_profile.loan_profile_scoring_specs.build
|
46
|
-
end
|
47
|
-
|
48
|
-
loan_profile.loan_profile_scoring_specs.where.not(id: scoring_spec.id).update_all(active: false)
|
49
|
-
|
50
|
-
scoring_spec.assign_attributes(
|
51
|
-
score: result[:score],
|
52
|
-
total_limit: result[:facility_limit],
|
53
|
-
scoring_input_data: result[:scoring_input_data] || scoring_spec.scoring_input_data || {},
|
54
|
-
active: true,
|
55
|
-
created_by: scoring_spec.new_record? ? current_user : scoring_spec.created_by
|
56
|
-
)
|
57
|
-
|
58
|
-
scoring_spec.save!
|
59
|
-
|
60
|
-
update_loan_profile_status(loan_profile, result[:score])
|
61
|
-
end
|
62
|
-
|
63
32
|
loan_profile.reload
|
64
|
-
|
65
33
|
render_success(
|
66
|
-
"loan_profile.success.
|
34
|
+
"loan_profile.success.calculate_facility_limits",
|
67
35
|
data: {
|
68
36
|
loan_profile: loan_profile,
|
69
|
-
|
37
|
+
facility_result: result
|
70
38
|
},
|
71
|
-
serializer_options: { include: [ :
|
39
|
+
serializer_options: { include: [ :eligible_credit_lines ] }
|
72
40
|
)
|
73
41
|
else
|
74
42
|
render_error(
|
75
|
-
"loan_profile.errors.
|
43
|
+
"loan_profile.errors.calculate_facility_limits",
|
76
44
|
errors: [ result[:error] ],
|
77
45
|
status: :unprocessable_entity
|
78
46
|
)
|
@@ -82,6 +50,23 @@ module Dscf::Credit
|
|
82
50
|
"loan_profile.errors.not_found",
|
83
51
|
status: :not_found
|
84
52
|
)
|
53
|
+
rescue => e
|
54
|
+
Rails.logger.error("Facility limit calculation error: #{e.class} - #{e.message}")
|
55
|
+
Rails.logger.error(e.backtrace.join("\n"))
|
56
|
+
|
57
|
+
if Rails.env.development? || Rails.env.test?
|
58
|
+
render_error(
|
59
|
+
"loan_profile.errors.calculate_facility_limits",
|
60
|
+
errors: [ e.message ],
|
61
|
+
status: :unprocessable_entity
|
62
|
+
)
|
63
|
+
else
|
64
|
+
render_error(
|
65
|
+
"loan_profile.errors.calculate_facility_limits",
|
66
|
+
errors: [ "An error occurred while calculating facility limits" ],
|
67
|
+
status: :unprocessable_entity
|
68
|
+
)
|
69
|
+
end
|
85
70
|
end
|
86
71
|
|
87
72
|
private
|
@@ -96,10 +81,10 @@ module Dscf::Credit
|
|
96
81
|
"approved"
|
97
82
|
end
|
98
83
|
|
99
|
-
loan_profile.
|
84
|
+
loan_profile.reviews.create!(
|
100
85
|
status: new_status,
|
101
|
-
|
102
|
-
|
86
|
+
context: "default",
|
87
|
+
reviewed_by: current_user
|
103
88
|
)
|
104
89
|
end
|
105
90
|
|
@@ -107,31 +92,39 @@ module Dscf::Credit
|
|
107
92
|
|
108
93
|
def model_params
|
109
94
|
params.require(:loan_profile).permit(
|
110
|
-
:
|
111
|
-
:
|
112
|
-
:
|
113
|
-
:
|
114
|
-
:backer_type,
|
115
|
-
:status,
|
116
|
-
:total_amount,
|
117
|
-
:available_amount
|
95
|
+
:loan_application_id,
|
96
|
+
:code,
|
97
|
+
:score,
|
98
|
+
:total_limit
|
118
99
|
)
|
119
100
|
end
|
120
101
|
|
102
|
+
def facility_params
|
103
|
+
params.permit(:category_id)
|
104
|
+
end
|
105
|
+
|
121
106
|
def eager_loaded_associations
|
122
|
-
[
|
107
|
+
[
|
108
|
+
:loan_application, :loan_profile_scoring_specs, :loans, :eligible_credit_lines,
|
109
|
+
reviews: { reviewed_by: :user_profile }
|
110
|
+
]
|
123
111
|
end
|
124
112
|
|
125
113
|
def allowed_order_columns
|
126
|
-
%w[id
|
114
|
+
%w[id code score total_limit created_at updated_at]
|
127
115
|
end
|
128
116
|
|
129
117
|
def default_serializer_includes
|
130
118
|
{
|
131
|
-
index: [ :
|
132
|
-
show: [
|
133
|
-
|
134
|
-
|
119
|
+
index: [ :loan_application, reviews: { reviewed_by: :user_profile } ],
|
120
|
+
show: [
|
121
|
+
:loan_application, :loan_profile_scoring_specs, :loans, :eligible_credit_lines, reviews: { reviewed_by: :user_profile }
|
122
|
+
],
|
123
|
+
create: [ :loan_application, :loan_profile_scoring_specs, :reviews ],
|
124
|
+
update: [
|
125
|
+
:loan_application, :loan_profile_scoring_specs, :loans, :eligible_credit_lines,
|
126
|
+
reviews: { reviewed_by: :user_profile }
|
127
|
+
]
|
135
128
|
}
|
136
129
|
end
|
137
130
|
end
|
@@ -36,11 +36,9 @@ module Dscf::Credit
|
|
36
36
|
# Find all eligible credit lines for the user through their approved loan profiles
|
37
37
|
# Only include credit lines with available credit
|
38
38
|
eligible_credit_lines = Dscf::Credit::EligibleCreditLine
|
39
|
-
.joins(loan_profile: :user)
|
40
|
-
.where(
|
41
|
-
|
42
|
-
status: [ "approved", "kyc_approved" ] # Only approved loan profiles
|
43
|
-
})
|
39
|
+
.joins(loan_profile: [ { loan_application: :user }, :reviews ])
|
40
|
+
.where(dscf_credit_loan_applications: { user_id: payment_request.user_id })
|
41
|
+
.where(dscf_core_reviews: { status: "approved" }) # Only approved loan profiles via reviews
|
44
42
|
.where("dscf_credit_eligible_credit_lines.available_limit > 0") # Only with available credit
|
45
43
|
.includes(:loan_profile, :credit_line)
|
46
44
|
.order("dscf_credit_eligible_credit_lines.available_limit DESC") # Order by available limit
|
@@ -1,23 +1,40 @@
|
|
1
1
|
module Dscf::Credit
|
2
2
|
class ScoringParametersController < ApplicationController
|
3
3
|
include Dscf::Core::Common
|
4
|
-
include Dscf::
|
4
|
+
include Dscf::Core::ReviewableController
|
5
5
|
|
6
6
|
def create
|
7
7
|
super do
|
8
8
|
scoring_parameter = @clazz.new(model_params)
|
9
9
|
scoring_parameter.created_by = current_user
|
10
|
-
scoring_parameter.
|
10
|
+
scoring_parameter.reviews.build(context: "default", status: "pending")
|
11
11
|
|
12
|
-
scoring_parameter
|
12
|
+
if validate_category_weight_limit(scoring_parameter)
|
13
|
+
scoring_parameter
|
14
|
+
else
|
15
|
+
scoring_parameter.errors.add(:weight, "would exceed the total weight limit of 1.0 for this category")
|
16
|
+
render_error(errors: scoring_parameter.errors.full_messages, status: :unprocessable_entity)
|
17
|
+
return
|
18
|
+
end
|
13
19
|
end
|
14
20
|
end
|
15
21
|
|
22
|
+
def update
|
23
|
+
unless validate_category_weight_limit(@obj, exclude_current: true)
|
24
|
+
@obj.errors.add(:weight, "would exceed the total weight limit of 1.0 for this category")
|
25
|
+
render_error(errors: @obj.errors.full_messages, status: :unprocessable_entity)
|
26
|
+
return
|
27
|
+
end
|
28
|
+
|
29
|
+
super
|
30
|
+
end
|
31
|
+
|
16
32
|
private
|
17
33
|
|
18
34
|
def model_params
|
19
35
|
params.require(:scoring_parameter).permit(
|
20
36
|
:bank_id,
|
37
|
+
:category_id,
|
21
38
|
:name,
|
22
39
|
:description,
|
23
40
|
:data_type,
|
@@ -26,30 +43,59 @@ module Dscf::Credit
|
|
26
43
|
:max_value,
|
27
44
|
:active,
|
28
45
|
:previous_version_id,
|
29
|
-
:review_date,
|
30
46
|
:source,
|
31
47
|
:scoring_param_type_id,
|
32
|
-
:document_reference
|
33
|
-
:status,
|
34
|
-
:review_feedback
|
48
|
+
:document_reference
|
35
49
|
)
|
36
50
|
end
|
37
51
|
|
38
52
|
def eager_loaded_associations
|
39
|
-
[
|
53
|
+
[
|
54
|
+
:bank, :category, :created_by, :scoring_param_type, :previous_version, :parameter_normalizers,
|
55
|
+
reviews: { reviewed_by: :user_profile }
|
56
|
+
]
|
40
57
|
end
|
41
58
|
|
42
59
|
def allowed_order_columns
|
43
|
-
%w[id name data_type weight active
|
60
|
+
%w[id name data_type weight active created_at updated_at]
|
44
61
|
end
|
45
62
|
|
46
63
|
def default_serializer_includes
|
47
64
|
{
|
48
|
-
index: [ :bank ],
|
49
|
-
show: [
|
50
|
-
|
51
|
-
|
65
|
+
index: [ :bank, :category, reviews: { reviewed_by: :user_profile } ],
|
66
|
+
show: [
|
67
|
+
:bank, :category, :created_by, :scoring_param_type, :previous_version, :parameter_normalizers,
|
68
|
+
reviews: { reviewed_by: :user_profile }
|
69
|
+
],
|
70
|
+
create: [ :bank, :category, :created_by, :scoring_param_type ],
|
71
|
+
update: [
|
72
|
+
:bank, :category, :created_by, :scoring_param_type, :parameter_normalizers,
|
73
|
+
reviews: { reviewed_by: :user_profile }
|
74
|
+
]
|
52
75
|
}
|
53
76
|
end
|
77
|
+
|
78
|
+
def validate_category_weight_limit(scoring_parameter, exclude_current: false)
|
79
|
+
return true unless scoring_parameter.category_id && scoring_parameter.weight
|
80
|
+
|
81
|
+
# Get existing parameters in the same category for the same bank
|
82
|
+
existing_params = ScoringParameter.where(
|
83
|
+
bank_id: scoring_parameter.bank_id,
|
84
|
+
category_id: scoring_parameter.category_id,
|
85
|
+
active: true
|
86
|
+
)
|
87
|
+
|
88
|
+
# Exclude current parameter from the calculation when updating
|
89
|
+
if exclude_current && scoring_parameter.persisted?
|
90
|
+
existing_params = existing_params.where.not(id: scoring_parameter.id)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Calculate total weight
|
94
|
+
current_total_weight = existing_params.sum(:weight)
|
95
|
+
new_total_weight = current_total_weight + scoring_parameter.weight.to_f
|
96
|
+
|
97
|
+
# Allow small floating point tolerance (e.g., 1.0001 should be acceptable)
|
98
|
+
new_total_weight <= 1.001
|
99
|
+
end
|
54
100
|
end
|
55
101
|
end
|
@@ -1,13 +1,14 @@
|
|
1
1
|
module Dscf::Credit
|
2
2
|
class SystemConfigsController < ApplicationController
|
3
3
|
include Dscf::Core::Common
|
4
|
-
include Dscf::
|
4
|
+
include Dscf::Core::ReviewableController
|
5
5
|
|
6
6
|
def create
|
7
7
|
super do
|
8
8
|
obj = @clazz.new(model_params)
|
9
|
-
obj.reviewed_by = current_user
|
10
9
|
obj.last_updated_by = current_user
|
10
|
+
obj.reviews.build(context: "default", status: "pending")
|
11
|
+
|
11
12
|
obj
|
12
13
|
end
|
13
14
|
end
|
@@ -25,27 +26,44 @@ module Dscf::Credit
|
|
25
26
|
def model_params
|
26
27
|
params.require(:system_config).permit(
|
27
28
|
:config_definition_id,
|
28
|
-
:config_value
|
29
|
-
:status,
|
30
|
-
:review_date,
|
31
|
-
:review_feedback
|
29
|
+
:config_value
|
32
30
|
)
|
33
31
|
end
|
34
32
|
|
35
33
|
def eager_loaded_associations
|
36
|
-
[
|
34
|
+
[
|
35
|
+
:config_definition,
|
36
|
+
:last_updated_by,
|
37
|
+
reviews: { reviewed_by: :user_profile }
|
38
|
+
]
|
37
39
|
end
|
38
40
|
|
39
41
|
def allowed_order_columns
|
40
|
-
%w[id config_value
|
42
|
+
%w[id config_value created_at updated_at]
|
41
43
|
end
|
42
44
|
|
43
45
|
def default_serializer_includes
|
44
46
|
{
|
45
|
-
index: [
|
46
|
-
|
47
|
-
|
48
|
-
|
47
|
+
index: [
|
48
|
+
:config_definition,
|
49
|
+
reviews: { reviewed_by: :user_profile }
|
50
|
+
],
|
51
|
+
show: [
|
52
|
+
:config_definition,
|
53
|
+
:last_updated_by,
|
54
|
+
reviews: { reviewed_by: :user_profile }
|
55
|
+
],
|
56
|
+
create: [
|
57
|
+
:config_definition,
|
58
|
+
:last_updated_by,
|
59
|
+
:reviews
|
60
|
+
|
61
|
+
],
|
62
|
+
update: [
|
63
|
+
:config_definition,
|
64
|
+
:last_updated_by,
|
65
|
+
reviews: { reviewed_by: :user_profile }
|
66
|
+
]
|
49
67
|
}
|
50
68
|
end
|
51
69
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Dscf
|
2
|
+
module Core
|
3
|
+
module ReviewableModel
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
has_many :reviews, as: :reviewable, class_name: "Dscf::Core::Review", dependent: :destroy
|
8
|
+
end
|
9
|
+
|
10
|
+
def review_for(context = :default)
|
11
|
+
reviews.with_context(context).order(created_at: :desc)
|
12
|
+
end
|
13
|
+
|
14
|
+
def current_review_for(context = :default)
|
15
|
+
review_for(context).first
|
16
|
+
end
|
17
|
+
|
18
|
+
def current_status_for(context = :default)
|
19
|
+
current_review = current_review_for(context)
|
20
|
+
return current_review.status if current_review
|
21
|
+
|
22
|
+
# Return default initial status for zero-config
|
23
|
+
"pending"
|
24
|
+
end
|
25
|
+
|
26
|
+
def build_review_for(context = :default)
|
27
|
+
reviews.build(context: context.to_s)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -13,10 +13,10 @@ module Dscf::Credit
|
|
13
13
|
foreign_key: "bank_id", dependent: :destroy
|
14
14
|
has_many :scoring_parameters, class_name: "Dscf::Credit::ScoringParameter",
|
15
15
|
foreign_key: "bank_id", dependent: :destroy
|
16
|
-
has_many :
|
17
|
-
foreign_key: "bank_id", dependent: :destroy
|
18
|
-
has_many :facilitators, class_name: "Dscf::Credit::Facilitator",
|
16
|
+
has_many :loan_applications, class_name: "Dscf::Credit::LoanApplication",
|
19
17
|
foreign_key: "bank_id", dependent: :destroy
|
18
|
+
has_many :facilitators, through: :facilitator_applications, class_name: "Dscf::Credit::Facilitator"
|
19
|
+
has_many :facilitator_applications, class_name: "Dscf::Credit::FacilitatorApplication"
|
20
20
|
has_many :bank_staffs, through: :bank_branches, class_name: "Dscf::Credit::BankStaff"
|
21
21
|
|
22
22
|
validates :name, presence: true
|