dscf-credit 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) 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/concerns/dscf/credit/reviewable.rb +112 -0
  9. data/app/controllers/dscf/credit/categories_controller.rb +6 -5
  10. data/app/controllers/dscf/credit/credit_limit_calculations_controller.rb +50 -0
  11. data/app/controllers/dscf/credit/credit_line_specs_controller.rb +2 -1
  12. data/app/controllers/dscf/credit/credit_lines_controller.rb +11 -38
  13. data/app/controllers/dscf/credit/disbursements_controller.rb +55 -0
  14. data/app/controllers/dscf/credit/eligible_credit_lines_controller.rb +50 -0
  15. data/app/controllers/dscf/credit/facilitators_controller.rb +39 -150
  16. data/app/controllers/dscf/credit/loan_profiles_controller.rb +138 -0
  17. data/app/controllers/dscf/credit/payment_requests_controller.rb +54 -5
  18. data/app/controllers/dscf/credit/repayments_controller.rb +53 -0
  19. data/app/controllers/dscf/credit/scoring_param_types_controller.rb +31 -0
  20. data/app/controllers/dscf/credit/scoring_parameters_controller.rb +13 -8
  21. data/app/controllers/dscf/credit/scoring_tables_controller.rb +8 -8
  22. data/app/controllers/dscf/credit/system_configs_controller.rb +10 -7
  23. data/app/models/dscf/credit/bank_branch.rb +2 -1
  24. data/app/models/dscf/credit/category.rb +4 -2
  25. data/app/models/dscf/credit/credit_line.rb +9 -6
  26. data/app/models/dscf/credit/credit_line_spec.rb +3 -3
  27. data/app/models/dscf/credit/eligible_credit_line.rb +28 -0
  28. data/app/models/dscf/credit/facilitator.rb +5 -4
  29. data/app/models/dscf/credit/facilitator_performance.rb +1 -2
  30. data/app/models/dscf/credit/loan_profile.rb +8 -4
  31. data/app/models/dscf/credit/loan_profile_scoring_spec.rb +4 -6
  32. data/app/models/dscf/credit/scoring_param_type.rb +17 -0
  33. data/app/models/dscf/credit/scoring_parameter.rb +8 -7
  34. data/app/models/dscf/credit/scoring_table.rb +4 -4
  35. data/app/models/dscf/credit/system_config.rb +5 -4
  36. data/app/serializers/dscf/credit/bank_branch_serializer.rb +1 -0
  37. data/app/serializers/dscf/credit/category_serializer.rb +3 -1
  38. data/app/serializers/dscf/credit/credit_line_serializer.rb +4 -2
  39. data/app/serializers/dscf/credit/credit_line_spec_serializer.rb +1 -1
  40. data/app/serializers/dscf/credit/daily_routine_transaction_serializer.rb +8 -0
  41. data/app/serializers/dscf/credit/eligible_credit_line_serializer.rb +8 -0
  42. data/app/serializers/dscf/credit/facilitator_performance_serializer.rb +1 -1
  43. data/app/serializers/dscf/credit/facilitator_serializer.rb +2 -2
  44. data/app/serializers/dscf/credit/loan_profile_scoring_spec_serializer.rb +8 -0
  45. data/app/serializers/dscf/credit/loan_profile_serializer.rb +15 -0
  46. data/app/serializers/dscf/credit/loan_serializer.rb +12 -0
  47. data/app/serializers/dscf/credit/loan_transaction_serializer.rb +8 -0
  48. data/app/serializers/dscf/credit/payment_request_serializer.rb +10 -0
  49. data/app/serializers/dscf/credit/payment_serializer.rb +8 -0
  50. data/app/serializers/dscf/credit/scoring_param_type_serializer.rb +7 -0
  51. data/app/serializers/dscf/credit/scoring_parameter_serializer.rb +6 -3
  52. data/app/serializers/dscf/credit/scoring_table_serializer.rb +1 -1
  53. data/app/serializers/dscf/credit/system_config_serializer.rb +2 -2
  54. data/app/services/dscf/credit/credit_limit_calculation_service.rb +153 -0
  55. data/app/services/dscf/credit/disbursement_service.rb +180 -0
  56. data/app/services/dscf/credit/facilitator_approval_service.rb +4 -3
  57. data/app/services/dscf/credit/facilitator_creation_service.rb +157 -0
  58. data/app/services/dscf/credit/repayment_service.rb +216 -0
  59. data/app/services/dscf/credit/risk_application_service.rb +27 -0
  60. data/app/services/dscf/credit/scoring_service.rb +297 -0
  61. data/config/locales/en.yml +125 -8
  62. data/config/routes.rb +42 -11
  63. data/db/migrate/20250822091011_create_dscf_credit_categories.rb +2 -0
  64. data/db/migrate/20250822091131_create_dscf_credit_credit_lines.rb +7 -4
  65. data/db/migrate/20250822091527_create_dscf_credit_credit_line_specs.rb +1 -0
  66. data/db/migrate/20250822091820_create_dscf_credit_system_configs.rb +5 -2
  67. data/db/migrate/20250822092040_create_dscf_credit_scoring_param_types.rb +12 -0
  68. data/db/migrate/20250822092050_create_dscf_credit_scoring_parameters.rb +11 -6
  69. data/db/migrate/20250822092246_create_dscf_credit_loan_profiles.rb +6 -3
  70. data/db/migrate/20250822092417_create_dscf_credit_loan_profile_scoring_specs.rb +5 -7
  71. data/db/migrate/20250822092436_create_dscf_credit_facilitators.rb +5 -2
  72. data/db/migrate/20250822092528_create_dscf_credit_facilitator_performances.rb +0 -3
  73. data/db/migrate/20250901172842_create_dscf_credit_scoring_tables.rb +2 -2
  74. data/db/migrate/20250917120000_create_dscf_credit_eligible_credit_lines.rb +18 -0
  75. data/db/seeds.rb +134 -40
  76. data/lib/dscf/credit/version.rb +1 -1
  77. data/spec/factories/dscf/credit/categories.rb +1 -0
  78. data/spec/factories/dscf/credit/credit_line_specs.rb +1 -0
  79. data/spec/factories/dscf/credit/credit_lines.rb +9 -7
  80. data/spec/factories/dscf/credit/eligible_credit_lines.rb +33 -0
  81. data/spec/factories/dscf/credit/facilitator_performances.rb +0 -5
  82. data/spec/factories/dscf/credit/facilitators.rb +6 -1
  83. data/spec/factories/dscf/credit/loan_profile_scoring_specs.rb +1 -7
  84. data/spec/factories/dscf/credit/loan_profiles.rb +11 -6
  85. data/spec/factories/dscf/credit/scoring_param_types.rb +31 -0
  86. data/spec/factories/dscf/credit/scoring_parameters.rb +26 -4
  87. data/spec/factories/dscf/credit/scoring_tables.rb +1 -1
  88. data/spec/factories/dscf/credit/system_configs.rb +8 -2
  89. metadata +50 -2
@@ -0,0 +1,8 @@
1
+ module Dscf::Credit
2
+ class DailyRoutineTransactionSerializer < ActiveModel::Serializer
3
+ attributes :id, :routine_type, :amount, :status, :processed_at,
4
+ :failure_reason, :created_at, :updated_at
5
+
6
+ belongs_to :loan, serializer: LoanSerializer
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ module Dscf::Credit
2
+ class EligibleCreditLineSerializer < ActiveModel::Serializer
3
+ attributes :id, :credit_limit, :available_limit, :risk, :created_at, :updated_at
4
+
5
+ belongs_to :loan_profile, serializer: Dscf::Credit::LoanProfileSerializer
6
+ belongs_to :credit_line, serializer: Dscf::Credit::CreditLineSerializer
7
+ end
8
+ end
@@ -2,7 +2,7 @@ module Dscf::Credit
2
2
  class FacilitatorPerformanceSerializer < ActiveModel::Serializer
3
3
  attributes :id, :score, :total_outstanding_loans, :total_outstanding_amount,
4
4
  :approval_required, :previous_performance_id, :input_data,
5
- :expires_at, :created_at, :updated_at
5
+ :created_at, :updated_at
6
6
 
7
7
  belongs_to :facilitator, serializer: Dscf::Credit::FacilitatorSerializer
8
8
  belongs_to :created_by, polymorphic: true
@@ -1,11 +1,11 @@
1
1
  module Dscf::Credit
2
2
  class FacilitatorSerializer < ActiveModel::Serializer
3
- attributes :id, :name, :type, :total_limit, :kyc_status,
3
+ attributes :id, :name, :type, :total_limit, :kyc_status, :kyc_review_date, :review_feedback,
4
4
  :created_at, :updated_at
5
5
 
6
6
  belongs_to :user, serializer: Dscf::Core::UserSerializer
7
7
  belongs_to :bank, serializer: Dscf::Credit::BankSerializer
8
- belongs_to :kyc_approved_by, polymorphic: true
8
+ belongs_to :kyc_reviewed_by, polymorphic: true
9
9
  has_many :facilitator_performances, serializer: Dscf::Credit::FacilitatorPerformanceSerializer
10
10
  end
11
11
  end
@@ -0,0 +1,8 @@
1
+ module Dscf::Credit
2
+ class LoanProfileScoringSpecSerializer < ActiveModel::Serializer
3
+ attributes :id, :scoring_input_data, :score, :total_limit, :active, :created_at, :updated_at
4
+
5
+ belongs_to :loan_profile, serializer: Dscf::Credit::LoanProfileSerializer
6
+ belongs_to :created_by
7
+ end
8
+ end
@@ -0,0 +1,15 @@
1
+ module Dscf::Credit
2
+ class LoanProfileSerializer < ActiveModel::Serializer
3
+ attributes :id, :status, :total_amount, :available_amount, :review_date, :review_feedback, :created_at, :updated_at
4
+
5
+ belongs_to :bank, serializer: Dscf::Credit::BankSerializer
6
+ belongs_to :review_branch, serializer: Dscf::Credit::BankBranchSerializer
7
+ belongs_to :reviewed_by
8
+ belongs_to :backer
9
+ belongs_to :user, serializer: Dscf::Core::UserSerializer
10
+
11
+ has_many :loan_profile_scoring_specs, serializer: Dscf::Credit::LoanProfileScoringSpecSerializer
12
+ has_many :loans, serializer: Dscf::Credit::LoanSerializer
13
+ has_many :eligible_credit_lines, serializer: Dscf::Credit::EligibleCreditLineSerializer
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ module Dscf::Credit
2
+ class LoanSerializer < ActiveModel::Serializer
3
+ attributes :id, :status, :principal_amount, :accrued_interest, :accrued_penalty, :facilitation_fee,
4
+ :total_loan_amount, :remaining_amount, :due_date, :disbursed_at, :created_at, :updated_at
5
+
6
+ belongs_to :loan_profile, serializer: LoanProfileSerializer
7
+ belongs_to :credit_line, serializer: CreditLineSerializer
8
+ belongs_to :payment_request, serializer: PaymentRequestSerializer
9
+ has_many :loan_transactions, serializer: LoanTransactionSerializer
10
+ has_many :daily_routine_transactions, serializer: DailyRoutineTransactionSerializer
11
+ end
12
+ end
@@ -0,0 +1,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
@@ -0,0 +1,7 @@
1
+ module Dscf::Credit
2
+ class ScoringParamTypeSerializer < ActiveModel::Serializer
3
+ attributes :id, :name, :description, :created_at, :updated_at
4
+
5
+ has_many :scoring_parameters, serializer: Dscf::Credit::ScoringParameterSerializer
6
+ end
7
+ end
@@ -1,12 +1,15 @@
1
1
  module Dscf::Credit
2
2
  class ScoringParameterSerializer < ActiveModel::Serializer
3
- attributes :id, :name, :description, :data_type, :type, :weight, :min_value,
4
- :max_value, :active, :expires_at, :created_at, :updated_at
3
+ attributes :id, :name, :description, :data_type, :weight, :min_value,
4
+ :max_value, :active, :status, :review_date, :review_feedback, :source, :document_reference, :created_at, :updated_at
5
5
 
6
6
  belongs_to :bank, serializer: Dscf::Credit::BankSerializer
7
7
  belongs_to :created_by, polymorphic: true
8
- belongs_to :approved_by, polymorphic: true
8
+ belongs_to :reviewed_by, polymorphic: true
9
+ belongs_to :scoring_param_type, serializer: Dscf::Credit::ScoringParamTypeSerializer
9
10
  belongs_to :previous_version, serializer: Dscf::Credit::ScoringParameterSerializer
10
11
  has_many :parameter_normalizers, serializer: Dscf::Credit::ParameterNormalizerSerializer
12
+ has_many :scoring_tables, serializer: Dscf::Credit::ScoringTableSerializer
13
+ has_many :categories, serializer: Dscf::Credit::CategorySerializer
11
14
  end
12
15
  end
@@ -2,7 +2,7 @@ module Dscf::Credit
2
2
  class ScoringTableSerializer < ActiveModel::Serializer
3
3
  attributes :id, :weight, :active, :created_at, :updated_at
4
4
 
5
- belongs_to :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
@@ -1,6 +1,6 @@
1
1
  module Dscf::Credit
2
2
  class SystemConfigSerializer < ActiveModel::Serializer
3
- attributes :id, :config_value, :status, :created_at, :updated_at
3
+ attributes :id, :config_value, :status, :review_date, :review_feedback, :created_at, :updated_at
4
4
 
5
5
  attribute :config_definition do
6
6
  {
@@ -13,6 +13,6 @@ module Dscf::Credit
13
13
  end
14
14
 
15
15
  belongs_to :last_updated_by, serializer: Dscf::Core::UserSerializer
16
- belongs_to :approved_by, serializer: Dscf::Core::UserSerializer
16
+ belongs_to :reviewed_by, serializer: Dscf::Core::UserSerializer
17
17
  end
18
18
  end
@@ -0,0 +1,153 @@
1
+ module Dscf::Credit
2
+ class CreditLimitCalculationService
3
+ attr_reader :loan_profile, :category
4
+
5
+ def initialize(loan_profile, category)
6
+ @loan_profile = loan_profile
7
+ @category = category
8
+ end
9
+
10
+ # Calculate credit limits for all credit lines in a category for a specific loan profile
11
+ # @return [Hash] Result containing eligible credit lines with calculated limits
12
+ def calculate_credit_limits
13
+ return error_result("Loan profile not found") unless loan_profile
14
+ return error_result("Category not found") unless category
15
+
16
+ scoring_spec = loan_profile.loan_profile_scoring_specs.active.first
17
+ return error_result("No active scoring found for loan profile") unless scoring_spec
18
+
19
+ credit_lines = category.credit_lines.approved.includes(:bank, :category, :credit_line_specs)
20
+ return error_result("No approved credit lines found for category") if credit_lines.empty?
21
+
22
+ eligible_credit_lines = calculate_limits_for_credit_lines(credit_lines, scoring_spec)
23
+
24
+ success_result(eligible_credit_lines)
25
+ rescue StandardError => e
26
+ error_result("Credit limit calculation failed: #{e.message}")
27
+ end
28
+
29
+ private
30
+
31
+ def calculate_limits_for_credit_lines(credit_lines, scoring_spec)
32
+ results = []
33
+
34
+ credit_lines.each do |credit_line|
35
+ limit_data = calculate_individual_credit_limit(credit_line, scoring_spec)
36
+ next if limit_data[:credit_limit] <= 0
37
+
38
+ eligible_credit_line = find_or_create_eligible_credit_line(credit_line)
39
+ eligible_credit_line.assign_attributes(limit_data)
40
+
41
+ if eligible_credit_line.save
42
+ results << {
43
+ credit_line: credit_line,
44
+ eligible_credit_line: eligible_credit_line,
45
+ calculated_limit: limit_data[:credit_limit],
46
+ available_limit: limit_data[:available_limit],
47
+ risk_factor: limit_data[:risk]
48
+ }
49
+ end
50
+ end
51
+
52
+ results
53
+ end
54
+
55
+ def calculate_individual_credit_limit(credit_line, scoring_spec)
56
+ base_limit = scoring_spec.total_limit
57
+
58
+ credit_line_specs = credit_line.credit_line_specs.active
59
+ adjusted_limit = apply_credit_line_specifications(base_limit, credit_line_specs)
60
+
61
+ risk_factor = calculate_risk_factor(credit_line, loan_profile)
62
+ final_limit = adjusted_limit * (1 - risk_factor)
63
+
64
+ used_limit = calculate_used_limit(credit_line)
65
+ available_limit = [ final_limit - used_limit, 0 ].max
66
+
67
+ {
68
+ credit_limit: final_limit.round(2),
69
+ available_limit: available_limit.round(2),
70
+ risk: risk_factor.round(4)
71
+ }
72
+ end
73
+
74
+ def apply_credit_line_specifications(base_limit, credit_line_specs)
75
+ return base_limit if credit_line_specs.empty?
76
+
77
+ spec = credit_line_specs.first
78
+ return base_limit unless spec
79
+
80
+ if spec.credit_line_multiplier && spec.credit_line_multiplier > 0
81
+ adjustment_factor = spec.credit_line_multiplier / 30.0 # Normalize against default 30 days
82
+ adjusted_limit = base_limit * adjustment_factor
83
+ else
84
+ adjusted_limit = base_limit
85
+ end
86
+
87
+ if spec.max_amount && adjusted_limit > spec.max_amount
88
+ adjusted_limit = spec.max_amount
89
+ end
90
+
91
+ if spec.min_amount && adjusted_limit < spec.min_amount
92
+ adjusted_limit = spec.min_amount
93
+ end
94
+
95
+ adjusted_limit
96
+ end
97
+
98
+ def calculate_risk_factor(credit_line, loan_profile)
99
+ base_risk = 0.1 # 10% base risk
100
+
101
+ # Increase risk based on loan profile characteristics
102
+ risk_adjustments = 0.0
103
+
104
+ # Risk based on previous loan performance
105
+ if loan_profile.loans.overdue.any?
106
+ risk_adjustments += 0.2 # 20% additional risk for overdue loans
107
+ end
108
+
109
+ # Risk based on available amount vs total amount ratio
110
+ if loan_profile.total_amount > 0
111
+ utilization_ratio = (loan_profile.total_amount - loan_profile.available_amount) / loan_profile.total_amount
112
+ risk_adjustments += utilization_ratio * 0.1 # Up to 10% additional risk
113
+ end
114
+
115
+ # Risk based on credit line category
116
+ case credit_line.category.name.downcase
117
+ when "high_risk"
118
+ risk_adjustments += 0.15
119
+ when "medium_risk"
120
+ risk_adjustments += 0.05
121
+ end
122
+
123
+ # Cap total risk at 90%
124
+ [ base_risk + risk_adjustments, 0.9 ].min
125
+ end
126
+
127
+ def calculate_used_limit(credit_line)
128
+ credit_line.loans.where(status: [ "active", "overdue" ]).sum(:remaining_amount)
129
+ end
130
+
131
+ def find_or_create_eligible_credit_line(credit_line)
132
+ loan_profile.eligible_credit_lines.find_or_initialize_by(credit_line: credit_line)
133
+ end
134
+
135
+ def success_result(eligible_credit_lines)
136
+ {
137
+ success: true,
138
+ data: eligible_credit_lines,
139
+ message: "Credit limits calculated successfully",
140
+ count: eligible_credit_lines.size
141
+ }
142
+ end
143
+
144
+ def error_result(message)
145
+ {
146
+ success: false,
147
+ data: [],
148
+ error: message,
149
+ count: 0
150
+ }
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,180 @@
1
+ module Dscf::Credit
2
+ class DisbursementService
3
+ attr_reader :credit_line, :payment_request, :current_user
4
+
5
+ def initialize(credit_line, payment_request, current_user)
6
+ @credit_line = credit_line
7
+ @payment_request = payment_request
8
+ @current_user = current_user
9
+ end
10
+
11
+ # Process disbursement by creating a loan record from selected credit line
12
+ # @return [Hash] Result containing created loan and disbursement details
13
+ def process_disbursement
14
+ return error_result("Credit line not found") unless credit_line
15
+ return error_result("Payment request not found") unless payment_request
16
+ return error_result("Credit line is not approved") unless credit_line.status == "approved"
17
+
18
+ eligible_credit_line = find_eligible_credit_line
19
+ return error_result("No eligible credit line found") unless eligible_credit_line
20
+
21
+ loan_profile = eligible_credit_line.loan_profile
22
+ return error_result("Loan profile not approved") unless loan_profile.status == "approved"
23
+
24
+ validation_result = validate_disbursement_amount(eligible_credit_line)
25
+ return validation_result unless validation_result[:success]
26
+
27
+ ActiveRecord::Base.transaction do
28
+ loan = create_loan_record(loan_profile, eligible_credit_line)
29
+ process_payment_and_charges(loan)
30
+ update_credit_line_limits(eligible_credit_line, loan)
31
+ lock_other_credit_lines(loan_profile, credit_line)
32
+
33
+ success_result(loan)
34
+ end
35
+ rescue StandardError => e
36
+ error_result("Disbursement processing failed: #{e.message}")
37
+ end
38
+
39
+ private
40
+
41
+ def find_eligible_credit_line
42
+ credit_line.eligible_credit_lines
43
+ .joins(:loan_profile)
44
+ .where(dscf_credit_loan_profiles: { status: "approved" })
45
+ .first
46
+ end
47
+
48
+ def validate_disbursement_amount(eligible_credit_line)
49
+ requested_amount = payment_request.amount
50
+ available_limit = eligible_credit_line.available_limit
51
+
52
+ if requested_amount <= 0
53
+ return error_result("Invalid disbursement amount")
54
+ end
55
+
56
+ if requested_amount > available_limit
57
+ return error_result("Requested amount exceeds available credit limit")
58
+ end
59
+
60
+ { success: true }
61
+ end
62
+
63
+ def create_loan_record(loan_profile, eligible_credit_line)
64
+ loan_terms = calculate_loan_terms(eligible_credit_line)
65
+
66
+ loan = Dscf::Credit::Loan.new(
67
+ loan_profile: loan_profile,
68
+ credit_line: credit_line,
69
+ payment_request: payment_request,
70
+ principal_amount: payment_request.amount,
71
+ facilitation_fee: loan_terms[:facilitation_fee],
72
+ total_loan_amount: loan_terms[:total_amount],
73
+ remaining_amount: loan_terms[:total_amount],
74
+ accrued_interest: 0,
75
+ accrued_penalty: 0,
76
+ status: "disbursed",
77
+ due_date: loan_terms[:due_date],
78
+ disbursed_at: Time.current
79
+ )
80
+
81
+ raise "Failed to create loan: #{loan.errors.full_messages.join(', ')}" unless loan.save
82
+
83
+ loan
84
+ end
85
+
86
+ def calculate_loan_terms(eligible_credit_line)
87
+ principal = payment_request.amount
88
+
89
+ credit_line_spec = credit_line.credit_line_specs.active.first
90
+
91
+ if credit_line_spec
92
+ facilitation_rate = credit_line_spec.facilitation_fee_rate
93
+ vat_rate = credit_line_spec.vat
94
+ loan_duration = credit_line_spec.loan_duration
95
+ else
96
+ facilitation_rate = 0.02 # 2%
97
+ vat_rate = 0.15 # 15%
98
+ loan_duration = 30 # 30 days
99
+ end
100
+
101
+ facilitation_fee = principal * facilitation_rate
102
+ vat_amount = facilitation_fee * vat_rate
103
+ total_amount = principal + facilitation_fee + vat_amount
104
+
105
+ due_date = Date.current + loan_duration.days
106
+
107
+ {
108
+ facilitation_fee: facilitation_fee.round(2),
109
+ vat_amount: vat_amount.round(2),
110
+ total_amount: total_amount.round(2),
111
+ due_date: due_date
112
+ }
113
+ end
114
+
115
+ def process_payment_and_charges(loan)
116
+ payment_request.update!(
117
+ status: "processed",
118
+ approved_at: Time.current
119
+ )
120
+
121
+ Dscf::Credit::LoanTransaction.create!(
122
+ loan: loan,
123
+ transaction_type: "disbursement",
124
+ amount: loan.principal_amount,
125
+ transaction_reference: "DISB-#{payment_request.order_id}-#{Time.current.to_i}",
126
+ status: "completed"
127
+ )
128
+
129
+ if loan.facilitation_fee > 0
130
+ Dscf::Credit::LoanTransaction.create!(
131
+ loan: loan,
132
+ transaction_type: "fee_charge",
133
+ amount: loan.facilitation_fee,
134
+ transaction_reference: "FEE-#{loan.id}-#{Time.current.to_i}",
135
+ status: "completed"
136
+ )
137
+ end
138
+ end
139
+
140
+ def update_credit_line_limits(eligible_credit_line, loan)
141
+ new_available_limit = eligible_credit_line.available_limit - loan.total_loan_amount
142
+ eligible_credit_line.update!(available_limit: [ new_available_limit, 0 ].max)
143
+
144
+ # Update loan profile available amount
145
+ loan_profile = eligible_credit_line.loan_profile
146
+ new_profile_available = loan_profile.available_amount - loan.total_loan_amount
147
+ loan_profile.update!(available_amount: [ new_profile_available, 0 ].max)
148
+ end
149
+
150
+ def lock_other_credit_lines(loan_profile, current_credit_line)
151
+ other_eligible_lines = loan_profile.eligible_credit_lines
152
+ .where.not(credit_line: current_credit_line)
153
+
154
+ other_eligible_lines.update_all(available_limit: 0)
155
+ end
156
+
157
+ def success_result(loan)
158
+ {
159
+ success: true,
160
+ loan: loan,
161
+ disbursement_details: {
162
+ principal_amount: loan.principal_amount,
163
+ facilitation_fee: loan.facilitation_fee,
164
+ total_loan_amount: loan.total_loan_amount,
165
+ due_date: loan.due_date,
166
+ disbursed_at: loan.disbursed_at
167
+ },
168
+ message: "Disbursement processed successfully"
169
+ }
170
+ end
171
+
172
+ def error_result(message)
173
+ {
174
+ success: false,
175
+ loan: nil,
176
+ error: message
177
+ }
178
+ end
179
+ end
180
+ end
@@ -29,15 +29,16 @@ module Dscf::Credit
29
29
  end
30
30
  end
31
31
 
32
- def reject(rejection_reason = nil)
32
+ def reject(review_feedback = nil)
33
33
  ActiveRecord::Base.transaction do
34
34
  facilitator.update!(
35
35
  kyc_status: "rejected",
36
- kyc_approved_by: approver
36
+ kyc_approved_by: approver,
37
+ review_feedback: review_feedback
37
38
  )
38
39
 
39
40
  begin
40
- FacilitatorMailer.rejection_notification(facilitator, rejection_reason).deliver_now
41
+ FacilitatorMailer.rejection_notification(facilitator, review_feedback).deliver_now
41
42
  rescue => e
42
43
  Rails.logger.error "Failed to send rejection email: #{e.message}"
43
44
  end
@@ -0,0 +1,157 @@
1
+ module Dscf::Credit
2
+ class FacilitatorCreationService
3
+ attr_reader :current_user, :facilitator_class
4
+
5
+ def initialize(current_user, facilitator_class = Dscf::Credit::Facilitator)
6
+ @current_user = current_user
7
+ @facilitator_class = facilitator_class
8
+ end
9
+
10
+ # Creates a single facilitator
11
+ # @param facilitator_params [Hash] The facilitator parameters
12
+ # @return [Dscf::Credit::Facilitator] The created facilitator
13
+ # @raise [StandardError] If creation fails
14
+ def create_single(facilitator_params)
15
+ ActiveRecord::Base.transaction do
16
+ facilitator_data = build_facilitator_data(facilitator_params)
17
+ facilitator = facilitator_class.new(facilitator_data)
18
+ facilitator.kyc_reviewed_by = current_user
19
+ facilitator.kyc_review_date = Time.current
20
+
21
+ if facilitator.save
22
+ create_initial_performance(facilitator)
23
+ facilitator
24
+ else
25
+ raise StandardError, facilitator.errors.full_messages.join(", ")
26
+ end
27
+ end
28
+ end
29
+
30
+ # Creates multiple facilitators in batch
31
+ # @param facilitators_params [Array<Hash>] Array of facilitator parameters
32
+ # @return [Hash] Results containing successful and failed creations
33
+ def create_batch(facilitators_params)
34
+ validate_batch_params(facilitators_params)
35
+
36
+ results = initialize_batch_results(facilitators_params.length)
37
+
38
+ facilitators_params.each_with_index do |facilitator_attrs, index|
39
+ process_single_facilitator_in_batch(facilitator_attrs, index, results)
40
+ end
41
+
42
+ results
43
+ end
44
+
45
+ private
46
+
47
+ def build_facilitator_data(facilitator_params)
48
+ user = find_user(facilitator_params[:user_id])
49
+ business = find_user_business(user)
50
+
51
+ facilitator_params.to_h.merge(
52
+ name: business.name,
53
+ type: business.business_type.name.downcase
54
+ )
55
+ end
56
+
57
+ def find_user(user_id)
58
+ user = Dscf::Core::User.find(user_id)
59
+ raise StandardError, "User not found with ID: #{user_id}" unless user
60
+ user
61
+ rescue ActiveRecord::RecordNotFound
62
+ raise StandardError, "User not found with ID: #{user_id}"
63
+ end
64
+
65
+ def find_user_business(user)
66
+ business = user.businesses.first
67
+ raise StandardError, "User must have a business associated to become a facilitator" unless business
68
+ business
69
+ end
70
+
71
+ def create_initial_performance(facilitator)
72
+ facilitator.facilitator_performances.create!(
73
+ total_outstanding_loans: 0,
74
+ total_outstanding_amount: 0.0,
75
+ approval_required: false,
76
+ created_by: current_user
77
+ )
78
+ end
79
+
80
+ def validate_batch_params(facilitators_params)
81
+ unless facilitators_params.is_a?(Array)
82
+ raise StandardError, "Expected an array of facilitator objects"
83
+ end
84
+
85
+ if facilitators_params.empty?
86
+ raise StandardError, "At least one facilitator is required"
87
+ end
88
+
89
+ if facilitators_params.length > 100
90
+ raise StandardError, "Maximum 100 facilitators allowed per batch"
91
+ end
92
+ end
93
+
94
+ def initialize_batch_results(total_count)
95
+ {
96
+ successful: [],
97
+ failed: [],
98
+ total_count: total_count,
99
+ success_count: 0,
100
+ failure_count: 0
101
+ }
102
+ end
103
+
104
+ def process_single_facilitator_in_batch(facilitator_attrs, index, results)
105
+ begin
106
+ permitted_attrs = permit_facilitator_attrs(facilitator_attrs)
107
+ facilitator = create_single(permitted_attrs)
108
+
109
+ add_successful_result(results, index, facilitator)
110
+ rescue StandardError => e
111
+ add_failed_result(results, index, facilitator_attrs, e)
112
+ end
113
+ end
114
+
115
+ def permit_facilitator_attrs(facilitator_attrs)
116
+ if facilitator_attrs.is_a?(ActionController::Parameters)
117
+ facilitator_attrs.permit(:user_id, :bank_id, :kyc_status)
118
+ else
119
+ ActionController::Parameters.new(facilitator_attrs)
120
+ .permit(:user_id, :bank_id, :kyc_status)
121
+ end
122
+ end
123
+
124
+ def add_successful_result(results, index, facilitator)
125
+ results[:successful] << {
126
+ index: index,
127
+ id: facilitator.id,
128
+ name: facilitator.name,
129
+ message: "Successfully created"
130
+ }
131
+ results[:success_count] += 1
132
+ end
133
+
134
+ def add_failed_result(results, index, facilitator_attrs, error)
135
+ business_name = extract_business_name_for_error(facilitator_attrs)
136
+
137
+ results[:failed] << {
138
+ index: index,
139
+ name: business_name,
140
+ errors: [ error.message ]
141
+ }
142
+ results[:failure_count] += 1
143
+ end
144
+
145
+ def extract_business_name_for_error(facilitator_attrs)
146
+ user_id = facilitator_attrs.try(:[], :user_id) || facilitator_attrs[:user_id]
147
+ return "Unknown Business" unless user_id
148
+
149
+ begin
150
+ user = Dscf::Core::User.find(user_id)
151
+ user.businesses.first&.name || "No Business Associated"
152
+ rescue
153
+ "Invalid User ID"
154
+ end
155
+ end
156
+ end
157
+ end