dscf-credit 0.1.9 → 0.2.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 +4 -4
- data/app/controllers/dscf/credit/facilitator_applications_controller.rb +2 -2
- data/app/controllers/dscf/credit/facilitators_controller.rb +7 -3
- data/app/controllers/dscf/credit/scoring_parameters_controller.rb +1 -1
- data/app/controllers/dscf/credit/users_controller.rb +21 -8
- data/lib/dscf/credit/version.rb +1 -1
- metadata +2 -12
- data/app/controllers/concerns/dscf/core/authenticatable.rb +0 -81
- data/app/controllers/concerns/dscf/core/common.rb +0 -200
- data/app/controllers/concerns/dscf/core/copilot-instructions.md +0 -683
- data/app/controllers/concerns/dscf/core/filterable.rb +0 -12
- data/app/controllers/concerns/dscf/core/json_response.rb +0 -77
- data/app/controllers/concerns/dscf/core/pagination.rb +0 -71
- data/app/controllers/concerns/dscf/core/reviewable_controller.rb +0 -347
- data/app/controllers/concerns/dscf/core/token_authenticatable.rb +0 -53
- data/app/jobs/dscf/credit/generate_daily_accruals_job.rb +0 -78
- data/app/models/concerns/core/reviewable_model.rb +0 -31
@@ -1,77 +0,0 @@
|
|
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
|
@@ -1,71 +0,0 @@
|
|
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
|
@@ -1,347 +0,0 @@
|
|
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
|
@@ -1,53 +0,0 @@
|
|
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
|
@@ -1,78 +0,0 @@
|
|
1
|
-
module Dscf::Credit
|
2
|
-
# Background job to generate daily loan accruals
|
3
|
-
#
|
4
|
-
# This job should be scheduled to run daily (typically at midnight or early morning)
|
5
|
-
# to generate interest and penalty accruals for all active loans.
|
6
|
-
#
|
7
|
-
# @example Schedule with Sidekiq (config/sidekiq.yml)
|
8
|
-
# :schedule:
|
9
|
-
# generate_daily_accruals:
|
10
|
-
# cron: '0 1 * * *' # Run at 1 AM daily
|
11
|
-
# class: Dscf::Credit::GenerateDailyAccrualsJob
|
12
|
-
#
|
13
|
-
# @example Schedule with whenever gem (config/schedule.rb)
|
14
|
-
# every 1.day, at: '1:00 am' do
|
15
|
-
# runner "Dscf::Credit::GenerateDailyAccrualsJob.perform_later"
|
16
|
-
# end
|
17
|
-
#
|
18
|
-
# @example Manual execution
|
19
|
-
# Dscf::Credit::GenerateDailyAccrualsJob.perform_now
|
20
|
-
#
|
21
|
-
# @example With specific parameters
|
22
|
-
# Dscf::Credit::GenerateDailyAccrualsJob.perform_later(
|
23
|
-
# loan_ids: [1, 2, 3],
|
24
|
-
# accrual_date: Date.yesterday
|
25
|
-
# )
|
26
|
-
class GenerateDailyAccrualsJob < ApplicationJob
|
27
|
-
queue_as :default
|
28
|
-
|
29
|
-
# Perform the job to generate daily accruals
|
30
|
-
#
|
31
|
-
# @param loan_ids [Array<Integer>, nil] Optional specific loan IDs to process
|
32
|
-
# @param accrual_date [String, Date] Date for accrual generation (defaults to today)
|
33
|
-
# @param force_regenerate [Boolean] Whether to regenerate existing accruals
|
34
|
-
def perform(loan_ids: nil, accrual_date: nil, force_regenerate: false)
|
35
|
-
date = accrual_date ? Date.parse(accrual_date.to_s) : Date.current
|
36
|
-
|
37
|
-
service = LoanAccrualGeneratorService.new(
|
38
|
-
loan_ids: loan_ids,
|
39
|
-
accrual_date: date,
|
40
|
-
force_regenerate: force_regenerate
|
41
|
-
)
|
42
|
-
|
43
|
-
result = service.generate_daily_accruals
|
44
|
-
|
45
|
-
log_result(result)
|
46
|
-
|
47
|
-
result
|
48
|
-
end
|
49
|
-
|
50
|
-
private
|
51
|
-
|
52
|
-
# Log the result of accrual generation
|
53
|
-
#
|
54
|
-
# @param result [Hash] The result from the service
|
55
|
-
def log_result(result)
|
56
|
-
if result[:success]
|
57
|
-
Rails.logger.info(
|
58
|
-
"Loan Accrual Generation: #{result[:message]}"
|
59
|
-
)
|
60
|
-
|
61
|
-
if result[:errors].any?
|
62
|
-
Rails.logger.warn(
|
63
|
-
"Loan Accrual Generation Warnings: #{result[:errors].size} loans had errors"
|
64
|
-
)
|
65
|
-
result[:errors].each do |error|
|
66
|
-
Rails.logger.warn(
|
67
|
-
"Loan #{error[:loan_id]}: #{error[:error]}"
|
68
|
-
)
|
69
|
-
end
|
70
|
-
end
|
71
|
-
else
|
72
|
-
Rails.logger.error(
|
73
|
-
"Loan Accrual Generation Failed: #{result[:message]}"
|
74
|
-
)
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
@@ -1,31 +0,0 @@
|
|
1
|
-
module Dscf
|
2
|
-
module Core
|
3
|
-
module ReviewableModel
|
4
|
-
extend ActiveSupport::Concern
|
5
|
-
|
6
|
-
included do
|
7
|
-
has_many :reviews, as: :reviewable, class_name: "Dscf::Core::Review", dependent: :destroy
|
8
|
-
end
|
9
|
-
|
10
|
-
def review_for(context = :default)
|
11
|
-
reviews.with_context(context).order(created_at: :desc)
|
12
|
-
end
|
13
|
-
|
14
|
-
def current_review_for(context = :default)
|
15
|
-
review_for(context).first
|
16
|
-
end
|
17
|
-
|
18
|
-
def current_status_for(context = :default)
|
19
|
-
current_review = current_review_for(context)
|
20
|
-
return current_review.status if current_review
|
21
|
-
|
22
|
-
# Return default initial status for zero-config
|
23
|
-
"pending"
|
24
|
-
end
|
25
|
-
|
26
|
-
def build_review_for(context = :default)
|
27
|
-
reviews.build(context: context.to_s)
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|