dscf-credit 0.3.1 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e3b6a324773633a199dfe48d49baa98a8e410671da8ad4916f107ad38ad33fb
4
- data.tar.gz: 5accc6330524779a8720ebc16b9db48bd8ee9673552216d8fce8d38ebd6e40cd
3
+ metadata.gz: 2e43f1b7fa03ac6fe72f99412259a4d0345b41cf0487f3d2c3f3ba63e895d589
4
+ data.tar.gz: af9eb779b069774cbed9fdb356762637ac6ca6f5f61d181485d23e4d64419178
5
5
  SHA512:
6
- metadata.gz: 2979387747dc0f40646df26c2cdcd266443cf07fb1c417deed6f704b62292f650469a490149891f0594df3e245e8ec1e3884f65abab9af58692aae4dd71c13ce
7
- data.tar.gz: 8263bd4bc902d6525d3674736fe6301e1d032f84e9c1db21dcc038b7c36c2049e532e23097b4d03532f99e6a8288caf186f9793a2189522e9ffe7eaa9a532e6b
6
+ metadata.gz: 87ac35ad44c00672dd4d47ab33fa4f3a27757849b963e7db80aac172844790c08f2af6b45607c1f2526451131fe802fe7df0fbfc985c42c6485ee038df8b5a1c
7
+ data.tar.gz: a26cc03842c4366d42ae16f6349d6406890da27a1419f4b824e349cf37d8ce4e9e4b0ba1fc540370885bcf93eda99b7b3e5e2e9eb0dc519c907d55d5140857d3
@@ -49,12 +49,13 @@ module Dscf::Credit
49
49
  :max_interest_calculation_days,
50
50
  :penalty_frequency,
51
51
  :penalty_income_tax,
52
- :active
52
+ :active,
53
+ :base_scoring_parameter_id
53
54
  )
54
55
  end
55
56
 
56
57
  def eager_loaded_associations
57
- [ :credit_line, :created_by ]
58
+ [ :credit_line, :created_by, :base_scoring_parameter ]
58
59
  end
59
60
 
60
61
  def allowed_order_columns
@@ -63,10 +64,10 @@ module Dscf::Credit
63
64
 
64
65
  def default_serializer_includes
65
66
  {
66
- index: [ :credit_line ],
67
- show: [ :credit_line, :created_by ],
68
- create: [ :credit_line, :created_by ],
69
- update: [ :credit_line, :created_by ]
67
+ index: [ :credit_line, :base_scoring_parameter ],
68
+ show: [ :credit_line, :created_by, :base_scoring_parameter ],
69
+ create: [ :credit_line, :created_by, :base_scoring_parameter ],
70
+ update: [ :credit_line, :created_by, :base_scoring_parameter ]
70
71
  }
71
72
  end
72
73
  end
@@ -4,11 +4,13 @@ module Dscf::Credit
4
4
 
5
5
  belongs_to :credit_line, class_name: "Dscf::Credit::CreditLine", foreign_key: "credit_line_id"
6
6
  belongs_to :created_by, polymorphic: true
7
+ belongs_to :base_scoring_parameter, class_name: "Dscf::Credit::ScoringParameter", foreign_key: "base_scoring_parameter_id", optional: true
7
8
 
8
9
  validates :min_amount, :max_amount, :interest_rate, :penalty_rate, :facilitation_fee_rate, :tax_rate, :credit_line_multiplier, :max_penalty_days, :loan_duration, :interest_frequency, :interest_income_tax, :vat, :max_interest_calculation_days, :penalty_frequency, :penalty_income_tax, presence: true
9
10
  validates :min_amount, :max_amount, :credit_line_multiplier, numericality: { greater_than: 0 }
10
11
  validates :interest_rate, :penalty_rate, :facilitation_fee_rate, :tax_rate, :interest_income_tax, :vat, :penalty_income_tax, numericality: { greater_than_or_equal_to: 0, less_than: 10 }
11
12
  validates :max_penalty_days, :loan_duration, :max_interest_calculation_days, numericality: { greater_than: 0 }
13
+ validates :credit_line_divider, numericality: { greater_than: 0, less_than_or_equal_to: 365 }, allow_nil: true
12
14
  validates :interest_frequency, inclusion: { in: %w[daily weekly monthly quarterly annually] }
13
15
  validates :penalty_frequency, inclusion: { in: %w[daily weekly monthly] }
14
16
  validate :max_amount_greater_than_min_amount
@@ -19,11 +21,11 @@ module Dscf::Credit
19
21
  scope :by_penalty_frequency, ->(frequency) { where(penalty_frequency: frequency) }
20
22
 
21
23
  def self.ransackable_attributes(auth_object = nil)
22
- %w[id min_amount max_amount interest_rate penalty_rate facilitation_fee_rate tax_rate credit_line_multiplier max_penalty_days loan_duration interest_frequency interest_income_tax vat max_interest_calculation_days penalty_frequency penalty_income_tax active created_at updated_at]
24
+ %w[id min_amount max_amount interest_rate penalty_rate facilitation_fee_rate tax_rate credit_line_multiplier max_penalty_days loan_duration interest_frequency interest_income_tax vat max_interest_calculation_days penalty_frequency penalty_income_tax credit_line_divider active created_at updated_at]
23
25
  end
24
26
 
25
27
  def self.ransackable_associations(auth_object = nil)
26
- %w[credit_line created_by]
28
+ %w[credit_line created_by base_scoring_parameter]
27
29
  end
28
30
 
29
31
  private
@@ -4,9 +4,10 @@ module Dscf::Credit
4
4
  :facilitation_fee_rate, :tax_rate, :credit_line_multiplier, :max_penalty_days,
5
5
  :loan_duration, :interest_frequency, :interest_income_tax,
6
6
  :vat, :max_interest_calculation_days, :penalty_frequency,
7
- :penalty_income_tax, :active, :created_at, :updated_at
7
+ :penalty_income_tax, :credit_line_divider, :active, :created_at, :updated_at
8
8
 
9
9
  belongs_to :credit_line, serializer: Dscf::Credit::CreditLineSerializer
10
10
  belongs_to :created_by, serializer: Dscf::Core::UserSerializer
11
+ belongs_to :base_scoring_parameter, serializer: Dscf::Credit::ScoringParameterSerializer
11
12
  end
12
13
  end
@@ -4,5 +4,9 @@ module Dscf::Credit
4
4
 
5
5
  belongs_to :loan_profile, serializer: Dscf::Credit::LoanProfileSerializer
6
6
  belongs_to :credit_line, serializer: Dscf::Credit::CreditLineSerializer
7
+
8
+ attribute :user_id do
9
+ object.loan_profile.loan_application.user_id if object.loan_profile && object.loan_profile.loan_application
10
+ end
7
11
  end
8
12
  end
@@ -24,7 +24,7 @@ module Dscf::Credit
24
24
  loan_profile_status = loan_profile.current_status_for(:default)
25
25
  return error_result("Loan profile not approved") unless loan_profile_status == "approved"
26
26
 
27
- validation_result = validate_disbursement_amount
27
+ validation_result = validate_disbursement_amount(credit_line)
28
28
  return validation_result unless validation_result[:success]
29
29
 
30
30
  ActiveRecord::Base.transaction do
@@ -49,11 +49,12 @@ module Dscf::Credit
49
49
 
50
50
  private
51
51
 
52
- def validate_disbursement_amount
52
+ def validate_disbursement_amount(credit_line)
53
53
  available_limit = eligible_credit_line.available_limit
54
+ credit_line_spec = credit_line.credit_line_specs.active.first
54
55
 
55
- if amount <= 0
56
- return error_result("Invalid disbursement amount")
56
+ if credit_line_spec && amount < credit_line_spec.min_amount
57
+ return error_result("Amount must be at least #{credit_line_spec.min_amount}")
57
58
  end
58
59
 
59
60
  if amount > available_limit
@@ -1,7 +1,38 @@
1
1
  module Dscf::Credit
2
+ # FacilityLimitCalculationEngine calculates credit facility limits for loan profiles
3
+ # based on dynamic scoring parameters and credit line specifications.
4
+ #
5
+ # @example Basic usage
6
+ # loan_profile = Dscf::Credit::LoanProfile.find(profile_id)
7
+ # category = Dscf::Credit::Category.find(category_id)
8
+ # engine = Dscf::Credit::FacilityLimitCalculationEngine.new(loan_profile.id, category.id)
9
+ # result = engine.calculate_facility_limits
10
+ #
11
+ # if result[:success]
12
+ # puts "Limits calculated: #{result[:message]}"
13
+ # puts "Eligible credit lines: #{result[:data].count}"
14
+ # else
15
+ # puts "Error: #{result[:error]}"
16
+ # end
17
+ #
18
+ # @example Result structure
19
+ # {
20
+ # success: true,
21
+ # data: [#<EligibleCreditLine>, ...],
22
+ # message: "Facility limits calculated successfully"
23
+ # }
24
+ #
25
+ # @see docs/CREDIT_SCORING_ARCHITECTURE.md for scoring architecture
2
26
  class FacilityLimitCalculationEngine
3
27
  attr_reader :loan_profile, :category_id, :errors
4
28
 
29
+ # Initialize the facility limit calculation engine
30
+ #
31
+ # @param loan_profile_id [Integer] The ID of the loan profile to calculate limits for
32
+ # @param category_id [Integer] The ID of the credit line category
33
+ #
34
+ # @example
35
+ # engine = FacilityLimitCalculationEngine.new(123, 456)
5
36
  def initialize(loan_profile_id, category_id)
6
37
  @errors = []
7
38
  @loan_profile = LoanProfile.find(loan_profile_id)
@@ -10,42 +41,75 @@ module Dscf::Credit
10
41
  @errors << "Loan profile not found: #{e.message}"
11
42
  end
12
43
 
13
- # Calculate facility limits for all credit lines in the category
44
+ # Calculate facility limits for all eligible credit lines
45
+ #
46
+ # Process steps:
47
+ # 1. Retrieve active credit lines for the category and bank
48
+ # 2. For each credit line, get the latest active credit line spec
49
+ # 3. Calculate base metric from scoring parameter (dynamic based on parameter type)
50
+ # 4. Calculate credit limit using: base_metric × multiplier × score_percentage
51
+ # 5. Apply min/max constraints from credit line spec
52
+ # 6. Create eligible credit line records with calculated limits
53
+ # 7. Update loan profile's total_limit
54
+ #
55
+ # @return [Hash] Result hash with the following structure:
56
+ # - :success [Boolean] Whether the operation succeeded
57
+ # - :data [Array<EligibleCreditLine>] Created eligible credit lines (empty array on failure)
58
+ # - :message [String] Success message
59
+ # - :error [String] Error message (only present on failure)
60
+ # - :errors [Array<String>] Detailed error messages for debugging
61
+ #
62
+ # @example Success response
63
+ # {
64
+ # success: true,
65
+ # data: [#<EligibleCreditLine id: 1>, #<EligibleCreditLine id: 2>],
66
+ # message: "Facility limits calculated successfully"
67
+ # }
68
+ #
69
+ # @example Error response
70
+ # {
71
+ # success: false,
72
+ # error: "No credit lines found for category 5 and bank 10",
73
+ # errors: ["No credit lines found for category 5 and bank 10"]
74
+ # }
14
75
  def calculate_facility_limits
15
76
  return error_result("Loan profile is required") unless loan_profile
16
77
  return error_result("Category ID is required") unless category_id
17
78
 
18
79
  begin
19
80
  ActiveRecord::Base.transaction do
20
- # Get all credit lines for the category and bank
21
81
  credit_lines = get_credit_lines_for_calculation
22
-
23
82
  if credit_lines.empty?
24
83
  return error_result("No credit lines found for category #{category_id} and bank #{loan_profile.loan_application.bank_id}")
25
84
  end
26
85
 
27
- # Calculate base metric from facilitator info
28
- base_metric = calculate_base_metric
29
-
30
- if base_metric <= 0
31
- return error_result("Invalid base metric calculated: #{base_metric}. Check facilitator_info avg_monthly_purchase.")
32
- end
33
-
34
- # Calculate facility limits for each credit line
35
86
  eligible_credit_lines = []
36
-
37
87
  credit_lines.each do |credit_line|
38
- limit_result = calculate_credit_line_limit(credit_line, base_metric)
88
+ credit_line_spec = credit_line.credit_line_specs.active.order(created_at: :desc).first
89
+ unless credit_line_spec
90
+ @errors << "No active credit line spec for credit line #{credit_line.id}"
91
+ next
92
+ end
93
+
94
+ base_metric = calculate_base_metric(credit_line_spec)
95
+ if base_metric <= 0
96
+ @errors << "Invalid base metric for credit line #{credit_line.id}: #{base_metric}"
97
+ next
98
+ end
39
99
 
40
- if limit_result[:success] && limit_result[:limit] >= 0
100
+ limit_result = calculate_credit_line_limit(credit_line, base_metric, credit_line_spec)
101
+ unless limit_result[:success]
102
+ @errors << limit_result[:error]
103
+ next
104
+ end
105
+
106
+ if limit_result[:limit] >= 0
41
107
  eligible_credit_line = create_eligible_credit_line(credit_line, limit_result[:limit])
42
108
  eligible_credit_lines << eligible_credit_line
43
109
  end
44
110
  end
45
111
 
46
- # Update loan profile total limit
47
112
  update_loan_profile_total_limit
48
-
49
113
  success_result(eligible_credit_lines, "Facility limits calculated successfully")
50
114
  end
51
115
  rescue StandardError => e
@@ -57,7 +121,17 @@ module Dscf::Credit
57
121
 
58
122
  private
59
123
 
60
- # Get credit lines for the specified category and bank
124
+ # Retrieve credit lines eligible for calculation
125
+ #
126
+ # Filters credit lines by:
127
+ # - Category ID
128
+ # - Bank ID from loan application
129
+ # - Active credit line specs
130
+ #
131
+ # Includes:
132
+ # - Credit line specs with base_scoring_parameter association
133
+ #
134
+ # @return [ActiveRecord::Relation<CreditLine>] Filtered credit lines
61
135
  def get_credit_lines_for_calculation
62
136
  CreditLine.joins(:category, :credit_line_specs)
63
137
  .where(
@@ -65,66 +139,186 @@ module Dscf::Credit
65
139
  bank_id: loan_profile.loan_application.bank_id,
66
140
  dscf_credit_credit_line_specs: { active: true }
67
141
  )
68
- .includes(:credit_line_specs)
69
- .distinct
142
+ .includes(credit_line_specs: :base_scoring_parameter)
70
143
  end
71
144
 
72
- # Calculate base metric from facilitator info
73
- def calculate_base_metric
74
- facilitator_info = loan_profile.loan_application.facilitator_info || {}
145
+ # Calculate the base metric for credit limit calculation
146
+ #
147
+ # This method dynamically determines the base metric value based on the
148
+ # base_scoring_parameter associated with the credit line spec. The base metric
149
+ # is extracted from the loan application's facilitator_info and normalized
150
+ # using the credit_line_divider from the credit line spec.
151
+ #
152
+ # Flow:
153
+ # 1. Check if credit_line_spec has a base_scoring_parameter
154
+ # 2. If not, use fallback_base_metric (looks for avg_monthly_purchase manually)
155
+ # 3. Extract the raw_value from facilitator_info using parameter ID
156
+ # 4. Normalize the value using credit_line_divider:
157
+ # - If divider is present and > 0: divide raw_value by divider
158
+ # - If divider is nil or <= 0: use raw_value as-is
159
+ #
160
+ # @param credit_line_spec [CreditLineSpec] The credit line specification
161
+ # @return [Float] The calculated base metric (0.0 if invalid or missing data)
162
+ #
163
+ # @example With divider = 30
164
+ # # credit_line_spec.credit_line_divider = 30
165
+ # # facilitator_info[param_id]["raw_value"] = 30000.0
166
+ # # Returns: 1000.0 (30000 / 30)
167
+ #
168
+ # @example With divider = 365
169
+ # # credit_line_spec.credit_line_divider = 365
170
+ # # facilitator_info[param_id]["raw_value"] = 365000.0
171
+ # # Returns: 1000.0 (365000 / 365)
172
+ #
173
+ # @example With nil divider
174
+ # # credit_line_spec.credit_line_divider = nil
175
+ # # facilitator_info[param_id]["raw_value"] = 1000.0
176
+ # # Returns: 1000.0 (no normalization)
177
+ def calculate_base_metric(credit_line_spec)
178
+ return fallback_base_metric unless credit_line_spec.base_scoring_parameter
75
179
 
76
- # Find avg_monthly_purchase from the nested JSON structure
77
- # facilitator_info structure: { "301": { "param_name": "avg_monthly_purchase", "value": 0.1, "raw_value": 30000 } }
78
- avg_monthly_purchase = nil
180
+ base_param = credit_line_spec.base_scoring_parameter
181
+ facilitator_info = loan_profile.loan_application.facilitator_info || {}
182
+ param_id = base_param.id.to_s
79
183
 
80
- facilitator_info.each do |param_id, param_data|
81
- if param_data.is_a?(Hash) &&
82
- (param_data["param_name"] == "avg_monthly_purchase" || param_data[:param_name] == "avg_monthly_purchase")
83
- avg_monthly_purchase = param_data["raw_value"] || param_data[:raw_value]
84
- break
85
- end
184
+ param_data = facilitator_info[param_id]
185
+ unless param_data
186
+ Rails.logger.warn "No data for scoring parameter #{base_param.id} (#{base_param.name}) in facilitator_info"
187
+ return 0.0
86
188
  end
87
189
 
88
- # Handle string and numeric values
89
- if avg_monthly_purchase.is_a?(String)
90
- avg_monthly_purchase = avg_monthly_purchase.to_f
91
- else
92
- avg_monthly_purchase = avg_monthly_purchase.to_f if avg_monthly_purchase
190
+ raw_value = param_data["raw_value"] || param_data[:raw_value]
191
+ value = raw_value.to_f rescue 0.0
192
+ if value <= 0
193
+ Rails.logger.warn "Invalid value #{raw_value} for scoring parameter #{base_param.id} (#{base_param.name})"
194
+ return 0.0
93
195
  end
94
196
 
95
- return 0.0 if avg_monthly_purchase.nil? || avg_monthly_purchase <= 0
96
-
97
- avg_monthly_purchase / 30.0
197
+ normalize_value(value, credit_line_spec)
98
198
  end
99
199
 
100
- # Calculate facility limit for a specific credit line
101
- def calculate_credit_line_limit(credit_line, base_metric)
102
- credit_line_spec = credit_line.credit_line_specs.active.order(created_at: :desc).first
200
+ # Fallback method to calculate base metric when base_scoring_parameter is not set
201
+ #
202
+ # This method searches facilitator_info for a parameter named "avg_monthly_purchase"
203
+ # and uses it as the base metric, normalized to a daily value.
204
+ #
205
+ # This provides backward compatibility for credit line specs that don't have
206
+ # a base_scoring_parameter association yet.
207
+ #
208
+ # @return [Float] The calculated base metric (0.0 if not found or invalid)
209
+ #
210
+ # @example Found avg_monthly_purchase
211
+ # # facilitator_info = { "123" => { "param_name" => "avg_monthly_purchase", "raw_value" => 30000.0 } }
212
+ # # Returns: 1000.0 (30000 / 30)
213
+ #
214
+ # @example Not found
215
+ # # facilitator_info = { "123" => { "param_name" => "other_param", "raw_value" => 5000.0 } }
216
+ # # Returns: 0.0
217
+ def fallback_base_metric
218
+ facilitator_info = loan_profile.loan_application.facilitator_info || {}
219
+ param_value = nil
103
220
 
104
- unless credit_line_spec
105
- return { success: false, error: "No active credit line spec found for credit line #{credit_line.id}" }
221
+ facilitator_info.each do |param_id, param_data|
222
+ next unless param_data.is_a?(Hash)
223
+ if param_data["param_name"] == "avg_monthly_purchase" || param_data[:param_name] == "avg_monthly_purchase"
224
+ param_value = param_data["raw_value"] || param_data[:raw_value]
225
+ break
226
+ end
106
227
  end
107
228
 
229
+ value = param_value.to_f rescue 0.0
230
+ return 0.0 if value <= 0
231
+ value / 30.0
232
+ end
233
+
234
+ # Normalize a parameter value based on the credit line spec's divider
235
+ #
236
+ # Divides the raw parameter value by the credit_line_divider from the credit line spec.
237
+ # If the divider is nil or <= 0, returns the raw value without normalization.
238
+ #
239
+ # @param value [Float] The raw parameter value to normalize
240
+ # @param credit_line_spec [CreditLineSpec] The credit line specification with divider
241
+ # @return [Float] The normalized value (or raw value if divider is invalid)
242
+ #
243
+ # @example With valid divider
244
+ # # credit_line_spec.credit_line_divider = 30
245
+ # # normalize_value(30000.0, credit_line_spec) → 1000.0
246
+ #
247
+ # @example With nil divider
248
+ # # credit_line_spec.credit_line_divider = nil
249
+ # # normalize_value(30000.0, credit_line_spec) → 30000.0
250
+ #
251
+ # @example With zero divider
252
+ # # credit_line_spec.credit_line_divider = 0
253
+ # # normalize_value(30000.0, credit_line_spec) → 30000.0
254
+ def normalize_value(value, credit_line_spec)
255
+ divider = credit_line_spec.credit_line_divider
256
+ return value if divider.nil? || divider <= 0 # No normalization if divider is NULL or invalid
257
+ value / divider
258
+ end
259
+
260
+ # Calculate the credit limit for a specific credit line
261
+ #
262
+ # Uses the formula:
263
+ # credit_limit = base_metric × credit_line_multiplier × score_percentage
264
+ #
265
+ # Then applies min/max constraints from the credit line spec.
266
+ #
267
+ # @param credit_line [CreditLine] The credit line being calculated
268
+ # @param base_metric [Float] The normalized base metric value
269
+ # @param credit_line_spec [CreditLineSpec] The credit line specification with multiplier and constraints
270
+ # @return [Hash] Result with :success, :limit, and :credit_line_spec
271
+ #
272
+ # @example Successful calculation
273
+ # # base_metric = 1000.0
274
+ # # credit_line_spec.credit_line_multiplier = 30.0
275
+ # # loan_profile.score = 75.0
276
+ # # calculated_limit = 1000 × 30 × 0.75 = 22500.0
277
+ # # { success: true, limit: 22500.0, credit_line_spec: spec }
278
+ def calculate_credit_line_limit(credit_line, base_metric, credit_line_spec)
108
279
  score_percentage = loan_profile.score / 100.0
109
280
  calculated_limit = base_metric * credit_line_spec.credit_line_multiplier * score_percentage
110
-
111
281
  final_limit = apply_min_max_constraints(calculated_limit, credit_line_spec)
112
-
113
282
  { success: true, limit: final_limit, credit_line_spec: credit_line_spec }
114
283
  end
115
284
 
285
+ # Apply minimum and maximum constraints to a calculated credit limit
286
+ #
287
+ # Constraint rules:
288
+ # - If calculated_limit > max_amount → cap at max_amount
289
+ # - If calculated_limit < min_amount → set to 0.0 (ineligible)
290
+ # - Otherwise → use calculated_limit
291
+ #
292
+ # @param calculated_limit [Float] The initially calculated credit limit
293
+ # @param credit_line_spec [CreditLineSpec] The spec with min_amount and max_amount
294
+ # @return [Float] The constrained credit limit
295
+ #
296
+ # @example Within constraints
297
+ # # calculated_limit = 22500.0, min = 5000, max = 50000
298
+ # # Returns: 22500.0
299
+ #
300
+ # @example Exceeds maximum
301
+ # # calculated_limit = 60000.0, min = 5000, max = 50000
302
+ # # Returns: 50000.0 (capped)
303
+ #
304
+ # @example Below minimum
305
+ # # calculated_limit = 3000.0, min = 5000, max = 50000
306
+ # # Returns: 0.0 (ineligible)
116
307
  def apply_min_max_constraints(calculated_limit, credit_line_spec)
117
- if calculated_limit > credit_line_spec.max_amount
118
- return credit_line_spec.max_amount
119
- end
120
-
121
- if calculated_limit < credit_line_spec.min_amount
122
- return 0.0
123
- end
124
-
308
+ return credit_line_spec.max_amount if calculated_limit > credit_line_spec.max_amount
309
+ return 0.0 if calculated_limit < credit_line_spec.min_amount
125
310
  calculated_limit
126
311
  end
127
312
 
313
+ # Create an eligible credit line record with the calculated limit
314
+ #
315
+ # Creates a new EligibleCreditLine associated with the loan profile,
316
+ # setting both credit_limit and available_limit to the calculated amount.
317
+ # Risk is initially set to nil.
318
+ #
319
+ # @param credit_line [CreditLine] The credit line to create eligibility for
320
+ # @param limit [Float] The calculated credit limit
321
+ # @return [EligibleCreditLine] The created eligible credit line record
128
322
  def create_eligible_credit_line(credit_line, limit)
129
323
  loan_profile.eligible_credit_lines.create!(
130
324
  credit_line: credit_line,
@@ -134,26 +328,33 @@ module Dscf::Credit
134
328
  )
135
329
  end
136
330
 
331
+ # Update the loan profile's total limit
332
+ #
333
+ # Sums all available_limit values from the loan profile's eligible credit lines
334
+ # and updates the loan_profile.total_limit to reflect the current state.
335
+ #
336
+ # @return [void]
137
337
  def update_loan_profile_total_limit
138
338
  total_limit = loan_profile.eligible_credit_lines.sum(:available_limit)
139
339
  loan_profile.update!(total_limit: total_limit)
140
340
  end
141
341
 
342
+ # Build success result hash
343
+ #
344
+ # @param data [Array<EligibleCreditLine>] The created eligible credit lines
345
+ # @param message [String] Success message describing the result
346
+ # @return [Hash] Success response with data and message
142
347
  def success_result(data, message)
143
- {
144
- success: true,
145
- data: data,
146
- message: message
147
- }
348
+ { success: true, data: data, message: message }
148
349
  end
149
350
 
351
+ # Build error result hash
352
+ #
353
+ # @param error_message [String] Error message describing what went wrong
354
+ # @return [Hash] Error response with success: false, error message, and detailed errors array
150
355
  def error_result(error_message)
151
356
  @errors << error_message unless @errors.include?(error_message)
152
- {
153
- success: false,
154
- error: error_message,
155
- errors: @errors
156
- }
357
+ { success: false, error: error_message, errors: @errors }
157
358
  end
158
359
  end
159
360
  end
@@ -0,0 +1,5 @@
1
+ class AddBaseScoringParameterToDscfCreditCreditLineSpecs < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_reference :dscf_credit_credit_line_specs, :base_scoring_parameter, null: true, foreign_key: { to_table: :dscf_credit_scoring_parameters }, index: true
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class AddCreditLineDividerToDscfCreditCreditLineSpecs < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :dscf_credit_credit_line_specs, :credit_line_divider, :integer, null: true
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class RemoveDefaultFromCreditLineMultiplierInCreditLineSpecs < ActiveRecord::Migration[8.0]
2
+ def change
3
+ change_column_default :dscf_credit_credit_line_specs, :credit_line_multiplier, from: 30.0, to: nil
4
+ end
5
+ end
@@ -1,5 +1,5 @@
1
1
  module Dscf
2
2
  module Credit
3
- VERSION = "0.3.1"
3
+ VERSION = "0.3.3"
4
4
  end
5
5
  end
@@ -2,6 +2,7 @@ FactoryBot.define do
2
2
  factory :credit_line_spec, class: "Dscf::Credit::CreditLineSpec" do
3
3
  association :credit_line, factory: :credit_line
4
4
  association :created_by, factory: :user if defined?(Dscf::Core::User)
5
+ base_scoring_parameter { nil }
5
6
  min_amount { Faker::Number.decimal(l_digits: 4, r_digits: 2).to_f }
6
7
  max_amount { min_amount + Faker::Number.decimal(l_digits: 5, r_digits: 2).to_f }
7
8
  interest_rate { Faker::Number.decimal(l_digits: 1, r_digits: 4).to_f }
@@ -9,6 +10,7 @@ FactoryBot.define do
9
10
  facilitation_fee_rate { Faker::Number.decimal(l_digits: 1, r_digits: 4).to_f }
10
11
  tax_rate { Faker::Number.decimal(l_digits: 1, r_digits: 4).to_f }
11
12
  credit_line_multiplier { Faker::Number.between(from: 15, to: 60) }
13
+ credit_line_divider { [ nil, Faker::Number.between(from: 1, to: 365) ].sample }
12
14
  max_penalty_days { Faker::Number.between(from: 30, to: 90) }
13
15
  loan_duration { Faker::Number.between(from: 30, to: 365) }
14
16
  interest_frequency { %w[daily weekly monthly quarterly annually].sample }
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.3.1
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adoniyas
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-10-10 00:00:00.000000000 Z
10
+ date: 2025-10-12 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: dscf-core
@@ -446,6 +446,9 @@ files:
446
446
  - db/migrate/20250825231109_create_dscf_credit_bank_staff.rb
447
447
  - db/migrate/20250917120000_create_dscf_credit_eligible_credit_lines.rb
448
448
  - db/migrate/20251003132939_create_dscf_credit_loan_accruals.rb
449
+ - db/migrate/20251011202425_add_base_scoring_parameter_to_dscf_credit_credit_line_specs.rb
450
+ - db/migrate/20251012100039_add_credit_line_divider_to_dscf_credit_credit_line_specs.rb
451
+ - db/migrate/20251012115159_remove_default_from_credit_line_multiplier_in_credit_line_specs.rb
449
452
  - db/seeds.rb
450
453
  - lib/dscf/credit.rb
451
454
  - lib/dscf/credit/engine.rb