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,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
|
@@ -2,7 +2,7 @@ module Dscf::Credit
|
|
2
2
|
class FacilitatorPerformanceSerializer < ActiveModel::Serializer
|
3
3
|
attributes :id, :score, :total_outstanding_loans, :total_outstanding_amount,
|
4
4
|
:approval_required, :previous_performance_id, :input_data,
|
5
|
-
:
|
5
|
+
:created_at, :updated_at
|
6
6
|
|
7
7
|
belongs_to :facilitator, serializer: Dscf::Credit::FacilitatorSerializer
|
8
8
|
belongs_to :created_by, polymorphic: true
|
@@ -1,11 +1,11 @@
|
|
1
1
|
module Dscf::Credit
|
2
2
|
class FacilitatorSerializer < ActiveModel::Serializer
|
3
|
-
attributes :id, :name, :type, :total_limit, :kyc_status,
|
3
|
+
attributes :id, :name, :type, :total_limit, :kyc_status, :kyc_review_date, :review_feedback,
|
4
4
|
:created_at, :updated_at
|
5
5
|
|
6
6
|
belongs_to :user, serializer: Dscf::Core::UserSerializer
|
7
7
|
belongs_to :bank, serializer: Dscf::Credit::BankSerializer
|
8
|
-
belongs_to :
|
8
|
+
belongs_to :kyc_reviewed_by, polymorphic: true
|
9
9
|
has_many :facilitator_performances, serializer: Dscf::Credit::FacilitatorPerformanceSerializer
|
10
10
|
end
|
11
11
|
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
|
@@ -1,12 +1,15 @@
|
|
1
1
|
module Dscf::Credit
|
2
2
|
class ScoringParameterSerializer < ActiveModel::Serializer
|
3
|
-
attributes :id, :name, :description, :data_type, :
|
4
|
-
:max_value, :active, :
|
3
|
+
attributes :id, :name, :description, :data_type, :weight, :min_value,
|
4
|
+
:max_value, :active, :status, :review_date, :review_feedback, :source, :document_reference, :created_at, :updated_at
|
5
5
|
|
6
6
|
belongs_to :bank, serializer: Dscf::Credit::BankSerializer
|
7
7
|
belongs_to :created_by, polymorphic: true
|
8
|
-
belongs_to :
|
8
|
+
belongs_to :reviewed_by, polymorphic: true
|
9
|
+
belongs_to :scoring_param_type, serializer: Dscf::Credit::ScoringParamTypeSerializer
|
9
10
|
belongs_to :previous_version, serializer: Dscf::Credit::ScoringParameterSerializer
|
10
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
|
11
14
|
end
|
12
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
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module Dscf::Credit
|
2
2
|
class SystemConfigSerializer < ActiveModel::Serializer
|
3
|
-
attributes :id, :config_value, :status, :created_at, :updated_at
|
3
|
+
attributes :id, :config_value, :status, :review_date, :review_feedback, :created_at, :updated_at
|
4
4
|
|
5
5
|
attribute :config_definition do
|
6
6
|
{
|
@@ -13,6 +13,6 @@ module Dscf::Credit
|
|
13
13
|
end
|
14
14
|
|
15
15
|
belongs_to :last_updated_by, serializer: Dscf::Core::UserSerializer
|
16
|
-
belongs_to :
|
16
|
+
belongs_to :reviewed_by, serializer: Dscf::Core::UserSerializer
|
17
17
|
end
|
18
18
|
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
|
@@ -29,15 +29,16 @@ module Dscf::Credit
|
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
|
-
def reject(
|
32
|
+
def reject(review_feedback = nil)
|
33
33
|
ActiveRecord::Base.transaction do
|
34
34
|
facilitator.update!(
|
35
35
|
kyc_status: "rejected",
|
36
|
-
kyc_approved_by: approver
|
36
|
+
kyc_approved_by: approver,
|
37
|
+
review_feedback: review_feedback
|
37
38
|
)
|
38
39
|
|
39
40
|
begin
|
40
|
-
FacilitatorMailer.rejection_notification(facilitator,
|
41
|
+
FacilitatorMailer.rejection_notification(facilitator, review_feedback).deliver_now
|
41
42
|
rescue => e
|
42
43
|
Rails.logger.error "Failed to send rejection email: #{e.message}"
|
43
44
|
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
module Dscf::Credit
|
2
|
+
class FacilitatorCreationService
|
3
|
+
attr_reader :current_user, :facilitator_class
|
4
|
+
|
5
|
+
def initialize(current_user, facilitator_class = Dscf::Credit::Facilitator)
|
6
|
+
@current_user = current_user
|
7
|
+
@facilitator_class = facilitator_class
|
8
|
+
end
|
9
|
+
|
10
|
+
# Creates a single facilitator
|
11
|
+
# @param facilitator_params [Hash] The facilitator parameters
|
12
|
+
# @return [Dscf::Credit::Facilitator] The created facilitator
|
13
|
+
# @raise [StandardError] If creation fails
|
14
|
+
def create_single(facilitator_params)
|
15
|
+
ActiveRecord::Base.transaction do
|
16
|
+
facilitator_data = build_facilitator_data(facilitator_params)
|
17
|
+
facilitator = facilitator_class.new(facilitator_data)
|
18
|
+
facilitator.kyc_reviewed_by = current_user
|
19
|
+
facilitator.kyc_review_date = Time.current
|
20
|
+
|
21
|
+
if facilitator.save
|
22
|
+
create_initial_performance(facilitator)
|
23
|
+
facilitator
|
24
|
+
else
|
25
|
+
raise StandardError, facilitator.errors.full_messages.join(", ")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Creates multiple facilitators in batch
|
31
|
+
# @param facilitators_params [Array<Hash>] Array of facilitator parameters
|
32
|
+
# @return [Hash] Results containing successful and failed creations
|
33
|
+
def create_batch(facilitators_params)
|
34
|
+
validate_batch_params(facilitators_params)
|
35
|
+
|
36
|
+
results = initialize_batch_results(facilitators_params.length)
|
37
|
+
|
38
|
+
facilitators_params.each_with_index do |facilitator_attrs, index|
|
39
|
+
process_single_facilitator_in_batch(facilitator_attrs, index, results)
|
40
|
+
end
|
41
|
+
|
42
|
+
results
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def build_facilitator_data(facilitator_params)
|
48
|
+
user = find_user(facilitator_params[:user_id])
|
49
|
+
business = find_user_business(user)
|
50
|
+
|
51
|
+
facilitator_params.to_h.merge(
|
52
|
+
name: business.name,
|
53
|
+
type: business.business_type.name.downcase
|
54
|
+
)
|
55
|
+
end
|
56
|
+
|
57
|
+
def find_user(user_id)
|
58
|
+
user = Dscf::Core::User.find(user_id)
|
59
|
+
raise StandardError, "User not found with ID: #{user_id}" unless user
|
60
|
+
user
|
61
|
+
rescue ActiveRecord::RecordNotFound
|
62
|
+
raise StandardError, "User not found with ID: #{user_id}"
|
63
|
+
end
|
64
|
+
|
65
|
+
def find_user_business(user)
|
66
|
+
business = user.businesses.first
|
67
|
+
raise StandardError, "User must have a business associated to become a facilitator" unless business
|
68
|
+
business
|
69
|
+
end
|
70
|
+
|
71
|
+
def create_initial_performance(facilitator)
|
72
|
+
facilitator.facilitator_performances.create!(
|
73
|
+
total_outstanding_loans: 0,
|
74
|
+
total_outstanding_amount: 0.0,
|
75
|
+
approval_required: false,
|
76
|
+
created_by: current_user
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
def validate_batch_params(facilitators_params)
|
81
|
+
unless facilitators_params.is_a?(Array)
|
82
|
+
raise StandardError, "Expected an array of facilitator objects"
|
83
|
+
end
|
84
|
+
|
85
|
+
if facilitators_params.empty?
|
86
|
+
raise StandardError, "At least one facilitator is required"
|
87
|
+
end
|
88
|
+
|
89
|
+
if facilitators_params.length > 100
|
90
|
+
raise StandardError, "Maximum 100 facilitators allowed per batch"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def initialize_batch_results(total_count)
|
95
|
+
{
|
96
|
+
successful: [],
|
97
|
+
failed: [],
|
98
|
+
total_count: total_count,
|
99
|
+
success_count: 0,
|
100
|
+
failure_count: 0
|
101
|
+
}
|
102
|
+
end
|
103
|
+
|
104
|
+
def process_single_facilitator_in_batch(facilitator_attrs, index, results)
|
105
|
+
begin
|
106
|
+
permitted_attrs = permit_facilitator_attrs(facilitator_attrs)
|
107
|
+
facilitator = create_single(permitted_attrs)
|
108
|
+
|
109
|
+
add_successful_result(results, index, facilitator)
|
110
|
+
rescue StandardError => e
|
111
|
+
add_failed_result(results, index, facilitator_attrs, e)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def permit_facilitator_attrs(facilitator_attrs)
|
116
|
+
if facilitator_attrs.is_a?(ActionController::Parameters)
|
117
|
+
facilitator_attrs.permit(:user_id, :bank_id, :kyc_status)
|
118
|
+
else
|
119
|
+
ActionController::Parameters.new(facilitator_attrs)
|
120
|
+
.permit(:user_id, :bank_id, :kyc_status)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def add_successful_result(results, index, facilitator)
|
125
|
+
results[:successful] << {
|
126
|
+
index: index,
|
127
|
+
id: facilitator.id,
|
128
|
+
name: facilitator.name,
|
129
|
+
message: "Successfully created"
|
130
|
+
}
|
131
|
+
results[:success_count] += 1
|
132
|
+
end
|
133
|
+
|
134
|
+
def add_failed_result(results, index, facilitator_attrs, error)
|
135
|
+
business_name = extract_business_name_for_error(facilitator_attrs)
|
136
|
+
|
137
|
+
results[:failed] << {
|
138
|
+
index: index,
|
139
|
+
name: business_name,
|
140
|
+
errors: [ error.message ]
|
141
|
+
}
|
142
|
+
results[:failure_count] += 1
|
143
|
+
end
|
144
|
+
|
145
|
+
def extract_business_name_for_error(facilitator_attrs)
|
146
|
+
user_id = facilitator_attrs.try(:[], :user_id) || facilitator_attrs[:user_id]
|
147
|
+
return "Unknown Business" unless user_id
|
148
|
+
|
149
|
+
begin
|
150
|
+
user = Dscf::Core::User.find(user_id)
|
151
|
+
user.businesses.first&.name || "No Business Associated"
|
152
|
+
rescue
|
153
|
+
"Invalid User ID"
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|