dscf-credit 0.4.0 → 0.4.1

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: b308d3cfccda1cb39e00a905f357869e8cd539b1e47553e184f14cf92eb4e568
4
- data.tar.gz: 2b20dbde292d37abf3ab7081b8b40359602e2fae66795ce560e2e1aebdbecc89
3
+ metadata.gz: 0be229f7298a4e4f905fec15eae5ede2fcd755f6f56510f73f12b32e6d5b3453
4
+ data.tar.gz: 7de36c9ecca064d851e13b7ceb8ed7aea01cc7c6d8794fef6e203d5af63400a8
5
5
  SHA512:
6
- metadata.gz: 1dc0056daf90929f9442b09236f29893ae9f92bb5efda81b0a08c9a477f411e1b34dc63b8e776249ce37ddfc9d8258b30a9f714cb60ac9cf5265c5466bdab4a1
7
- data.tar.gz: a6f1380794da537e0f2c8c0dd467ba7896d4a5c2d0b81e11eb5fcddda676a8c8c0552031484e05ae42db9c77a2016e85250cfbae8a4d9c42097dc4ad07743fb8
6
+ metadata.gz: eb8742b82a81552545e811623eb176fca2a12f06fbf301aa90d060a64293ef2f77c5ad5f3e7db59a77b08e0659e81f0ee84956e0ba0d7b012cbc1cf1873de5cf
7
+ data.tar.gz: e8350b9ce353bf7c88992e00ca287fe7791be962449b10feefb4afbcc8b4af89157b5677957f2ec2cbccc64398fb4b58d40cf54a0896561ab58a33316ab87d54
@@ -4,9 +4,11 @@ module Dscf::Credit
4
4
  include Dscf::Core::ReviewableController
5
5
  include Dscf::Core::AuditableController
6
6
 
7
- auditable associated: :reviews,
8
- only: [ :status ],
9
- on: %i[approve reject request_modification resubmit]
7
+ auditable associated: [ :reviews ],
8
+ on: %i[approve submit reject request_modification resubmit]
9
+
10
+ auditable on: %i[create],
11
+ associated: { reviews: { only: [ :status ] } }
10
12
 
11
13
  def create
12
14
  super do
@@ -7,13 +7,14 @@ module Dscf::Credit
7
7
  before_action :set_object, only: %i[ show update activate deactivate]
8
8
 
9
9
  auditable associated: [ :reviews ],
10
- on: %i[approve reject request_modification resubmit]
10
+ on: %i[approve submit reject request_modification resubmit]
11
11
 
12
12
  auditable only: [ :active ],
13
13
  on: %i[activate deactivate]
14
14
 
15
15
  auditable on: %i[create],
16
16
  associated: { reviews: { only: [ :status ] } }
17
+
17
18
  def create
18
19
  super do
19
20
  scoring_parameter = @clazz.new(model_params)
@@ -1,5 +1,5 @@
1
1
  module Dscf
2
2
  module Credit
3
- VERSION = "0.4.0"
3
+ VERSION = "0.4.1"
4
4
  end
5
5
  end
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.4.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adoniyas
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-11-17 00:00:00.000000000 Z
10
+ date: 2025-12-24 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: dscf-core
@@ -393,8 +393,6 @@ files:
393
393
  - app/services/dscf/credit/credit_limit_calculation_service.rb
394
394
  - app/services/dscf/credit/credit_scoring_engine.rb
395
395
  - app/services/dscf/credit/disbursement_service.rb
396
- - app/services/dscf/credit/facilitator_additional_info_service.rb
397
- - app/services/dscf/credit/facilitator_approval_service.rb
398
396
  - app/services/dscf/credit/facilitator_creation_service.rb
399
397
  - app/services/dscf/credit/facility_limit_calculation_engine.rb
400
398
  - app/services/dscf/credit/loan_accrual_generator_service.rb
@@ -402,7 +400,6 @@ files:
402
400
  - app/services/dscf/credit/loan_transaction_creator_service.rb
403
401
  - app/services/dscf/credit/repayment_service.rb
404
402
  - app/services/dscf/credit/risk_application_service.rb
405
- - app/services/dscf/credit/scoring_service.rb
406
403
  - app/views/dscf/credit/bank_staff_welcome_mailer/welcome_email.html.erb
407
404
  - app/views/dscf/credit/bank_staff_welcome_mailer/welcome_email.text.erb
408
405
  - app/views/dscf/credit/facilitator_mailer/additional_info_received.html.erb
@@ -1,50 +0,0 @@
1
- module Dscf::Credit
2
- class FacilitatorAdditionalInfoService
3
- def submit_info(facilitator_id, additional_info)
4
- facilitator = Dscf::Credit::Facilitator.find(facilitator_id)
5
-
6
- performance = facilitator.facilitator_performances.current.first
7
-
8
- if performance&.input_data&.present? && performance.input_data["additional_info_submitted"]
9
- raise StandardError, "Additional information has already been submitted for this facilitator"
10
- end
11
-
12
- if performance
13
- updated_data = performance.input_data || {}
14
- updated_data.merge!(additional_info)
15
- updated_data["additional_info_submitted"] = true
16
- updated_data["submission_date"] = Time.current.iso8601
17
-
18
- performance.update!(
19
- input_data: updated_data,
20
- updated_at: Time.current
21
- )
22
- else
23
- input_data = additional_info.dup
24
- input_data["additional_info_submitted"] = true
25
- input_data["submission_date"] = Time.current.iso8601
26
-
27
- performance = facilitator.facilitator_performances.create!(
28
- total_outstanding_loans: 0,
29
- total_outstanding_amount: 0.0,
30
- approval_required: false,
31
- input_data: input_data,
32
- created_by_type: "System",
33
- created_by_id: nil
34
- )
35
- end
36
-
37
- begin
38
- FacilitatorMailer.additional_info_received(facilitator).deliver_now
39
- rescue => e
40
- Rails.logger.error "Failed to send additional info confirmation email: #{e.message}"
41
- end
42
-
43
- {
44
- facilitator: facilitator,
45
- performance: performance,
46
- message: "Additional information submitted successfully"
47
- }
48
- end
49
- end
50
- end
@@ -1,88 +0,0 @@
1
- module Dscf::Credit
2
- class FacilitatorApprovalService
3
- attr_reader :facilitator, :approver
4
-
5
- def initialize(facilitator, approver)
6
- @facilitator = facilitator
7
- @approver = approver
8
- end
9
-
10
- def approve
11
- ActiveRecord::Base.transaction do
12
- facilitator.update!(
13
- kyc_status: "approved",
14
- kyc_reviewed_by: approver
15
- )
16
-
17
- token = generate_additional_info_token
18
-
19
- begin
20
- FacilitatorMailer.approval_notification(facilitator, token).deliver_now
21
- rescue => e
22
- Rails.logger.error "Failed to send approval email: #{e.message}"
23
- end
24
-
25
- update_facilitator_performance
26
-
27
- facilitator
28
- end
29
- end
30
-
31
- def reject(review_feedback = nil)
32
- ActiveRecord::Base.transaction do
33
- facilitator.update!(
34
- kyc_status: "rejected",
35
- kyc_reviewed_by: approver,
36
- review_feedback: review_feedback
37
- )
38
-
39
- begin
40
- FacilitatorMailer.rejection_notification(facilitator, review_feedback).deliver_now
41
- rescue => e
42
- Rails.logger.error "Failed to send rejection email: #{e.message}"
43
- end
44
-
45
- facilitator
46
- end
47
- end
48
-
49
- def set_limit(total_limit)
50
- facilitator.update!(total_limit: total_limit)
51
-
52
- begin
53
- FacilitatorMailer.limit_update_notification(facilitator).deliver_now
54
- rescue => e
55
- Rails.logger.error "Failed to send limit update email: #{e.message}"
56
- end
57
-
58
- facilitator
59
- end
60
-
61
- private
62
-
63
- def generate_additional_info_token
64
- payload = {
65
- facilitator_id: facilitator.id,
66
- purpose: "additional_info",
67
- exp: 30.days.from_now.to_i
68
- }
69
-
70
- Rails.application.message_verifier("additional_info").generate(payload)
71
- end
72
-
73
- def update_facilitator_performance
74
- performance = facilitator.facilitator_performances.current.first
75
-
76
- if performance
77
- performance.update!(approval_required: false)
78
- else
79
- facilitator.facilitator_performances.create!(
80
- total_outstanding_loans: 0,
81
- total_outstanding_amount: 0.0,
82
- approval_required: false,
83
- created_by: approver
84
- )
85
- end
86
- end
87
- end
88
- end
@@ -1,297 +0,0 @@
1
- module Dscf::Credit
2
- class ScoringService
3
- attr_reader :loan_profile
4
-
5
- def initialize(loan_profile)
6
- @loan_profile = loan_profile
7
- end
8
-
9
- # Calculate credit score for a loan profile using the eligibility category scoring table
10
- # @param external_scoring_data [Hash] Optional external scoring input data
11
- # @return [Hash] Result containing score, total_limit, and success status
12
- def calculate_credit_score(external_scoring_data = nil)
13
- eligibility_category = find_eligibility_category
14
- return error_result("Eligibility category not found") unless eligibility_category
15
-
16
- scoring_tables = eligibility_category.scoring_tables.active.includes(:scoring_parameter)
17
- return error_result("No active scoring parameters found for eligibility category") if scoring_tables.empty?
18
-
19
- scoring_data = get_scoring_data(external_scoring_data)
20
- score = calculate_weighted_score(scoring_tables, scoring_data)
21
- facility_limit = calculate_facility_limit(score, scoring_data)
22
-
23
- success_result(score, facility_limit)
24
- rescue StandardError => e
25
- error_result("Score calculation failed: #{e.message}")
26
- end
27
-
28
- private
29
-
30
- def get_scoring_data(external_scoring_data)
31
- if external_scoring_data.present?
32
- external_scoring_data
33
- else
34
- extract_scoring_data_from_profile
35
- end
36
- end
37
-
38
- def extract_scoring_data_from_profile
39
- latest_spec = loan_profile.loan_profile_scoring_specs.order(created_at: :desc).first
40
-
41
- if latest_spec&.scoring_input_data.present? && !latest_spec.scoring_input_data.empty?
42
- default_data = prepare_scoring_data
43
- default_data.merge(latest_spec.scoring_input_data.symbolize_keys)
44
- else
45
- prepare_scoring_data
46
- end
47
- end
48
-
49
- def find_eligibility_category
50
- Dscf::Credit::Category.find_by(type: "eligibility", name: "default") ||
51
- Dscf::Credit::Category.find_by(type: "eligibility")
52
- end
53
-
54
- def prepare_scoring_data
55
- {
56
- average_daily_purchase: extract_average_daily_purchase,
57
- years_at_location: extract_years_at_location,
58
- marital_status: extract_marital_status,
59
- dependents_count: extract_dependents_count,
60
- education_level: extract_education_level,
61
- bill_payment_timing: extract_bill_payment_timing,
62
- other_accounts: extract_other_accounts,
63
- money_preference: extract_money_preference,
64
- business_license: extract_business_license,
65
- bank_statements: extract_bank_statements,
66
- transaction_frequency: extract_transaction_frequency,
67
- purchase_volume: extract_purchase_volume,
68
- cancellation_rate: extract_cancellation_rate,
69
- reliability_score: extract_reliability_score
70
- }
71
- end
72
-
73
- # Calculate weighted score based on scoring parameters
74
- def calculate_weighted_score(scoring_tables, scoring_data)
75
- total_score = 0.0
76
- total_weight = 0.0
77
-
78
- scoring_tables.each do |table|
79
- parameter = table.scoring_parameter
80
- raw_value = find_parameter_value(parameter, scoring_data)
81
-
82
- next if raw_value.nil?
83
-
84
- normalized_score = normalize_parameter_value(parameter, raw_value)
85
- weighted_score = normalized_score * table.weight
86
-
87
- total_score += weighted_score
88
- total_weight += table.weight
89
- end
90
-
91
- return 0.0 if total_weight.zero?
92
-
93
- (total_score / total_weight) * 100
94
- end
95
-
96
- # Find parameter value from scoring data using flexible key matching
97
- def find_parameter_value(parameter, scoring_data)
98
- param_name = parameter.name
99
-
100
- # Try exact match first
101
- if scoring_data.key?(param_name.to_sym)
102
- return scoring_data[param_name.to_sym]
103
- end
104
-
105
- if scoring_data.key?(param_name)
106
- return scoring_data[param_name]
107
- end
108
-
109
- # Try snake_case version
110
- snake_case_name = param_name.downcase.gsub(/\s+/, "_")
111
- if scoring_data.key?(snake_case_name.to_sym)
112
- return scoring_data[snake_case_name.to_sym]
113
- end
114
-
115
- if scoring_data.key?(snake_case_name)
116
- return scoring_data[snake_case_name]
117
- end
118
-
119
- # Try common mappings
120
- case param_name.downcase
121
- when "monthly income"
122
- scoring_data[:monthly_income] || scoring_data["monthly_income"]
123
- when "credit history score"
124
- scoring_data[:credit_history_score] || scoring_data["credit_history_score"]
125
- when "employment type"
126
- scoring_data[:employment_type] || scoring_data["employment_type"]
127
- else
128
- nil
129
- end
130
- end
131
-
132
- # Calculate facility limit based on score and business metrics
133
- def calculate_facility_limit(score, scoring_data)
134
- return 0 if score < 50 # Below 50% score = no facility
135
-
136
- average_daily_purchase = scoring_data[:average_daily_purchase] || 0
137
-
138
- credit_line_multiplier = get_default_credit_line_multiplier
139
-
140
- base_limit = average_daily_purchase * credit_line_multiplier * (score / 100.0)
141
-
142
- apply_limit_constraints(base_limit)
143
- end
144
-
145
- # Get default credit line multiplier from the bank's approved credit line specs
146
- def get_default_credit_line_multiplier
147
- return 30.0 unless loan_profile.bank
148
-
149
- credit_line_spec = loan_profile.bank.credit_lines.approved
150
- .joins(:credit_line_specs)
151
- .merge(Dscf::Credit::CreditLineSpec.active)
152
- .select("dscf_credit_credit_line_specs.credit_line_multiplier")
153
- .first&.credit_line_specs&.active&.first
154
-
155
- credit_line_spec&.credit_line_multiplier || 30.0
156
- end
157
-
158
- def apply_limit_constraints(base_limit)
159
- min_limit = get_bank_config("min_credit_limit", 0)
160
- max_limit = get_bank_config("max_credit_limit", 1000000)
161
-
162
- return 0 if base_limit < min_limit
163
- return max_limit if base_limit > max_limit
164
-
165
- base_limit
166
- end
167
-
168
- # Get bank configuration value from system configs
169
- def get_bank_config(config_key, default_value)
170
- return default_value unless loan_profile.bank
171
-
172
- config_definition = loan_profile.bank.system_config_definitions
173
- .find_by(config_key: config_key)
174
- return default_value unless config_definition
175
-
176
- system_config = Dscf::Credit::SystemConfig.approved
177
- .find_by(config_definition: config_definition)
178
-
179
- if system_config&.config_value
180
- numeric_value = system_config.config_value.to_f
181
- numeric_value > 0 ? numeric_value : default_value
182
- else
183
- default_value
184
- end
185
- end
186
-
187
- # Normalize parameter value to 0-1 scale
188
- def normalize_parameter_value(parameter, raw_value)
189
- case parameter.data_type
190
- when "boolean"
191
- raw_value ? 1.0 : 0.0
192
- when "integer", "decimal"
193
- normalize_numeric_value(parameter, raw_value.to_f)
194
- when "string"
195
- normalize_string_value(parameter, raw_value)
196
- else
197
- 0.5 # Default neutral score
198
- end
199
- end
200
-
201
- def normalize_numeric_value(parameter, value)
202
- min_val = parameter.min_value || 0
203
- max_val = parameter.max_value || 100
204
-
205
- return 0.0 if value < min_val
206
- return 1.0 if value > max_val
207
-
208
- (value - min_val) / (max_val - min_val)
209
- end
210
-
211
- def normalize_string_value(parameter, value)
212
- normalizer = parameter.parameter_normalizers.find { |n| n.raw_value == value }
213
- return normalizer.normalized_value if normalizer
214
-
215
- case value.downcase
216
- when "excellent", "high", "good" then 1.0
217
- when "fair", "medium", "average" then 0.7
218
- when "poor", "low", "bad" then 0.3
219
- else 0.5
220
- end
221
- end
222
-
223
- def extract_average_daily_purchase
224
- 5000.0 # Default average daily purchase
225
- end
226
-
227
- def extract_years_at_location
228
- 2 # Default years at current location
229
- end
230
-
231
- def extract_marital_status
232
- "single" # Default marital status
233
- end
234
-
235
- def extract_dependents_count
236
- 0 # Default number of dependents
237
- end
238
-
239
- def extract_education_level
240
- "bachelor" # Default education level
241
- end
242
-
243
- def extract_bill_payment_timing
244
- "on_time" # Default payment timing
245
- end
246
-
247
- def extract_other_accounts
248
- false # Default: no other active accounts
249
- end
250
-
251
- def extract_money_preference
252
- "save" # Default money preference
253
- end
254
-
255
- def extract_business_license
256
- false # Default: no business license
257
- end
258
-
259
- def extract_bank_statements
260
- true # Default: bank statements available
261
- end
262
-
263
- def extract_transaction_frequency
264
- 15 # Default monthly transaction frequency
265
- end
266
-
267
- def extract_purchase_volume
268
- 50000.0 # Default monthly purchase volume
269
- end
270
-
271
- def extract_cancellation_rate
272
- 0.05 # Default 5% cancellation rate
273
- end
274
-
275
- def extract_reliability_score
276
- 75.0 # Default reliability score (0-100)
277
- end
278
-
279
- def success_result(score, facility_limit)
280
- {
281
- success: true,
282
- score: score.round(2),
283
- facility_limit: facility_limit.round(2),
284
- message: "Credit score calculated successfully"
285
- }
286
- end
287
-
288
- def error_result(message)
289
- {
290
- success: false,
291
- score: 0,
292
- facility_limit: 0,
293
- error: message
294
- }
295
- end
296
- end
297
- end