dscf-credit 0.1.7 → 0.1.9
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/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 +9 -4
- data/app/controllers/dscf/credit/loans_controller.rb +2 -6
- data/app/controllers/dscf/credit/repayments_controller.rb +2 -3
- data/app/controllers/dscf/credit/scoring_parameters_controller.rb +1 -1
- 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/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 +35 -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 +34 -0
- data/config/routes.rb +6 -0
- 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: 3a206be7d1f68e8a83476b508b3238c990eb67226243a5f242d3149828454891
|
4
|
+
data.tar.gz: 2fb1cc62bf33eaf81c4dd52e8413ab9bb2bfa186287b4714b603cb95360fe522
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cc931ad8d941a12b6d4680a761ea5210982ed5714e54bd2aec47a74444fea96e92a526250301d4391194da9bf389e92c4253818e6540fafdbf2d9d3d7728b0d7
|
7
|
+
data.tar.gz: c24155a0a4ef04ebfa0366d523feeba334842be98dcff13708447af5b286a81b0a5118786f785748b0f65c1e76c263b98b6e44196cc55e2501381eceadaf2bf4
|
@@ -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,8 @@ 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,
|
109
|
+
{ eligible_credit_lines: :credit_line },
|
109
110
|
reviews: { reviewed_by: :user_profile }
|
110
111
|
]
|
111
112
|
end
|
@@ -116,13 +117,17 @@ module Dscf::Credit
|
|
116
117
|
|
117
118
|
def default_serializer_includes
|
118
119
|
{
|
119
|
-
index: [ :loan_application,
|
120
|
+
index: [ :loan_application, { eligible_credit_lines: :credit_line },
|
121
|
+
reviews: { reviewed_by: :user_profile } ],
|
120
122
|
show: [
|
121
|
-
:loan_application, :loan_profile_scoring_specs, :loans,
|
123
|
+
:loan_application, :loan_profile_scoring_specs, :loans,
|
124
|
+
{ eligible_credit_lines: :credit_line },
|
125
|
+
reviews: { reviewed_by: :user_profile }
|
122
126
|
],
|
123
127
|
create: [ :loan_application, :loan_profile_scoring_specs, :reviews ],
|
124
128
|
update: [
|
125
|
-
:loan_application, :loan_profile_scoring_specs, :loans,
|
129
|
+
:loan_application, :loan_profile_scoring_specs, :loans,
|
130
|
+
{ eligible_credit_lines: :credit_line },
|
126
131
|
reviews: { reviewed_by: :user_profile }
|
127
132
|
]
|
128
133
|
}
|
@@ -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
|
@@ -4,7 +4,7 @@ module Dscf::Credit
|
|
4
4
|
loan = Dscf::Credit::Loan.find(params[:loan_id])
|
5
5
|
payment_amount = params[:amount].to_f
|
6
6
|
|
7
|
-
service = Dscf::Credit::RepaymentService.new(loan, payment_amount
|
7
|
+
service = Dscf::Credit::RepaymentService.new(loan, payment_amount)
|
8
8
|
result = service.process_repayment
|
9
9
|
|
10
10
|
if result[:success]
|
@@ -18,8 +18,7 @@ module Dscf::Credit
|
|
18
18
|
include: [
|
19
19
|
:loan_profile,
|
20
20
|
:credit_line,
|
21
|
-
:
|
22
|
-
:loan_transactions
|
21
|
+
:loan_accruals
|
23
22
|
]
|
24
23
|
}
|
25
24
|
)
|
@@ -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 }
|
@@ -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
|
@@ -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,44 @@
|
|
1
1
|
module Dscf::Credit
|
2
2
|
class LoanSerializer < ActiveModel::Serializer
|
3
|
-
attributes :id,
|
4
|
-
:
|
3
|
+
attributes :id,
|
4
|
+
:status,
|
5
|
+
:principal_amount,
|
6
|
+
:remaining_amount,
|
7
|
+
:due_date,
|
8
|
+
:disbursed_at,
|
9
|
+
:total_interest,
|
10
|
+
:total_penalty,
|
11
|
+
:total_vat,
|
12
|
+
:total_facilitation_fee,
|
13
|
+
:total_outstanding,
|
14
|
+
:active,
|
15
|
+
:created_at,
|
16
|
+
:updated_at
|
5
17
|
|
6
18
|
belongs_to :loan_profile, serializer: LoanProfileSerializer
|
7
19
|
belongs_to :credit_line, serializer: CreditLineSerializer
|
8
20
|
has_many :loan_transactions, serializer: LoanTransactionSerializer
|
9
21
|
has_many :daily_routine_transactions, serializer: DailyRoutineTransactionSerializer
|
22
|
+
has_many :loan_accruals, serializer: LoanAccrualSerializer
|
23
|
+
|
24
|
+
def total_interest
|
25
|
+
object.total_interest
|
26
|
+
end
|
27
|
+
|
28
|
+
def total_penalty
|
29
|
+
object.total_penalty
|
30
|
+
end
|
31
|
+
|
32
|
+
def total_vat
|
33
|
+
object.total_vat
|
34
|
+
end
|
35
|
+
|
36
|
+
def total_facilitation_fee
|
37
|
+
object.total_facilitation_fee
|
38
|
+
end
|
39
|
+
|
40
|
+
def total_outstanding
|
41
|
+
object.total_outstanding
|
42
|
+
end
|
10
43
|
end
|
11
44
|
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,
|
@@ -60,11 +60,7 @@ module Dscf::Credit
|
|
60
60
|
loan_profile: loan_profile,
|
61
61
|
credit_line: credit_line,
|
62
62
|
principal_amount: amount,
|
63
|
-
|
64
|
-
total_loan_amount: loan_terms[:total_amount],
|
65
|
-
remaining_amount: loan_terms[:total_amount],
|
66
|
-
accrued_interest: 0,
|
67
|
-
accrued_penalty: 0,
|
63
|
+
remaining_amount: amount, # Only principal amount, not total
|
68
64
|
status: "disbursed",
|
69
65
|
due_date: loan_terms[:due_date],
|
70
66
|
disbursed_at: Time.current,
|
@@ -73,6 +69,12 @@ module Dscf::Credit
|
|
73
69
|
|
74
70
|
raise "Failed to create loan: #{loan.errors.full_messages.join(', ')}" unless loan.save
|
75
71
|
|
72
|
+
# Create facilitator fee accrual
|
73
|
+
create_facilitator_fee_accrual(loan, loan_terms[:facilitation_fee])
|
74
|
+
|
75
|
+
# Create VAT accrual if applicable
|
76
|
+
create_vat_accrual(loan, loan_terms[:vat_amount]) if loan_terms[:vat_amount] > 0
|
77
|
+
|
76
78
|
loan
|
77
79
|
end
|
78
80
|
|
@@ -104,19 +106,59 @@ module Dscf::Credit
|
|
104
106
|
}
|
105
107
|
end
|
106
108
|
|
109
|
+
def create_facilitator_fee_accrual(loan, facilitation_fee)
|
110
|
+
return if facilitation_fee <= 0
|
111
|
+
|
112
|
+
Dscf::Credit::LoanAccrual.create!(
|
113
|
+
loan: loan,
|
114
|
+
accrual_type: "facilitation_fee",
|
115
|
+
amount: facilitation_fee,
|
116
|
+
applied_on: Date.current,
|
117
|
+
status: "pending"
|
118
|
+
)
|
119
|
+
end
|
120
|
+
|
121
|
+
def create_vat_accrual(loan, vat_amount)
|
122
|
+
return if vat_amount <= 0
|
123
|
+
|
124
|
+
Dscf::Credit::LoanAccrual.create!(
|
125
|
+
loan: loan,
|
126
|
+
accrual_type: "tax",
|
127
|
+
amount: vat_amount,
|
128
|
+
applied_on: Date.current,
|
129
|
+
status: "pending"
|
130
|
+
)
|
131
|
+
end
|
132
|
+
|
107
133
|
def update_credit_line_limits(loan)
|
108
|
-
|
134
|
+
# Calculate total amount from accruals: principal + facilitation_fee + vat
|
135
|
+
facilitation_fee = loan.loan_accruals.find_by(accrual_type: "facilitation_fee")&.amount || 0
|
136
|
+
vat = loan.loan_accruals.find_by(accrual_type: "tax")&.amount || 0
|
137
|
+
total_amount = loan.principal_amount + facilitation_fee + vat
|
138
|
+
|
139
|
+
new_available_limit = eligible_credit_line.available_limit - total_amount
|
109
140
|
eligible_credit_line.update!(available_limit: [ new_available_limit, 0 ].max)
|
110
141
|
end
|
111
142
|
|
112
143
|
def success_result(loan)
|
144
|
+
# Reload to get associated accruals
|
145
|
+
loan.reload
|
146
|
+
|
147
|
+
facilitation_fee_accrual = loan.loan_accruals.find_by(accrual_type: "facilitation_fee")
|
148
|
+
vat_accrual = loan.loan_accruals.find_by(accrual_type: "tax")
|
149
|
+
|
150
|
+
facilitation_fee = facilitation_fee_accrual&.amount&.to_f || 0.0
|
151
|
+
vat_amount = vat_accrual&.amount&.to_f || 0.0
|
152
|
+
total_amount = loan.principal_amount.to_f + facilitation_fee + vat_amount
|
153
|
+
|
113
154
|
{
|
114
155
|
success: true,
|
115
156
|
loan: loan,
|
116
157
|
disbursement_details: {
|
117
158
|
principal_amount: loan.principal_amount.to_f,
|
118
|
-
facilitation_fee:
|
119
|
-
|
159
|
+
facilitation_fee: facilitation_fee,
|
160
|
+
vat_amount: vat_amount,
|
161
|
+
total_loan_amount: total_amount,
|
120
162
|
due_date: loan.due_date,
|
121
163
|
disbursed_at: loan.disbursed_at
|
122
164
|
},
|