dscf-credit 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/dscf/core/authenticatable.rb +81 -0
  3. data/app/controllers/concerns/dscf/core/common.rb +200 -0
  4. data/app/controllers/concerns/dscf/core/filterable.rb +12 -0
  5. data/app/controllers/concerns/dscf/core/json_response.rb +77 -0
  6. data/app/controllers/concerns/dscf/core/pagination.rb +71 -0
  7. data/app/controllers/concerns/dscf/core/token_authenticatable.rb +53 -0
  8. data/app/controllers/dscf/credit/categories_controller.rb +3 -3
  9. data/app/controllers/dscf/credit/credit_limit_calculations_controller.rb +50 -0
  10. data/app/controllers/dscf/credit/credit_line_specs_controller.rb +2 -1
  11. data/app/controllers/dscf/credit/credit_lines_controller.rb +3 -3
  12. data/app/controllers/dscf/credit/disbursements_controller.rb +55 -0
  13. data/app/controllers/dscf/credit/eligible_credit_lines_controller.rb +50 -0
  14. data/app/controllers/dscf/credit/loan_profiles_controller.rb +138 -0
  15. data/app/controllers/dscf/credit/payment_requests_controller.rb +54 -5
  16. data/app/controllers/dscf/credit/repayments_controller.rb +53 -0
  17. data/app/controllers/dscf/credit/scoring_parameters_controller.rb +3 -3
  18. data/app/controllers/dscf/credit/scoring_tables_controller.rb +8 -8
  19. data/app/models/dscf/credit/bank_branch.rb +2 -1
  20. data/app/models/dscf/credit/category.rb +3 -1
  21. data/app/models/dscf/credit/credit_line.rb +3 -3
  22. data/app/models/dscf/credit/credit_line_spec.rb +3 -3
  23. data/app/models/dscf/credit/eligible_credit_line.rb +28 -0
  24. data/app/models/dscf/credit/loan_profile.rb +8 -5
  25. data/app/models/dscf/credit/loan_profile_scoring_spec.rb +4 -5
  26. data/app/models/dscf/credit/scoring_parameter.rb +2 -2
  27. data/app/models/dscf/credit/scoring_table.rb +4 -4
  28. data/app/serializers/dscf/credit/bank_branch_serializer.rb +1 -0
  29. data/app/serializers/dscf/credit/category_serializer.rb +2 -0
  30. data/app/serializers/dscf/credit/credit_line_serializer.rb +2 -0
  31. data/app/serializers/dscf/credit/credit_line_spec_serializer.rb +1 -1
  32. data/app/serializers/dscf/credit/daily_routine_transaction_serializer.rb +8 -0
  33. data/app/serializers/dscf/credit/eligible_credit_line_serializer.rb +8 -0
  34. data/app/serializers/dscf/credit/loan_profile_scoring_spec_serializer.rb +8 -0
  35. data/app/serializers/dscf/credit/loan_profile_serializer.rb +15 -0
  36. data/app/serializers/dscf/credit/loan_serializer.rb +12 -0
  37. data/app/serializers/dscf/credit/loan_transaction_serializer.rb +8 -0
  38. data/app/serializers/dscf/credit/payment_request_serializer.rb +10 -0
  39. data/app/serializers/dscf/credit/payment_serializer.rb +8 -0
  40. data/app/serializers/dscf/credit/scoring_parameter_serializer.rb +2 -0
  41. data/app/serializers/dscf/credit/scoring_table_serializer.rb +1 -1
  42. data/app/services/dscf/credit/credit_limit_calculation_service.rb +153 -0
  43. data/app/services/dscf/credit/disbursement_service.rb +180 -0
  44. data/app/services/dscf/credit/repayment_service.rb +216 -0
  45. data/app/services/dscf/credit/risk_application_service.rb +27 -0
  46. data/app/services/dscf/credit/scoring_service.rb +297 -0
  47. data/config/locales/en.yml +79 -0
  48. data/config/routes.rb +19 -5
  49. data/db/migrate/20250822091527_create_dscf_credit_credit_line_specs.rb +1 -0
  50. data/db/migrate/20250822092246_create_dscf_credit_loan_profiles.rb +2 -1
  51. data/db/migrate/20250822092417_create_dscf_credit_loan_profile_scoring_specs.rb +5 -8
  52. data/db/migrate/20250901172842_create_dscf_credit_scoring_tables.rb +2 -2
  53. data/db/migrate/20250917120000_create_dscf_credit_eligible_credit_lines.rb +18 -0
  54. data/db/seeds.rb +88 -22
  55. data/lib/dscf/credit/version.rb +1 -1
  56. data/spec/factories/dscf/credit/credit_line_specs.rb +1 -0
  57. data/spec/factories/dscf/credit/credit_lines.rb +3 -3
  58. data/spec/factories/dscf/credit/eligible_credit_lines.rb +33 -0
  59. data/spec/factories/dscf/credit/loan_profile_scoring_specs.rb +1 -4
  60. data/spec/factories/dscf/credit/loan_profiles.rb +1 -0
  61. data/spec/factories/dscf/credit/scoring_tables.rb +1 -1
  62. metadata +29 -2
@@ -0,0 +1,8 @@
1
+ module Dscf::Credit
2
+ class EligibleCreditLineSerializer < ActiveModel::Serializer
3
+ attributes :id, :credit_limit, :available_limit, :risk, :created_at, :updated_at
4
+
5
+ belongs_to :loan_profile, serializer: Dscf::Credit::LoanProfileSerializer
6
+ belongs_to :credit_line, serializer: Dscf::Credit::CreditLineSerializer
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ module Dscf::Credit
2
+ class LoanProfileScoringSpecSerializer < ActiveModel::Serializer
3
+ attributes :id, :scoring_input_data, :score, :total_limit, :active, :created_at, :updated_at
4
+
5
+ belongs_to :loan_profile, serializer: Dscf::Credit::LoanProfileSerializer
6
+ belongs_to :created_by
7
+ end
8
+ end
@@ -0,0 +1,15 @@
1
+ module Dscf::Credit
2
+ class LoanProfileSerializer < ActiveModel::Serializer
3
+ attributes :id, :status, :total_amount, :available_amount, :review_date, :review_feedback, :created_at, :updated_at
4
+
5
+ belongs_to :bank, serializer: Dscf::Credit::BankSerializer
6
+ belongs_to :review_branch, serializer: Dscf::Credit::BankBranchSerializer
7
+ belongs_to :reviewed_by
8
+ belongs_to :backer
9
+ belongs_to :user, serializer: Dscf::Core::UserSerializer
10
+
11
+ has_many :loan_profile_scoring_specs, serializer: Dscf::Credit::LoanProfileScoringSpecSerializer
12
+ has_many :loans, serializer: Dscf::Credit::LoanSerializer
13
+ has_many :eligible_credit_lines, serializer: Dscf::Credit::EligibleCreditLineSerializer
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ module Dscf::Credit
2
+ class LoanSerializer < ActiveModel::Serializer
3
+ attributes :id, :status, :principal_amount, :accrued_interest, :accrued_penalty, :facilitation_fee,
4
+ :total_loan_amount, :remaining_amount, :due_date, :disbursed_at, :created_at, :updated_at
5
+
6
+ belongs_to :loan_profile, serializer: LoanProfileSerializer
7
+ belongs_to :credit_line, serializer: CreditLineSerializer
8
+ belongs_to :payment_request, serializer: PaymentRequestSerializer
9
+ has_many :loan_transactions, serializer: LoanTransactionSerializer
10
+ has_many :daily_routine_transactions, serializer: DailyRoutineTransactionSerializer
11
+ end
12
+ end
@@ -0,0 +1,8 @@
1
+ module Dscf::Credit
2
+ class LoanTransactionSerializer < ActiveModel::Serializer
3
+ attributes :id, :transaction_type, :amount, :transaction_reference, :status,
4
+ :created_at, :updated_at
5
+
6
+ belongs_to :loan, serializer: LoanSerializer
7
+ end
8
+ end
@@ -0,0 +1,10 @@
1
+ module Dscf::Credit
2
+ class PaymentRequestSerializer < ActiveModel::Serializer
3
+ attributes :id, :order_id, :request_type, :amount, :receiver_account_reference,
4
+ :status, :failure_reason, :initiated_at, :approved_at, :created_at, :updated_at
5
+
6
+ belongs_to :user, serializer: Dscf::Core::UserSerializer
7
+ has_many :loans, serializer: Dscf::Credit::LoanSerializer
8
+ has_many :payments, serializer: Dscf::Credit::PaymentSerializer
9
+ end
10
+ end
@@ -0,0 +1,8 @@
1
+ module Dscf::Credit
2
+ class PaymentSerializer < ActiveModel::Serializer
3
+ attributes :id, :amount, :receiver_account_reference, :transaction_reference,
4
+ :status, :failure_reason, :processed_at, :created_at, :updated_at
5
+
6
+ belongs_to :payment_request, serializer: PaymentRequestSerializer
7
+ end
8
+ end
@@ -9,5 +9,7 @@ module Dscf::Credit
9
9
  belongs_to :scoring_param_type, serializer: Dscf::Credit::ScoringParamTypeSerializer
10
10
  belongs_to :previous_version, serializer: Dscf::Credit::ScoringParameterSerializer
11
11
  has_many :parameter_normalizers, serializer: Dscf::Credit::ParameterNormalizerSerializer
12
+ has_many :scoring_tables, serializer: Dscf::Credit::ScoringTableSerializer
13
+ has_many :categories, serializer: Dscf::Credit::CategorySerializer
12
14
  end
13
15
  end
@@ -2,7 +2,7 @@ module Dscf::Credit
2
2
  class ScoringTableSerializer < ActiveModel::Serializer
3
3
  attributes :id, :weight, :active, :created_at, :updated_at
4
4
 
5
- belongs_to :credit_line, serializer: Dscf::Credit::CreditLineSerializer
5
+ belongs_to :category, serializer: Dscf::Credit::CategorySerializer
6
6
  belongs_to :scoring_parameter, serializer: Dscf::Credit::ScoringParameterSerializer
7
7
  belongs_to :created_by, serializer: Dscf::Core::UserSerializer
8
8
  end
@@ -0,0 +1,153 @@
1
+ module Dscf::Credit
2
+ class CreditLimitCalculationService
3
+ attr_reader :loan_profile, :category
4
+
5
+ def initialize(loan_profile, category)
6
+ @loan_profile = loan_profile
7
+ @category = category
8
+ end
9
+
10
+ # Calculate credit limits for all credit lines in a category for a specific loan profile
11
+ # @return [Hash] Result containing eligible credit lines with calculated limits
12
+ def calculate_credit_limits
13
+ return error_result("Loan profile not found") unless loan_profile
14
+ return error_result("Category not found") unless category
15
+
16
+ scoring_spec = loan_profile.loan_profile_scoring_specs.active.first
17
+ return error_result("No active scoring found for loan profile") unless scoring_spec
18
+
19
+ credit_lines = category.credit_lines.approved.includes(:bank, :category, :credit_line_specs)
20
+ return error_result("No approved credit lines found for category") if credit_lines.empty?
21
+
22
+ eligible_credit_lines = calculate_limits_for_credit_lines(credit_lines, scoring_spec)
23
+
24
+ success_result(eligible_credit_lines)
25
+ rescue StandardError => e
26
+ error_result("Credit limit calculation failed: #{e.message}")
27
+ end
28
+
29
+ private
30
+
31
+ def calculate_limits_for_credit_lines(credit_lines, scoring_spec)
32
+ results = []
33
+
34
+ credit_lines.each do |credit_line|
35
+ limit_data = calculate_individual_credit_limit(credit_line, scoring_spec)
36
+ next if limit_data[:credit_limit] <= 0
37
+
38
+ eligible_credit_line = find_or_create_eligible_credit_line(credit_line)
39
+ eligible_credit_line.assign_attributes(limit_data)
40
+
41
+ if eligible_credit_line.save
42
+ results << {
43
+ credit_line: credit_line,
44
+ eligible_credit_line: eligible_credit_line,
45
+ calculated_limit: limit_data[:credit_limit],
46
+ available_limit: limit_data[:available_limit],
47
+ risk_factor: limit_data[:risk]
48
+ }
49
+ end
50
+ end
51
+
52
+ results
53
+ end
54
+
55
+ def calculate_individual_credit_limit(credit_line, scoring_spec)
56
+ base_limit = scoring_spec.total_limit
57
+
58
+ credit_line_specs = credit_line.credit_line_specs.active
59
+ adjusted_limit = apply_credit_line_specifications(base_limit, credit_line_specs)
60
+
61
+ risk_factor = calculate_risk_factor(credit_line, loan_profile)
62
+ final_limit = adjusted_limit * (1 - risk_factor)
63
+
64
+ used_limit = calculate_used_limit(credit_line)
65
+ available_limit = [ final_limit - used_limit, 0 ].max
66
+
67
+ {
68
+ credit_limit: final_limit.round(2),
69
+ available_limit: available_limit.round(2),
70
+ risk: risk_factor.round(4)
71
+ }
72
+ end
73
+
74
+ def apply_credit_line_specifications(base_limit, credit_line_specs)
75
+ return base_limit if credit_line_specs.empty?
76
+
77
+ spec = credit_line_specs.first
78
+ return base_limit unless spec
79
+
80
+ if spec.credit_line_multiplier && spec.credit_line_multiplier > 0
81
+ adjustment_factor = spec.credit_line_multiplier / 30.0 # Normalize against default 30 days
82
+ adjusted_limit = base_limit * adjustment_factor
83
+ else
84
+ adjusted_limit = base_limit
85
+ end
86
+
87
+ if spec.max_amount && adjusted_limit > spec.max_amount
88
+ adjusted_limit = spec.max_amount
89
+ end
90
+
91
+ if spec.min_amount && adjusted_limit < spec.min_amount
92
+ adjusted_limit = spec.min_amount
93
+ end
94
+
95
+ adjusted_limit
96
+ end
97
+
98
+ def calculate_risk_factor(credit_line, loan_profile)
99
+ base_risk = 0.1 # 10% base risk
100
+
101
+ # Increase risk based on loan profile characteristics
102
+ risk_adjustments = 0.0
103
+
104
+ # Risk based on previous loan performance
105
+ if loan_profile.loans.overdue.any?
106
+ risk_adjustments += 0.2 # 20% additional risk for overdue loans
107
+ end
108
+
109
+ # Risk based on available amount vs total amount ratio
110
+ if loan_profile.total_amount > 0
111
+ utilization_ratio = (loan_profile.total_amount - loan_profile.available_amount) / loan_profile.total_amount
112
+ risk_adjustments += utilization_ratio * 0.1 # Up to 10% additional risk
113
+ end
114
+
115
+ # Risk based on credit line category
116
+ case credit_line.category.name.downcase
117
+ when "high_risk"
118
+ risk_adjustments += 0.15
119
+ when "medium_risk"
120
+ risk_adjustments += 0.05
121
+ end
122
+
123
+ # Cap total risk at 90%
124
+ [ base_risk + risk_adjustments, 0.9 ].min
125
+ end
126
+
127
+ def calculate_used_limit(credit_line)
128
+ credit_line.loans.where(status: [ "active", "overdue" ]).sum(:remaining_amount)
129
+ end
130
+
131
+ def find_or_create_eligible_credit_line(credit_line)
132
+ loan_profile.eligible_credit_lines.find_or_initialize_by(credit_line: credit_line)
133
+ end
134
+
135
+ def success_result(eligible_credit_lines)
136
+ {
137
+ success: true,
138
+ data: eligible_credit_lines,
139
+ message: "Credit limits calculated successfully",
140
+ count: eligible_credit_lines.size
141
+ }
142
+ end
143
+
144
+ def error_result(message)
145
+ {
146
+ success: false,
147
+ data: [],
148
+ error: message,
149
+ count: 0
150
+ }
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,180 @@
1
+ module Dscf::Credit
2
+ class DisbursementService
3
+ attr_reader :credit_line, :payment_request, :current_user
4
+
5
+ def initialize(credit_line, payment_request, current_user)
6
+ @credit_line = credit_line
7
+ @payment_request = payment_request
8
+ @current_user = current_user
9
+ end
10
+
11
+ # Process disbursement by creating a loan record from selected credit line
12
+ # @return [Hash] Result containing created loan and disbursement details
13
+ def process_disbursement
14
+ return error_result("Credit line not found") unless credit_line
15
+ return error_result("Payment request not found") unless payment_request
16
+ return error_result("Credit line is not approved") unless credit_line.status == "approved"
17
+
18
+ eligible_credit_line = find_eligible_credit_line
19
+ return error_result("No eligible credit line found") unless eligible_credit_line
20
+
21
+ loan_profile = eligible_credit_line.loan_profile
22
+ return error_result("Loan profile not approved") unless loan_profile.status == "approved"
23
+
24
+ validation_result = validate_disbursement_amount(eligible_credit_line)
25
+ return validation_result unless validation_result[:success]
26
+
27
+ ActiveRecord::Base.transaction do
28
+ loan = create_loan_record(loan_profile, eligible_credit_line)
29
+ process_payment_and_charges(loan)
30
+ update_credit_line_limits(eligible_credit_line, loan)
31
+ lock_other_credit_lines(loan_profile, credit_line)
32
+
33
+ success_result(loan)
34
+ end
35
+ rescue StandardError => e
36
+ error_result("Disbursement processing failed: #{e.message}")
37
+ end
38
+
39
+ private
40
+
41
+ def find_eligible_credit_line
42
+ credit_line.eligible_credit_lines
43
+ .joins(:loan_profile)
44
+ .where(dscf_credit_loan_profiles: { status: "approved" })
45
+ .first
46
+ end
47
+
48
+ def validate_disbursement_amount(eligible_credit_line)
49
+ requested_amount = payment_request.amount
50
+ available_limit = eligible_credit_line.available_limit
51
+
52
+ if requested_amount <= 0
53
+ return error_result("Invalid disbursement amount")
54
+ end
55
+
56
+ if requested_amount > available_limit
57
+ return error_result("Requested amount exceeds available credit limit")
58
+ end
59
+
60
+ { success: true }
61
+ end
62
+
63
+ def create_loan_record(loan_profile, eligible_credit_line)
64
+ loan_terms = calculate_loan_terms(eligible_credit_line)
65
+
66
+ loan = Dscf::Credit::Loan.new(
67
+ loan_profile: loan_profile,
68
+ credit_line: credit_line,
69
+ payment_request: payment_request,
70
+ principal_amount: payment_request.amount,
71
+ facilitation_fee: loan_terms[:facilitation_fee],
72
+ total_loan_amount: loan_terms[:total_amount],
73
+ remaining_amount: loan_terms[:total_amount],
74
+ accrued_interest: 0,
75
+ accrued_penalty: 0,
76
+ status: "disbursed",
77
+ due_date: loan_terms[:due_date],
78
+ disbursed_at: Time.current
79
+ )
80
+
81
+ raise "Failed to create loan: #{loan.errors.full_messages.join(', ')}" unless loan.save
82
+
83
+ loan
84
+ end
85
+
86
+ def calculate_loan_terms(eligible_credit_line)
87
+ principal = payment_request.amount
88
+
89
+ credit_line_spec = credit_line.credit_line_specs.active.first
90
+
91
+ if credit_line_spec
92
+ facilitation_rate = credit_line_spec.facilitation_fee_rate
93
+ vat_rate = credit_line_spec.vat
94
+ loan_duration = credit_line_spec.loan_duration
95
+ else
96
+ facilitation_rate = 0.02 # 2%
97
+ vat_rate = 0.15 # 15%
98
+ loan_duration = 30 # 30 days
99
+ end
100
+
101
+ facilitation_fee = principal * facilitation_rate
102
+ vat_amount = facilitation_fee * vat_rate
103
+ total_amount = principal + facilitation_fee + vat_amount
104
+
105
+ due_date = Date.current + loan_duration.days
106
+
107
+ {
108
+ facilitation_fee: facilitation_fee.round(2),
109
+ vat_amount: vat_amount.round(2),
110
+ total_amount: total_amount.round(2),
111
+ due_date: due_date
112
+ }
113
+ end
114
+
115
+ def process_payment_and_charges(loan)
116
+ payment_request.update!(
117
+ status: "processed",
118
+ approved_at: Time.current
119
+ )
120
+
121
+ Dscf::Credit::LoanTransaction.create!(
122
+ loan: loan,
123
+ transaction_type: "disbursement",
124
+ amount: loan.principal_amount,
125
+ transaction_reference: "DISB-#{payment_request.order_id}-#{Time.current.to_i}",
126
+ status: "completed"
127
+ )
128
+
129
+ if loan.facilitation_fee > 0
130
+ Dscf::Credit::LoanTransaction.create!(
131
+ loan: loan,
132
+ transaction_type: "fee_charge",
133
+ amount: loan.facilitation_fee,
134
+ transaction_reference: "FEE-#{loan.id}-#{Time.current.to_i}",
135
+ status: "completed"
136
+ )
137
+ end
138
+ end
139
+
140
+ def update_credit_line_limits(eligible_credit_line, loan)
141
+ new_available_limit = eligible_credit_line.available_limit - loan.total_loan_amount
142
+ eligible_credit_line.update!(available_limit: [ new_available_limit, 0 ].max)
143
+
144
+ # Update loan profile available amount
145
+ loan_profile = eligible_credit_line.loan_profile
146
+ new_profile_available = loan_profile.available_amount - loan.total_loan_amount
147
+ loan_profile.update!(available_amount: [ new_profile_available, 0 ].max)
148
+ end
149
+
150
+ def lock_other_credit_lines(loan_profile, current_credit_line)
151
+ other_eligible_lines = loan_profile.eligible_credit_lines
152
+ .where.not(credit_line: current_credit_line)
153
+
154
+ other_eligible_lines.update_all(available_limit: 0)
155
+ end
156
+
157
+ def success_result(loan)
158
+ {
159
+ success: true,
160
+ loan: loan,
161
+ disbursement_details: {
162
+ principal_amount: loan.principal_amount,
163
+ facilitation_fee: loan.facilitation_fee,
164
+ total_loan_amount: loan.total_loan_amount,
165
+ due_date: loan.due_date,
166
+ disbursed_at: loan.disbursed_at
167
+ },
168
+ message: "Disbursement processed successfully"
169
+ }
170
+ end
171
+
172
+ def error_result(message)
173
+ {
174
+ success: false,
175
+ loan: nil,
176
+ error: message
177
+ }
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,216 @@
1
+ module Dscf::Credit
2
+ class RepaymentService
3
+ attr_reader :loan, :payment_amount, :current_user
4
+
5
+ def initialize(loan, payment_amount, current_user)
6
+ @loan = loan
7
+ @payment_amount = payment_amount.to_f
8
+ @current_user = current_user
9
+ end
10
+
11
+ # Process loan repayment by decreasing loan balance and updating related records
12
+ # @return [Hash] Result containing payment details and updated loan status
13
+ def process_repayment
14
+ return error_result("Loan not found") unless loan
15
+ return error_result("Invalid payment amount") unless payment_amount > 0
16
+ return error_result("Loan is not active") unless loan.status.in?([ "active", "overdue", "disbursed" ])
17
+
18
+ payment_allocation = calculate_payment_allocation
19
+
20
+ return error_result("Payment amount exceeds total outstanding balance") if payment_amount > payment_allocation[:total_outstanding]
21
+
22
+ ActiveRecord::Base.transaction do
23
+ process_payment_allocation(payment_allocation)
24
+
25
+ update_loan_status
26
+
27
+ create_payment_transaction(payment_allocation)
28
+
29
+ reactivate_facilities_if_paid_off
30
+
31
+ success_result(payment_allocation)
32
+ end
33
+ rescue StandardError => e
34
+ error_result("Repayment processing failed: #{e.message}")
35
+ end
36
+
37
+ private
38
+
39
+ def calculate_payment_allocation
40
+ current_facilitation_fee = loan.facilitation_fee || 0
41
+ current_penalty = loan.accrued_penalty || 0
42
+ current_interest = loan.accrued_interest || 0
43
+ current_principal = loan.remaining_amount || 0
44
+
45
+ total_outstanding = current_facilitation_fee + current_penalty + current_interest + current_principal
46
+
47
+ remaining_payment = payment_amount
48
+
49
+ facilitation_payment = [ remaining_payment, current_facilitation_fee ].min
50
+ remaining_payment -= facilitation_payment
51
+
52
+ penalty_payment = [ remaining_payment, current_penalty ].min
53
+ remaining_payment -= penalty_payment
54
+
55
+ interest_payment = [ remaining_payment, current_interest ].min
56
+ remaining_payment -= interest_payment
57
+
58
+ principal_payment = [ remaining_payment, current_principal ].min
59
+
60
+ {
61
+ total_outstanding: total_outstanding,
62
+ payment_amount: payment_amount,
63
+ facilitation_payment: facilitation_payment,
64
+ penalty_payment: penalty_payment,
65
+ interest_payment: interest_payment,
66
+ principal_payment: principal_payment,
67
+ remaining_facilitation_fee: current_facilitation_fee - facilitation_payment,
68
+ remaining_penalty: current_penalty - penalty_payment,
69
+ remaining_interest: current_interest - interest_payment,
70
+ remaining_principal: current_principal - principal_payment
71
+ }
72
+ end
73
+
74
+ def process_payment_allocation(allocation)
75
+ loan.update!(
76
+ facilitation_fee: allocation[:remaining_facilitation_fee],
77
+ accrued_penalty: allocation[:remaining_penalty],
78
+ accrued_interest: allocation[:remaining_interest],
79
+ remaining_amount: allocation[:remaining_principal]
80
+ )
81
+ end
82
+
83
+ def update_loan_status
84
+ total_remaining = (loan.facilitation_fee || 0) +
85
+ (loan.accrued_penalty || 0) +
86
+ (loan.accrued_interest || 0) +
87
+ (loan.remaining_amount || 0)
88
+
89
+ if total_remaining <= 0.01
90
+ loan.update!(status: "paid")
91
+ else
92
+ loan.update!(status: "active") if loan.status == "overdue"
93
+ end
94
+ end
95
+
96
+ def create_payment_transaction(allocation)
97
+ Dscf::Credit::LoanTransaction.create!(
98
+ loan: loan,
99
+ transaction_type: "repayment",
100
+ amount: payment_amount,
101
+ transaction_reference: "REPAY-#{loan.id}-#{Time.current.to_i}",
102
+ status: "completed"
103
+ )
104
+
105
+ create_allocation_transactions(allocation) if payment_amount > 100 # Only for significant payments
106
+ end
107
+
108
+ def create_allocation_transactions(allocation)
109
+ transactions = []
110
+
111
+ if allocation[:facilitation_payment] > 0
112
+ transactions << {
113
+ transaction_type: "fee_charge",
114
+ amount: allocation[:facilitation_payment],
115
+ reference_suffix: "FEE"
116
+ }
117
+ end
118
+
119
+ if allocation[:penalty_payment] > 0
120
+ transactions << {
121
+ transaction_type: "penalty",
122
+ amount: allocation[:penalty_payment],
123
+ reference_suffix: "PENALTY"
124
+ }
125
+ end
126
+
127
+ if allocation[:interest_payment] > 0
128
+ transactions << {
129
+ transaction_type: "interest_accrual",
130
+ amount: allocation[:interest_payment],
131
+ reference_suffix: "INTEREST"
132
+ }
133
+ end
134
+
135
+ if allocation[:principal_payment] > 0
136
+ transactions << {
137
+ transaction_type: "repayment",
138
+ amount: allocation[:principal_payment],
139
+ reference_suffix: "PRINCIPAL"
140
+ }
141
+ end
142
+
143
+ transactions.each_with_index do |transaction_data, index|
144
+ Dscf::Credit::LoanTransaction.create!(
145
+ loan: loan,
146
+ transaction_type: transaction_data[:transaction_type],
147
+ amount: transaction_data[:amount],
148
+ transaction_reference: "#{transaction_data[:reference_suffix]}-#{loan.id}-#{Time.current.to_i}-#{index}",
149
+ status: "completed"
150
+ )
151
+ end
152
+ end
153
+
154
+ def reactivate_facilities_if_paid_off
155
+ return unless loan.status == "paid"
156
+
157
+ loan_profile = loan.loan_profile
158
+
159
+ locked_eligible_lines = loan_profile.eligible_credit_lines
160
+ .where(available_limit: 0)
161
+ .includes(:credit_line)
162
+
163
+ locked_eligible_lines.each do |eligible_line|
164
+ current_usage = calculate_current_usage_for_credit_line(eligible_line.credit_line)
165
+ new_available_limit = [ eligible_line.credit_limit - current_usage, 0 ].max
166
+
167
+ eligible_line.update!(available_limit: new_available_limit)
168
+ end
169
+
170
+ update_loan_profile_available_amount(loan_profile)
171
+ end
172
+
173
+ def calculate_current_usage_for_credit_line(credit_line)
174
+ credit_line.loans
175
+ .where(status: [ "active", "overdue", "disbursed" ])
176
+ .where.not(id: loan.id)
177
+ .sum(:remaining_amount)
178
+ end
179
+
180
+ def update_loan_profile_available_amount(loan_profile)
181
+ # Recalculate available amount based on total limit minus current usage
182
+ total_usage = loan_profile.loans
183
+ .where(status: [ "active", "overdue", "disbursed" ])
184
+ .sum(:remaining_amount)
185
+
186
+ new_available_amount = [ loan_profile.total_amount - total_usage, 0 ].max
187
+ loan_profile.update!(available_amount: new_available_amount)
188
+ end
189
+
190
+ def success_result(allocation)
191
+ {
192
+ success: true,
193
+ loan: loan.reload,
194
+ payment_details: {
195
+ payment_amount: payment_amount,
196
+ allocation: allocation,
197
+ new_loan_status: loan.status,
198
+ paid_off: loan.status == "paid",
199
+ remaining_balance: (loan.facilitation_fee || 0) +
200
+ (loan.accrued_penalty || 0) +
201
+ (loan.accrued_interest || 0) +
202
+ (loan.remaining_amount || 0)
203
+ },
204
+ message: loan.status == "paid" ? "Loan fully paid off" : "Payment processed successfully"
205
+ }
206
+ end
207
+
208
+ def error_result(message)
209
+ {
210
+ success: false,
211
+ loan: nil,
212
+ error: message
213
+ }
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,27 @@
1
+ module Dscf::Credit
2
+ class RiskApplicationService
3
+ def initialize(eligible_credit_line, risk_value)
4
+ @eligible_credit_line = eligible_credit_line
5
+ @risk_value = risk_value.to_f
6
+ end
7
+
8
+ def apply_risk
9
+ return { success: false, errors: @eligible_credit_line.errors.full_messages } unless @eligible_credit_line.valid?
10
+
11
+ @eligible_credit_line.risk = @risk_value
12
+ @eligible_credit_line.available_limit = calculate_available_limit
13
+
14
+ if @eligible_credit_line.save
15
+ { success: true, data: @eligible_credit_line }
16
+ else
17
+ { success: false, errors: @eligible_credit_line.errors.full_messages }
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def calculate_available_limit
24
+ @eligible_credit_line.credit_limit * (1 - @risk_value)
25
+ end
26
+ end
27
+ end