dscf-credit 0.1.4 → 0.1.5
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/concerns/dscf/core/copilot-instructions.md +683 -0
- data/app/controllers/concerns/dscf/core/reviewable_controller.rb +347 -0
- data/app/controllers/dscf/credit/categories_controller.rb +3 -3
- data/app/controllers/dscf/credit/credit_lines_controller.rb +21 -13
- data/app/controllers/dscf/credit/eligible_credit_lines_controller.rb +50 -8
- data/app/controllers/dscf/credit/facilitator_applications_controller.rb +35 -0
- data/app/controllers/dscf/credit/facilitators_controller.rb +8 -96
- data/app/controllers/dscf/credit/loan_applications_controller.rb +252 -0
- data/app/controllers/dscf/credit/loan_profiles_controller.rb +61 -68
- data/app/controllers/dscf/credit/payment_requests_controller.rb +3 -5
- data/app/controllers/dscf/credit/scoring_parameters_controller.rb +59 -13
- data/app/controllers/dscf/credit/system_configs_controller.rb +30 -12
- data/app/models/concerns/core/reviewable_model.rb +31 -0
- data/app/models/dscf/credit/bank.rb +3 -3
- data/app/models/dscf/credit/bank_branch.rb +1 -1
- data/app/models/dscf/credit/category.rb +1 -2
- data/app/models/dscf/credit/credit_line.rb +4 -10
- data/app/models/dscf/credit/eligible_credit_line.rb +2 -2
- data/app/models/dscf/credit/facilitator.rb +6 -17
- data/app/models/dscf/credit/facilitator_application.rb +20 -0
- data/app/models/dscf/credit/loan_application.rb +30 -0
- data/app/models/dscf/credit/loan_profile.rb +10 -30
- data/app/models/dscf/credit/parameter_normalizer.rb +1 -1
- data/app/models/dscf/credit/scoring_parameter.rb +5 -7
- data/app/models/dscf/credit/system_config.rb +4 -9
- data/app/serializers/dscf/credit/category_serializer.rb +0 -1
- data/app/serializers/dscf/credit/credit_line_serializer.rb +2 -2
- data/app/serializers/dscf/credit/facilitator_application_serializer.rb +7 -0
- data/app/serializers/dscf/credit/facilitator_serializer.rb +3 -6
- data/app/serializers/dscf/credit/loan_application_serializer.rb +12 -0
- data/app/serializers/dscf/credit/loan_profile_serializer.rb +3 -6
- data/app/serializers/dscf/credit/scoring_parameter_serializer.rb +3 -4
- data/app/serializers/dscf/credit/system_config_serializer.rb +2 -2
- data/app/services/dscf/credit/credit_scoring_engine.rb +258 -0
- data/app/services/dscf/credit/facility_limit_calculation_engine.rb +159 -0
- data/app/services/dscf/credit/loan_profile_creation_service.rb +91 -0
- data/app/services/dscf/credit/risk_application_service.rb +61 -11
- data/config/locales/en.yml +63 -48
- data/config/routes.rb +31 -17
- data/db/migrate/20250822091131_create_dscf_credit_credit_lines.rb +1 -8
- data/db/migrate/20250822091820_create_dscf_credit_system_configs.rb +0 -7
- data/db/migrate/20250822092050_create_dscf_credit_scoring_parameters.rb +2 -6
- data/db/migrate/20250822092225_create_dscf_credit_parameter_normalizers.rb +1 -1
- data/db/migrate/20250822092236_create_dscf_credit_loan_applications.rb +20 -0
- data/db/migrate/20250822092246_create_dscf_credit_loan_profiles.rb +7 -19
- data/db/migrate/20250822092426_create_dscf_credit_facilitator_applications.rb +10 -0
- data/db/migrate/20250822092436_create_dscf_credit_facilitators.rb +1 -16
- data/db/seeds.rb +316 -203
- data/lib/dscf/credit/version.rb +1 -1
- data/spec/factories/dscf/credit/banks.rb +1 -1
- data/spec/factories/dscf/credit/credit_lines.rb +0 -23
- data/spec/factories/dscf/credit/facilitator_applications.rb +37 -0
- data/spec/factories/dscf/credit/facilitators.rb +8 -30
- data/spec/factories/dscf/credit/loan_applications.rb +42 -0
- data/spec/factories/dscf/credit/loan_profiles.rb +20 -34
- data/spec/factories/dscf/credit/parameter_normalizers.rb +4 -4
- data/spec/factories/dscf/credit/scoring_parameters.rb +14 -11
- data/spec/factories/dscf/credit/system_configs.rb +21 -5
- metadata +20 -10
- data/app/controllers/concerns/dscf/credit/reviewable.rb +0 -112
- data/app/controllers/dscf/credit/scoring_tables_controller.rb +0 -63
- data/app/models/dscf/credit/scoring_table.rb +0 -24
- data/app/serializers/dscf/credit/scoring_table_serializer.rb +0 -9
- data/db/migrate/20250901172842_create_dscf_credit_scoring_tables.rb +0 -18
- data/spec/factories/dscf/credit/scoring_tables.rb +0 -25
@@ -0,0 +1,347 @@
|
|
1
|
+
module Dscf
|
2
|
+
module Core
|
3
|
+
module ReviewableController
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
include Dscf::Core::JsonResponse
|
6
|
+
|
7
|
+
included do
|
8
|
+
class_attribute :review_context_configs, default: {}
|
9
|
+
class_attribute :reviewable_model_class, default: nil
|
10
|
+
|
11
|
+
# Initialize with default context config for zero-config support
|
12
|
+
self.review_context_configs = default_context_config.dup
|
13
|
+
|
14
|
+
# Auto-define default actions for zero-config
|
15
|
+
define_default_review_actions
|
16
|
+
|
17
|
+
before_action :set_reviewable_resource, if: :review_action?
|
18
|
+
before_action :authorize_review_action!, if: :review_action?
|
19
|
+
end
|
20
|
+
|
21
|
+
class_methods do
|
22
|
+
def reviewable_model(model_class)
|
23
|
+
self.reviewable_model_class = model_class
|
24
|
+
end
|
25
|
+
|
26
|
+
def reviewable_context(context_name, options = {})
|
27
|
+
statuses = options[:statuses]&.map(&:to_s) || %w[pending approved rejected modify]
|
28
|
+
initial_status = options[:initial_status]&.to_s || "pending"
|
29
|
+
|
30
|
+
# Convert linear transitions array to hash if provided
|
31
|
+
transitions = if options[:transitions].is_a?(Array)
|
32
|
+
validate_linear_transitions(options[:transitions], statuses, context_name)
|
33
|
+
build_linear_transitions(options[:transitions])
|
34
|
+
else
|
35
|
+
options[:transitions] || {}
|
36
|
+
end
|
37
|
+
|
38
|
+
defaults = {
|
39
|
+
statuses: statuses,
|
40
|
+
initial_status: initial_status,
|
41
|
+
transitions: transitions,
|
42
|
+
actions: options[:actions] || default_actions
|
43
|
+
}
|
44
|
+
|
45
|
+
validate_action_config(defaults[:actions], defaults[:statuses], context_name)
|
46
|
+
unless defaults[:statuses].include?(defaults[:initial_status])
|
47
|
+
raise ArgumentError,
|
48
|
+
"Initial status '#{defaults[:initial_status]}' in context '#{context_name}' must be one of: #{defaults[:statuses].join(', ')}"
|
49
|
+
end
|
50
|
+
|
51
|
+
# Add to existing configs instead of replacing them
|
52
|
+
self.review_context_configs = review_context_configs.merge(context_name.to_sym => defaults)
|
53
|
+
|
54
|
+
# Define methods for all actions in this context
|
55
|
+
defaults[:actions].each_key do |action_name|
|
56
|
+
# Only define if not already defined
|
57
|
+
define_review_action_method(action_name) unless method_defined?(action_name)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def define_default_review_actions
|
62
|
+
default_context_config.each do |context_name, config|
|
63
|
+
validate_action_config(config[:actions], config[:statuses], context_name)
|
64
|
+
config[:actions].each_key do |action_name|
|
65
|
+
define_review_action_method(action_name)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def define_review_action_method(action_name)
|
71
|
+
define_method(action_name) do
|
72
|
+
context = current_review_context
|
73
|
+
action_config = context_config[:actions][action_name.to_sym]
|
74
|
+
unless action_config
|
75
|
+
return render_error(
|
76
|
+
errors: [ "Action '#{action_name}' not defined for context '#{context}'" ],
|
77
|
+
status: :bad_request
|
78
|
+
)
|
79
|
+
end
|
80
|
+
perform_review_action(**action_config.slice(:status, :require_feedback, :after, :update_model))
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def default_actions
|
85
|
+
{
|
86
|
+
approve: { status: "approved" },
|
87
|
+
reject: { status: "rejected", require_feedback: true },
|
88
|
+
request_modification: { status: "modify", require_feedback: true },
|
89
|
+
resubmit: { status: "pending", update_model: true }
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
def default_context_config
|
94
|
+
{
|
95
|
+
default: {
|
96
|
+
statuses: %w[pending approved rejected modify],
|
97
|
+
initial_status: "pending",
|
98
|
+
transitions: { "pending" => %w[approved rejected modify], "modify" => [ "pending" ] },
|
99
|
+
actions: default_actions
|
100
|
+
}
|
101
|
+
}
|
102
|
+
end
|
103
|
+
|
104
|
+
def validate_action_config(actions, statuses, context_name)
|
105
|
+
actions.each do |action_name, opts|
|
106
|
+
unless statuses.include?(opts[:status])
|
107
|
+
raise ArgumentError,
|
108
|
+
"Action '#{action_name}' in context '#{context_name}' maps to invalid status '#{opts[:status]}'. Must be one of: #{statuses.join(', ')}"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def validate_linear_transitions(transitions, statuses, context_name)
|
114
|
+
transitions.each do |status|
|
115
|
+
unless statuses.include?(status.to_s)
|
116
|
+
raise ArgumentError, "Transition status '#{status}' in context '#{context_name}' must be one of: #{statuses.join(', ')}"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def build_linear_transitions(transitions)
|
122
|
+
transitions.each_with_object({}).with_index do |(status, hash), index|
|
123
|
+
next_status = transitions[index + 1]
|
124
|
+
hash[status.to_s] = next_status ? [ next_status.to_s ] : []
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def review_action_names
|
129
|
+
# Get actions from initialized contexts + default fallback
|
130
|
+
all_actions = review_context_configs.values.flat_map { |config| config[:actions].keys }
|
131
|
+
all_actions += default_context_config[:default][:actions].keys
|
132
|
+
all_actions.uniq
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
|
138
|
+
def review_action?
|
139
|
+
self.class.review_action_names.include?(action_name.to_sym)
|
140
|
+
end
|
141
|
+
|
142
|
+
def perform_review_action(status:, require_feedback: false, after: nil, update_model: false)
|
143
|
+
# Validate context exists before proceeding
|
144
|
+
begin
|
145
|
+
context_config
|
146
|
+
rescue ArgumentError => e
|
147
|
+
return render_error(errors: [ e.message ], status: :bad_request)
|
148
|
+
end
|
149
|
+
|
150
|
+
current_review = reviewable_resource.current_review_for(current_review_context)
|
151
|
+
|
152
|
+
feedback = if require_feedback
|
153
|
+
feedback_data = params[:review_feedback].presence || params.dig(:review_feedback)&.to_unsafe_h
|
154
|
+
return render_error(errors: [ "Feedback is required for #{status}" ]) if feedback_data.blank?
|
155
|
+
|
156
|
+
feedback_data
|
157
|
+
else
|
158
|
+
nil
|
159
|
+
end
|
160
|
+
|
161
|
+
unless valid_transition?(current_review&.status, status)
|
162
|
+
return render_error(errors: [ "Cannot transition from '#{current_review&.status || context_config[:initial_status]}' to '#{status}'" ])
|
163
|
+
end
|
164
|
+
|
165
|
+
begin
|
166
|
+
ActiveRecord::Base.transaction do
|
167
|
+
# Update the model if required (e.g., for resubmit with changes)
|
168
|
+
if update_model
|
169
|
+
begin
|
170
|
+
model_updates = model_params
|
171
|
+
if model_updates.present? && !reviewable_resource.update(model_updates)
|
172
|
+
return render_error(errors: reviewable_resource.errors.full_messages)
|
173
|
+
end
|
174
|
+
rescue ActionController::ParameterMissing => e
|
175
|
+
return render_error(errors: [ e.message ])
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# Update existing review or create new one if none exists
|
180
|
+
review = current_review || reviewable_resource.build_review_for(current_review_context)
|
181
|
+
review.assign_attributes(
|
182
|
+
status: status,
|
183
|
+
feedback: feedback,
|
184
|
+
reviewed_by: current_user,
|
185
|
+
reviewed_at: Time.current
|
186
|
+
)
|
187
|
+
|
188
|
+
before_review_action(review)
|
189
|
+
review.save!
|
190
|
+
execute_after_callbacks(review, after)
|
191
|
+
after_review_action(review)
|
192
|
+
end
|
193
|
+
render_success(data: reviewable_resource.reload)
|
194
|
+
rescue StandardError => e
|
195
|
+
render_error(errors: [ e.message ], status: :unprocessable_entity)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def execute_after_callbacks(review, after_callbacks = nil)
|
200
|
+
# Get action callbacks from the current action config or passed callbacks
|
201
|
+
action_config = context_config[:actions][action_name.to_sym]
|
202
|
+
action_callbacks = after_callbacks || action_config&.[](:after)
|
203
|
+
return unless action_callbacks
|
204
|
+
|
205
|
+
Array(action_callbacks).each do |callback|
|
206
|
+
if callback.is_a?(Symbol)
|
207
|
+
send(callback, review)
|
208
|
+
elsif callback.respond_to?(:call)
|
209
|
+
callback.call(review)
|
210
|
+
else
|
211
|
+
raise "Invalid callback: #{callback.inspect}"
|
212
|
+
end
|
213
|
+
rescue StandardError => e
|
214
|
+
Rails.logger.error "Callback failed: #{callback.inspect}, error: #{e.message}"
|
215
|
+
raise
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def current_review_context
|
220
|
+
params[:context]&.to_sym || :default
|
221
|
+
end
|
222
|
+
|
223
|
+
def available_contexts
|
224
|
+
# Get all configured contexts plus default if not explicitly configured
|
225
|
+
contexts = self.class.review_context_configs.keys
|
226
|
+
contexts << :default unless contexts.include?(:default)
|
227
|
+
contexts
|
228
|
+
end
|
229
|
+
|
230
|
+
def context_config
|
231
|
+
context = current_review_context
|
232
|
+
config = self.class.review_context_configs[context]
|
233
|
+
|
234
|
+
# If requesting default context and it's not configured, use the built-in default
|
235
|
+
config = self.class.default_context_config[:default] if context == :default && config.nil?
|
236
|
+
|
237
|
+
# If context is not found and it's not the default, it's invalid
|
238
|
+
raise ArgumentError, "Invalid context '#{context}'. Available contexts: #{available_contexts.join(', ')}" if config.nil?
|
239
|
+
|
240
|
+
Rails.logger.debug "Context config for '#{context}': #{config.inspect}"
|
241
|
+
config
|
242
|
+
end
|
243
|
+
|
244
|
+
def allowed_statuses
|
245
|
+
context_config[:statuses]
|
246
|
+
end
|
247
|
+
|
248
|
+
def valid_transition?(from, to)
|
249
|
+
return true if context_config[:transitions].blank?
|
250
|
+
|
251
|
+
allowed_to = context_config[:transitions][from || context_config[:initial_status]] || []
|
252
|
+
allowed_to.include?(to)
|
253
|
+
end
|
254
|
+
|
255
|
+
def authorize_review_action!
|
256
|
+
# Override for custom auth
|
257
|
+
end
|
258
|
+
|
259
|
+
def before_review_action(review)
|
260
|
+
# Default implementation - can be overridden
|
261
|
+
# Check if there's a custom hook method defined
|
262
|
+
return unless respond_to?(:before_reviewable_action, true)
|
263
|
+
|
264
|
+
before_reviewable_action(review, reviewable_resource, current_review_context)
|
265
|
+
|
266
|
+
# Custom pre-save logic override point
|
267
|
+
end
|
268
|
+
|
269
|
+
def after_review_action(review)
|
270
|
+
# New hook for post-action logic
|
271
|
+
return unless respond_to?(:after_reviewable_action, true)
|
272
|
+
|
273
|
+
after_reviewable_action(review, reviewable_resource, current_review_context)
|
274
|
+
end
|
275
|
+
|
276
|
+
def current_user
|
277
|
+
# Default implementation that can be overridden
|
278
|
+
# Check if there's a custom user method defined
|
279
|
+
if respond_to?(:current_reviewer, true)
|
280
|
+
current_reviewer
|
281
|
+
else
|
282
|
+
# Default fallback - can be overridden in controllers
|
283
|
+
# Try common patterns or return nil if none exist
|
284
|
+
return super if defined?(super)
|
285
|
+
return @current_user if defined?(@current_user)
|
286
|
+
return session[:user_id] if session && session[:user_id]
|
287
|
+
|
288
|
+
nil
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
def model_params
|
293
|
+
# Default implementation that can be overridden
|
294
|
+
# Try different common parameter patterns
|
295
|
+
resource_name = controller_name.singularize
|
296
|
+
|
297
|
+
# First try the standard Rails pattern: resource_name_params
|
298
|
+
params_method = "#{resource_name}_params"
|
299
|
+
return send(params_method) if respond_to?(params_method, true)
|
300
|
+
|
301
|
+
# Fallback to generic reviewable_params
|
302
|
+
return reviewable_params if respond_to?(:reviewable_params, true)
|
303
|
+
|
304
|
+
# If neither exists, return empty hash (no model updates)
|
305
|
+
{}
|
306
|
+
rescue ActionController::ParameterMissing => e
|
307
|
+
raise e # Re-raise parameter missing errors
|
308
|
+
rescue StandardError => e
|
309
|
+
Rails.logger.warn "Failed to get model params: #{e.message}"
|
310
|
+
{}
|
311
|
+
end
|
312
|
+
|
313
|
+
def model_class
|
314
|
+
return reviewable_model_class if reviewable_model_class
|
315
|
+
|
316
|
+
model_name = controller_name.classify
|
317
|
+
|
318
|
+
controller_namespace = self.class.name.deconstantize
|
319
|
+
if controller_namespace.present?
|
320
|
+
namespaced_model = "#{controller_namespace}::#{model_name}"
|
321
|
+
return namespaced_model.constantize if Object.const_defined?(namespaced_model)
|
322
|
+
end
|
323
|
+
|
324
|
+
return model_name.constantize if Object.const_defined?(model_name)
|
325
|
+
|
326
|
+
raise NameError, "Could not determine model class for #{self.class.name}. " \
|
327
|
+
"Please configure it explicitly using 'reviewable_model ModelClass' " \
|
328
|
+
"in your controller class."
|
329
|
+
rescue NameError => e
|
330
|
+
Rails.logger.error "Model resolution failed for #{self.class.name}: #{e.message}"
|
331
|
+
raise
|
332
|
+
end
|
333
|
+
|
334
|
+
def set_reviewable_resource
|
335
|
+
@reviewable_resource = model_class.find(params[:id])
|
336
|
+
rescue ActiveRecord::RecordNotFound => e
|
337
|
+
render_error(status: :not_found, errors: [ e.message ])
|
338
|
+
rescue NameError => e
|
339
|
+
render_error(status: :internal_server_error, errors: [ "Configuration error: #{e.message}" ])
|
340
|
+
end
|
341
|
+
|
342
|
+
def reviewable_resource
|
343
|
+
@reviewable_resource
|
344
|
+
end
|
345
|
+
end
|
346
|
+
end
|
347
|
+
end
|
@@ -14,7 +14,7 @@ module Dscf::Credit
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def eager_loaded_associations
|
17
|
-
[ :credit_lines, :
|
17
|
+
[ :credit_lines, :scoring_parameters ]
|
18
18
|
end
|
19
19
|
|
20
20
|
def allowed_order_columns
|
@@ -24,9 +24,9 @@ module Dscf::Credit
|
|
24
24
|
def default_serializer_includes
|
25
25
|
{
|
26
26
|
index: [],
|
27
|
-
show: [ :credit_lines, :
|
27
|
+
show: [ :credit_lines, :scoring_parameters ],
|
28
28
|
create: [],
|
29
|
-
update: [ :credit_lines, :
|
29
|
+
update: [ :credit_lines, :scoring_parameters ]
|
30
30
|
}
|
31
31
|
end
|
32
32
|
end
|
@@ -1,12 +1,16 @@
|
|
1
1
|
module Dscf::Credit
|
2
2
|
class CreditLinesController < ApplicationController
|
3
3
|
include Dscf::Core::Common
|
4
|
-
include Dscf::
|
4
|
+
include Dscf::Core::ReviewableController
|
5
5
|
|
6
6
|
def create
|
7
7
|
super do
|
8
8
|
credit_line = @clazz.new(model_params)
|
9
9
|
credit_line.created_by = current_user
|
10
|
+
credit_line.reviews.build(
|
11
|
+
status: "pending",
|
12
|
+
context: "default",
|
13
|
+
)
|
10
14
|
credit_line
|
11
15
|
end
|
12
16
|
end
|
@@ -20,29 +24,33 @@ module Dscf::Credit
|
|
20
24
|
:name,
|
21
25
|
:code,
|
22
26
|
:description,
|
23
|
-
:
|
24
|
-
:reviewed_by_type,
|
25
|
-
:reviewed_by_id,
|
26
|
-
:review_date,
|
27
|
-
:document_reference,
|
28
|
-
:review_feedback
|
27
|
+
:document_reference
|
29
28
|
)
|
30
29
|
end
|
31
30
|
|
32
31
|
def eager_loaded_associations
|
33
|
-
[
|
32
|
+
[
|
33
|
+
:bank, :category, :created_by, :credit_line_specs, :loans, :eligible_credit_lines,
|
34
|
+
reviews: { reviewed_by: :user_profile }
|
35
|
+
]
|
34
36
|
end
|
35
37
|
|
36
38
|
def allowed_order_columns
|
37
|
-
%w[id name code
|
39
|
+
%w[id name code created_at updated_at category_id]
|
38
40
|
end
|
39
41
|
|
40
42
|
def default_serializer_includes
|
41
43
|
{
|
42
|
-
index: [ :bank, :category ],
|
43
|
-
show: [
|
44
|
-
|
45
|
-
|
44
|
+
index: [ :bank, :category, reviews: { reviewed_by: :user_profile } ],
|
45
|
+
show: [
|
46
|
+
:bank, :category, :created_by, :credit_line_specs, :loans, :eligible_credit_lines,
|
47
|
+
reviews: { reviewed_by: :user_profile }
|
48
|
+
],
|
49
|
+
create: [ :bank, :category, :created_by, :reviews ],
|
50
|
+
update: [
|
51
|
+
:bank, :category, :created_by, :credit_line_specs, :loans, :eligible_credit_lines,
|
52
|
+
reviews: { reviewed_by: :user_profile }
|
53
|
+
]
|
46
54
|
}
|
47
55
|
end
|
48
56
|
end
|
@@ -2,31 +2,73 @@ module Dscf::Credit
|
|
2
2
|
class EligibleCreditLinesController < ApplicationController
|
3
3
|
include Dscf::Core::Common
|
4
4
|
|
5
|
-
before_action :set_object, only: [ :show, :apply_risk ]
|
6
|
-
|
7
5
|
def apply_risk
|
8
|
-
|
9
|
-
|
6
|
+
eligible_credit_line = @clazz.find(params[:id])
|
7
|
+
risk_factor = risk_params[:risk_factor]
|
8
|
+
|
9
|
+
if risk_factor.nil? || risk_factor == "" || (risk_factor.is_a?(Hash) && risk_factor.empty?)
|
10
|
+
return render_error(
|
11
|
+
"eligible_credit_line.errors.apply_risk",
|
12
|
+
errors: [ "Risk factor is required" ],
|
13
|
+
status: :unprocessable_entity
|
14
|
+
)
|
10
15
|
end
|
11
16
|
|
12
|
-
|
17
|
+
risk_factor_float = risk_factor.to_f
|
18
|
+
service = RiskApplicationService.new(eligible_credit_line, risk_factor_float)
|
13
19
|
result = service.apply_risk
|
14
20
|
|
15
21
|
if result[:success]
|
16
|
-
render_success(
|
22
|
+
render_success(
|
23
|
+
"eligible_credit_line.success.apply_risk",
|
24
|
+
data: result[:data],
|
25
|
+
serializer_options: { include: [ :loan_profile, :credit_line ] }
|
26
|
+
)
|
27
|
+
else
|
28
|
+
render_error(
|
29
|
+
"eligible_credit_line.errors.apply_risk",
|
30
|
+
errors: result[:errors],
|
31
|
+
status: :unprocessable_entity
|
32
|
+
)
|
33
|
+
end
|
34
|
+
rescue ActiveRecord::RecordNotFound
|
35
|
+
render_error(
|
36
|
+
"eligible_credit_line.errors.not_found",
|
37
|
+
errors: [ "Eligible credit line not found" ],
|
38
|
+
status: :not_found
|
39
|
+
)
|
40
|
+
rescue => e
|
41
|
+
Rails.logger.error("Risk application error: #{e.class} - #{e.message}")
|
42
|
+
Rails.logger.error(e.backtrace.join("\n"))
|
43
|
+
|
44
|
+
if Rails.env.development? || Rails.env.test?
|
45
|
+
render_error(
|
46
|
+
"eligible_credit_line.errors.apply_risk",
|
47
|
+
errors: [ e.message ],
|
48
|
+
status: :unprocessable_entity
|
49
|
+
)
|
17
50
|
else
|
18
|
-
render_error(
|
51
|
+
render_error(
|
52
|
+
"eligible_credit_line.errors.apply_risk",
|
53
|
+
errors: [ "An error occurred while applying risk factor" ],
|
54
|
+
status: :unprocessable_entity
|
55
|
+
)
|
19
56
|
end
|
20
57
|
end
|
21
58
|
|
22
59
|
private
|
23
60
|
|
61
|
+
def risk_params
|
62
|
+
params.permit(:risk_factor)
|
63
|
+
end
|
64
|
+
|
24
65
|
def model_params
|
25
66
|
params.require(:eligible_credit_line).permit(
|
26
67
|
:loan_profile_id,
|
27
68
|
:credit_line_id,
|
28
69
|
:credit_limit,
|
29
|
-
:available_limit
|
70
|
+
:available_limit,
|
71
|
+
:risk
|
30
72
|
)
|
31
73
|
end
|
32
74
|
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Dscf::Credit
|
2
|
+
class FacilitatorApplicationsController < ApplicationController
|
3
|
+
include Dscf::Core::Common
|
4
|
+
include Dscf::Core::ReviewableController
|
5
|
+
|
6
|
+
def create
|
7
|
+
super
|
8
|
+
facilitator_application = @clazz.new(model_params)
|
9
|
+
facilitator_application.user = current_user
|
10
|
+
facilitator_application.reviews.build(context: "default", status: "pending")
|
11
|
+
|
12
|
+
facilitator_application
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def model_params
|
18
|
+
params.require(:facilitator_application).permit(:user_id, :bank_id, facilitator_info: {})
|
19
|
+
end
|
20
|
+
|
21
|
+
def eager_loaded_associations
|
22
|
+
[ :user, :bank, reviews: { reviewed_by: :user_profile } ]
|
23
|
+
end
|
24
|
+
|
25
|
+
def allowed_order_columns
|
26
|
+
%w[id created_at updated_at]
|
27
|
+
end
|
28
|
+
|
29
|
+
def default_serializer_includes
|
30
|
+
{
|
31
|
+
default: [ :user, :bank, reviews: { reviewed_by: :user_profile } ]
|
32
|
+
}
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -1,116 +1,28 @@
|
|
1
1
|
module Dscf::Credit
|
2
2
|
class FacilitatorsController < ApplicationController
|
3
3
|
include Dscf::Core::Common
|
4
|
-
|
5
|
-
before_action :set_facilitator_for_approval, only: [ :approve, :reject, :set_limit ]
|
6
|
-
|
7
|
-
def create
|
8
|
-
super do
|
9
|
-
begin
|
10
|
-
FacilitatorCreationService.new(current_user, @clazz).create_single(model_params)
|
11
|
-
rescue StandardError => e
|
12
|
-
return render_error(errors: e.message)
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
def approve
|
18
|
-
begin
|
19
|
-
FacilitatorApprovalService.new(@facilitator, current_user).approve
|
20
|
-
render_success(data: @facilitator)
|
21
|
-
rescue StandardError => e
|
22
|
-
render_error(errors: e.message)
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
def reject
|
27
|
-
begin
|
28
|
-
FacilitatorApprovalService.new(@facilitator, current_user).reject(params[:review_feedback])
|
29
|
-
render_success(data: @facilitator)
|
30
|
-
rescue StandardError => e
|
31
|
-
render_error(errors: e.message)
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
def set_limit
|
36
|
-
begin
|
37
|
-
FacilitatorApprovalService.new(@facilitator, current_user).set_limit(params[:total_limit])
|
38
|
-
render_success(data: @facilitator)
|
39
|
-
rescue StandardError => e
|
40
|
-
render_error(errors: e.message)
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
def submit_additional_info
|
45
|
-
facilitator_id = params[:id] || params[:facilitator_id]
|
46
|
-
additional_info = params[:additional_info]&.permit!.to_h
|
47
|
-
|
48
|
-
begin
|
49
|
-
result = FacilitatorAdditionalInfoService.new.submit_info(facilitator_id, additional_info)
|
50
|
-
render_success(data: result)
|
51
|
-
rescue StandardError => e
|
52
|
-
render_error(errors: e.message)
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
def batch_create
|
57
|
-
facilitators_params = params[:facilitators]
|
58
|
-
|
59
|
-
begin
|
60
|
-
service = FacilitatorCreationService.new(current_user, @clazz)
|
61
|
-
results = service.create_batch(facilitators_params)
|
62
|
-
|
63
|
-
if results[:failure_count] == 0
|
64
|
-
render_success("facilitator.success.batch_created_all", data: results)
|
65
|
-
elsif results[:success_count] == 0
|
66
|
-
render_error("facilitator.errors.batch_failed_all", errors: results)
|
67
|
-
else
|
68
|
-
render json: {
|
69
|
-
success: true,
|
70
|
-
message: I18n.t("facilitator.success.batch_created_partial"),
|
71
|
-
data: results
|
72
|
-
}, status: :multi_status
|
73
|
-
end
|
74
|
-
rescue StandardError => e
|
75
|
-
render_error("facilitator.errors.batch_creation_failed", errors: e.message)
|
76
|
-
end
|
77
|
-
end
|
4
|
+
include Dscf::Core::ReviewableController
|
78
5
|
|
79
6
|
private
|
80
7
|
|
81
8
|
def model_params
|
82
|
-
params.require(:facilitator).permit(
|
83
|
-
:user_id,
|
84
|
-
:bank_id,
|
85
|
-
:kyc_status,
|
86
|
-
:total_limit,
|
87
|
-
:kyc_review_date,
|
88
|
-
:additional_info,
|
89
|
-
:review_feedback
|
90
|
-
)
|
91
|
-
end
|
92
|
-
|
93
|
-
def set_facilitator_for_approval
|
94
|
-
@facilitator = @clazz.find(params[:id])
|
9
|
+
params.require(:facilitator).permit(:facilitator_application_id, :total_limit)
|
95
10
|
end
|
96
11
|
|
97
12
|
def eager_loaded_associations
|
98
|
-
[ :
|
13
|
+
[ :facilitator_application, reviews: { reviewed_by: :user_profile } ]
|
99
14
|
end
|
100
15
|
|
101
16
|
def allowed_order_columns
|
102
|
-
%w[id
|
17
|
+
%w[id total_limit created_at updated_at]
|
103
18
|
end
|
104
19
|
|
105
20
|
def default_serializer_includes
|
106
21
|
{
|
107
|
-
index: [ :
|
108
|
-
show: [ :
|
109
|
-
create: [ :
|
110
|
-
update: [ :
|
111
|
-
approve: [ :user, :bank, :kyc_reviewed_by, :facilitator_performances ],
|
112
|
-
reject: [ :user, :bank, :kyc_reviewed_by ],
|
113
|
-
set_limit: [ :user, :bank, :kyc_reviewed_by ]
|
22
|
+
index: [ :facilitator_application, reviews: { reviewed_by: :user_profile } ],
|
23
|
+
show: [ :facilitator_application, reviews: { reviewed_by: :user_profile } ],
|
24
|
+
create: [ :reviews ],
|
25
|
+
update: [ :facilitator_application, reviews: { reviewed_by: :user_profile } ]
|
114
26
|
}
|
115
27
|
end
|
116
28
|
end
|