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
@@ -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, :current_user
19
+ attr_reader :loan, :payment_amount
4
20
 
5
- def initialize(loan, payment_amount, current_user)
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 by decreasing loan balance and updating related records
12
- # @return [Hash] Result containing payment details and updated loan status
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 || 0
41
- current_penalty = loan.accrued_penalty || 0
42
- current_interest = loan.accrued_interest || 0
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
- transactions << {
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
- transactions << {
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
- transactions << {
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
- if allocation[:principal_payment] > 0
136
- transactions << {
137
- transaction_type: "repayment",
138
- amount: allocation[:principal_payment],
139
- reference_suffix: "PRINCIPAL"
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
- 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
- )
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
- 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
-
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.reload,
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: (loan.facilitation_fee || 0) +
200
- (loan.accrued_penalty || 0) +
201
- (loan.accrued_interest || 0) +
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,
@@ -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: false, default: {}
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
- accrued_interest: 2500.00,
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}"
@@ -1,5 +1,5 @@
1
1
  module Dscf
2
2
  module Credit
3
- VERSION = "0.1.6"
3
+ VERSION = "0.1.8"
4
4
  end
5
5
  end
@@ -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.6
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-01 00:00:00.000000000 Z
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