dscf-credit 0.1.1 → 0.1.3
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/authenticatable.rb +81 -0
- data/app/controllers/concerns/dscf/core/common.rb +200 -0
- data/app/controllers/concerns/dscf/core/filterable.rb +12 -0
- data/app/controllers/concerns/dscf/core/json_response.rb +77 -0
- data/app/controllers/concerns/dscf/core/pagination.rb +71 -0
- data/app/controllers/concerns/dscf/core/token_authenticatable.rb +53 -0
- data/app/controllers/concerns/dscf/credit/reviewable.rb +112 -0
- data/app/controllers/dscf/credit/categories_controller.rb +6 -5
- data/app/controllers/dscf/credit/credit_limit_calculations_controller.rb +50 -0
- data/app/controllers/dscf/credit/credit_line_specs_controller.rb +2 -1
- data/app/controllers/dscf/credit/credit_lines_controller.rb +11 -38
- data/app/controllers/dscf/credit/disbursements_controller.rb +55 -0
- data/app/controllers/dscf/credit/eligible_credit_lines_controller.rb +50 -0
- data/app/controllers/dscf/credit/facilitators_controller.rb +39 -150
- data/app/controllers/dscf/credit/loan_profiles_controller.rb +138 -0
- data/app/controllers/dscf/credit/payment_requests_controller.rb +54 -5
- data/app/controllers/dscf/credit/repayments_controller.rb +53 -0
- data/app/controllers/dscf/credit/scoring_param_types_controller.rb +31 -0
- data/app/controllers/dscf/credit/scoring_parameters_controller.rb +13 -8
- data/app/controllers/dscf/credit/scoring_tables_controller.rb +8 -8
- data/app/controllers/dscf/credit/system_configs_controller.rb +10 -7
- data/app/models/dscf/credit/bank_branch.rb +2 -1
- data/app/models/dscf/credit/category.rb +4 -2
- data/app/models/dscf/credit/credit_line.rb +9 -6
- data/app/models/dscf/credit/credit_line_spec.rb +3 -3
- data/app/models/dscf/credit/eligible_credit_line.rb +28 -0
- data/app/models/dscf/credit/facilitator.rb +5 -4
- data/app/models/dscf/credit/facilitator_performance.rb +1 -2
- data/app/models/dscf/credit/loan_profile.rb +8 -4
- data/app/models/dscf/credit/loan_profile_scoring_spec.rb +4 -6
- data/app/models/dscf/credit/scoring_param_type.rb +17 -0
- data/app/models/dscf/credit/scoring_parameter.rb +8 -7
- data/app/models/dscf/credit/scoring_table.rb +4 -4
- data/app/models/dscf/credit/system_config.rb +5 -4
- data/app/serializers/dscf/credit/bank_branch_serializer.rb +1 -0
- data/app/serializers/dscf/credit/category_serializer.rb +3 -1
- data/app/serializers/dscf/credit/credit_line_serializer.rb +4 -2
- data/app/serializers/dscf/credit/credit_line_spec_serializer.rb +1 -1
- data/app/serializers/dscf/credit/daily_routine_transaction_serializer.rb +8 -0
- data/app/serializers/dscf/credit/eligible_credit_line_serializer.rb +8 -0
- data/app/serializers/dscf/credit/facilitator_performance_serializer.rb +1 -1
- data/app/serializers/dscf/credit/facilitator_serializer.rb +2 -2
- data/app/serializers/dscf/credit/loan_profile_scoring_spec_serializer.rb +8 -0
- data/app/serializers/dscf/credit/loan_profile_serializer.rb +15 -0
- data/app/serializers/dscf/credit/loan_serializer.rb +12 -0
- data/app/serializers/dscf/credit/loan_transaction_serializer.rb +8 -0
- data/app/serializers/dscf/credit/payment_request_serializer.rb +10 -0
- data/app/serializers/dscf/credit/payment_serializer.rb +8 -0
- data/app/serializers/dscf/credit/scoring_param_type_serializer.rb +7 -0
- data/app/serializers/dscf/credit/scoring_parameter_serializer.rb +6 -3
- data/app/serializers/dscf/credit/scoring_table_serializer.rb +1 -1
- data/app/serializers/dscf/credit/system_config_serializer.rb +2 -2
- data/app/services/dscf/credit/credit_limit_calculation_service.rb +153 -0
- data/app/services/dscf/credit/disbursement_service.rb +180 -0
- data/app/services/dscf/credit/facilitator_approval_service.rb +4 -3
- data/app/services/dscf/credit/facilitator_creation_service.rb +157 -0
- data/app/services/dscf/credit/repayment_service.rb +216 -0
- data/app/services/dscf/credit/risk_application_service.rb +27 -0
- data/app/services/dscf/credit/scoring_service.rb +297 -0
- data/config/locales/en.yml +125 -8
- data/config/routes.rb +42 -11
- data/db/migrate/20250822091011_create_dscf_credit_categories.rb +2 -0
- data/db/migrate/20250822091131_create_dscf_credit_credit_lines.rb +7 -4
- data/db/migrate/20250822091527_create_dscf_credit_credit_line_specs.rb +1 -0
- data/db/migrate/20250822091820_create_dscf_credit_system_configs.rb +5 -2
- data/db/migrate/20250822092040_create_dscf_credit_scoring_param_types.rb +12 -0
- data/db/migrate/20250822092050_create_dscf_credit_scoring_parameters.rb +11 -6
- data/db/migrate/20250822092246_create_dscf_credit_loan_profiles.rb +6 -3
- data/db/migrate/20250822092417_create_dscf_credit_loan_profile_scoring_specs.rb +5 -7
- data/db/migrate/20250822092436_create_dscf_credit_facilitators.rb +5 -2
- data/db/migrate/20250822092528_create_dscf_credit_facilitator_performances.rb +0 -3
- data/db/migrate/20250901172842_create_dscf_credit_scoring_tables.rb +2 -2
- data/db/migrate/20250917120000_create_dscf_credit_eligible_credit_lines.rb +18 -0
- data/db/seeds.rb +134 -40
- data/lib/dscf/credit/version.rb +1 -1
- data/spec/factories/dscf/credit/categories.rb +1 -0
- data/spec/factories/dscf/credit/credit_line_specs.rb +1 -0
- data/spec/factories/dscf/credit/credit_lines.rb +9 -7
- data/spec/factories/dscf/credit/eligible_credit_lines.rb +33 -0
- data/spec/factories/dscf/credit/facilitator_performances.rb +0 -5
- data/spec/factories/dscf/credit/facilitators.rb +6 -1
- data/spec/factories/dscf/credit/loan_profile_scoring_specs.rb +1 -7
- data/spec/factories/dscf/credit/loan_profiles.rb +11 -6
- data/spec/factories/dscf/credit/scoring_param_types.rb +31 -0
- data/spec/factories/dscf/credit/scoring_parameters.rb +26 -4
- data/spec/factories/dscf/credit/scoring_tables.rb +1 -1
- data/spec/factories/dscf/credit/system_configs.rb +8 -2
- metadata +50 -2
@@ -0,0 +1,216 @@
|
|
1
|
+
module Dscf::Credit
|
2
|
+
class RepaymentService
|
3
|
+
attr_reader :loan, :payment_amount, :current_user
|
4
|
+
|
5
|
+
def initialize(loan, payment_amount, current_user)
|
6
|
+
@loan = loan
|
7
|
+
@payment_amount = payment_amount.to_f
|
8
|
+
@current_user = current_user
|
9
|
+
end
|
10
|
+
|
11
|
+
# Process loan repayment by decreasing loan balance and updating related records
|
12
|
+
# @return [Hash] Result containing payment details and updated loan status
|
13
|
+
def process_repayment
|
14
|
+
return error_result("Loan not found") unless loan
|
15
|
+
return error_result("Invalid payment amount") unless payment_amount > 0
|
16
|
+
return error_result("Loan is not active") unless loan.status.in?([ "active", "overdue", "disbursed" ])
|
17
|
+
|
18
|
+
payment_allocation = calculate_payment_allocation
|
19
|
+
|
20
|
+
return error_result("Payment amount exceeds total outstanding balance") if payment_amount > payment_allocation[:total_outstanding]
|
21
|
+
|
22
|
+
ActiveRecord::Base.transaction do
|
23
|
+
process_payment_allocation(payment_allocation)
|
24
|
+
|
25
|
+
update_loan_status
|
26
|
+
|
27
|
+
create_payment_transaction(payment_allocation)
|
28
|
+
|
29
|
+
reactivate_facilities_if_paid_off
|
30
|
+
|
31
|
+
success_result(payment_allocation)
|
32
|
+
end
|
33
|
+
rescue StandardError => e
|
34
|
+
error_result("Repayment processing failed: #{e.message}")
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def calculate_payment_allocation
|
40
|
+
current_facilitation_fee = loan.facilitation_fee || 0
|
41
|
+
current_penalty = loan.accrued_penalty || 0
|
42
|
+
current_interest = loan.accrued_interest || 0
|
43
|
+
current_principal = loan.remaining_amount || 0
|
44
|
+
|
45
|
+
total_outstanding = current_facilitation_fee + current_penalty + current_interest + current_principal
|
46
|
+
|
47
|
+
remaining_payment = payment_amount
|
48
|
+
|
49
|
+
facilitation_payment = [ remaining_payment, current_facilitation_fee ].min
|
50
|
+
remaining_payment -= facilitation_payment
|
51
|
+
|
52
|
+
penalty_payment = [ remaining_payment, current_penalty ].min
|
53
|
+
remaining_payment -= penalty_payment
|
54
|
+
|
55
|
+
interest_payment = [ remaining_payment, current_interest ].min
|
56
|
+
remaining_payment -= interest_payment
|
57
|
+
|
58
|
+
principal_payment = [ remaining_payment, current_principal ].min
|
59
|
+
|
60
|
+
{
|
61
|
+
total_outstanding: total_outstanding,
|
62
|
+
payment_amount: payment_amount,
|
63
|
+
facilitation_payment: facilitation_payment,
|
64
|
+
penalty_payment: penalty_payment,
|
65
|
+
interest_payment: interest_payment,
|
66
|
+
principal_payment: principal_payment,
|
67
|
+
remaining_facilitation_fee: current_facilitation_fee - facilitation_payment,
|
68
|
+
remaining_penalty: current_penalty - penalty_payment,
|
69
|
+
remaining_interest: current_interest - interest_payment,
|
70
|
+
remaining_principal: current_principal - principal_payment
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
def process_payment_allocation(allocation)
|
75
|
+
loan.update!(
|
76
|
+
facilitation_fee: allocation[:remaining_facilitation_fee],
|
77
|
+
accrued_penalty: allocation[:remaining_penalty],
|
78
|
+
accrued_interest: allocation[:remaining_interest],
|
79
|
+
remaining_amount: allocation[:remaining_principal]
|
80
|
+
)
|
81
|
+
end
|
82
|
+
|
83
|
+
def update_loan_status
|
84
|
+
total_remaining = (loan.facilitation_fee || 0) +
|
85
|
+
(loan.accrued_penalty || 0) +
|
86
|
+
(loan.accrued_interest || 0) +
|
87
|
+
(loan.remaining_amount || 0)
|
88
|
+
|
89
|
+
if total_remaining <= 0.01
|
90
|
+
loan.update!(status: "paid")
|
91
|
+
else
|
92
|
+
loan.update!(status: "active") if loan.status == "overdue"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def create_payment_transaction(allocation)
|
97
|
+
Dscf::Credit::LoanTransaction.create!(
|
98
|
+
loan: loan,
|
99
|
+
transaction_type: "repayment",
|
100
|
+
amount: payment_amount,
|
101
|
+
transaction_reference: "REPAY-#{loan.id}-#{Time.current.to_i}",
|
102
|
+
status: "completed"
|
103
|
+
)
|
104
|
+
|
105
|
+
create_allocation_transactions(allocation) if payment_amount > 100 # Only for significant payments
|
106
|
+
end
|
107
|
+
|
108
|
+
def create_allocation_transactions(allocation)
|
109
|
+
transactions = []
|
110
|
+
|
111
|
+
if allocation[:facilitation_payment] > 0
|
112
|
+
transactions << {
|
113
|
+
transaction_type: "fee_charge",
|
114
|
+
amount: allocation[:facilitation_payment],
|
115
|
+
reference_suffix: "FEE"
|
116
|
+
}
|
117
|
+
end
|
118
|
+
|
119
|
+
if allocation[:penalty_payment] > 0
|
120
|
+
transactions << {
|
121
|
+
transaction_type: "penalty",
|
122
|
+
amount: allocation[:penalty_payment],
|
123
|
+
reference_suffix: "PENALTY"
|
124
|
+
}
|
125
|
+
end
|
126
|
+
|
127
|
+
if allocation[:interest_payment] > 0
|
128
|
+
transactions << {
|
129
|
+
transaction_type: "interest_accrual",
|
130
|
+
amount: allocation[:interest_payment],
|
131
|
+
reference_suffix: "INTEREST"
|
132
|
+
}
|
133
|
+
end
|
134
|
+
|
135
|
+
if allocation[:principal_payment] > 0
|
136
|
+
transactions << {
|
137
|
+
transaction_type: "repayment",
|
138
|
+
amount: allocation[:principal_payment],
|
139
|
+
reference_suffix: "PRINCIPAL"
|
140
|
+
}
|
141
|
+
end
|
142
|
+
|
143
|
+
transactions.each_with_index do |transaction_data, index|
|
144
|
+
Dscf::Credit::LoanTransaction.create!(
|
145
|
+
loan: loan,
|
146
|
+
transaction_type: transaction_data[:transaction_type],
|
147
|
+
amount: transaction_data[:amount],
|
148
|
+
transaction_reference: "#{transaction_data[:reference_suffix]}-#{loan.id}-#{Time.current.to_i}-#{index}",
|
149
|
+
status: "completed"
|
150
|
+
)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def reactivate_facilities_if_paid_off
|
155
|
+
return unless loan.status == "paid"
|
156
|
+
|
157
|
+
loan_profile = loan.loan_profile
|
158
|
+
|
159
|
+
locked_eligible_lines = loan_profile.eligible_credit_lines
|
160
|
+
.where(available_limit: 0)
|
161
|
+
.includes(:credit_line)
|
162
|
+
|
163
|
+
locked_eligible_lines.each do |eligible_line|
|
164
|
+
current_usage = calculate_current_usage_for_credit_line(eligible_line.credit_line)
|
165
|
+
new_available_limit = [ eligible_line.credit_limit - current_usage, 0 ].max
|
166
|
+
|
167
|
+
eligible_line.update!(available_limit: new_available_limit)
|
168
|
+
end
|
169
|
+
|
170
|
+
update_loan_profile_available_amount(loan_profile)
|
171
|
+
end
|
172
|
+
|
173
|
+
def calculate_current_usage_for_credit_line(credit_line)
|
174
|
+
credit_line.loans
|
175
|
+
.where(status: [ "active", "overdue", "disbursed" ])
|
176
|
+
.where.not(id: loan.id)
|
177
|
+
.sum(:remaining_amount)
|
178
|
+
end
|
179
|
+
|
180
|
+
def update_loan_profile_available_amount(loan_profile)
|
181
|
+
# Recalculate available amount based on total limit minus current usage
|
182
|
+
total_usage = loan_profile.loans
|
183
|
+
.where(status: [ "active", "overdue", "disbursed" ])
|
184
|
+
.sum(:remaining_amount)
|
185
|
+
|
186
|
+
new_available_amount = [ loan_profile.total_amount - total_usage, 0 ].max
|
187
|
+
loan_profile.update!(available_amount: new_available_amount)
|
188
|
+
end
|
189
|
+
|
190
|
+
def success_result(allocation)
|
191
|
+
{
|
192
|
+
success: true,
|
193
|
+
loan: loan.reload,
|
194
|
+
payment_details: {
|
195
|
+
payment_amount: payment_amount,
|
196
|
+
allocation: allocation,
|
197
|
+
new_loan_status: loan.status,
|
198
|
+
paid_off: loan.status == "paid",
|
199
|
+
remaining_balance: (loan.facilitation_fee || 0) +
|
200
|
+
(loan.accrued_penalty || 0) +
|
201
|
+
(loan.accrued_interest || 0) +
|
202
|
+
(loan.remaining_amount || 0)
|
203
|
+
},
|
204
|
+
message: loan.status == "paid" ? "Loan fully paid off" : "Payment processed successfully"
|
205
|
+
}
|
206
|
+
end
|
207
|
+
|
208
|
+
def error_result(message)
|
209
|
+
{
|
210
|
+
success: false,
|
211
|
+
loan: nil,
|
212
|
+
error: message
|
213
|
+
}
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Dscf::Credit
|
2
|
+
class RiskApplicationService
|
3
|
+
def initialize(eligible_credit_line, risk_value)
|
4
|
+
@eligible_credit_line = eligible_credit_line
|
5
|
+
@risk_value = risk_value.to_f
|
6
|
+
end
|
7
|
+
|
8
|
+
def apply_risk
|
9
|
+
return { success: false, errors: @eligible_credit_line.errors.full_messages } unless @eligible_credit_line.valid?
|
10
|
+
|
11
|
+
@eligible_credit_line.risk = @risk_value
|
12
|
+
@eligible_credit_line.available_limit = calculate_available_limit
|
13
|
+
|
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 }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def calculate_available_limit
|
24
|
+
@eligible_credit_line.credit_limit * (1 - @risk_value)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,297 @@
|
|
1
|
+
module Dscf::Credit
|
2
|
+
class ScoringService
|
3
|
+
attr_reader :loan_profile
|
4
|
+
|
5
|
+
def initialize(loan_profile)
|
6
|
+
@loan_profile = loan_profile
|
7
|
+
end
|
8
|
+
|
9
|
+
# Calculate credit score for a loan profile using the eligibility category scoring table
|
10
|
+
# @param external_scoring_data [Hash] Optional external scoring input data
|
11
|
+
# @return [Hash] Result containing score, total_limit, and success status
|
12
|
+
def calculate_credit_score(external_scoring_data = nil)
|
13
|
+
eligibility_category = find_eligibility_category
|
14
|
+
return error_result("Eligibility category not found") unless eligibility_category
|
15
|
+
|
16
|
+
scoring_tables = eligibility_category.scoring_tables.active.includes(:scoring_parameter)
|
17
|
+
return error_result("No active scoring parameters found for eligibility category") if scoring_tables.empty?
|
18
|
+
|
19
|
+
scoring_data = get_scoring_data(external_scoring_data)
|
20
|
+
score = calculate_weighted_score(scoring_tables, scoring_data)
|
21
|
+
facility_limit = calculate_facility_limit(score, scoring_data)
|
22
|
+
|
23
|
+
success_result(score, facility_limit)
|
24
|
+
rescue StandardError => e
|
25
|
+
error_result("Score calculation failed: #{e.message}")
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def get_scoring_data(external_scoring_data)
|
31
|
+
if external_scoring_data.present?
|
32
|
+
external_scoring_data
|
33
|
+
else
|
34
|
+
extract_scoring_data_from_profile
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def extract_scoring_data_from_profile
|
39
|
+
latest_spec = loan_profile.loan_profile_scoring_specs.order(created_at: :desc).first
|
40
|
+
|
41
|
+
if latest_spec&.scoring_input_data.present? && !latest_spec.scoring_input_data.empty?
|
42
|
+
default_data = prepare_scoring_data
|
43
|
+
default_data.merge(latest_spec.scoring_input_data.symbolize_keys)
|
44
|
+
else
|
45
|
+
prepare_scoring_data
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def find_eligibility_category
|
50
|
+
Dscf::Credit::Category.find_by(type: "eligibility", name: "default") ||
|
51
|
+
Dscf::Credit::Category.find_by(type: "eligibility")
|
52
|
+
end
|
53
|
+
|
54
|
+
def prepare_scoring_data
|
55
|
+
{
|
56
|
+
average_daily_purchase: extract_average_daily_purchase,
|
57
|
+
years_at_location: extract_years_at_location,
|
58
|
+
marital_status: extract_marital_status,
|
59
|
+
dependents_count: extract_dependents_count,
|
60
|
+
education_level: extract_education_level,
|
61
|
+
bill_payment_timing: extract_bill_payment_timing,
|
62
|
+
other_accounts: extract_other_accounts,
|
63
|
+
money_preference: extract_money_preference,
|
64
|
+
business_license: extract_business_license,
|
65
|
+
bank_statements: extract_bank_statements,
|
66
|
+
transaction_frequency: extract_transaction_frequency,
|
67
|
+
purchase_volume: extract_purchase_volume,
|
68
|
+
cancellation_rate: extract_cancellation_rate,
|
69
|
+
reliability_score: extract_reliability_score
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
# Calculate weighted score based on scoring parameters
|
74
|
+
def calculate_weighted_score(scoring_tables, scoring_data)
|
75
|
+
total_score = 0.0
|
76
|
+
total_weight = 0.0
|
77
|
+
|
78
|
+
scoring_tables.each do |table|
|
79
|
+
parameter = table.scoring_parameter
|
80
|
+
raw_value = find_parameter_value(parameter, scoring_data)
|
81
|
+
|
82
|
+
next if raw_value.nil?
|
83
|
+
|
84
|
+
normalized_score = normalize_parameter_value(parameter, raw_value)
|
85
|
+
weighted_score = normalized_score * table.weight
|
86
|
+
|
87
|
+
total_score += weighted_score
|
88
|
+
total_weight += table.weight
|
89
|
+
end
|
90
|
+
|
91
|
+
return 0.0 if total_weight.zero?
|
92
|
+
|
93
|
+
(total_score / total_weight) * 100
|
94
|
+
end
|
95
|
+
|
96
|
+
# Find parameter value from scoring data using flexible key matching
|
97
|
+
def find_parameter_value(parameter, scoring_data)
|
98
|
+
param_name = parameter.name
|
99
|
+
|
100
|
+
# Try exact match first
|
101
|
+
if scoring_data.key?(param_name.to_sym)
|
102
|
+
return scoring_data[param_name.to_sym]
|
103
|
+
end
|
104
|
+
|
105
|
+
if scoring_data.key?(param_name)
|
106
|
+
return scoring_data[param_name]
|
107
|
+
end
|
108
|
+
|
109
|
+
# Try snake_case version
|
110
|
+
snake_case_name = param_name.downcase.gsub(/\s+/, "_")
|
111
|
+
if scoring_data.key?(snake_case_name.to_sym)
|
112
|
+
return scoring_data[snake_case_name.to_sym]
|
113
|
+
end
|
114
|
+
|
115
|
+
if scoring_data.key?(snake_case_name)
|
116
|
+
return scoring_data[snake_case_name]
|
117
|
+
end
|
118
|
+
|
119
|
+
# Try common mappings
|
120
|
+
case param_name.downcase
|
121
|
+
when "monthly income"
|
122
|
+
scoring_data[:monthly_income] || scoring_data["monthly_income"]
|
123
|
+
when "credit history score"
|
124
|
+
scoring_data[:credit_history_score] || scoring_data["credit_history_score"]
|
125
|
+
when "employment type"
|
126
|
+
scoring_data[:employment_type] || scoring_data["employment_type"]
|
127
|
+
else
|
128
|
+
nil
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Calculate facility limit based on score and business metrics
|
133
|
+
def calculate_facility_limit(score, scoring_data)
|
134
|
+
return 0 if score < 50 # Below 50% score = no facility
|
135
|
+
|
136
|
+
average_daily_purchase = scoring_data[:average_daily_purchase] || 0
|
137
|
+
|
138
|
+
credit_line_multiplier = get_default_credit_line_multiplier
|
139
|
+
|
140
|
+
base_limit = average_daily_purchase * credit_line_multiplier * (score / 100.0)
|
141
|
+
|
142
|
+
apply_limit_constraints(base_limit)
|
143
|
+
end
|
144
|
+
|
145
|
+
# Get default credit line multiplier from the bank's approved credit line specs
|
146
|
+
def get_default_credit_line_multiplier
|
147
|
+
return 30.0 unless loan_profile.bank
|
148
|
+
|
149
|
+
credit_line_spec = loan_profile.bank.credit_lines.approved
|
150
|
+
.joins(:credit_line_specs)
|
151
|
+
.merge(Dscf::Credit::CreditLineSpec.active)
|
152
|
+
.select("dscf_credit_credit_line_specs.credit_line_multiplier")
|
153
|
+
.first&.credit_line_specs&.active&.first
|
154
|
+
|
155
|
+
credit_line_spec&.credit_line_multiplier || 30.0
|
156
|
+
end
|
157
|
+
|
158
|
+
def apply_limit_constraints(base_limit)
|
159
|
+
min_limit = get_bank_config("min_credit_limit", 0)
|
160
|
+
max_limit = get_bank_config("max_credit_limit", 1000000)
|
161
|
+
|
162
|
+
return 0 if base_limit < min_limit
|
163
|
+
return max_limit if base_limit > max_limit
|
164
|
+
|
165
|
+
base_limit
|
166
|
+
end
|
167
|
+
|
168
|
+
# Get bank configuration value from system configs
|
169
|
+
def get_bank_config(config_key, default_value)
|
170
|
+
return default_value unless loan_profile.bank
|
171
|
+
|
172
|
+
config_definition = loan_profile.bank.system_config_definitions
|
173
|
+
.find_by(config_key: config_key)
|
174
|
+
return default_value unless config_definition
|
175
|
+
|
176
|
+
system_config = Dscf::Credit::SystemConfig.approved
|
177
|
+
.find_by(config_definition: config_definition)
|
178
|
+
|
179
|
+
if system_config&.config_value
|
180
|
+
numeric_value = system_config.config_value.to_f
|
181
|
+
numeric_value > 0 ? numeric_value : default_value
|
182
|
+
else
|
183
|
+
default_value
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# Normalize parameter value to 0-1 scale
|
188
|
+
def normalize_parameter_value(parameter, raw_value)
|
189
|
+
case parameter.data_type
|
190
|
+
when "boolean"
|
191
|
+
raw_value ? 1.0 : 0.0
|
192
|
+
when "integer", "decimal"
|
193
|
+
normalize_numeric_value(parameter, raw_value.to_f)
|
194
|
+
when "string"
|
195
|
+
normalize_string_value(parameter, raw_value)
|
196
|
+
else
|
197
|
+
0.5 # Default neutral score
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def normalize_numeric_value(parameter, value)
|
202
|
+
min_val = parameter.min_value || 0
|
203
|
+
max_val = parameter.max_value || 100
|
204
|
+
|
205
|
+
return 0.0 if value < min_val
|
206
|
+
return 1.0 if value > max_val
|
207
|
+
|
208
|
+
(value - min_val) / (max_val - min_val)
|
209
|
+
end
|
210
|
+
|
211
|
+
def normalize_string_value(parameter, value)
|
212
|
+
normalizer = parameter.parameter_normalizers.find { |n| n.raw_value == value }
|
213
|
+
return normalizer.normalized_value if normalizer
|
214
|
+
|
215
|
+
case value.downcase
|
216
|
+
when "excellent", "high", "good" then 1.0
|
217
|
+
when "fair", "medium", "average" then 0.7
|
218
|
+
when "poor", "low", "bad" then 0.3
|
219
|
+
else 0.5
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def extract_average_daily_purchase
|
224
|
+
5000.0 # Default average daily purchase
|
225
|
+
end
|
226
|
+
|
227
|
+
def extract_years_at_location
|
228
|
+
2 # Default years at current location
|
229
|
+
end
|
230
|
+
|
231
|
+
def extract_marital_status
|
232
|
+
"single" # Default marital status
|
233
|
+
end
|
234
|
+
|
235
|
+
def extract_dependents_count
|
236
|
+
0 # Default number of dependents
|
237
|
+
end
|
238
|
+
|
239
|
+
def extract_education_level
|
240
|
+
"bachelor" # Default education level
|
241
|
+
end
|
242
|
+
|
243
|
+
def extract_bill_payment_timing
|
244
|
+
"on_time" # Default payment timing
|
245
|
+
end
|
246
|
+
|
247
|
+
def extract_other_accounts
|
248
|
+
false # Default: no other active accounts
|
249
|
+
end
|
250
|
+
|
251
|
+
def extract_money_preference
|
252
|
+
"save" # Default money preference
|
253
|
+
end
|
254
|
+
|
255
|
+
def extract_business_license
|
256
|
+
false # Default: no business license
|
257
|
+
end
|
258
|
+
|
259
|
+
def extract_bank_statements
|
260
|
+
true # Default: bank statements available
|
261
|
+
end
|
262
|
+
|
263
|
+
def extract_transaction_frequency
|
264
|
+
15 # Default monthly transaction frequency
|
265
|
+
end
|
266
|
+
|
267
|
+
def extract_purchase_volume
|
268
|
+
50000.0 # Default monthly purchase volume
|
269
|
+
end
|
270
|
+
|
271
|
+
def extract_cancellation_rate
|
272
|
+
0.05 # Default 5% cancellation rate
|
273
|
+
end
|
274
|
+
|
275
|
+
def extract_reliability_score
|
276
|
+
75.0 # Default reliability score (0-100)
|
277
|
+
end
|
278
|
+
|
279
|
+
def success_result(score, facility_limit)
|
280
|
+
{
|
281
|
+
success: true,
|
282
|
+
score: score.round(2),
|
283
|
+
facility_limit: facility_limit.round(2),
|
284
|
+
message: "Credit score calculated successfully"
|
285
|
+
}
|
286
|
+
end
|
287
|
+
|
288
|
+
def error_result(message)
|
289
|
+
{
|
290
|
+
success: false,
|
291
|
+
score: 0,
|
292
|
+
facility_limit: 0,
|
293
|
+
error: message
|
294
|
+
}
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|