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.
- checksums.yaml +4 -4
- data/app/controllers/dscf/credit/categories_controller.rb +29 -25
- data/app/controllers/dscf/credit/credit_limit_calculations_controller.rb +4 -3
- data/app/controllers/dscf/credit/credit_line_specs_controller.rb +2 -1
- data/app/controllers/dscf/credit/information_sources_controller.rb +93 -0
- data/app/controllers/dscf/credit/loan_application_data_controller.rb +35 -0
- data/app/controllers/dscf/credit/loan_applications_controller.rb +186 -125
- data/app/controllers/dscf/credit/loan_profiles_controller.rb +4 -4
- data/app/controllers/dscf/credit/scoring_parameters_controller.rb +8 -45
- data/app/controllers/dscf/credit/scoring_table_normalizers_controller.rb +38 -0
- data/app/controllers/dscf/credit/scoring_table_parameters_controller.rb +45 -0
- data/app/controllers/dscf/credit/scoring_tables_controller.rb +98 -0
- data/app/models/dscf/credit/category.rb +21 -13
- data/app/models/dscf/credit/credit_line_spec.rb +56 -36
- data/app/models/dscf/credit/information_source.rb +24 -0
- data/app/models/dscf/credit/loan_application.rb +3 -2
- data/app/models/dscf/credit/loan_application_datum.rb +20 -0
- data/app/models/dscf/credit/loan_profile.rb +2 -2
- data/app/models/dscf/credit/scoring_parameter.rb +24 -21
- data/app/models/dscf/credit/scoring_result.rb +22 -0
- data/app/models/dscf/credit/scoring_table.rb +47 -0
- data/app/models/dscf/credit/scoring_table_normalizer.rb +53 -0
- data/app/models/dscf/credit/scoring_table_parameter.rb +46 -0
- data/app/serializers/dscf/credit/category_serializer.rb +9 -5
- data/app/serializers/dscf/credit/credit_line_spec_serializer.rb +13 -10
- data/app/serializers/dscf/credit/information_source_serializer.rb +10 -0
- data/app/serializers/dscf/credit/loan_application_datum_serializer.rb +8 -0
- data/app/serializers/dscf/credit/loan_application_serializer.rb +3 -2
- data/app/serializers/dscf/credit/loan_profile_serializer.rb +1 -1
- data/app/serializers/dscf/credit/scoring_parameter_serializer.rb +10 -12
- data/app/serializers/dscf/credit/scoring_result_serializer.rb +7 -0
- data/app/serializers/dscf/credit/scoring_table_normalizer_serializer.rb +17 -0
- data/app/serializers/dscf/credit/scoring_table_parameter_serializer.rb +12 -0
- data/app/serializers/dscf/credit/scoring_table_serializer.rb +15 -0
- data/app/services/dscf/credit/credit_scoring_engine.rb +98 -240
- data/app/services/dscf/credit/facility_limit_calculation_engine.rb +69 -252
- data/app/services/dscf/credit/loan_profile_creation_service.rb +31 -9
- data/app/services/dscf/credit/repayment_service.rb +19 -2
- data/config/locales/en.yml +100 -34
- data/config/routes.rb +27 -6
- data/db/dev_seeds.rb +1235 -0
- data/db/migrate/20250822091009_create_dscf_credit_scoring_tables.rb +27 -0
- data/db/migrate/20250822091011_create_dscf_credit_categories.rb +6 -0
- data/db/migrate/20250822091500_create_dscf_credit_information_sources.rb +19 -0
- data/db/migrate/20250822091526_create_dscf_credit_scoring_parameters.rb +4 -8
- data/db/migrate/20250822091527_create_dscf_credit_credit_line_specs.rb +4 -0
- data/db/migrate/20250822091528_create_dscf_credit_scoring_table_parameters.rb +29 -0
- data/db/migrate/20250822091529_create_dscf_credit_scoring_table_normalizers.rb +28 -0
- data/db/migrate/20250822092236_create_dscf_credit_loan_applications.rb +1 -4
- data/db/migrate/20250822092417_create_dscf_credit_scoring_results.rb +19 -0
- data/db/migrate/20260218100000_create_dscf_credit_loan_application_data.rb +17 -0
- data/db/seeds.rb +438 -55
- data/lib/dscf/credit/version.rb +1 -1
- data/spec/factories/dscf/credit/categories.rb +13 -8
- data/spec/factories/dscf/credit/credit_line_specs.rb +25 -16
- data/spec/factories/dscf/credit/information_sources.rb +50 -0
- data/spec/factories/dscf/credit/loan_application_data.rb +16 -0
- data/spec/factories/dscf/credit/loan_application_datum.rb +16 -0
- data/spec/factories/dscf/credit/loan_applications.rb +5 -8
- data/spec/factories/dscf/credit/scoring_parameters.rb +9 -21
- data/spec/factories/dscf/credit/scoring_results.rb +44 -0
- data/spec/factories/dscf/credit/scoring_table_normalizers.rb +34 -0
- data/spec/factories/dscf/credit/scoring_table_parameters.rb +30 -0
- data/spec/factories/dscf/credit/scoring_tables.rb +33 -0
- metadata +34 -18
- data/app/controllers/dscf/credit/parameter_normalizers_controller.rb +0 -31
- data/app/controllers/dscf/credit/scoring_param_types_controller.rb +0 -31
- data/app/models/dscf/credit/loan_profile_scoring_spec.rb +0 -21
- data/app/models/dscf/credit/parameter_normalizer.rb +0 -11
- data/app/models/dscf/credit/scoring_param_type.rb +0 -17
- data/app/serializers/dscf/credit/loan_profile_scoring_spec_serializer.rb +0 -8
- data/app/serializers/dscf/credit/parameter_normalizer_serializer.rb +0 -8
- data/app/serializers/dscf/credit/scoring_param_type_serializer.rb +0 -7
- data/app/services/dscf/credit/credit_limit_calculation_service.rb +0 -153
- data/db/migrate/20250822091525_create_dscf_credit_scoring_param_types.rb +0 -12
- data/db/migrate/20250822092225_create_dscf_credit_parameter_normalizers.rb +0 -18
- data/db/migrate/20250822092417_create_dscf_credit_loan_profile_scoring_specs.rb +0 -21
- data/spec/factories/dscf/credit/loan_profile_scoring_specs.rb +0 -25
- data/spec/factories/dscf/credit/parameter_normalizers.rb +0 -24
- data/spec/factories/dscf/credit/scoring_param_types.rb +0 -31
- /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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6588b8a4895fec7dc6cbbbaafb11bbfd06a2ccf1342c8edf6f0467b8e322ff89
|
|
4
|
+
data.tar.gz: 598e9e952de5f1c510fd95efa3a7b6686f34f29b1dd3c5cee5f24e2857e1b340
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9468e717d617bab5fedad24fc41429d3cae8d9eb3c66c94515ce94ce0d01a9ade439d7412fe33a9c9f6c10f19c48733b7deefda7eafa66bf17bb9969a51d6410
|
|
7
|
+
data.tar.gz: '0316249a1df4a801ab43c25dda370650516877651a5a2dd818223f24d828f7dbd9eb648ce9b77786e3326eb44cd0209c67bf945045fecb1684bdbe843cc76e31'
|
|
@@ -1,33 +1,37 @@
|
|
|
1
|
-
module Dscf
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
module Dscf
|
|
2
|
+
module Credit
|
|
3
|
+
class CategoriesController < ApplicationController
|
|
4
|
+
include Dscf::Core::Common
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
private
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
def eager_loaded_associations
|
|
20
|
+
[ :bank, :scoring_table, :credit_lines, :scoring_parameters ]
|
|
21
|
+
end
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
def allowed_order_columns
|
|
24
|
+
%w[id category_type name description document_reference created_at updated_at]
|
|
25
|
+
end
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
8
|
-
result =
|
|
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[:
|
|
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
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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(
|
|
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]
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
loan_application
|
|
232
|
-
|
|
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
|
-
|
|
240
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
}
|