dscf-credit 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/dscf/credit/facilitator_applications_controller.rb +88 -5
- data/app/controllers/dscf/credit/loan_accruals_controller.rb +113 -0
- data/app/controllers/dscf/credit/loan_applications_controller.rb +5 -5
- data/app/controllers/dscf/credit/loan_profiles_controller.rb +2 -2
- data/app/controllers/dscf/credit/loans_controller.rb +2 -6
- data/app/controllers/dscf/credit/repayments_controller.rb +3 -3
- data/app/controllers/dscf/credit/scoring_parameters_controller.rb +9 -13
- data/app/controllers/dscf/credit/users_controller.rb +6 -0
- data/app/jobs/dscf/credit/generate_daily_accruals_job.rb +78 -0
- data/app/models/dscf/credit/facilitator_application.rb +0 -1
- data/app/models/dscf/credit/loan.rb +26 -5
- data/app/models/dscf/credit/loan_accrual.rb +25 -0
- data/app/serializers/dscf/credit/loan_accrual_serializer.rb +7 -0
- data/app/serializers/dscf/credit/loan_serializer.rb +24 -2
- data/app/services/dscf/credit/credit_scoring_engine.rb +8 -8
- data/app/services/dscf/credit/disbursement_service.rb +50 -8
- data/app/services/dscf/credit/loan_accrual_generator_service.rb +272 -0
- data/app/services/dscf/credit/repayment_service.rb +216 -87
- data/config/locales/en.yml +36 -0
- data/config/routes.rb +10 -0
- data/db/migrate/20250822092426_create_dscf_credit_facilitator_applications.rb +1 -1
- data/db/migrate/20250822092654_create_dscf_credit_loans.rb +0 -4
- data/db/migrate/20251003132939_create_dscf_credit_loan_accruals.rb +19 -0
- data/db/seeds.rb +20 -5
- data/lib/dscf/credit/version.rb +1 -1
- data/spec/factories/dscf/credit/loan_accruals.rb +29 -0
- data/spec/factories/dscf/credit/loans.rb +0 -4
- metadata +9 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1e5ce384f2229c93db978e4eff5599a0d0fb9fe74e586da8bab5209a63dcb9f6
|
4
|
+
data.tar.gz: 889029fa9ee854b75198eed3a539215139d59914f09f8337674583ea153b3703
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b0772af1d0a922a4826de14d34c8d18f78480fe17a8c7db23d22e1e2e463ccb8e95cc7a6db8d6ff8ffb178036239316c26b8e2b8c18c2808e03f2639bae028b2
|
7
|
+
data.tar.gz: 27587c70165aeda76c4577ef9e109b03539f0a5188681a3108408724e84fa39a681e7ba699cc6e213c886df5344f8cd196717947b83a882f7db43e49c80909e2
|
@@ -4,12 +4,95 @@ module Dscf::Credit
|
|
4
4
|
include Dscf::Core::ReviewableController
|
5
5
|
|
6
6
|
def create
|
7
|
-
super
|
8
|
-
|
9
|
-
facilitator_application.user = current_user
|
10
|
-
facilitator_application.reviews.build(context: "default", status: "pending")
|
7
|
+
super do
|
8
|
+
facilitator_application = @clazz.new(model_params)
|
11
9
|
|
12
|
-
|
10
|
+
user = Dscf::Core::User.find(model_params[:user_id])
|
11
|
+
facilitator_application.user = user
|
12
|
+
facilitator_application.reviews.build(context: "default", status: "pending")
|
13
|
+
|
14
|
+
facilitator_application
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def bulk_create
|
19
|
+
if params[:facilitator_applications].blank?
|
20
|
+
return render_error(
|
21
|
+
"facilitator_application.errors.bulk_create",
|
22
|
+
errors: [ "At least one facilitator application is required" ],
|
23
|
+
status: :unprocessable_entity
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
applications_params = params.require(:facilitator_applications)
|
28
|
+
|
29
|
+
unless applications_params.is_a?(Array)
|
30
|
+
return render_error(
|
31
|
+
"facilitator_application.errors.bulk_create",
|
32
|
+
errors: [ "Expected an array of facilitator applications" ],
|
33
|
+
status: :unprocessable_entity
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
results = {
|
38
|
+
successful: [],
|
39
|
+
failed: [],
|
40
|
+
total_count: applications_params.length,
|
41
|
+
success_count: 0,
|
42
|
+
failure_count: 0
|
43
|
+
}
|
44
|
+
|
45
|
+
applications_params.each_with_index do |app_params, index|
|
46
|
+
begin
|
47
|
+
facilitator_application = @clazz.new(app_params.permit(:user_id, :bank_id))
|
48
|
+
|
49
|
+
if app_params[:user_id].present?
|
50
|
+
facilitator_application.user = Dscf::Core::User.find(app_params[:user_id])
|
51
|
+
else
|
52
|
+
raise ActiveRecord::RecordInvalid.new(facilitator_application), "User ID is required"
|
53
|
+
end
|
54
|
+
|
55
|
+
facilitator_application.reviews.build(context: "default", status: "pending")
|
56
|
+
|
57
|
+
if facilitator_application.save
|
58
|
+
results[:successful] << {
|
59
|
+
index: index,
|
60
|
+
id: facilitator_application.id,
|
61
|
+
user_id: facilitator_application.user_id,
|
62
|
+
bank_id: facilitator_application.bank_id,
|
63
|
+
message: "Successfully created"
|
64
|
+
}
|
65
|
+
results[:success_count] += 1
|
66
|
+
else
|
67
|
+
results[:failed] << {
|
68
|
+
index: index,
|
69
|
+
user_id: app_params[:user_id],
|
70
|
+
errors: facilitator_application.errors.full_messages
|
71
|
+
}
|
72
|
+
results[:failure_count] += 1
|
73
|
+
end
|
74
|
+
rescue ActiveRecord::RecordNotFound => e
|
75
|
+
results[:failed] << {
|
76
|
+
index: index,
|
77
|
+
user_id: app_params[:user_id],
|
78
|
+
errors: [ "User not found: #{e.message}" ]
|
79
|
+
}
|
80
|
+
results[:failure_count] += 1
|
81
|
+
rescue StandardError => e
|
82
|
+
results[:failed] << {
|
83
|
+
index: index,
|
84
|
+
user_id: app_params[:user_id],
|
85
|
+
errors: [ e.message ]
|
86
|
+
}
|
87
|
+
results[:failure_count] += 1
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
render_success(
|
92
|
+
"facilitator_application.success.bulk_create",
|
93
|
+
data: results,
|
94
|
+
status: :created
|
95
|
+
)
|
13
96
|
end
|
14
97
|
|
15
98
|
private
|
@@ -0,0 +1,113 @@
|
|
1
|
+
module Dscf::Credit
|
2
|
+
class LoanAccrualsController < ApplicationController
|
3
|
+
include Dscf::Core::Common
|
4
|
+
|
5
|
+
def generate
|
6
|
+
service = LoanAccrualGeneratorService.new(
|
7
|
+
loan_ids: params[:loan_ids],
|
8
|
+
accrual_date: parse_accrual_date,
|
9
|
+
force_regenerate: ActiveModel::Type::Boolean.new.cast(params[:force_regenerate])
|
10
|
+
)
|
11
|
+
|
12
|
+
result = service.generate_daily_accruals
|
13
|
+
|
14
|
+
if result[:success]
|
15
|
+
render_success(
|
16
|
+
result[:message],
|
17
|
+
data: result,
|
18
|
+
status: :created
|
19
|
+
)
|
20
|
+
else
|
21
|
+
render_error(
|
22
|
+
result[:message],
|
23
|
+
errors: result[:errors],
|
24
|
+
status: :unprocessable_entity
|
25
|
+
)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def statistics
|
30
|
+
loan = Loan.find(params[:loan_id])
|
31
|
+
|
32
|
+
stats = {
|
33
|
+
loan_id: loan.id,
|
34
|
+
principal_amount: loan.principal_amount,
|
35
|
+
remaining_amount: loan.remaining_amount,
|
36
|
+
total_facilitation_fee: loan.total_facilitation_fee,
|
37
|
+
total_interest: loan.total_interest,
|
38
|
+
total_penalty: loan.total_penalty,
|
39
|
+
total_vat: loan.total_vat,
|
40
|
+
total_outstanding: loan.total_outstanding,
|
41
|
+
accrual_breakdown: {
|
42
|
+
facilitation_fee: {
|
43
|
+
pending: loan.loan_accruals.where(accrual_type: 'facilitation_fee', status: 'pending').sum(:amount),
|
44
|
+
paid: loan.loan_accruals.where(accrual_type: 'facilitation_fee', status: 'paid').sum(:amount)
|
45
|
+
},
|
46
|
+
tax: {
|
47
|
+
pending: loan.loan_accruals.where(accrual_type: 'tax', status: 'pending').sum(:amount),
|
48
|
+
paid: loan.loan_accruals.where(accrual_type: 'tax', status: 'paid').sum(:amount)
|
49
|
+
},
|
50
|
+
interest: {
|
51
|
+
pending: loan.loan_accruals.where(accrual_type: 'interest', status: 'pending').sum(:amount),
|
52
|
+
paid: loan.loan_accruals.where(accrual_type: 'interest', status: 'paid').sum(:amount)
|
53
|
+
},
|
54
|
+
penalty: {
|
55
|
+
pending: loan.loan_accruals.where(accrual_type: 'penalty', status: 'pending').sum(:amount),
|
56
|
+
paid: loan.loan_accruals.where(accrual_type: 'penalty', status: 'paid').sum(:amount)
|
57
|
+
}
|
58
|
+
},
|
59
|
+
accrual_count: {
|
60
|
+
total: loan.loan_accruals.count,
|
61
|
+
pending: loan.loan_accruals.where(status: 'pending').count,
|
62
|
+
paid: loan.loan_accruals.where(status: 'paid').count,
|
63
|
+
cancelled: loan.loan_accruals.where(status: 'cancelled').count
|
64
|
+
}
|
65
|
+
}
|
66
|
+
|
67
|
+
render_success(
|
68
|
+
'Loan accrual statistics retrieved successfully',
|
69
|
+
data: stats
|
70
|
+
)
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
# Parse accrual_date parameter
|
76
|
+
#
|
77
|
+
# @return [Date] Parsed date or Date.current
|
78
|
+
def parse_accrual_date
|
79
|
+
return Date.current unless params[:accrual_date].present?
|
80
|
+
|
81
|
+
Date.parse(params[:accrual_date].to_s)
|
82
|
+
rescue ArgumentError
|
83
|
+
Date.current
|
84
|
+
end
|
85
|
+
|
86
|
+
def model_params
|
87
|
+
params.require(:loan_accrual).permit(
|
88
|
+
:loan_id,
|
89
|
+
:accrual_type,
|
90
|
+
:amount,
|
91
|
+
:applied_on,
|
92
|
+
:status
|
93
|
+
)
|
94
|
+
end
|
95
|
+
|
96
|
+
def eager_loaded_associations
|
97
|
+
[:loan]
|
98
|
+
end
|
99
|
+
|
100
|
+
def allowed_order_columns
|
101
|
+
%w[id accrual_type amount applied_on status created_at updated_at]
|
102
|
+
end
|
103
|
+
|
104
|
+
def default_serializer_includes
|
105
|
+
{
|
106
|
+
index: [:loan],
|
107
|
+
show: [:loan],
|
108
|
+
create: [:loan],
|
109
|
+
update: [:loan]
|
110
|
+
}
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -62,17 +62,17 @@ module Dscf::Credit
|
|
62
62
|
|
63
63
|
def calculate_credit_score
|
64
64
|
loan_application = @clazz.find(params[:id])
|
65
|
-
|
65
|
+
scoring_param_type_id = score_params[:scoring_param_type_id]
|
66
66
|
|
67
|
-
unless
|
67
|
+
unless scoring_param_type_id
|
68
68
|
return render_error(
|
69
69
|
"loan_application.errors.calculate_credit_score",
|
70
|
-
errors: [ "
|
70
|
+
errors: [ "Scoring parameter type ID is required for credit scoring" ],
|
71
71
|
status: :unprocessable_entity
|
72
72
|
)
|
73
73
|
end
|
74
74
|
|
75
|
-
scoring_engine = CreditScoringEngine.new(loan_application.id,
|
75
|
+
scoring_engine = CreditScoringEngine.new(loan_application.id, scoring_param_type_id)
|
76
76
|
result = scoring_engine.calculate_score
|
77
77
|
|
78
78
|
if result[:success]
|
@@ -215,7 +215,7 @@ module Dscf::Credit
|
|
215
215
|
end
|
216
216
|
|
217
217
|
def score_params
|
218
|
-
params.permit(:
|
218
|
+
params.permit(:scoring_param_type_id)
|
219
219
|
end
|
220
220
|
|
221
221
|
def eager_loaded_associations
|
@@ -105,7 +105,7 @@ module Dscf::Credit
|
|
105
105
|
|
106
106
|
def eager_loaded_associations
|
107
107
|
[
|
108
|
-
:loan_application, :loan_profile_scoring_specs, :loans, :
|
108
|
+
:loan_application, :loan_profile_scoring_specs, :loans, eligible_credit_lines: { credit_line: :credit_line },
|
109
109
|
reviews: { reviewed_by: :user_profile }
|
110
110
|
]
|
111
111
|
end
|
@@ -118,7 +118,7 @@ module Dscf::Credit
|
|
118
118
|
{
|
119
119
|
index: [ :loan_application, reviews: { reviewed_by: :user_profile } ],
|
120
120
|
show: [
|
121
|
-
:loan_application, :loan_profile_scoring_specs, :loans, :
|
121
|
+
:loan_application, :loan_profile_scoring_specs, :loans, eligible_credit_lines: { credit_line: :credit_line }, reviews: { reviewed_by: :user_profile }
|
122
122
|
],
|
123
123
|
create: [ :loan_application, :loan_profile_scoring_specs, :reviews ],
|
124
124
|
update: [
|
@@ -10,10 +10,6 @@ module Dscf::Credit
|
|
10
10
|
:credit_line_id,
|
11
11
|
:status,
|
12
12
|
:principal_amount,
|
13
|
-
:accrued_interest,
|
14
|
-
:accrued_penalty,
|
15
|
-
:facilitation_fee,
|
16
|
-
:total_loan_amount,
|
17
13
|
:remaining_amount,
|
18
14
|
:due_date,
|
19
15
|
:disbursed_at,
|
@@ -22,11 +18,11 @@ module Dscf::Credit
|
|
22
18
|
end
|
23
19
|
|
24
20
|
def eager_loaded_associations
|
25
|
-
[ :loan_profile, :credit_line, :loan_transactions ]
|
21
|
+
[ :loan_profile, :credit_line, :loan_transactions, :loan_accruals ]
|
26
22
|
end
|
27
23
|
|
28
24
|
def allowed_order_columns
|
29
|
-
%w[id status principal_amount
|
25
|
+
%w[id status principal_amount remaining_amount due_date disbursed_at created_at updated_at]
|
30
26
|
end
|
31
27
|
|
32
28
|
def default_serializer_includes
|
@@ -1,10 +1,11 @@
|
|
1
1
|
module Dscf::Credit
|
2
2
|
class RepaymentsController < ApplicationController
|
3
|
+
|
3
4
|
def create
|
4
5
|
loan = Dscf::Credit::Loan.find(params[:loan_id])
|
5
6
|
payment_amount = params[:amount].to_f
|
6
7
|
|
7
|
-
service = Dscf::Credit::RepaymentService.new(loan, payment_amount
|
8
|
+
service = Dscf::Credit::RepaymentService.new(loan, payment_amount)
|
8
9
|
result = service.process_repayment
|
9
10
|
|
10
11
|
if result[:success]
|
@@ -18,8 +19,7 @@ module Dscf::Credit
|
|
18
19
|
include: [
|
19
20
|
:loan_profile,
|
20
21
|
:credit_line,
|
21
|
-
:
|
22
|
-
:loan_transactions
|
22
|
+
:loan_accruals
|
23
23
|
]
|
24
24
|
}
|
25
25
|
)
|
@@ -9,10 +9,10 @@ module Dscf::Credit
|
|
9
9
|
scoring_parameter.created_by = current_user
|
10
10
|
scoring_parameter.reviews.build(context: "default", status: "pending")
|
11
11
|
|
12
|
-
if
|
12
|
+
if validate_scoring_param_type_weight_limit(scoring_parameter)
|
13
13
|
scoring_parameter
|
14
14
|
else
|
15
|
-
scoring_parameter.errors.add(:weight, "would exceed the total weight limit of 1.0 for this
|
15
|
+
scoring_parameter.errors.add(:weight, "would exceed the total weight limit of 1.0 for this scoring parameter type")
|
16
16
|
render_error(errors: scoring_parameter.errors.full_messages, status: :unprocessable_entity)
|
17
17
|
return
|
18
18
|
end
|
@@ -20,8 +20,8 @@ module Dscf::Credit
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def update
|
23
|
-
unless
|
24
|
-
@obj.errors.add(:weight, "would exceed the total weight limit of 1.0 for this
|
23
|
+
unless validate_scoring_param_type_weight_limit(@obj, exclude_current: true)
|
24
|
+
@obj.errors.add(:weight, "would exceed the total weight limit of 1.0 for this scoring parameter type")
|
25
25
|
render_error(errors: @obj.errors.full_messages, status: :unprocessable_entity)
|
26
26
|
return
|
27
27
|
end
|
@@ -62,7 +62,7 @@ module Dscf::Credit
|
|
62
62
|
|
63
63
|
def default_serializer_includes
|
64
64
|
{
|
65
|
-
index: [ :bank, :category, reviews: { reviewed_by: :user_profile } ],
|
65
|
+
index: [ :bank, :category, :parameter_normalizers, reviews: { reviewed_by: :user_profile } ],
|
66
66
|
show: [
|
67
67
|
:bank, :category, :created_by, :scoring_param_type, :previous_version, :parameter_normalizers,
|
68
68
|
reviews: { reviewed_by: :user_profile }
|
@@ -75,27 +75,23 @@ module Dscf::Credit
|
|
75
75
|
}
|
76
76
|
end
|
77
77
|
|
78
|
-
def
|
79
|
-
return true unless scoring_parameter.
|
78
|
+
def validate_scoring_param_type_weight_limit(scoring_parameter, exclude_current: false)
|
79
|
+
return true unless scoring_parameter.scoring_param_type_id && scoring_parameter.weight
|
80
80
|
|
81
|
-
# Get existing parameters in the same category for the same bank
|
82
81
|
existing_params = ScoringParameter.where(
|
83
82
|
bank_id: scoring_parameter.bank_id,
|
84
|
-
|
83
|
+
scoring_param_type_id: scoring_parameter.scoring_param_type_id,
|
85
84
|
active: true
|
86
85
|
)
|
87
86
|
|
88
|
-
# Exclude current parameter from the calculation when updating
|
89
87
|
if exclude_current && scoring_parameter.persisted?
|
90
88
|
existing_params = existing_params.where.not(id: scoring_parameter.id)
|
91
89
|
end
|
92
90
|
|
93
|
-
# Calculate total weight
|
94
91
|
current_total_weight = existing_params.sum(:weight)
|
95
92
|
new_total_weight = current_total_weight + scoring_parameter.weight.to_f
|
96
93
|
|
97
|
-
|
98
|
-
new_total_weight <= 1.001
|
94
|
+
new_total_weight <= 1.00
|
99
95
|
end
|
100
96
|
end
|
101
97
|
end
|
@@ -16,8 +16,14 @@ module Dscf::Credit
|
|
16
16
|
)
|
17
17
|
end
|
18
18
|
|
19
|
+
rejected_user_ids = Dscf::Credit::FacilitatorApplication.joins(:reviews)
|
20
|
+
.where(dscf_core_reviews: { status: 'rejected' })
|
21
|
+
.pluck(:user_id)
|
22
|
+
.uniq
|
23
|
+
|
19
24
|
@clazz.joins(businesses: :business_type)
|
20
25
|
.where(dscf_core_business_types: { name: business_type_name })
|
26
|
+
.where(id: rejected_user_ids)
|
21
27
|
.distinct
|
22
28
|
else
|
23
29
|
@clazz.all
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Dscf::Credit
|
2
|
+
# Background job to generate daily loan accruals
|
3
|
+
#
|
4
|
+
# This job should be scheduled to run daily (typically at midnight or early morning)
|
5
|
+
# to generate interest and penalty accruals for all active loans.
|
6
|
+
#
|
7
|
+
# @example Schedule with Sidekiq (config/sidekiq.yml)
|
8
|
+
# :schedule:
|
9
|
+
# generate_daily_accruals:
|
10
|
+
# cron: '0 1 * * *' # Run at 1 AM daily
|
11
|
+
# class: Dscf::Credit::GenerateDailyAccrualsJob
|
12
|
+
#
|
13
|
+
# @example Schedule with whenever gem (config/schedule.rb)
|
14
|
+
# every 1.day, at: '1:00 am' do
|
15
|
+
# runner "Dscf::Credit::GenerateDailyAccrualsJob.perform_later"
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# @example Manual execution
|
19
|
+
# Dscf::Credit::GenerateDailyAccrualsJob.perform_now
|
20
|
+
#
|
21
|
+
# @example With specific parameters
|
22
|
+
# Dscf::Credit::GenerateDailyAccrualsJob.perform_later(
|
23
|
+
# loan_ids: [1, 2, 3],
|
24
|
+
# accrual_date: Date.yesterday
|
25
|
+
# )
|
26
|
+
class GenerateDailyAccrualsJob < ApplicationJob
|
27
|
+
queue_as :default
|
28
|
+
|
29
|
+
# Perform the job to generate daily accruals
|
30
|
+
#
|
31
|
+
# @param loan_ids [Array<Integer>, nil] Optional specific loan IDs to process
|
32
|
+
# @param accrual_date [String, Date] Date for accrual generation (defaults to today)
|
33
|
+
# @param force_regenerate [Boolean] Whether to regenerate existing accruals
|
34
|
+
def perform(loan_ids: nil, accrual_date: nil, force_regenerate: false)
|
35
|
+
date = accrual_date ? Date.parse(accrual_date.to_s) : Date.current
|
36
|
+
|
37
|
+
service = LoanAccrualGeneratorService.new(
|
38
|
+
loan_ids: loan_ids,
|
39
|
+
accrual_date: date,
|
40
|
+
force_regenerate: force_regenerate
|
41
|
+
)
|
42
|
+
|
43
|
+
result = service.generate_daily_accruals
|
44
|
+
|
45
|
+
log_result(result)
|
46
|
+
|
47
|
+
result
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
# Log the result of accrual generation
|
53
|
+
#
|
54
|
+
# @param result [Hash] The result from the service
|
55
|
+
def log_result(result)
|
56
|
+
if result[:success]
|
57
|
+
Rails.logger.info(
|
58
|
+
"Loan Accrual Generation: #{result[:message]}"
|
59
|
+
)
|
60
|
+
|
61
|
+
if result[:errors].any?
|
62
|
+
Rails.logger.warn(
|
63
|
+
"Loan Accrual Generation Warnings: #{result[:errors].size} loans had errors"
|
64
|
+
)
|
65
|
+
result[:errors].each do |error|
|
66
|
+
Rails.logger.warn(
|
67
|
+
"Loan #{error[:loan_id]}: #{error[:error]}"
|
68
|
+
)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
else
|
72
|
+
Rails.logger.error(
|
73
|
+
"Loan Accrual Generation Failed: #{result[:message]}"
|
74
|
+
)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -7,7 +7,6 @@ module Dscf::Credit
|
|
7
7
|
belongs_to :user, class_name: "Dscf::Core::User", foreign_key: "user_id"
|
8
8
|
belongs_to :bank, class_name: "Dscf::Credit::Bank", foreign_key: "bank_id"
|
9
9
|
|
10
|
-
validates :facilitator_info, presence: true
|
11
10
|
|
12
11
|
def self.ransackable_attributes(auth_object = nil)
|
13
12
|
%w[id user_id bank_id created_at updated_at]
|
@@ -6,10 +6,11 @@ module Dscf::Credit
|
|
6
6
|
belongs_to :credit_line, class_name: "Dscf::Credit::CreditLine", foreign_key: "credit_line_id"
|
7
7
|
has_many :loan_transactions, class_name: "Dscf::Credit::LoanTransaction", foreign_key: "loan_id", dependent: :destroy
|
8
8
|
has_many :daily_routine_transactions, class_name: "Dscf::Credit::DailyRoutineTransaction", foreign_key: "loan_id", dependent: :destroy
|
9
|
+
has_many :loan_accruals, class_name: "Dscf::Credit::LoanAccrual", foreign_key: "loan_id", dependent: :destroy
|
9
10
|
|
10
|
-
validates :principal_amount, :
|
11
|
-
validates :principal_amount,
|
12
|
-
validates :
|
11
|
+
validates :principal_amount, :remaining_amount, :due_date, presence: true
|
12
|
+
validates :principal_amount, numericality: { greater_than: 0 }
|
13
|
+
validates :remaining_amount, numericality: { greater_than_or_equal_to: 0 }
|
13
14
|
validates :status, inclusion: { in: %w[pending approved disbursed active overdue paid closed] }
|
14
15
|
|
15
16
|
scope :active, -> { where(active: true) }
|
@@ -20,14 +21,34 @@ module Dscf::Credit
|
|
20
21
|
scope :past_due, -> { where("due_date < ? AND status IN (?)", Date.current, [ "active", "overdue" ]) }
|
21
22
|
|
22
23
|
def self.ransackable_attributes(auth_object = nil)
|
23
|
-
%w[id status principal_amount
|
24
|
+
%w[id status principal_amount remaining_amount due_date disbursed_at active created_at updated_at]
|
24
25
|
end
|
25
26
|
|
26
27
|
def self.ransackable_associations(auth_object = nil)
|
27
|
-
%w[loan_profile credit_line loan_transactions daily_routine_transactions]
|
28
|
+
%w[loan_profile credit_line loan_transactions daily_routine_transactions loan_accruals]
|
28
29
|
end
|
29
30
|
|
30
31
|
delegate :user, to: :loan_profile, allow_nil: true
|
31
32
|
delegate :bank, to: :credit_line, allow_nil: true
|
33
|
+
|
34
|
+
def total_facilitation_fee
|
35
|
+
loan_accruals.where(accrual_type: 'facilitation_fee', status: 'pending').sum(:amount)
|
36
|
+
end
|
37
|
+
|
38
|
+
def total_interest
|
39
|
+
loan_accruals.where(accrual_type: 'interest', status: 'pending').sum(:amount)
|
40
|
+
end
|
41
|
+
|
42
|
+
def total_penalty
|
43
|
+
loan_accruals.where(accrual_type: 'penalty', status: 'pending').sum(:amount)
|
44
|
+
end
|
45
|
+
|
46
|
+
def total_vat
|
47
|
+
loan_accruals.where(accrual_type: 'tax', status: 'pending').sum(:amount)
|
48
|
+
end
|
49
|
+
|
50
|
+
def total_outstanding
|
51
|
+
remaining_amount + loan_accruals.where(status: 'pending').sum(:amount)
|
52
|
+
end
|
32
53
|
end
|
33
54
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Dscf::Credit
|
2
|
+
class LoanAccrual < ApplicationRecord
|
3
|
+
self.table_name = "dscf_credit_loan_accruals"
|
4
|
+
|
5
|
+
belongs_to :loan, class_name: "Dscf::Credit::Loan", foreign_key: "loan_id"
|
6
|
+
|
7
|
+
validates :accrual_type, :amount, :applied_on, presence: true
|
8
|
+
validates :amount, numericality: { greater_than: 0 }
|
9
|
+
validates :accrual_type, inclusion: { in: %w[interest penalty tax facilitation_fee late_fee other] }
|
10
|
+
validates :status, inclusion: { in: %w[pending paid cancelled] }
|
11
|
+
|
12
|
+
scope :pending, -> { where(status: "pending") }
|
13
|
+
scope :paid, -> { where(status: "paid") }
|
14
|
+
scope :by_type, ->(type) { where(accrual_type: type) }
|
15
|
+
scope :for_date_range, ->(start_date, end_date) { where(applied_on: start_date..end_date) }
|
16
|
+
|
17
|
+
def self.ransackable_attributes(auth_object = nil)
|
18
|
+
%w[id accrual_type amount applied_on status created_at updated_at]
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.ransackable_associations(auth_object = nil)
|
22
|
+
%w[loan]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -1,11 +1,33 @@
|
|
1
1
|
module Dscf::Credit
|
2
2
|
class LoanSerializer < ActiveModel::Serializer
|
3
|
-
attributes :id, :status, :principal_amount, :
|
4
|
-
:
|
3
|
+
attributes :id, :status, :principal_amount, :remaining_amount, :due_date, :disbursed_at, :total_interest, :total_penalty,
|
4
|
+
:total_vat, :total_facilitation_fee, :total_outstanding, :active, :created_at, :updated_at
|
5
5
|
|
6
6
|
belongs_to :loan_profile, serializer: LoanProfileSerializer
|
7
7
|
belongs_to :credit_line, serializer: CreditLineSerializer
|
8
8
|
has_many :loan_transactions, serializer: LoanTransactionSerializer
|
9
9
|
has_many :daily_routine_transactions, serializer: DailyRoutineTransactionSerializer
|
10
|
+
has_many :loan_accruals, serializer: LoanAccrualSerializer
|
11
|
+
|
12
|
+
def total_interest
|
13
|
+
object.total_interest
|
14
|
+
end
|
15
|
+
|
16
|
+
def total_penalty
|
17
|
+
object.total_penalty
|
18
|
+
end
|
19
|
+
|
20
|
+
def total_vat
|
21
|
+
object.total_vat
|
22
|
+
end
|
23
|
+
|
24
|
+
def total_facilitation_fee
|
25
|
+
object.total_facilitation_fee
|
26
|
+
end
|
27
|
+
|
28
|
+
def total_outstanding
|
29
|
+
object.total_outstanding
|
30
|
+
end
|
31
|
+
|
10
32
|
end
|
11
33
|
end
|
@@ -1,22 +1,22 @@
|
|
1
1
|
module Dscf::Credit
|
2
2
|
class CreditScoringEngine
|
3
|
-
attr_reader :loan_application, :
|
3
|
+
attr_reader :loan_application, :scoring_param_type_id, :errors
|
4
4
|
|
5
|
-
def initialize(loan_application_id,
|
5
|
+
def initialize(loan_application_id, scoring_param_type_id)
|
6
6
|
@loan_application = find_loan_application(loan_application_id)
|
7
|
-
@
|
7
|
+
@scoring_param_type_id = scoring_param_type_id
|
8
8
|
@errors = []
|
9
9
|
end
|
10
10
|
|
11
11
|
def calculate_score
|
12
12
|
return error_result("Loan application not found") unless loan_application
|
13
|
-
return error_result("
|
13
|
+
return error_result("Scoring parameter type ID is required") unless scoring_param_type_id
|
14
14
|
|
15
15
|
begin
|
16
16
|
scoring_parameters = get_active_scoring_parameters
|
17
17
|
|
18
18
|
if scoring_parameters.empty?
|
19
|
-
return error_result("No active scoring parameters found for this bank for
|
19
|
+
return error_result("No active scoring parameters found for this bank for scoring parameter type ID #{scoring_param_type_id}")
|
20
20
|
end
|
21
21
|
|
22
22
|
# Calculate weighted score using the formula
|
@@ -45,7 +45,7 @@ module Dscf::Credit
|
|
45
45
|
loan_application.bank
|
46
46
|
.scoring_parameters
|
47
47
|
.active
|
48
|
-
.where(
|
48
|
+
.where(scoring_param_type_id: scoring_param_type_id)
|
49
49
|
.includes(:scoring_param_type, :parameter_normalizers, :category)
|
50
50
|
end
|
51
51
|
|
@@ -217,7 +217,7 @@ module Dscf::Credit
|
|
217
217
|
success: true,
|
218
218
|
loan_application_id: loan_application.id,
|
219
219
|
bank_id: loan_application.bank_id,
|
220
|
-
|
220
|
+
scoring_param_type_id: scoring_param_type_id,
|
221
221
|
calculated_at: Time.current,
|
222
222
|
score: final_score,
|
223
223
|
status: review_status,
|
@@ -235,7 +235,7 @@ module Dscf::Credit
|
|
235
235
|
{
|
236
236
|
success: false,
|
237
237
|
loan_application_id: loan_application&.id,
|
238
|
-
|
238
|
+
scoring_param_type_id: scoring_param_type_id,
|
239
239
|
error: message,
|
240
240
|
errors: @errors,
|
241
241
|
score: 0.0,
|