dscf-credit 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/dscf/credit/facilitator_applications_controller.rb +88 -5
- data/app/controllers/dscf/credit/loan_accruals_controller.rb +113 -0
- data/app/controllers/dscf/credit/loan_applications_controller.rb +5 -5
- data/app/controllers/dscf/credit/loan_profiles_controller.rb +2 -2
- data/app/controllers/dscf/credit/loans_controller.rb +2 -6
- data/app/controllers/dscf/credit/repayments_controller.rb +3 -3
- data/app/controllers/dscf/credit/scoring_parameters_controller.rb +9 -13
- data/app/controllers/dscf/credit/users_controller.rb +6 -0
- data/app/jobs/dscf/credit/generate_daily_accruals_job.rb +78 -0
- data/app/models/dscf/credit/facilitator_application.rb +0 -1
- data/app/models/dscf/credit/loan.rb +26 -5
- data/app/models/dscf/credit/loan_accrual.rb +25 -0
- data/app/serializers/dscf/credit/loan_accrual_serializer.rb +7 -0
- data/app/serializers/dscf/credit/loan_serializer.rb +24 -2
- data/app/services/dscf/credit/credit_scoring_engine.rb +8 -8
- data/app/services/dscf/credit/disbursement_service.rb +50 -8
- data/app/services/dscf/credit/loan_accrual_generator_service.rb +272 -0
- data/app/services/dscf/credit/repayment_service.rb +216 -87
- data/config/locales/en.yml +36 -0
- data/config/routes.rb +10 -0
- data/db/migrate/20250822092426_create_dscf_credit_facilitator_applications.rb +1 -1
- data/db/migrate/20250822092654_create_dscf_credit_loans.rb +0 -4
- data/db/migrate/20251003132939_create_dscf_credit_loan_accruals.rb +19 -0
- data/db/seeds.rb +20 -5
- data/lib/dscf/credit/version.rb +1 -1
- data/spec/factories/dscf/credit/loan_accruals.rb +29 -0
- data/spec/factories/dscf/credit/loans.rb +0 -4
- metadata +9 -2
@@ -1,15 +1,74 @@
|
|
1
1
|
module Dscf::Credit
|
2
|
+
# RepaymentService handles loan repayment processing with automatic payment allocation
|
3
|
+
# across facilitation fees, penalties, interest, and principal.
|
4
|
+
#
|
5
|
+
# @example Basic usage
|
6
|
+
# loan = Dscf::Credit::Loan.find(loan_id)
|
7
|
+
# service = Dscf::Credit::RepaymentService.new(loan, 1500.00)
|
8
|
+
# result = service.process_repayment
|
9
|
+
#
|
10
|
+
# if result[:success]
|
11
|
+
# puts "Payment processed: #{result[:message]}"
|
12
|
+
# puts "Remaining balance: #{result[:payment_details][:remaining_balance]}"
|
13
|
+
# else
|
14
|
+
# puts "Error: #{result[:error]}"
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# @see docs/REPAYMENT_SERVICE_DOCUMENTATION.md for detailed documentation
|
2
18
|
class RepaymentService
|
3
|
-
attr_reader :loan, :payment_amount
|
19
|
+
attr_reader :loan, :payment_amount
|
4
20
|
|
5
|
-
|
21
|
+
# Initialize the repayment service
|
22
|
+
#
|
23
|
+
# @param loan [Dscf::Credit::Loan] The loan to process repayment for
|
24
|
+
# @param payment_amount [Numeric] The amount being paid (converted to float)
|
25
|
+
def initialize(loan, payment_amount)
|
6
26
|
@loan = loan
|
7
27
|
@payment_amount = payment_amount.to_f
|
8
|
-
@current_user = current_user
|
9
28
|
end
|
10
29
|
|
11
|
-
# Process loan repayment
|
12
|
-
#
|
30
|
+
# Process loan repayment with automatic allocation and status updates
|
31
|
+
#
|
32
|
+
# Payment allocation priority (waterfall method):
|
33
|
+
# 1. Facilitation Fee (from loan_accruals where accrual_type='facilitation_fee' and status='pending')
|
34
|
+
# 2. Penalties (from loan_accruals where accrual_type='penalty' and status='pending')
|
35
|
+
# 3. Interest (from loan_accruals where accrual_type='interest' and status='pending')
|
36
|
+
# 4. Principal (from loan.remaining_amount)
|
37
|
+
#
|
38
|
+
# Process steps:
|
39
|
+
# 1. Calculate payment allocation across loan components
|
40
|
+
# 2. Update loan record (remaining_amount only)
|
41
|
+
# 3. Mark accruals as paid (with splitting for partial payments)
|
42
|
+
# 4. Update loan status based on remaining balance
|
43
|
+
# 5. Reactivate credit facilities if loan fully paid
|
44
|
+
#
|
45
|
+
# @return [Hash] Result hash with the following structure:
|
46
|
+
# - :success [Boolean] Whether the operation succeeded
|
47
|
+
# - :loan [Dscf::Credit::Loan, nil] Updated loan object (nil on failure)
|
48
|
+
# - :payment_details [Hash] Detailed payment allocation information
|
49
|
+
# - :message [String] Success message
|
50
|
+
# - :error [String] Error message (only present on failure)
|
51
|
+
#
|
52
|
+
# @example Success response
|
53
|
+
# {
|
54
|
+
# success: true,
|
55
|
+
# loan: #<Dscf::Credit::Loan>,
|
56
|
+
# payment_details: {
|
57
|
+
# payment_amount: 1500.00,
|
58
|
+
# allocation: { ... },
|
59
|
+
# new_loan_status: "active",
|
60
|
+
# paid_off: false,
|
61
|
+
# remaining_balance: 3600.00
|
62
|
+
# },
|
63
|
+
# message: "Payment processed successfully"
|
64
|
+
# }
|
65
|
+
#
|
66
|
+
# @example Error response
|
67
|
+
# {
|
68
|
+
# success: false,
|
69
|
+
# loan: nil,
|
70
|
+
# error: "Payment amount exceeds total outstanding balance"
|
71
|
+
# }
|
13
72
|
def process_repayment
|
14
73
|
return error_result("Loan not found") unless loan
|
15
74
|
return error_result("Invalid payment amount") unless payment_amount > 0
|
@@ -24,8 +83,6 @@ module Dscf::Credit
|
|
24
83
|
|
25
84
|
update_loan_status
|
26
85
|
|
27
|
-
create_payment_transaction(payment_allocation)
|
28
|
-
|
29
86
|
reactivate_facilities_if_paid_off
|
30
87
|
|
31
88
|
success_result(payment_allocation)
|
@@ -36,10 +93,23 @@ module Dscf::Credit
|
|
36
93
|
|
37
94
|
private
|
38
95
|
|
96
|
+
# Calculate how the payment should be allocated across loan components
|
97
|
+
#
|
98
|
+
# Uses waterfall allocation: payment is applied in priority order until exhausted.
|
99
|
+
# Queries loan_accruals table for current pending penalties and interest.
|
100
|
+
#
|
101
|
+
# @return [Hash] Allocation details including:
|
102
|
+
# - :total_outstanding - Total amount owed
|
103
|
+
# - :payment_amount - Amount being paid
|
104
|
+
# - :facilitation_payment - Amount applied to facilitation fee
|
105
|
+
# - :penalty_payment - Amount applied to penalties
|
106
|
+
# - :interest_payment - Amount applied to interest
|
107
|
+
# - :principal_payment - Amount applied to principal
|
108
|
+
# - :remaining_* - Amounts still owed for each component
|
39
109
|
def calculate_payment_allocation
|
40
|
-
current_facilitation_fee = loan.facilitation_fee
|
41
|
-
current_penalty = loan.
|
42
|
-
current_interest = loan.
|
110
|
+
current_facilitation_fee = loan.loan_accruals.pending.by_type("facilitation_fee").sum(:amount)
|
111
|
+
current_penalty = loan.loan_accruals.pending.by_type("penalty").sum(:amount)
|
112
|
+
current_interest = loan.loan_accruals.pending.by_type("interest").sum(:amount)
|
43
113
|
current_principal = loan.remaining_amount || 0
|
44
114
|
|
45
115
|
total_outstanding = current_facilitation_fee + current_penalty + current_interest + current_principal
|
@@ -71,86 +141,138 @@ module Dscf::Credit
|
|
71
141
|
}
|
72
142
|
end
|
73
143
|
|
144
|
+
# Apply the calculated payment allocation to the loan and accruals
|
145
|
+
#
|
146
|
+
# Updates:
|
147
|
+
# 1. Loan record (remaining_amount only)
|
148
|
+
# 2. Loan accruals (marks facilitation_fee, penalties, and interest as paid)
|
149
|
+
#
|
150
|
+
# @param allocation [Hash] The allocation hash from calculate_payment_allocation
|
151
|
+
# @return [void]
|
74
152
|
def process_payment_allocation(allocation)
|
153
|
+
# Update loan record (only remaining_amount now)
|
75
154
|
loan.update!(
|
76
|
-
facilitation_fee: allocation[:remaining_facilitation_fee],
|
77
|
-
accrued_penalty: allocation[:remaining_penalty],
|
78
|
-
accrued_interest: allocation[:remaining_interest],
|
79
155
|
remaining_amount: allocation[:remaining_principal]
|
80
156
|
)
|
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
157
|
|
158
|
+
# Mark paid facilitation fee accruals
|
111
159
|
if allocation[:facilitation_payment] > 0
|
112
|
-
|
113
|
-
transaction_type: "fee_charge",
|
114
|
-
amount: allocation[:facilitation_payment],
|
115
|
-
reference_suffix: "FEE"
|
116
|
-
}
|
160
|
+
mark_accruals_as_paid("facilitation_fee", allocation[:facilitation_payment])
|
117
161
|
end
|
118
162
|
|
163
|
+
# Mark paid penalty accruals
|
119
164
|
if allocation[:penalty_payment] > 0
|
120
|
-
|
121
|
-
transaction_type: "penalty",
|
122
|
-
amount: allocation[:penalty_payment],
|
123
|
-
reference_suffix: "PENALTY"
|
124
|
-
}
|
165
|
+
mark_accruals_as_paid("penalty", allocation[:penalty_payment])
|
125
166
|
end
|
126
167
|
|
168
|
+
# Mark paid interest accruals
|
127
169
|
if allocation[:interest_payment] > 0
|
128
|
-
|
129
|
-
transaction_type: "interest_accrual",
|
130
|
-
amount: allocation[:interest_payment],
|
131
|
-
reference_suffix: "INTEREST"
|
132
|
-
}
|
170
|
+
mark_accruals_as_paid("interest", allocation[:interest_payment])
|
133
171
|
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# Mark loan accruals as paid, handling partial payments by splitting accruals
|
175
|
+
#
|
176
|
+
# Processes accruals in chronological order (oldest first by applied_on date).
|
177
|
+
# When payment is insufficient to cover an entire accrual:
|
178
|
+
# 1. Updates the original accrual to "paid" status with the paid amount
|
179
|
+
# 2. Creates a new "pending" accrual for the remaining unpaid amount
|
180
|
+
#
|
181
|
+
# @param accrual_type [String] Type of accrual to pay ('penalty' or 'interest')
|
182
|
+
# @param payment_amount [Float] Amount available to pay towards these accruals
|
183
|
+
# @return [void]
|
184
|
+
#
|
185
|
+
# @example Full payment
|
186
|
+
# # Accrual: { id: 1, amount: 100, status: 'pending' }
|
187
|
+
# # Payment: 100
|
188
|
+
# # Result: { id: 1, amount: 100, status: 'paid' }
|
189
|
+
#
|
190
|
+
# @example Partial payment with split
|
191
|
+
# # Accrual: { id: 1, amount: 100, status: 'pending' }
|
192
|
+
# # Payment: 60
|
193
|
+
# # Result:
|
194
|
+
# # { id: 1, amount: 60, status: 'paid' } # Paid portion
|
195
|
+
# # { id: 2, amount: 40, status: 'pending' } # New accrual for remainder
|
196
|
+
def mark_accruals_as_paid(accrual_type, payment_amount)
|
197
|
+
pending_accruals = loan.loan_accruals.pending.by_type(accrual_type).order(:applied_on)
|
198
|
+
remaining_payment = payment_amount
|
134
199
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
200
|
+
pending_accruals.each do |accrual|
|
201
|
+
break if remaining_payment <= 0
|
202
|
+
|
203
|
+
if remaining_payment >= accrual.amount
|
204
|
+
accrual.update!(status: "paid")
|
205
|
+
remaining_payment -= accrual.amount
|
206
|
+
else
|
207
|
+
# Partial payment: split the accrual
|
208
|
+
paid_amount = remaining_payment
|
209
|
+
remaining_amount = accrual.amount - paid_amount
|
210
|
+
|
211
|
+
# Update original accrual to paid status with reduced amount
|
212
|
+
accrual.update!(amount: paid_amount, status: "paid")
|
213
|
+
|
214
|
+
# Create a new pending accrual for the remaining amount
|
215
|
+
Dscf::Credit::LoanAccrual.create!(
|
216
|
+
loan: loan,
|
217
|
+
accrual_type: accrual_type,
|
218
|
+
amount: remaining_amount,
|
219
|
+
applied_on: accrual.applied_on,
|
220
|
+
status: "pending"
|
221
|
+
)
|
222
|
+
|
223
|
+
remaining_payment = 0
|
224
|
+
end
|
141
225
|
end
|
226
|
+
end
|
142
227
|
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
228
|
+
# Update loan status based on remaining balance
|
229
|
+
#
|
230
|
+
# Status transitions:
|
231
|
+
# - If total remaining ≤ 0.01 ETB → status: "paid"
|
232
|
+
# - If was "overdue" and balance > 0 → status: "active"
|
233
|
+
# - Otherwise → status unchanged
|
234
|
+
#
|
235
|
+
# Total remaining includes:
|
236
|
+
# - Facilitation fee (from loan_accruals)
|
237
|
+
# - Pending penalties (from loan_accruals)
|
238
|
+
# - Pending interest (from loan_accruals)
|
239
|
+
# - Remaining principal (from loan record)
|
240
|
+
#
|
241
|
+
# @return [void]
|
242
|
+
def update_loan_status
|
243
|
+
pending_facilitation_fee = loan.loan_accruals.pending.by_type("facilitation_fee").sum(:amount)
|
244
|
+
pending_penalty = loan.loan_accruals.pending.by_type("penalty").sum(:amount)
|
245
|
+
pending_interest = loan.loan_accruals.pending.by_type("interest").sum(:amount)
|
246
|
+
|
247
|
+
total_remaining = pending_facilitation_fee +
|
248
|
+
pending_penalty +
|
249
|
+
pending_interest +
|
250
|
+
(loan.remaining_amount || 0)
|
251
|
+
|
252
|
+
if total_remaining <= 0.01
|
253
|
+
loan.update!(status: "paid")
|
254
|
+
else
|
255
|
+
loan.update!(status: "active") if loan.status == "overdue"
|
151
256
|
end
|
152
257
|
end
|
153
258
|
|
259
|
+
# Reactivate locked credit facilities when loan is fully paid off
|
260
|
+
#
|
261
|
+
# Only runs if loan.status == "paid"
|
262
|
+
#
|
263
|
+
# Process:
|
264
|
+
# 1. Find all eligible credit lines with 0 available_limit (locked)
|
265
|
+
# 2. For each locked line, recalculate available_limit based on:
|
266
|
+
# - Credit limit (total allowed)
|
267
|
+
# - Minus current usage from other active loans
|
268
|
+
#
|
269
|
+
# This allows the borrower to access credit again after repayment.
|
270
|
+
#
|
271
|
+
# @return [void]
|
272
|
+
#
|
273
|
+
# @example
|
274
|
+
# # Before: eligible_line.available_limit = 0 (locked)
|
275
|
+
# # After payment: eligible_line.available_limit = 7000 (reactivated)
|
154
276
|
def reactivate_facilities_if_paid_off
|
155
277
|
return unless loan.status == "paid"
|
156
278
|
|
@@ -166,10 +288,14 @@ module Dscf::Credit
|
|
166
288
|
|
167
289
|
eligible_line.update!(available_limit: new_available_limit)
|
168
290
|
end
|
169
|
-
|
170
|
-
update_loan_profile_available_amount(loan_profile)
|
171
291
|
end
|
172
292
|
|
293
|
+
# Calculate total current usage for a credit line from all active loans
|
294
|
+
#
|
295
|
+
# Excludes the current loan (the one being paid off) from calculation.
|
296
|
+
#
|
297
|
+
# @param credit_line [Dscf::Credit::CreditLine] The credit line to check
|
298
|
+
# @return [Float] Sum of remaining_amount from active loans
|
173
299
|
def calculate_current_usage_for_credit_line(credit_line)
|
174
300
|
credit_line.loans
|
175
301
|
.where(status: [ "active", "overdue", "disbursed" ])
|
@@ -177,34 +303,37 @@ module Dscf::Credit
|
|
177
303
|
.sum(:remaining_amount)
|
178
304
|
end
|
179
305
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
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
|
-
|
306
|
+
# Build success result hash
|
307
|
+
#
|
308
|
+
# @param allocation [Hash] The allocation hash with payment details
|
309
|
+
# @return [Hash] Success response with loan, payment details, and message
|
190
310
|
def success_result(allocation)
|
311
|
+
loan.reload
|
312
|
+
pending_facilitation_fee = loan.loan_accruals.pending.by_type("facilitation_fee").sum(:amount)
|
313
|
+
pending_penalty = loan.loan_accruals.pending.by_type("penalty").sum(:amount)
|
314
|
+
pending_interest = loan.loan_accruals.pending.by_type("interest").sum(:amount)
|
315
|
+
|
191
316
|
{
|
192
317
|
success: true,
|
193
|
-
loan: loan
|
318
|
+
loan: loan,
|
194
319
|
payment_details: {
|
195
320
|
payment_amount: payment_amount,
|
196
321
|
allocation: allocation,
|
197
322
|
new_loan_status: loan.status,
|
198
323
|
paid_off: loan.status == "paid",
|
199
|
-
remaining_balance:
|
200
|
-
|
201
|
-
|
324
|
+
remaining_balance: pending_facilitation_fee +
|
325
|
+
pending_penalty +
|
326
|
+
pending_interest +
|
202
327
|
(loan.remaining_amount || 0)
|
203
328
|
},
|
204
329
|
message: loan.status == "paid" ? "Loan fully paid off" : "Payment processed successfully"
|
205
330
|
}
|
206
331
|
end
|
207
332
|
|
333
|
+
# Build error result hash
|
334
|
+
#
|
335
|
+
# @param message [String] Error message describing what went wrong
|
336
|
+
# @return [Hash] Error response with success: false and error message
|
208
337
|
def error_result(message)
|
209
338
|
{
|
210
339
|
success: false,
|
data/config/locales/en.yml
CHANGED
@@ -102,6 +102,40 @@ en:
|
|
102
102
|
calculate_facility_limits: "Failed to calculate facility limits"
|
103
103
|
apply_risk_factor: "Failed to apply risk factor"
|
104
104
|
|
105
|
+
loan_accrual:
|
106
|
+
success:
|
107
|
+
index: "Loan accruals retrieved successfully"
|
108
|
+
show: "Loan accrual details retrieved successfully"
|
109
|
+
create: "Loan accrual created successfully"
|
110
|
+
update: "Loan accrual updated successfully"
|
111
|
+
destroy: "Loan accrual deleted successfully"
|
112
|
+
mark_paid: "Loan accrual marked as paid successfully"
|
113
|
+
generate: "Loan accruals generated successfully"
|
114
|
+
statistics: "Loan accrual statistics retrieved successfully"
|
115
|
+
errors:
|
116
|
+
index: "Failed to retrieve loan accruals"
|
117
|
+
show: "Failed to retrieve loan accrual details"
|
118
|
+
create: "Failed to create loan accrual"
|
119
|
+
update: "Failed to update loan accrual"
|
120
|
+
destroy: "Failed to delete loan accrual"
|
121
|
+
mark_paid: "Failed to mark loan accrual as paid"
|
122
|
+
invalid_accrual_type: "Invalid accrual type"
|
123
|
+
amount_must_be_positive: "Accrual amount must be positive"
|
124
|
+
generate: "Failed to generate loan accruals"
|
125
|
+
statistics: "Failed to retrieve loan accrual statistics"
|
126
|
+
|
127
|
+
loan:
|
128
|
+
success:
|
129
|
+
index: "Loans retrieved successfully"
|
130
|
+
show: "Loan details retrieved successfully"
|
131
|
+
create: "Loan created successfully"
|
132
|
+
update: "Loan updated successfully"
|
133
|
+
errors:
|
134
|
+
index: "Failed to retrieve loans"
|
135
|
+
show: "Failed to retrieve loan details"
|
136
|
+
create: "Failed to create loan"
|
137
|
+
update: "Failed to update loan"
|
138
|
+
|
105
139
|
parameter_normalizer:
|
106
140
|
success:
|
107
141
|
index: "Parameter normalizers retrieved successfully"
|
@@ -299,6 +333,7 @@ en:
|
|
299
333
|
reject: "Facilitator application rejected successfully"
|
300
334
|
request_modification: "Modification requested for facilitator application successfully"
|
301
335
|
resubmit: "Facilitator application resubmitted successfully"
|
336
|
+
bulk_create: "Facilitator applications bulk creation completed"
|
302
337
|
errors:
|
303
338
|
index: "Failed to retrieve facilitator applications"
|
304
339
|
show: "Failed to retrieve facilitator application details"
|
@@ -308,6 +343,7 @@ en:
|
|
308
343
|
reject: "Failed to reject facilitator application"
|
309
344
|
request_modification: "Failed to request modification for facilitator application"
|
310
345
|
resubmit: "Failed to resubmit facilitator application"
|
346
|
+
bulk_create: "Failed to bulk create facilitator applications"
|
311
347
|
not_found: "Facilitator application not found"
|
312
348
|
|
313
349
|
# Global messages
|
data/config/routes.rb
CHANGED
@@ -3,6 +3,12 @@ Dscf::Credit::Engine.routes.draw do
|
|
3
3
|
resources :bank_branches
|
4
4
|
resources :payments
|
5
5
|
resources :loans
|
6
|
+
resources :loan_accruals do
|
7
|
+
collection do
|
8
|
+
post :generate
|
9
|
+
get :statistics
|
10
|
+
end
|
11
|
+
end
|
6
12
|
resources :scoring_param_types
|
7
13
|
|
8
14
|
resources :facilitators do
|
@@ -81,6 +87,10 @@ Dscf::Credit::Engine.routes.draw do
|
|
81
87
|
end
|
82
88
|
|
83
89
|
resources :facilitator_applications do
|
90
|
+
collection do
|
91
|
+
post "bulk_create"
|
92
|
+
end
|
93
|
+
|
84
94
|
member do
|
85
95
|
patch "approve"
|
86
96
|
patch "reject"
|
@@ -3,7 +3,7 @@ class CreateDscfCreditFacilitatorApplications < ActiveRecord::Migration[8.0]
|
|
3
3
|
create_table :dscf_credit_facilitator_applications do |t|
|
4
4
|
t.references :user, null: false, foreign_key: { to_table: :dscf_core_users }
|
5
5
|
t.references :bank, null: false, foreign_key: { to_table: :dscf_credit_banks }
|
6
|
-
t.jsonb :facilitator_info, null:
|
6
|
+
t.jsonb :facilitator_info, null: true, default: {}
|
7
7
|
t.timestamps
|
8
8
|
end
|
9
9
|
end
|
@@ -5,10 +5,6 @@ class CreateDscfCreditLoans < ActiveRecord::Migration[8.0]
|
|
5
5
|
t.references :credit_line, null: false, foreign_key: { to_table: :dscf_credit_credit_lines }
|
6
6
|
t.string :status, default: 'pending'
|
7
7
|
t.decimal :principal_amount, precision: 15, scale: 2, null: false
|
8
|
-
t.decimal :accrued_interest, precision: 15, scale: 2, default: 0
|
9
|
-
t.decimal :accrued_penalty, precision: 15, scale: 2, default: 0
|
10
|
-
t.decimal :facilitation_fee, precision: 15, scale: 2, default: 0
|
11
|
-
t.decimal :total_loan_amount, precision: 15, scale: 2, null: false
|
12
8
|
t.decimal :remaining_amount, precision: 15, scale: 2, null: false
|
13
9
|
t.date :due_date, null: false
|
14
10
|
t.datetime :disbursed_at
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class CreateDscfCreditLoanAccruals < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :dscf_credit_loan_accruals do |t|
|
4
|
+
t.references :loan, null: false, foreign_key: { to_table: :dscf_credit_loans }
|
5
|
+
t.string :accrual_type, null: false
|
6
|
+
t.decimal :amount, precision: 15, scale: 2, null: false
|
7
|
+
t.date :applied_on, null: false
|
8
|
+
t.string :status, default: 'pending', null: false
|
9
|
+
|
10
|
+
t.timestamps
|
11
|
+
end
|
12
|
+
|
13
|
+
add_index :dscf_credit_loan_accruals, :accrual_type
|
14
|
+
add_index :dscf_credit_loan_accruals, :applied_on
|
15
|
+
add_index :dscf_credit_loan_accruals, :status
|
16
|
+
add_index :dscf_credit_loan_accruals, [ :loan_id, :accrual_type ]
|
17
|
+
add_index :dscf_credit_loan_accruals, [ :loan_id, :status ]
|
18
|
+
end
|
19
|
+
end
|
data/db/seeds.rb
CHANGED
@@ -811,16 +811,30 @@ loan1 = Dscf::Credit::Loan.create!(
|
|
811
811
|
credit_line: credit_line1,
|
812
812
|
status: 'active',
|
813
813
|
principal_amount: 50000.00,
|
814
|
-
|
815
|
-
accrued_penalty: 0.00,
|
816
|
-
facilitation_fee: 1000.00,
|
817
|
-
total_loan_amount: 53500.00,
|
818
|
-
remaining_amount: 53500.00,
|
814
|
+
remaining_amount: 50000.00,
|
819
815
|
due_date: 3.months.from_now.to_date,
|
820
816
|
disbursed_at: 1.day.ago,
|
821
817
|
active: true
|
822
818
|
)
|
823
819
|
|
820
|
+
# 12.5. Loan Accruals (depends on loans)
|
821
|
+
puts "Seeding loan accruals..."
|
822
|
+
Dscf::Credit::LoanAccrual.create!(
|
823
|
+
loan: loan1,
|
824
|
+
accrual_type: 'interest',
|
825
|
+
amount: 250.00,
|
826
|
+
applied_on: 1.month.ago.to_date,
|
827
|
+
status: 'pending'
|
828
|
+
)
|
829
|
+
|
830
|
+
Dscf::Credit::LoanAccrual.create!(
|
831
|
+
loan: loan1,
|
832
|
+
accrual_type: 'facilitation_fee',
|
833
|
+
amount: 1000.00,
|
834
|
+
applied_on: 1.day.ago.to_date,
|
835
|
+
status: 'paid'
|
836
|
+
)
|
837
|
+
|
824
838
|
# 13. Loan Transactions (depends on loans)
|
825
839
|
puts "Seeding loan transactions..."
|
826
840
|
Dscf::Credit::LoanTransaction.create!(
|
@@ -863,6 +877,7 @@ puts "- Categories: #{Dscf::Credit::Category.count}"
|
|
863
877
|
puts "- Credit Lines: #{Dscf::Credit::CreditLine.count}"
|
864
878
|
puts "- Loan Profiles: #{Dscf::Credit::LoanProfile.count}"
|
865
879
|
puts "- Loans: #{Dscf::Credit::Loan.count}"
|
880
|
+
puts "- Loan Accruals: #{Dscf::Credit::LoanAccrual.count}"
|
866
881
|
puts "- Facilitators: #{Dscf::Credit::Facilitator.count} (will be created via API)"
|
867
882
|
puts "- System Configs: #{Dscf::Credit::SystemConfig.count}"
|
868
883
|
puts "- Scoring Parameters: #{Dscf::Credit::ScoringParameter.count}"
|
data/lib/dscf/credit/version.rb
CHANGED
@@ -0,0 +1,29 @@
|
|
1
|
+
FactoryBot.define do
|
2
|
+
factory :loan_accrual, class: "Dscf::Credit::LoanAccrual" do
|
3
|
+
association :loan, factory: :loan
|
4
|
+
accrual_type { %w[interest penalty tax facilitation_fee late_fee other].sample }
|
5
|
+
amount { Faker::Number.decimal(l_digits: 3, r_digits: 2) }
|
6
|
+
applied_on { Faker::Date.between(from: 1.month.ago, to: Date.current) }
|
7
|
+
status { %w[pending paid cancelled].sample }
|
8
|
+
|
9
|
+
trait :interest do
|
10
|
+
accrual_type { "interest" }
|
11
|
+
end
|
12
|
+
|
13
|
+
trait :penalty do
|
14
|
+
accrual_type { "penalty" }
|
15
|
+
end
|
16
|
+
|
17
|
+
trait :pending do
|
18
|
+
status { "pending" }
|
19
|
+
end
|
20
|
+
|
21
|
+
trait :paid do
|
22
|
+
status { "paid" }
|
23
|
+
end
|
24
|
+
|
25
|
+
trait :cancelled do
|
26
|
+
status { "cancelled" }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -4,10 +4,6 @@ FactoryBot.define do
|
|
4
4
|
association :credit_line, factory: :credit_line
|
5
5
|
status { %w[pending approved disbursed active overdue paid closed].sample }
|
6
6
|
principal_amount { Faker::Number.decimal(l_digits: 5, r_digits: 2) }
|
7
|
-
accrued_interest { Faker::Number.decimal(l_digits: 3, r_digits: 2) }
|
8
|
-
accrued_penalty { Faker::Number.decimal(l_digits: 3, r_digits: 2) }
|
9
|
-
facilitation_fee { Faker::Number.decimal(l_digits: 3, r_digits: 2) }
|
10
|
-
total_loan_amount { principal_amount + accrued_interest + facilitation_fee }
|
11
7
|
remaining_amount { Faker::Number.decimal(l_digits: 5, r_digits: 2) }
|
12
8
|
due_date { Faker::Date.between(from: 1.month.from_now, to: 6.months.from_now) }
|
13
9
|
disbursed_at { Faker::Time.between(from: 2.months.ago, to: Time.current) if status.in?(%w[disbursed active overdue paid closed]) }
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dscf-credit
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Adoniyas
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-10-
|
10
|
+
date: 2025-10-04 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: dscf-core
|
@@ -333,6 +333,7 @@ files:
|
|
333
333
|
- app/controllers/dscf/credit/eligible_credit_lines_controller.rb
|
334
334
|
- app/controllers/dscf/credit/facilitator_applications_controller.rb
|
335
335
|
- app/controllers/dscf/credit/facilitators_controller.rb
|
336
|
+
- app/controllers/dscf/credit/loan_accruals_controller.rb
|
336
337
|
- app/controllers/dscf/credit/loan_applications_controller.rb
|
337
338
|
- app/controllers/dscf/credit/loan_profiles_controller.rb
|
338
339
|
- app/controllers/dscf/credit/loans_controller.rb
|
@@ -344,6 +345,7 @@ files:
|
|
344
345
|
- app/controllers/dscf/credit/system_configs_controller.rb
|
345
346
|
- app/controllers/dscf/credit/users_controller.rb
|
346
347
|
- app/jobs/dscf/credit/application_job.rb
|
348
|
+
- app/jobs/dscf/credit/generate_daily_accruals_job.rb
|
347
349
|
- app/mailers/dscf/credit/application_mailer.rb
|
348
350
|
- app/mailers/dscf/credit/bank_staff_welcome_mailer.rb
|
349
351
|
- app/mailers/dscf/credit/facilitator_mailer.rb
|
@@ -364,6 +366,7 @@ files:
|
|
364
366
|
- app/models/dscf/credit/facilitator_performance.rb
|
365
367
|
- app/models/dscf/credit/failed_operations_log.rb
|
366
368
|
- app/models/dscf/credit/loan.rb
|
369
|
+
- app/models/dscf/credit/loan_accrual.rb
|
367
370
|
- app/models/dscf/credit/loan_application.rb
|
368
371
|
- app/models/dscf/credit/loan_profile.rb
|
369
372
|
- app/models/dscf/credit/loan_profile_scoring_spec.rb
|
@@ -390,6 +393,7 @@ files:
|
|
390
393
|
- app/serializers/dscf/credit/facilitator_application_serializer.rb
|
391
394
|
- app/serializers/dscf/credit/facilitator_performance_serializer.rb
|
392
395
|
- app/serializers/dscf/credit/facilitator_serializer.rb
|
396
|
+
- app/serializers/dscf/credit/loan_accrual_serializer.rb
|
393
397
|
- app/serializers/dscf/credit/loan_application_serializer.rb
|
394
398
|
- app/serializers/dscf/credit/loan_profile_scoring_spec_serializer.rb
|
395
399
|
- app/serializers/dscf/credit/loan_profile_serializer.rb
|
@@ -408,6 +412,7 @@ files:
|
|
408
412
|
- app/services/dscf/credit/facilitator_approval_service.rb
|
409
413
|
- app/services/dscf/credit/facilitator_creation_service.rb
|
410
414
|
- app/services/dscf/credit/facility_limit_calculation_engine.rb
|
415
|
+
- app/services/dscf/credit/loan_accrual_generator_service.rb
|
411
416
|
- app/services/dscf/credit/loan_profile_creation_service.rb
|
412
417
|
- app/services/dscf/credit/repayment_service.rb
|
413
418
|
- app/services/dscf/credit/risk_application_service.rb
|
@@ -450,6 +455,7 @@ files:
|
|
450
455
|
- db/migrate/20250822092936_create_dscf_credit_accounting_entries.rb
|
451
456
|
- db/migrate/20250825231109_create_dscf_credit_bank_staff.rb
|
452
457
|
- db/migrate/20250917120000_create_dscf_credit_eligible_credit_lines.rb
|
458
|
+
- db/migrate/20251003132939_create_dscf_credit_loan_accruals.rb
|
453
459
|
- db/seeds.rb
|
454
460
|
- lib/dscf/credit.rb
|
455
461
|
- lib/dscf/credit/engine.rb
|
@@ -469,6 +475,7 @@ files:
|
|
469
475
|
- spec/factories/dscf/credit/facilitator_performances.rb
|
470
476
|
- spec/factories/dscf/credit/facilitators.rb
|
471
477
|
- spec/factories/dscf/credit/failed_operations_logs.rb
|
478
|
+
- spec/factories/dscf/credit/loan_accruals.rb
|
472
479
|
- spec/factories/dscf/credit/loan_applications.rb
|
473
480
|
- spec/factories/dscf/credit/loan_profile_scoring_specs.rb
|
474
481
|
- spec/factories/dscf/credit/loan_profiles.rb
|