dscf-credit 0.1.2 → 0.1.4
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/dscf/credit/categories_controller.rb +3 -3
- 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 +3 -3
- 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 +2 -1
- 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_parameters_controller.rb +3 -3
- data/app/controllers/dscf/credit/scoring_tables_controller.rb +8 -8
- data/app/controllers/dscf/credit/users_controller.rb +18 -0
- data/app/models/dscf/credit/bank_branch.rb +2 -1
- data/app/models/dscf/credit/category.rb +3 -1
- data/app/models/dscf/credit/credit_line.rb +3 -3
- 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/loan_profile.rb +8 -5
- data/app/models/dscf/credit/loan_profile_scoring_spec.rb +4 -5
- data/app/models/dscf/credit/scoring_parameter.rb +2 -2
- data/app/models/dscf/credit/scoring_table.rb +4 -4
- data/app/serializers/dscf/credit/bank_branch_serializer.rb +1 -0
- data/app/serializers/dscf/credit/category_serializer.rb +2 -0
- data/app/serializers/dscf/credit/credit_line_serializer.rb +2 -0
- 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/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_parameter_serializer.rb +2 -0
- data/app/serializers/dscf/credit/scoring_table_serializer.rb +1 -1
- 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 +3 -4
- 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 +91 -0
- data/config/routes.rb +19 -7
- data/db/migrate/20250822091527_create_dscf_credit_credit_line_specs.rb +1 -0
- data/db/migrate/20250822092246_create_dscf_credit_loan_profiles.rb +2 -1
- data/db/migrate/20250822092417_create_dscf_credit_loan_profile_scoring_specs.rb +5 -8
- 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 +88 -22
- data/lib/dscf/credit/version.rb +1 -1
- data/spec/factories/dscf/credit/credit_line_specs.rb +1 -0
- data/spec/factories/dscf/credit/credit_lines.rb +3 -3
- data/spec/factories/dscf/credit/eligible_credit_lines.rb +33 -0
- data/spec/factories/dscf/credit/loan_profile_scoring_specs.rb +1 -4
- data/spec/factories/dscf/credit/loan_profiles.rb +1 -0
- data/spec/factories/dscf/credit/scoring_tables.rb +1 -1
- metadata +29 -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
|
data/config/locales/en.yml
CHANGED
@@ -77,6 +77,25 @@ en:
|
|
77
77
|
resubmit: "Failed to resubmit scoring parameter"
|
78
78
|
destroy: "Failed to delete scoring parameter"
|
79
79
|
|
80
|
+
loan_profile:
|
81
|
+
success:
|
82
|
+
index: "Loan profiles retrieved successfully"
|
83
|
+
show: "Loan profile details retrieved successfully"
|
84
|
+
create: "Loan profile created successfully"
|
85
|
+
update: "Loan profile updated successfully"
|
86
|
+
approve: "Loan profile approved successfully"
|
87
|
+
reject: "Loan profile rejected successfully"
|
88
|
+
calculate_score: "Credit score calculated successfully"
|
89
|
+
errors:
|
90
|
+
index: "Failed to retrieve loan profiles"
|
91
|
+
show: "Failed to retrieve loan profile details"
|
92
|
+
create: "Failed to create loan profile"
|
93
|
+
update: "Failed to update loan profile"
|
94
|
+
approve: "Failed to approve loan profile"
|
95
|
+
reject: "Failed to reject loan profile"
|
96
|
+
calculate_score: "Failed to calculate credit score"
|
97
|
+
not_found: "Loan profile not found"
|
98
|
+
|
80
99
|
parameter_normalizer:
|
81
100
|
success:
|
82
101
|
index: "Parameter normalizers retrieved successfully"
|
@@ -157,6 +176,20 @@ en:
|
|
157
176
|
request_modification: "Failed to request modification"
|
158
177
|
resubmit: "Failed to resubmit credit line"
|
159
178
|
|
179
|
+
eligible_credit_line:
|
180
|
+
success:
|
181
|
+
index: "Eligible credit lines retrieved successfully"
|
182
|
+
show: "Eligible credit line details retrieved successfully"
|
183
|
+
create: "Eligible credit line created successfully"
|
184
|
+
update: "Eligible credit line updated successfully"
|
185
|
+
apply_risk: "Risk applied successfully"
|
186
|
+
errors:
|
187
|
+
index: "Failed to retrieve eligible credit lines"
|
188
|
+
show: "Failed to retrieve eligible credit line details"
|
189
|
+
create: "Failed to create eligible credit line"
|
190
|
+
update: "Failed to update eligible credit line"
|
191
|
+
apply_risk: "Failed to apply risk"
|
192
|
+
|
160
193
|
system_config_definition:
|
161
194
|
success:
|
162
195
|
index: "System config definitions retrieved successfully"
|
@@ -225,6 +258,36 @@ en:
|
|
225
258
|
deactivate: "Failed to deactivate scoring table"
|
226
259
|
destroy: "Failed to delete scoring table"
|
227
260
|
|
261
|
+
payment_request:
|
262
|
+
success:
|
263
|
+
index: "Payment requests retrieved successfully"
|
264
|
+
show: "Payment request details retrieved successfully"
|
265
|
+
create: "Payment request created successfully"
|
266
|
+
update: "Payment request updated successfully"
|
267
|
+
approve: "Payment request approved successfully"
|
268
|
+
eligible_credit_lines: "Eligible credit lines retrieved successfully"
|
269
|
+
destroy: "Payment request deleted successfully"
|
270
|
+
errors:
|
271
|
+
index: "Failed to retrieve payment requests"
|
272
|
+
show: "Failed to retrieve payment request details"
|
273
|
+
create: "Failed to create payment request"
|
274
|
+
update: "Failed to update payment request"
|
275
|
+
approve: "Failed to approve payment request"
|
276
|
+
eligible_credit_lines: "Failed to retrieve eligible credit lines"
|
277
|
+
destroy: "Failed to delete payment request"
|
278
|
+
|
279
|
+
user:
|
280
|
+
success:
|
281
|
+
index: "Users retrieved successfully"
|
282
|
+
show: "User details retrieved successfully"
|
283
|
+
create: "User created successfully"
|
284
|
+
update: "User updated successfully"
|
285
|
+
errors:
|
286
|
+
index: "Failed to retrieve Users"
|
287
|
+
show: "Failed to retrieve User details"
|
288
|
+
create: "Failed to create User"
|
289
|
+
update: "Failed to update User"
|
290
|
+
|
228
291
|
# Global messages
|
229
292
|
operations:
|
230
293
|
success:
|
@@ -234,6 +297,34 @@ en:
|
|
234
297
|
failed: "Operation failed"
|
235
298
|
not_found: "Resource not found"
|
236
299
|
|
300
|
+
loan_profile:
|
301
|
+
success:
|
302
|
+
calculate_score: "Credit score calculated successfully"
|
303
|
+
errors:
|
304
|
+
calculate_score: "Failed to calculate credit score"
|
305
|
+
not_found: "Loan profile not found"
|
306
|
+
|
307
|
+
credit_limit_calculation:
|
308
|
+
success:
|
309
|
+
create: "Credit limits calculated successfully"
|
310
|
+
errors:
|
311
|
+
create: "Failed to calculate credit limits"
|
312
|
+
not_found: "Loan profile or category not found"
|
313
|
+
|
314
|
+
disbursement:
|
315
|
+
success:
|
316
|
+
create: "Disbursement processed successfully"
|
317
|
+
errors:
|
318
|
+
create: "Failed to process disbursement"
|
319
|
+
not_found: "Credit line or payment request not found"
|
320
|
+
|
321
|
+
repayment:
|
322
|
+
success:
|
323
|
+
create: "Repayment processed successfully"
|
324
|
+
errors:
|
325
|
+
create: "Failed to process repayment"
|
326
|
+
not_found: "Loan not found"
|
327
|
+
|
237
328
|
errors:
|
238
329
|
validation_failed: "Validation failed"
|
239
330
|
access_denied: "Access denied"
|
data/config/routes.rb
CHANGED
@@ -14,19 +14,22 @@ Dscf::Credit::Engine.routes.draw do
|
|
14
14
|
member do
|
15
15
|
patch "approve", to: "facilitators#approve"
|
16
16
|
patch "reject", to: "facilitators#reject"
|
17
|
-
patch "request_modification", to: "facilitators#request_modification"
|
18
|
-
patch "set_limit", to: "facilitators#set_limit"
|
19
17
|
end
|
20
18
|
end
|
21
19
|
|
22
|
-
resources :loan_profiles
|
20
|
+
resources :loan_profiles do
|
23
21
|
member do
|
24
|
-
|
25
|
-
patch "
|
22
|
+
patch "approve"
|
23
|
+
patch "reject"
|
24
|
+
post "calculate_score"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
resources :payment_requests do
|
28
|
+
member do
|
29
|
+
patch "approve"
|
30
|
+
get "eligible_credit_lines"
|
26
31
|
end
|
27
32
|
end
|
28
|
-
|
29
|
-
resources :payment_requests
|
30
33
|
resources :scoring_parameters do
|
31
34
|
member do
|
32
35
|
patch "approve"
|
@@ -54,6 +57,11 @@ Dscf::Credit::Engine.routes.draw do
|
|
54
57
|
end
|
55
58
|
end
|
56
59
|
resources :credit_line_specs
|
60
|
+
resources :eligible_credit_lines do
|
61
|
+
member do
|
62
|
+
patch "apply_risk", to: "eligible_credit_lines#apply_risk"
|
63
|
+
end
|
64
|
+
end
|
57
65
|
resources :categories
|
58
66
|
resources :scoring_tables do
|
59
67
|
member do
|
@@ -64,4 +72,8 @@ Dscf::Credit::Engine.routes.draw do
|
|
64
72
|
resources :users
|
65
73
|
resources :kyc_reviews, only: [ :index, :show ]
|
66
74
|
resources :bank_staffs
|
75
|
+
|
76
|
+
resources :credit_limit_calculations, only: [ :create ]
|
77
|
+
resources :disbursements, only: [ :create ]
|
78
|
+
resources :repayments, only: [ :create ]
|
67
79
|
end
|