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.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/dscf/credit/facilitator_applications_controller.rb +88 -5
  3. data/app/controllers/dscf/credit/loan_accruals_controller.rb +113 -0
  4. data/app/controllers/dscf/credit/loan_applications_controller.rb +5 -5
  5. data/app/controllers/dscf/credit/loan_profiles_controller.rb +2 -2
  6. data/app/controllers/dscf/credit/loans_controller.rb +2 -6
  7. data/app/controllers/dscf/credit/repayments_controller.rb +3 -3
  8. data/app/controllers/dscf/credit/scoring_parameters_controller.rb +9 -13
  9. data/app/controllers/dscf/credit/users_controller.rb +6 -0
  10. data/app/jobs/dscf/credit/generate_daily_accruals_job.rb +78 -0
  11. data/app/models/dscf/credit/facilitator_application.rb +0 -1
  12. data/app/models/dscf/credit/loan.rb +26 -5
  13. data/app/models/dscf/credit/loan_accrual.rb +25 -0
  14. data/app/serializers/dscf/credit/loan_accrual_serializer.rb +7 -0
  15. data/app/serializers/dscf/credit/loan_serializer.rb +24 -2
  16. data/app/services/dscf/credit/credit_scoring_engine.rb +8 -8
  17. data/app/services/dscf/credit/disbursement_service.rb +50 -8
  18. data/app/services/dscf/credit/loan_accrual_generator_service.rb +272 -0
  19. data/app/services/dscf/credit/repayment_service.rb +216 -87
  20. data/config/locales/en.yml +36 -0
  21. data/config/routes.rb +10 -0
  22. data/db/migrate/20250822092426_create_dscf_credit_facilitator_applications.rb +1 -1
  23. data/db/migrate/20250822092654_create_dscf_credit_loans.rb +0 -4
  24. data/db/migrate/20251003132939_create_dscf_credit_loan_accruals.rb +19 -0
  25. data/db/seeds.rb +20 -5
  26. data/lib/dscf/credit/version.rb +1 -1
  27. data/spec/factories/dscf/credit/loan_accruals.rb +29 -0
  28. data/spec/factories/dscf/credit/loans.rb +0 -4
  29. metadata +9 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 447633e68d0087ea77917d1630137c52d138096a52039af3c67c577ce3c5edd9
4
- data.tar.gz: 5f4b38640b2adf043787cca20d5138b3e40185333f04bb9d747203bd14abf575
3
+ metadata.gz: 1e5ce384f2229c93db978e4eff5599a0d0fb9fe74e586da8bab5209a63dcb9f6
4
+ data.tar.gz: 889029fa9ee854b75198eed3a539215139d59914f09f8337674583ea153b3703
5
5
  SHA512:
6
- metadata.gz: 02ad7c0149c073efb33bbc55e318d6cee9f6096aa29af50ef7316f3caf80ee7396c5cd3fdc7387287e88152e5af9b5854b90620b436fc6487c8c241ec8c1a98a
7
- data.tar.gz: '099dd81a6c2f8f788fb38fb6c4f2d8b7f636a0853fbbed972b1078e0da5ef8e6cfe4c7b7fdec5d4bd1d5965e68f93cc8861a3122b20c1f670ebea186dba7c67b'
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
- facilitator_application = @clazz.new(model_params)
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
- facilitator_application
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
- category_id = score_params[:category_id]
65
+ scoring_param_type_id = score_params[:scoring_param_type_id]
66
66
 
67
- unless category_id
67
+ unless scoring_param_type_id
68
68
  return render_error(
69
69
  "loan_application.errors.calculate_credit_score",
70
- errors: [ "Category ID is required for credit scoring" ],
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, category_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(:category_id)
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, :eligible_credit_lines,
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, :eligible_credit_lines, reviews: { reviewed_by: :user_profile }
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 total_loan_amount remaining_amount due_date disbursed_at created_at updated_at]
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, current_user)
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
- :payment_request,
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 validate_category_weight_limit(scoring_parameter)
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 category")
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 validate_category_weight_limit(@obj, exclude_current: true)
24
- @obj.errors.add(:weight, "would exceed the total weight limit of 1.0 for this category")
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 validate_category_weight_limit(scoring_parameter, exclude_current: false)
79
- return true unless scoring_parameter.category_id && scoring_parameter.weight
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
- category_id: scoring_parameter.category_id,
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
- # Allow small floating point tolerance (e.g., 1.0001 should be acceptable)
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, :total_loan_amount, :remaining_amount, :due_date, presence: true
11
- validates :principal_amount, :total_loan_amount, :remaining_amount, numericality: { greater_than: 0 }
12
- validates :accrued_interest, :accrued_penalty, :facilitation_fee, numericality: { greater_than_or_equal_to: 0 }
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 accrued_interest accrued_penalty facilitation_fee total_loan_amount remaining_amount due_date disbursed_at active created_at updated_at]
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
@@ -0,0 +1,7 @@
1
+ module Dscf::Credit
2
+ class LoanAccrualSerializer < ActiveModel::Serializer
3
+ attributes :id, :accrual_type, :amount, :applied_on, :status, :created_at, :updated_at
4
+
5
+ belongs_to :loan, serializer: LoanSerializer
6
+ end
7
+ end
@@ -1,11 +1,33 @@
1
1
  module Dscf::Credit
2
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, :active, :created_at, :updated_at
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, :category_id, :errors
3
+ attr_reader :loan_application, :scoring_param_type_id, :errors
4
4
 
5
- def initialize(loan_application_id, category_id)
5
+ def initialize(loan_application_id, scoring_param_type_id)
6
6
  @loan_application = find_loan_application(loan_application_id)
7
- @category_id = category_id
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("Category ID is required") unless category_id
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 category ID #{category_id}")
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(category_id: category_id)
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
- category_id: category_id,
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
- category_id: category_id,
238
+ scoring_param_type_id: scoring_param_type_id,
239
239
  error: message,
240
240
  errors: @errors,
241
241
  score: 0.0,