dscf-credit 0.1.2 → 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/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/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/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/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 +79 -0
- data/config/routes.rb +19 -5
- 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,8 @@
|
|
1
|
+
module Dscf::Credit
|
2
|
+
class EligibleCreditLineSerializer < ActiveModel::Serializer
|
3
|
+
attributes :id, :credit_limit, :available_limit, :risk, :created_at, :updated_at
|
4
|
+
|
5
|
+
belongs_to :loan_profile, serializer: Dscf::Credit::LoanProfileSerializer
|
6
|
+
belongs_to :credit_line, serializer: Dscf::Credit::CreditLineSerializer
|
7
|
+
end
|
8
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
module Dscf::Credit
|
2
|
+
class LoanProfileScoringSpecSerializer < ActiveModel::Serializer
|
3
|
+
attributes :id, :scoring_input_data, :score, :total_limit, :active, :created_at, :updated_at
|
4
|
+
|
5
|
+
belongs_to :loan_profile, serializer: Dscf::Credit::LoanProfileSerializer
|
6
|
+
belongs_to :created_by
|
7
|
+
end
|
8
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Dscf::Credit
|
2
|
+
class LoanProfileSerializer < ActiveModel::Serializer
|
3
|
+
attributes :id, :status, :total_amount, :available_amount, :review_date, :review_feedback, :created_at, :updated_at
|
4
|
+
|
5
|
+
belongs_to :bank, serializer: Dscf::Credit::BankSerializer
|
6
|
+
belongs_to :review_branch, serializer: Dscf::Credit::BankBranchSerializer
|
7
|
+
belongs_to :reviewed_by
|
8
|
+
belongs_to :backer
|
9
|
+
belongs_to :user, serializer: Dscf::Core::UserSerializer
|
10
|
+
|
11
|
+
has_many :loan_profile_scoring_specs, serializer: Dscf::Credit::LoanProfileScoringSpecSerializer
|
12
|
+
has_many :loans, serializer: Dscf::Credit::LoanSerializer
|
13
|
+
has_many :eligible_credit_lines, serializer: Dscf::Credit::EligibleCreditLineSerializer
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Dscf::Credit
|
2
|
+
class LoanSerializer < ActiveModel::Serializer
|
3
|
+
attributes :id, :status, :principal_amount, :accrued_interest, :accrued_penalty, :facilitation_fee,
|
4
|
+
:total_loan_amount, :remaining_amount, :due_date, :disbursed_at, :created_at, :updated_at
|
5
|
+
|
6
|
+
belongs_to :loan_profile, serializer: LoanProfileSerializer
|
7
|
+
belongs_to :credit_line, serializer: CreditLineSerializer
|
8
|
+
belongs_to :payment_request, serializer: PaymentRequestSerializer
|
9
|
+
has_many :loan_transactions, serializer: LoanTransactionSerializer
|
10
|
+
has_many :daily_routine_transactions, serializer: DailyRoutineTransactionSerializer
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Dscf::Credit
|
2
|
+
class PaymentRequestSerializer < ActiveModel::Serializer
|
3
|
+
attributes :id, :order_id, :request_type, :amount, :receiver_account_reference,
|
4
|
+
:status, :failure_reason, :initiated_at, :approved_at, :created_at, :updated_at
|
5
|
+
|
6
|
+
belongs_to :user, serializer: Dscf::Core::UserSerializer
|
7
|
+
has_many :loans, serializer: Dscf::Credit::LoanSerializer
|
8
|
+
has_many :payments, serializer: Dscf::Credit::PaymentSerializer
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
module Dscf::Credit
|
2
|
+
class PaymentSerializer < ActiveModel::Serializer
|
3
|
+
attributes :id, :amount, :receiver_account_reference, :transaction_reference,
|
4
|
+
:status, :failure_reason, :processed_at, :created_at, :updated_at
|
5
|
+
|
6
|
+
belongs_to :payment_request, serializer: PaymentRequestSerializer
|
7
|
+
end
|
8
|
+
end
|
@@ -9,5 +9,7 @@ module Dscf::Credit
|
|
9
9
|
belongs_to :scoring_param_type, serializer: Dscf::Credit::ScoringParamTypeSerializer
|
10
10
|
belongs_to :previous_version, serializer: Dscf::Credit::ScoringParameterSerializer
|
11
11
|
has_many :parameter_normalizers, serializer: Dscf::Credit::ParameterNormalizerSerializer
|
12
|
+
has_many :scoring_tables, serializer: Dscf::Credit::ScoringTableSerializer
|
13
|
+
has_many :categories, serializer: Dscf::Credit::CategorySerializer
|
12
14
|
end
|
13
15
|
end
|
@@ -2,7 +2,7 @@ module Dscf::Credit
|
|
2
2
|
class ScoringTableSerializer < ActiveModel::Serializer
|
3
3
|
attributes :id, :weight, :active, :created_at, :updated_at
|
4
4
|
|
5
|
-
belongs_to :
|
5
|
+
belongs_to :category, serializer: Dscf::Credit::CategorySerializer
|
6
6
|
belongs_to :scoring_parameter, serializer: Dscf::Credit::ScoringParameterSerializer
|
7
7
|
belongs_to :created_by, serializer: Dscf::Core::UserSerializer
|
8
8
|
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
module Dscf::Credit
|
2
|
+
class CreditLimitCalculationService
|
3
|
+
attr_reader :loan_profile, :category
|
4
|
+
|
5
|
+
def initialize(loan_profile, category)
|
6
|
+
@loan_profile = loan_profile
|
7
|
+
@category = category
|
8
|
+
end
|
9
|
+
|
10
|
+
# Calculate credit limits for all credit lines in a category for a specific loan profile
|
11
|
+
# @return [Hash] Result containing eligible credit lines with calculated limits
|
12
|
+
def calculate_credit_limits
|
13
|
+
return error_result("Loan profile not found") unless loan_profile
|
14
|
+
return error_result("Category not found") unless category
|
15
|
+
|
16
|
+
scoring_spec = loan_profile.loan_profile_scoring_specs.active.first
|
17
|
+
return error_result("No active scoring found for loan profile") unless scoring_spec
|
18
|
+
|
19
|
+
credit_lines = category.credit_lines.approved.includes(:bank, :category, :credit_line_specs)
|
20
|
+
return error_result("No approved credit lines found for category") if credit_lines.empty?
|
21
|
+
|
22
|
+
eligible_credit_lines = calculate_limits_for_credit_lines(credit_lines, scoring_spec)
|
23
|
+
|
24
|
+
success_result(eligible_credit_lines)
|
25
|
+
rescue StandardError => e
|
26
|
+
error_result("Credit limit calculation failed: #{e.message}")
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def calculate_limits_for_credit_lines(credit_lines, scoring_spec)
|
32
|
+
results = []
|
33
|
+
|
34
|
+
credit_lines.each do |credit_line|
|
35
|
+
limit_data = calculate_individual_credit_limit(credit_line, scoring_spec)
|
36
|
+
next if limit_data[:credit_limit] <= 0
|
37
|
+
|
38
|
+
eligible_credit_line = find_or_create_eligible_credit_line(credit_line)
|
39
|
+
eligible_credit_line.assign_attributes(limit_data)
|
40
|
+
|
41
|
+
if eligible_credit_line.save
|
42
|
+
results << {
|
43
|
+
credit_line: credit_line,
|
44
|
+
eligible_credit_line: eligible_credit_line,
|
45
|
+
calculated_limit: limit_data[:credit_limit],
|
46
|
+
available_limit: limit_data[:available_limit],
|
47
|
+
risk_factor: limit_data[:risk]
|
48
|
+
}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
results
|
53
|
+
end
|
54
|
+
|
55
|
+
def calculate_individual_credit_limit(credit_line, scoring_spec)
|
56
|
+
base_limit = scoring_spec.total_limit
|
57
|
+
|
58
|
+
credit_line_specs = credit_line.credit_line_specs.active
|
59
|
+
adjusted_limit = apply_credit_line_specifications(base_limit, credit_line_specs)
|
60
|
+
|
61
|
+
risk_factor = calculate_risk_factor(credit_line, loan_profile)
|
62
|
+
final_limit = adjusted_limit * (1 - risk_factor)
|
63
|
+
|
64
|
+
used_limit = calculate_used_limit(credit_line)
|
65
|
+
available_limit = [ final_limit - used_limit, 0 ].max
|
66
|
+
|
67
|
+
{
|
68
|
+
credit_limit: final_limit.round(2),
|
69
|
+
available_limit: available_limit.round(2),
|
70
|
+
risk: risk_factor.round(4)
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
def apply_credit_line_specifications(base_limit, credit_line_specs)
|
75
|
+
return base_limit if credit_line_specs.empty?
|
76
|
+
|
77
|
+
spec = credit_line_specs.first
|
78
|
+
return base_limit unless spec
|
79
|
+
|
80
|
+
if spec.credit_line_multiplier && spec.credit_line_multiplier > 0
|
81
|
+
adjustment_factor = spec.credit_line_multiplier / 30.0 # Normalize against default 30 days
|
82
|
+
adjusted_limit = base_limit * adjustment_factor
|
83
|
+
else
|
84
|
+
adjusted_limit = base_limit
|
85
|
+
end
|
86
|
+
|
87
|
+
if spec.max_amount && adjusted_limit > spec.max_amount
|
88
|
+
adjusted_limit = spec.max_amount
|
89
|
+
end
|
90
|
+
|
91
|
+
if spec.min_amount && adjusted_limit < spec.min_amount
|
92
|
+
adjusted_limit = spec.min_amount
|
93
|
+
end
|
94
|
+
|
95
|
+
adjusted_limit
|
96
|
+
end
|
97
|
+
|
98
|
+
def calculate_risk_factor(credit_line, loan_profile)
|
99
|
+
base_risk = 0.1 # 10% base risk
|
100
|
+
|
101
|
+
# Increase risk based on loan profile characteristics
|
102
|
+
risk_adjustments = 0.0
|
103
|
+
|
104
|
+
# Risk based on previous loan performance
|
105
|
+
if loan_profile.loans.overdue.any?
|
106
|
+
risk_adjustments += 0.2 # 20% additional risk for overdue loans
|
107
|
+
end
|
108
|
+
|
109
|
+
# Risk based on available amount vs total amount ratio
|
110
|
+
if loan_profile.total_amount > 0
|
111
|
+
utilization_ratio = (loan_profile.total_amount - loan_profile.available_amount) / loan_profile.total_amount
|
112
|
+
risk_adjustments += utilization_ratio * 0.1 # Up to 10% additional risk
|
113
|
+
end
|
114
|
+
|
115
|
+
# Risk based on credit line category
|
116
|
+
case credit_line.category.name.downcase
|
117
|
+
when "high_risk"
|
118
|
+
risk_adjustments += 0.15
|
119
|
+
when "medium_risk"
|
120
|
+
risk_adjustments += 0.05
|
121
|
+
end
|
122
|
+
|
123
|
+
# Cap total risk at 90%
|
124
|
+
[ base_risk + risk_adjustments, 0.9 ].min
|
125
|
+
end
|
126
|
+
|
127
|
+
def calculate_used_limit(credit_line)
|
128
|
+
credit_line.loans.where(status: [ "active", "overdue" ]).sum(:remaining_amount)
|
129
|
+
end
|
130
|
+
|
131
|
+
def find_or_create_eligible_credit_line(credit_line)
|
132
|
+
loan_profile.eligible_credit_lines.find_or_initialize_by(credit_line: credit_line)
|
133
|
+
end
|
134
|
+
|
135
|
+
def success_result(eligible_credit_lines)
|
136
|
+
{
|
137
|
+
success: true,
|
138
|
+
data: eligible_credit_lines,
|
139
|
+
message: "Credit limits calculated successfully",
|
140
|
+
count: eligible_credit_lines.size
|
141
|
+
}
|
142
|
+
end
|
143
|
+
|
144
|
+
def error_result(message)
|
145
|
+
{
|
146
|
+
success: false,
|
147
|
+
data: [],
|
148
|
+
error: message,
|
149
|
+
count: 0
|
150
|
+
}
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
module Dscf::Credit
|
2
|
+
class DisbursementService
|
3
|
+
attr_reader :credit_line, :payment_request, :current_user
|
4
|
+
|
5
|
+
def initialize(credit_line, payment_request, current_user)
|
6
|
+
@credit_line = credit_line
|
7
|
+
@payment_request = payment_request
|
8
|
+
@current_user = current_user
|
9
|
+
end
|
10
|
+
|
11
|
+
# Process disbursement by creating a loan record from selected credit line
|
12
|
+
# @return [Hash] Result containing created loan and disbursement details
|
13
|
+
def process_disbursement
|
14
|
+
return error_result("Credit line not found") unless credit_line
|
15
|
+
return error_result("Payment request not found") unless payment_request
|
16
|
+
return error_result("Credit line is not approved") unless credit_line.status == "approved"
|
17
|
+
|
18
|
+
eligible_credit_line = find_eligible_credit_line
|
19
|
+
return error_result("No eligible credit line found") unless eligible_credit_line
|
20
|
+
|
21
|
+
loan_profile = eligible_credit_line.loan_profile
|
22
|
+
return error_result("Loan profile not approved") unless loan_profile.status == "approved"
|
23
|
+
|
24
|
+
validation_result = validate_disbursement_amount(eligible_credit_line)
|
25
|
+
return validation_result unless validation_result[:success]
|
26
|
+
|
27
|
+
ActiveRecord::Base.transaction do
|
28
|
+
loan = create_loan_record(loan_profile, eligible_credit_line)
|
29
|
+
process_payment_and_charges(loan)
|
30
|
+
update_credit_line_limits(eligible_credit_line, loan)
|
31
|
+
lock_other_credit_lines(loan_profile, credit_line)
|
32
|
+
|
33
|
+
success_result(loan)
|
34
|
+
end
|
35
|
+
rescue StandardError => e
|
36
|
+
error_result("Disbursement processing failed: #{e.message}")
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def find_eligible_credit_line
|
42
|
+
credit_line.eligible_credit_lines
|
43
|
+
.joins(:loan_profile)
|
44
|
+
.where(dscf_credit_loan_profiles: { status: "approved" })
|
45
|
+
.first
|
46
|
+
end
|
47
|
+
|
48
|
+
def validate_disbursement_amount(eligible_credit_line)
|
49
|
+
requested_amount = payment_request.amount
|
50
|
+
available_limit = eligible_credit_line.available_limit
|
51
|
+
|
52
|
+
if requested_amount <= 0
|
53
|
+
return error_result("Invalid disbursement amount")
|
54
|
+
end
|
55
|
+
|
56
|
+
if requested_amount > available_limit
|
57
|
+
return error_result("Requested amount exceeds available credit limit")
|
58
|
+
end
|
59
|
+
|
60
|
+
{ success: true }
|
61
|
+
end
|
62
|
+
|
63
|
+
def create_loan_record(loan_profile, eligible_credit_line)
|
64
|
+
loan_terms = calculate_loan_terms(eligible_credit_line)
|
65
|
+
|
66
|
+
loan = Dscf::Credit::Loan.new(
|
67
|
+
loan_profile: loan_profile,
|
68
|
+
credit_line: credit_line,
|
69
|
+
payment_request: payment_request,
|
70
|
+
principal_amount: payment_request.amount,
|
71
|
+
facilitation_fee: loan_terms[:facilitation_fee],
|
72
|
+
total_loan_amount: loan_terms[:total_amount],
|
73
|
+
remaining_amount: loan_terms[:total_amount],
|
74
|
+
accrued_interest: 0,
|
75
|
+
accrued_penalty: 0,
|
76
|
+
status: "disbursed",
|
77
|
+
due_date: loan_terms[:due_date],
|
78
|
+
disbursed_at: Time.current
|
79
|
+
)
|
80
|
+
|
81
|
+
raise "Failed to create loan: #{loan.errors.full_messages.join(', ')}" unless loan.save
|
82
|
+
|
83
|
+
loan
|
84
|
+
end
|
85
|
+
|
86
|
+
def calculate_loan_terms(eligible_credit_line)
|
87
|
+
principal = payment_request.amount
|
88
|
+
|
89
|
+
credit_line_spec = credit_line.credit_line_specs.active.first
|
90
|
+
|
91
|
+
if credit_line_spec
|
92
|
+
facilitation_rate = credit_line_spec.facilitation_fee_rate
|
93
|
+
vat_rate = credit_line_spec.vat
|
94
|
+
loan_duration = credit_line_spec.loan_duration
|
95
|
+
else
|
96
|
+
facilitation_rate = 0.02 # 2%
|
97
|
+
vat_rate = 0.15 # 15%
|
98
|
+
loan_duration = 30 # 30 days
|
99
|
+
end
|
100
|
+
|
101
|
+
facilitation_fee = principal * facilitation_rate
|
102
|
+
vat_amount = facilitation_fee * vat_rate
|
103
|
+
total_amount = principal + facilitation_fee + vat_amount
|
104
|
+
|
105
|
+
due_date = Date.current + loan_duration.days
|
106
|
+
|
107
|
+
{
|
108
|
+
facilitation_fee: facilitation_fee.round(2),
|
109
|
+
vat_amount: vat_amount.round(2),
|
110
|
+
total_amount: total_amount.round(2),
|
111
|
+
due_date: due_date
|
112
|
+
}
|
113
|
+
end
|
114
|
+
|
115
|
+
def process_payment_and_charges(loan)
|
116
|
+
payment_request.update!(
|
117
|
+
status: "processed",
|
118
|
+
approved_at: Time.current
|
119
|
+
)
|
120
|
+
|
121
|
+
Dscf::Credit::LoanTransaction.create!(
|
122
|
+
loan: loan,
|
123
|
+
transaction_type: "disbursement",
|
124
|
+
amount: loan.principal_amount,
|
125
|
+
transaction_reference: "DISB-#{payment_request.order_id}-#{Time.current.to_i}",
|
126
|
+
status: "completed"
|
127
|
+
)
|
128
|
+
|
129
|
+
if loan.facilitation_fee > 0
|
130
|
+
Dscf::Credit::LoanTransaction.create!(
|
131
|
+
loan: loan,
|
132
|
+
transaction_type: "fee_charge",
|
133
|
+
amount: loan.facilitation_fee,
|
134
|
+
transaction_reference: "FEE-#{loan.id}-#{Time.current.to_i}",
|
135
|
+
status: "completed"
|
136
|
+
)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def update_credit_line_limits(eligible_credit_line, loan)
|
141
|
+
new_available_limit = eligible_credit_line.available_limit - loan.total_loan_amount
|
142
|
+
eligible_credit_line.update!(available_limit: [ new_available_limit, 0 ].max)
|
143
|
+
|
144
|
+
# Update loan profile available amount
|
145
|
+
loan_profile = eligible_credit_line.loan_profile
|
146
|
+
new_profile_available = loan_profile.available_amount - loan.total_loan_amount
|
147
|
+
loan_profile.update!(available_amount: [ new_profile_available, 0 ].max)
|
148
|
+
end
|
149
|
+
|
150
|
+
def lock_other_credit_lines(loan_profile, current_credit_line)
|
151
|
+
other_eligible_lines = loan_profile.eligible_credit_lines
|
152
|
+
.where.not(credit_line: current_credit_line)
|
153
|
+
|
154
|
+
other_eligible_lines.update_all(available_limit: 0)
|
155
|
+
end
|
156
|
+
|
157
|
+
def success_result(loan)
|
158
|
+
{
|
159
|
+
success: true,
|
160
|
+
loan: loan,
|
161
|
+
disbursement_details: {
|
162
|
+
principal_amount: loan.principal_amount,
|
163
|
+
facilitation_fee: loan.facilitation_fee,
|
164
|
+
total_loan_amount: loan.total_loan_amount,
|
165
|
+
due_date: loan.due_date,
|
166
|
+
disbursed_at: loan.disbursed_at
|
167
|
+
},
|
168
|
+
message: "Disbursement processed successfully"
|
169
|
+
}
|
170
|
+
end
|
171
|
+
|
172
|
+
def error_result(message)
|
173
|
+
{
|
174
|
+
success: false,
|
175
|
+
loan: nil,
|
176
|
+
error: message
|
177
|
+
}
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
@@ -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
|