dscf-credit 0.1.1 → 0.1.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 +4 -4
- data/app/controllers/concerns/dscf/core/authenticatable.rb +81 -0
- data/app/controllers/concerns/dscf/core/common.rb +200 -0
- data/app/controllers/concerns/dscf/core/filterable.rb +12 -0
- data/app/controllers/concerns/dscf/core/json_response.rb +77 -0
- data/app/controllers/concerns/dscf/core/pagination.rb +71 -0
- data/app/controllers/concerns/dscf/core/token_authenticatable.rb +53 -0
- data/app/controllers/concerns/dscf/credit/reviewable.rb +112 -0
- data/app/controllers/dscf/credit/categories_controller.rb +6 -5
- data/app/controllers/dscf/credit/credit_limit_calculations_controller.rb +50 -0
- data/app/controllers/dscf/credit/credit_line_specs_controller.rb +2 -1
- data/app/controllers/dscf/credit/credit_lines_controller.rb +11 -38
- data/app/controllers/dscf/credit/disbursements_controller.rb +55 -0
- data/app/controllers/dscf/credit/eligible_credit_lines_controller.rb +50 -0
- data/app/controllers/dscf/credit/facilitators_controller.rb +39 -150
- data/app/controllers/dscf/credit/loan_profiles_controller.rb +138 -0
- data/app/controllers/dscf/credit/payment_requests_controller.rb +54 -5
- data/app/controllers/dscf/credit/repayments_controller.rb +53 -0
- data/app/controllers/dscf/credit/scoring_param_types_controller.rb +31 -0
- data/app/controllers/dscf/credit/scoring_parameters_controller.rb +13 -8
- data/app/controllers/dscf/credit/scoring_tables_controller.rb +8 -8
- data/app/controllers/dscf/credit/system_configs_controller.rb +10 -7
- data/app/models/dscf/credit/bank_branch.rb +2 -1
- data/app/models/dscf/credit/category.rb +4 -2
- data/app/models/dscf/credit/credit_line.rb +9 -6
- data/app/models/dscf/credit/credit_line_spec.rb +3 -3
- data/app/models/dscf/credit/eligible_credit_line.rb +28 -0
- data/app/models/dscf/credit/facilitator.rb +5 -4
- data/app/models/dscf/credit/facilitator_performance.rb +1 -2
- data/app/models/dscf/credit/loan_profile.rb +8 -4
- data/app/models/dscf/credit/loan_profile_scoring_spec.rb +4 -6
- data/app/models/dscf/credit/scoring_param_type.rb +17 -0
- data/app/models/dscf/credit/scoring_parameter.rb +8 -7
- data/app/models/dscf/credit/scoring_table.rb +4 -4
- data/app/models/dscf/credit/system_config.rb +5 -4
- data/app/serializers/dscf/credit/bank_branch_serializer.rb +1 -0
- data/app/serializers/dscf/credit/category_serializer.rb +3 -1
- data/app/serializers/dscf/credit/credit_line_serializer.rb +4 -2
- data/app/serializers/dscf/credit/credit_line_spec_serializer.rb +1 -1
- data/app/serializers/dscf/credit/daily_routine_transaction_serializer.rb +8 -0
- data/app/serializers/dscf/credit/eligible_credit_line_serializer.rb +8 -0
- data/app/serializers/dscf/credit/facilitator_performance_serializer.rb +1 -1
- data/app/serializers/dscf/credit/facilitator_serializer.rb +2 -2
- data/app/serializers/dscf/credit/loan_profile_scoring_spec_serializer.rb +8 -0
- data/app/serializers/dscf/credit/loan_profile_serializer.rb +15 -0
- data/app/serializers/dscf/credit/loan_serializer.rb +12 -0
- data/app/serializers/dscf/credit/loan_transaction_serializer.rb +8 -0
- data/app/serializers/dscf/credit/payment_request_serializer.rb +10 -0
- data/app/serializers/dscf/credit/payment_serializer.rb +8 -0
- data/app/serializers/dscf/credit/scoring_param_type_serializer.rb +7 -0
- data/app/serializers/dscf/credit/scoring_parameter_serializer.rb +6 -3
- data/app/serializers/dscf/credit/scoring_table_serializer.rb +1 -1
- data/app/serializers/dscf/credit/system_config_serializer.rb +2 -2
- data/app/services/dscf/credit/credit_limit_calculation_service.rb +153 -0
- data/app/services/dscf/credit/disbursement_service.rb +180 -0
- data/app/services/dscf/credit/facilitator_approval_service.rb +4 -3
- data/app/services/dscf/credit/facilitator_creation_service.rb +157 -0
- data/app/services/dscf/credit/repayment_service.rb +216 -0
- data/app/services/dscf/credit/risk_application_service.rb +27 -0
- data/app/services/dscf/credit/scoring_service.rb +297 -0
- data/config/locales/en.yml +125 -8
- data/config/routes.rb +42 -11
- data/db/migrate/20250822091011_create_dscf_credit_categories.rb +2 -0
- data/db/migrate/20250822091131_create_dscf_credit_credit_lines.rb +7 -4
- data/db/migrate/20250822091527_create_dscf_credit_credit_line_specs.rb +1 -0
- data/db/migrate/20250822091820_create_dscf_credit_system_configs.rb +5 -2
- data/db/migrate/20250822092040_create_dscf_credit_scoring_param_types.rb +12 -0
- data/db/migrate/20250822092050_create_dscf_credit_scoring_parameters.rb +11 -6
- data/db/migrate/20250822092246_create_dscf_credit_loan_profiles.rb +6 -3
- data/db/migrate/20250822092417_create_dscf_credit_loan_profile_scoring_specs.rb +5 -7
- data/db/migrate/20250822092436_create_dscf_credit_facilitators.rb +5 -2
- data/db/migrate/20250822092528_create_dscf_credit_facilitator_performances.rb +0 -3
- data/db/migrate/20250901172842_create_dscf_credit_scoring_tables.rb +2 -2
- data/db/migrate/20250917120000_create_dscf_credit_eligible_credit_lines.rb +18 -0
- data/db/seeds.rb +134 -40
- data/lib/dscf/credit/version.rb +1 -1
- data/spec/factories/dscf/credit/categories.rb +1 -0
- data/spec/factories/dscf/credit/credit_line_specs.rb +1 -0
- data/spec/factories/dscf/credit/credit_lines.rb +9 -7
- data/spec/factories/dscf/credit/eligible_credit_lines.rb +33 -0
- data/spec/factories/dscf/credit/facilitator_performances.rb +0 -5
- data/spec/factories/dscf/credit/facilitators.rb +6 -1
- data/spec/factories/dscf/credit/loan_profile_scoring_specs.rb +1 -7
- data/spec/factories/dscf/credit/loan_profiles.rb +11 -6
- data/spec/factories/dscf/credit/scoring_param_types.rb +31 -0
- data/spec/factories/dscf/credit/scoring_parameters.rb +26 -4
- data/spec/factories/dscf/credit/scoring_tables.rb +1 -1
- data/spec/factories/dscf/credit/system_configs.rb +8 -2
- metadata +50 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b35f1bc98830d1d76e49d13013a97834b16869ec489051f568c46c4903cef9ec
|
4
|
+
data.tar.gz: 44a24524ca9e31bf9cdb5cff42f646797e0e0b37272af32a16e3360639bc4265
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 472712641c58af0d94c581c153a1123137224bcac23ad1f30fab921c8d327868de5a24cf5bac794720334b8a82a26b21ed047f52e933a55c1bccc6003965b992
|
7
|
+
data.tar.gz: 89205acba56392bae6b7da14ef7c096e22f685bd558544c421a25e6399642d3ada0e5eef182440c1acfe8f6ceff07e7f6d11ebe0dfac4497ecb7dcef05bd6416
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Dscf
|
2
|
+
module Core
|
3
|
+
module Authenticatable
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
before_action :authenticate_user, if: :authentication_required?
|
8
|
+
rescue_from AuthenticationError, with: :handle_authentication_error
|
9
|
+
end
|
10
|
+
|
11
|
+
def current_user
|
12
|
+
@current_user ||= authenticate_from_token
|
13
|
+
end
|
14
|
+
|
15
|
+
def authenticate_user!
|
16
|
+
raise AuthenticationError, "Authentication required" unless current_user
|
17
|
+
end
|
18
|
+
|
19
|
+
def authenticate_user
|
20
|
+
authenticate_user! if authentication_required?
|
21
|
+
end
|
22
|
+
|
23
|
+
def sign_in(user, request)
|
24
|
+
tokens = user.generate_auth_tokens(request)
|
25
|
+
@current_user = user
|
26
|
+
tokens
|
27
|
+
end
|
28
|
+
|
29
|
+
def sign_out
|
30
|
+
# Revoke the current refresh token if available
|
31
|
+
refresh_token_value = extract_refresh_token_from_params
|
32
|
+
AuthService.revoke_refresh_token(refresh_token_value) if refresh_token_value
|
33
|
+
@current_user = nil
|
34
|
+
end
|
35
|
+
|
36
|
+
def refresh_token
|
37
|
+
refresh_token_value = extract_refresh_token_from_params
|
38
|
+
return nil unless refresh_token_value
|
39
|
+
|
40
|
+
begin
|
41
|
+
AuthService.refresh_access_token(refresh_token_value, request)
|
42
|
+
rescue AuthenticationError
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def authenticate_from_token
|
50
|
+
access_token = extract_access_token_from_header
|
51
|
+
return nil unless access_token
|
52
|
+
|
53
|
+
payload = TokenService.decode(access_token)
|
54
|
+
return nil unless payload && payload["type"] == "access"
|
55
|
+
|
56
|
+
User.find_by(id: payload["user_id"])
|
57
|
+
rescue AuthenticationError
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
|
61
|
+
def extract_access_token_from_header
|
62
|
+
auth_header = request.headers["Authorization"]
|
63
|
+
return nil unless auth_header&.start_with?("Bearer ")
|
64
|
+
|
65
|
+
auth_header.split(" ").last
|
66
|
+
end
|
67
|
+
|
68
|
+
def extract_refresh_token_from_params
|
69
|
+
params[:refresh_token]
|
70
|
+
end
|
71
|
+
|
72
|
+
def authentication_required?
|
73
|
+
true # Override in specific controllers if needed
|
74
|
+
end
|
75
|
+
|
76
|
+
def handle_authentication_error(error)
|
77
|
+
render json: error.to_hash, status: error.status_code
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,200 @@
|
|
1
|
+
module Dscf
|
2
|
+
module Core
|
3
|
+
module Common
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
include Dscf::Core::Pagination
|
6
|
+
include Dscf::Core::JsonResponse
|
7
|
+
include Dscf::Core::Filterable
|
8
|
+
|
9
|
+
included do
|
10
|
+
before_action :set_clazz
|
11
|
+
before_action :set_object, only: %i[show update]
|
12
|
+
end
|
13
|
+
|
14
|
+
def index
|
15
|
+
data = nil
|
16
|
+
options = {}
|
17
|
+
if block_given?
|
18
|
+
incoming = yield
|
19
|
+
if incoming.instance_of?(Array)
|
20
|
+
data, options = incoming
|
21
|
+
elsif incoming.instance_of?(Hash)
|
22
|
+
options = incoming
|
23
|
+
else
|
24
|
+
data = incoming
|
25
|
+
end
|
26
|
+
else
|
27
|
+
data = @clazz.all
|
28
|
+
end
|
29
|
+
|
30
|
+
# Apply eager loading if defined
|
31
|
+
data = data.includes(eager_loaded_associations) if eager_loaded_associations.present?
|
32
|
+
|
33
|
+
# Apply Ransack filtering
|
34
|
+
data = filter_records(data)
|
35
|
+
|
36
|
+
# Get total count before pagination
|
37
|
+
total_count = data.count if params[:page]
|
38
|
+
|
39
|
+
data = data.then(&paginate) if params[:page]
|
40
|
+
|
41
|
+
# Add action specific serializer includes
|
42
|
+
includes = serializer_includes_for_action(:index)
|
43
|
+
options[:include] = includes if includes.present?
|
44
|
+
|
45
|
+
# Add pagination metadata if paginated
|
46
|
+
if params[:page]
|
47
|
+
total_pages = (total_count.to_f / per_page).ceil
|
48
|
+
count = data.is_a?(Array) ? data.length : data.count
|
49
|
+
|
50
|
+
options[:pagination] = {
|
51
|
+
current_page: page_no,
|
52
|
+
per_page: per_page,
|
53
|
+
count: count,
|
54
|
+
total_count: total_count,
|
55
|
+
total_pages: total_pages,
|
56
|
+
links: pagination_links(total_pages)
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
render_success(data: data, serializer_options: options)
|
61
|
+
end
|
62
|
+
|
63
|
+
def show
|
64
|
+
data = nil
|
65
|
+
options = {}
|
66
|
+
if block_given?
|
67
|
+
incoming = yield
|
68
|
+
if incoming.instance_of?(Array)
|
69
|
+
data, options = incoming
|
70
|
+
elsif incoming.instance_of?(Hash)
|
71
|
+
data = @obj
|
72
|
+
options = incoming
|
73
|
+
else
|
74
|
+
data = incoming
|
75
|
+
end
|
76
|
+
else
|
77
|
+
data = @obj
|
78
|
+
end
|
79
|
+
|
80
|
+
data = @clazz.includes(eager_loaded_associations).find(params[:id]) if data.is_a?(@clazz) && eager_loaded_associations.present?
|
81
|
+
|
82
|
+
includes = serializer_includes_for_action(:show)
|
83
|
+
options[:include] = includes if includes.present?
|
84
|
+
|
85
|
+
render_success(data: data, serializer_options: options)
|
86
|
+
end
|
87
|
+
|
88
|
+
def create
|
89
|
+
obj = nil
|
90
|
+
options = {}
|
91
|
+
if block_given?
|
92
|
+
incoming = yield
|
93
|
+
if incoming.instance_of?(Array)
|
94
|
+
obj, options = incoming
|
95
|
+
elsif incoming.instance_of?(Hash)
|
96
|
+
obj = @clazz.new(model_params)
|
97
|
+
options = incoming
|
98
|
+
else
|
99
|
+
obj = incoming
|
100
|
+
end
|
101
|
+
else
|
102
|
+
obj = @clazz.new(model_params)
|
103
|
+
end
|
104
|
+
|
105
|
+
if obj.save
|
106
|
+
obj = @clazz.includes(eager_loaded_associations).find(obj.id) if eager_loaded_associations.present?
|
107
|
+
|
108
|
+
includes = serializer_includes_for_action(:create)
|
109
|
+
options[:include] = includes if includes.present?
|
110
|
+
|
111
|
+
render_success(data: obj, serializer_options: options, status: :created)
|
112
|
+
else
|
113
|
+
render_error(errors: obj.errors.full_messages[0], status: :unprocessable_entity)
|
114
|
+
end
|
115
|
+
rescue StandardError => e
|
116
|
+
render_error(error: e.message)
|
117
|
+
end
|
118
|
+
|
119
|
+
def update
|
120
|
+
obj = nil
|
121
|
+
options = {}
|
122
|
+
if block_given?
|
123
|
+
incoming = yield
|
124
|
+
if incoming.instance_of?(Array)
|
125
|
+
obj, options = incoming
|
126
|
+
elsif incoming.instance_of?(Hash)
|
127
|
+
obj = set_object
|
128
|
+
options = incoming
|
129
|
+
else
|
130
|
+
obj = incoming
|
131
|
+
end
|
132
|
+
else
|
133
|
+
obj = set_object
|
134
|
+
end
|
135
|
+
|
136
|
+
if obj.update(model_params)
|
137
|
+
obj = @clazz.includes(eager_loaded_associations).find(obj.id) if eager_loaded_associations.present?
|
138
|
+
|
139
|
+
includes = serializer_includes_for_action(:update)
|
140
|
+
options[:include] = includes if includes.present?
|
141
|
+
|
142
|
+
render_success(data: obj, serializer_options: options)
|
143
|
+
else
|
144
|
+
render_error(errors: obj.errors.full_messages[0], status: :unprocessable_entity)
|
145
|
+
end
|
146
|
+
rescue StandardError => e
|
147
|
+
render_error(error: e.message)
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
|
152
|
+
def set_clazz
|
153
|
+
model_name = controller_name.classify
|
154
|
+
|
155
|
+
controller_namespace = self.class.name.deconstantize
|
156
|
+
engine_namespace = controller_namespace.split("::")[0..1].join("::")
|
157
|
+
|
158
|
+
@clazz = "#{engine_namespace}::#{model_name}".constantize
|
159
|
+
rescue NameError
|
160
|
+
@clazz = "Dscf::Core::#{model_name}".constantize
|
161
|
+
end
|
162
|
+
|
163
|
+
def set_object
|
164
|
+
@obj = if eager_loaded_associations.present?
|
165
|
+
@clazz.includes(eager_loaded_associations).find(params[:id])
|
166
|
+
else
|
167
|
+
@clazz.find(params[:id])
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# Override in controllers to define what to eager load
|
172
|
+
def eager_loaded_associations
|
173
|
+
[]
|
174
|
+
end
|
175
|
+
|
176
|
+
# Override in controllers to define allowed order columns for pagination
|
177
|
+
def allowed_order_columns
|
178
|
+
%w[id created_at updated_at]
|
179
|
+
end
|
180
|
+
|
181
|
+
# Override in controllers to define serializer includes
|
182
|
+
def default_serializer_includes
|
183
|
+
{}
|
184
|
+
end
|
185
|
+
|
186
|
+
def serializer_includes_for_action(action)
|
187
|
+
includes = default_serializer_includes
|
188
|
+
|
189
|
+
if includes.is_a?(Hash)
|
190
|
+
includes[action] || includes[:default] || []
|
191
|
+
else
|
192
|
+
includes.present? ? includes : []
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# This method should be overridden by respective child controllers
|
197
|
+
def model_params; end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Dscf
|
2
|
+
module Core
|
3
|
+
module JsonResponse
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def serialize(data, options = {})
|
7
|
+
case data
|
8
|
+
when ActiveRecord::Base, ActiveRecord::Relation
|
9
|
+
ActiveModelSerializers::SerializableResource.new(data, options)
|
10
|
+
when Hash
|
11
|
+
# Handle hash data with potential serializer options
|
12
|
+
serialized_hash = {}
|
13
|
+
data.each do |key, value|
|
14
|
+
if options[key] && value.is_a?(ActiveRecord::Base)
|
15
|
+
# Use specific serializer for this key if provided
|
16
|
+
serializer_opts = options[key].is_a?(Hash) ? options[key] : {}
|
17
|
+
serialized_hash[key] = ActiveModelSerializers::SerializableResource.new(value, serializer_opts)
|
18
|
+
else
|
19
|
+
serialized_hash[key] = serialize(value, options)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
serialized_hash
|
23
|
+
when Array
|
24
|
+
data.map { |value| serialize(value, options) }
|
25
|
+
else
|
26
|
+
data
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def render_success(message_key = nil, data: nil, status: :ok, serializer_options: {}, **options)
|
31
|
+
response = {success: true}
|
32
|
+
|
33
|
+
if message_key
|
34
|
+
response[:message] = I18n.t(message_key)
|
35
|
+
else
|
36
|
+
model_key = @clazz&.name&.demodulize&.underscore
|
37
|
+
action_key = action_name
|
38
|
+
i18n_key = "#{model_key}.success.#{action_key}"
|
39
|
+
|
40
|
+
response[:message] = I18n.t(i18n_key) if I18n.exists?(i18n_key)
|
41
|
+
end
|
42
|
+
|
43
|
+
if data
|
44
|
+
serialized_data = serialize(data, serializer_options)
|
45
|
+
response[:data] = serialized_data
|
46
|
+
end
|
47
|
+
|
48
|
+
# Add pagination metadata if present (handled by Common concern)
|
49
|
+
response[:pagination] = serializer_options[:pagination] if serializer_options[:pagination]
|
50
|
+
|
51
|
+
response.merge!(options)
|
52
|
+
render json: response, status: status
|
53
|
+
end
|
54
|
+
|
55
|
+
def render_error(message_key = nil, status: :unprocessable_entity, errors: nil, **options)
|
56
|
+
response = {
|
57
|
+
success: false
|
58
|
+
}
|
59
|
+
|
60
|
+
if message_key
|
61
|
+
response[:error] = I18n.t(message_key)
|
62
|
+
else
|
63
|
+
model_key = @clazz&.name&.demodulize&.underscore
|
64
|
+
action_key = action_name
|
65
|
+
i18n_key = "#{model_key}.errors.#{action_key}"
|
66
|
+
|
67
|
+
response[:error] = I18n.t(i18n_key) if I18n.exists?(i18n_key)
|
68
|
+
end
|
69
|
+
|
70
|
+
response[:errors] = errors if errors
|
71
|
+
response.merge!(options)
|
72
|
+
|
73
|
+
render json: response, status: status
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Dscf
|
2
|
+
module Core
|
3
|
+
module Pagination
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def default_per_page
|
7
|
+
10
|
8
|
+
end
|
9
|
+
|
10
|
+
def page_no
|
11
|
+
params[:page]&.to_i || 1
|
12
|
+
end
|
13
|
+
|
14
|
+
def per_page
|
15
|
+
params[:per_page]&.to_i || default_per_page
|
16
|
+
end
|
17
|
+
|
18
|
+
def paginate_offset
|
19
|
+
(page_no - 1) * per_page
|
20
|
+
end
|
21
|
+
|
22
|
+
def order_by
|
23
|
+
allowed_columns = if respond_to?(:allowed_order_columns, true)
|
24
|
+
allowed_order_columns
|
25
|
+
else
|
26
|
+
%w[id created_at updated_at]
|
27
|
+
end
|
28
|
+
requested_column = params.fetch(:order_by, "id").to_s
|
29
|
+
allowed_columns.include?(requested_column) ? requested_column : "id"
|
30
|
+
end
|
31
|
+
|
32
|
+
def order_direction
|
33
|
+
if %w[asc desc].include?(params.fetch(:order_direction, "asc").to_s.downcase)
|
34
|
+
params.fetch(:order_direction, "asc").to_s.downcase
|
35
|
+
else
|
36
|
+
"asc"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def paginate
|
41
|
+
->(it) { it.limit(per_page).offset(paginate_offset).order("#{order_by}": order_direction) }
|
42
|
+
end
|
43
|
+
|
44
|
+
# Generate HATEOAS pagination links
|
45
|
+
def pagination_links(total_pages)
|
46
|
+
base_path = request.path
|
47
|
+
current_params = request.query_parameters.except("page")
|
48
|
+
|
49
|
+
links = {}
|
50
|
+
|
51
|
+
links[:first] = build_page_url(base_path, current_params, 1)
|
52
|
+
|
53
|
+
links[:prev] = (build_page_url(base_path, current_params, page_no - 1) if page_no > 1)
|
54
|
+
|
55
|
+
links[:next] = (build_page_url(base_path, current_params, page_no + 1) if page_no < total_pages)
|
56
|
+
|
57
|
+
links[:last] = build_page_url(base_path, current_params, total_pages)
|
58
|
+
|
59
|
+
links
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def build_page_url(base_path, params, page)
|
65
|
+
params_with_page = params.merge(page: page, per_page: per_page)
|
66
|
+
query_string = params_with_page.to_query
|
67
|
+
"#{base_path}?#{query_string}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Dscf
|
2
|
+
module Core
|
3
|
+
module TokenAuthenticatable
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
before_action :validate_token_expiry
|
8
|
+
before_action :validate_device_consistency, if: :current_user
|
9
|
+
end
|
10
|
+
|
11
|
+
def validate_token_expiry
|
12
|
+
return unless current_user
|
13
|
+
|
14
|
+
access_token = extract_access_token_from_header
|
15
|
+
return unless access_token
|
16
|
+
|
17
|
+
payload = TokenService.decode(access_token)
|
18
|
+
return unless payload
|
19
|
+
|
20
|
+
# Check if token is close to expiry (within 5 minutes)
|
21
|
+
if payload["exp"] && payload["exp"] - Time.current.to_i < 300
|
22
|
+
Rails.logger.info("Access token close to expiry for user #{current_user.id}")
|
23
|
+
end
|
24
|
+
rescue AuthenticationError
|
25
|
+
handle_expired_token
|
26
|
+
end
|
27
|
+
|
28
|
+
def validate_device_consistency
|
29
|
+
return unless current_user && request.params[:device_id]
|
30
|
+
|
31
|
+
refresh_token_record = current_user.refresh_tokens.active.find_by(device: request.params[:device_id])
|
32
|
+
return if refresh_token_record
|
33
|
+
|
34
|
+
# Device mismatch - could indicate suspicious activity
|
35
|
+
Rails.logger.warn("Device mismatch for user #{current_user.id}: #{request.params[:device_id]}")
|
36
|
+
end
|
37
|
+
|
38
|
+
def require_valid_refresh_token
|
39
|
+
refresh_token_value = extract_refresh_token_from_params
|
40
|
+
return if refresh_token_value && RefreshToken.active.exists?(refresh_token: refresh_token_value)
|
41
|
+
|
42
|
+
raise AuthenticationError, "Valid refresh token required"
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def handle_expired_token
|
48
|
+
Rails.logger.warn("Expired token detected for request to #{request.path}")
|
49
|
+
raise AuthenticationError, "Session expired"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
module Dscf
|
2
|
+
module Credit
|
3
|
+
module Reviewable
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
before_action :set_reviewable_resource, only: %i[approve reject request_modification resubmit]
|
8
|
+
end
|
9
|
+
|
10
|
+
# PATCH /resource/:id/approve
|
11
|
+
def approve
|
12
|
+
if reviewable_resource.update(
|
13
|
+
status: :approved,
|
14
|
+
reviewed_by: current_reviewer,
|
15
|
+
review_date: Time.current
|
16
|
+
)
|
17
|
+
render_success(data: reviewable_resource)
|
18
|
+
else
|
19
|
+
render_error(errors: reviewable_resource.errors.full_messages)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# PATCH /resource/:id/request_modification
|
24
|
+
def request_modification
|
25
|
+
feedback = safe_feedback_param! or return
|
26
|
+
|
27
|
+
if reviewable_resource.update(
|
28
|
+
status: :modify,
|
29
|
+
review_feedback: feedback,
|
30
|
+
reviewed_by: current_reviewer,
|
31
|
+
review_date: Time.current
|
32
|
+
)
|
33
|
+
render_success(data: reviewable_resource)
|
34
|
+
else
|
35
|
+
render_error(errors: reviewable_resource.errors.full_messages)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# PATCH /resource/:id/reject
|
40
|
+
def reject
|
41
|
+
feedback = safe_feedback_param! or return
|
42
|
+
|
43
|
+
unless feedback.key?("message")
|
44
|
+
return render_error(errors: "Provide review_feedback with a 'message' key")
|
45
|
+
end
|
46
|
+
|
47
|
+
if reviewable_resource.update(
|
48
|
+
status: :rejected,
|
49
|
+
review_feedback: feedback,
|
50
|
+
reviewed_by: current_reviewer,
|
51
|
+
review_date: Time.current
|
52
|
+
)
|
53
|
+
render_success(data: reviewable_resource)
|
54
|
+
else
|
55
|
+
render_error(errors: reviewable_resource.errors.full_messages)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# PATCH /resource/:id/resubmit
|
60
|
+
def resubmit
|
61
|
+
unless reviewable_resource.status.to_s == "modify"
|
62
|
+
return render_error(errors: "Only items with status 'modify' can be resubmitted", status: :unprocessable_entity)
|
63
|
+
end
|
64
|
+
|
65
|
+
updates = model_params.merge(
|
66
|
+
status: :pending,
|
67
|
+
review_feedback: {},
|
68
|
+
)
|
69
|
+
|
70
|
+
if reviewable_resource.update(updates)
|
71
|
+
render_success(data: reviewable_resource)
|
72
|
+
else
|
73
|
+
render_error(errors: reviewable_resource.errors.full_messages)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def safe_feedback_param!
|
80
|
+
feedback = params[:review_feedback]
|
81
|
+
unless feedback.respond_to?(:to_unsafe_h)
|
82
|
+
render_error(errors: "review_feedback must be a JSON object") and return
|
83
|
+
end
|
84
|
+
feedback.to_unsafe_h
|
85
|
+
end
|
86
|
+
|
87
|
+
def model_class
|
88
|
+
model_name = controller_name.classify
|
89
|
+
|
90
|
+
controller_namespace = self.class.name.deconstantize
|
91
|
+
"#{controller_namespace}::#{model_name}".constantize
|
92
|
+
rescue NameError
|
93
|
+
model_name.constantize
|
94
|
+
end
|
95
|
+
|
96
|
+
def reviewable_resource
|
97
|
+
@reviewable_resource ||= model_class.find(params[:id])
|
98
|
+
end
|
99
|
+
|
100
|
+
# Override if current_user is not the reviewer
|
101
|
+
def current_reviewer
|
102
|
+
current_user
|
103
|
+
end
|
104
|
+
|
105
|
+
def set_reviewable_resource
|
106
|
+
reviewable_resource
|
107
|
+
rescue ActiveRecord::RecordNotFound => e
|
108
|
+
render_error("errors.record_not_found", status: :not_found, errors: [ e.message ])
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -8,24 +8,25 @@ module Dscf::Credit
|
|
8
8
|
params.require(:category).permit(
|
9
9
|
:type,
|
10
10
|
:name,
|
11
|
-
:description
|
11
|
+
:description,
|
12
|
+
:document_reference
|
12
13
|
)
|
13
14
|
end
|
14
15
|
|
15
16
|
def eager_loaded_associations
|
16
|
-
[ :credit_lines ]
|
17
|
+
[ :credit_lines, :scoring_tables, :scoring_parameters ]
|
17
18
|
end
|
18
19
|
|
19
20
|
def allowed_order_columns
|
20
|
-
%w[id type name description created_at updated_at]
|
21
|
+
%w[id type name description document_reference created_at updated_at]
|
21
22
|
end
|
22
23
|
|
23
24
|
def default_serializer_includes
|
24
25
|
{
|
25
26
|
index: [],
|
26
|
-
show: [ :credit_lines ],
|
27
|
+
show: [ :credit_lines, :scoring_tables, :scoring_parameters ],
|
27
28
|
create: [],
|
28
|
-
update: [ :credit_lines ]
|
29
|
+
update: [ :credit_lines, :scoring_tables, :scoring_parameters ]
|
29
30
|
}
|
30
31
|
end
|
31
32
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Dscf::Credit
|
2
|
+
class CreditLimitCalculationsController < ApplicationController
|
3
|
+
def create
|
4
|
+
loan_profile = Dscf::Credit::LoanProfile.find(params[:loan_profile_id])
|
5
|
+
category = Dscf::Credit::Category.find(params[:category_id])
|
6
|
+
|
7
|
+
service = Dscf::Credit::CreditLimitCalculationService.new(loan_profile, category)
|
8
|
+
result = service.calculate_credit_limits
|
9
|
+
|
10
|
+
if result[:success]
|
11
|
+
render_success(
|
12
|
+
"credit_limit_calculation.success.create",
|
13
|
+
data: {
|
14
|
+
loan_profile: loan_profile,
|
15
|
+
category: category,
|
16
|
+
eligible_credit_lines: result[:data],
|
17
|
+
calculation_summary: {
|
18
|
+
total_credit_lines_processed: result[:count],
|
19
|
+
calculated_at: Time.current
|
20
|
+
}
|
21
|
+
},
|
22
|
+
serializer_options: {
|
23
|
+
include: [
|
24
|
+
:eligible_credit_lines,
|
25
|
+
{ eligible_credit_lines: [ :credit_line ] }
|
26
|
+
]
|
27
|
+
}
|
28
|
+
)
|
29
|
+
else
|
30
|
+
render_error(
|
31
|
+
"credit_limit_calculation.errors.create",
|
32
|
+
errors: [ result[:error] ],
|
33
|
+
status: :unprocessable_entity
|
34
|
+
)
|
35
|
+
end
|
36
|
+
rescue ActiveRecord::RecordNotFound => e
|
37
|
+
render_error(
|
38
|
+
"credit_limit_calculation.errors.not_found",
|
39
|
+
errors: [ e.message ],
|
40
|
+
status: :not_found
|
41
|
+
)
|
42
|
+
rescue StandardError => e
|
43
|
+
render_error(
|
44
|
+
"credit_limit_calculation.errors.create",
|
45
|
+
errors: [ e.message ],
|
46
|
+
status: :internal_server_error
|
47
|
+
)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|