dscf-credit 0.1.2 → 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/dscf/credit/categories_controller.rb +3 -3
- 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 +3 -3
- 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/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_parameters_controller.rb +3 -3
- data/app/controllers/dscf/credit/scoring_tables_controller.rb +8 -8
- data/app/models/dscf/credit/bank_branch.rb +2 -1
- data/app/models/dscf/credit/category.rb +3 -1
- data/app/models/dscf/credit/credit_line.rb +3 -3
- 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/loan_profile.rb +8 -5
- data/app/models/dscf/credit/loan_profile_scoring_spec.rb +4 -5
- data/app/models/dscf/credit/scoring_parameter.rb +2 -2
- data/app/models/dscf/credit/scoring_table.rb +4 -4
- data/app/serializers/dscf/credit/bank_branch_serializer.rb +1 -0
- data/app/serializers/dscf/credit/category_serializer.rb +2 -0
- data/app/serializers/dscf/credit/credit_line_serializer.rb +2 -0
- 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/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_parameter_serializer.rb +2 -0
- data/app/serializers/dscf/credit/scoring_table_serializer.rb +1 -1
- 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/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 +79 -0
- data/config/routes.rb +19 -5
- data/db/migrate/20250822091527_create_dscf_credit_credit_line_specs.rb +1 -0
- data/db/migrate/20250822092246_create_dscf_credit_loan_profiles.rb +2 -1
- data/db/migrate/20250822092417_create_dscf_credit_loan_profile_scoring_specs.rb +5 -8
- 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 +88 -22
- data/lib/dscf/credit/version.rb +1 -1
- data/spec/factories/dscf/credit/credit_line_specs.rb +1 -0
- data/spec/factories/dscf/credit/credit_lines.rb +3 -3
- data/spec/factories/dscf/credit/eligible_credit_lines.rb +33 -0
- data/spec/factories/dscf/credit/loan_profile_scoring_specs.rb +1 -4
- data/spec/factories/dscf/credit/loan_profiles.rb +1 -0
- data/spec/factories/dscf/credit/scoring_tables.rb +1 -1
- metadata +29 -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
|
@@ -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_tables, :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_tables, :scoring_parameters ],
|
28
28
|
create: [],
|
29
|
-
update: [ :credit_lines ]
|
29
|
+
update: [ :credit_lines, :scoring_tables, :scoring_parameters ]
|
30
30
|
}
|
31
31
|
end
|
32
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
|
@@ -40,6 +40,7 @@ module Dscf::Credit
|
|
40
40
|
:penalty_rate,
|
41
41
|
:facilitation_fee_rate,
|
42
42
|
:tax_rate,
|
43
|
+
:credit_line_multiplier,
|
43
44
|
:max_penalty_days,
|
44
45
|
:loan_duration,
|
45
46
|
:interest_frequency,
|
@@ -57,7 +58,7 @@ module Dscf::Credit
|
|
57
58
|
end
|
58
59
|
|
59
60
|
def allowed_order_columns
|
60
|
-
%w[id min_amount max_amount interest_rate penalty_rate facilitation_fee_rate tax_rate max_penalty_days loan_duration interest_frequency interest_income_tax vat max_interest_calculation_days penalty_frequency penalty_income_tax active created_at updated_at]
|
61
|
+
%w[id min_amount max_amount interest_rate penalty_rate facilitation_fee_rate tax_rate credit_line_multiplier max_penalty_days loan_duration interest_frequency interest_income_tax vat max_interest_calculation_days penalty_frequency penalty_income_tax active created_at updated_at]
|
61
62
|
end
|
62
63
|
|
63
64
|
def default_serializer_includes
|
@@ -30,7 +30,7 @@ module Dscf::Credit
|
|
30
30
|
end
|
31
31
|
|
32
32
|
def eager_loaded_associations
|
33
|
-
[ :bank, :category, :created_by, :reviewed_by, :credit_line_specs ]
|
33
|
+
[ :bank, :category, :created_by, :reviewed_by, :credit_line_specs, :loans, :eligible_credit_lines ]
|
34
34
|
end
|
35
35
|
|
36
36
|
def allowed_order_columns
|
@@ -40,9 +40,9 @@ module Dscf::Credit
|
|
40
40
|
def default_serializer_includes
|
41
41
|
{
|
42
42
|
index: [ :bank, :category ],
|
43
|
-
show: [ :bank, :category, :created_by, :reviewed_by, :credit_line_specs ],
|
43
|
+
show: [ :bank, :category, :created_by, :reviewed_by, :credit_line_specs, :loans, :eligible_credit_lines ],
|
44
44
|
create: [ :bank, :category, :created_by, :reviewed_by ],
|
45
|
-
update: [ :bank, :category, :created_by, :reviewed_by, :credit_line_specs ]
|
45
|
+
update: [ :bank, :category, :created_by, :reviewed_by, :credit_line_specs, :loans, :eligible_credit_lines ]
|
46
46
|
}
|
47
47
|
end
|
48
48
|
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Dscf::Credit
|
2
|
+
class DisbursementsController < ApplicationController
|
3
|
+
def create
|
4
|
+
credit_line = Dscf::Credit::CreditLine.find(params[:credit_line_id])
|
5
|
+
payment_request = Dscf::Credit::PaymentRequest.find(params[:payment_request_id])
|
6
|
+
|
7
|
+
service = Dscf::Credit::DisbursementService.new(credit_line, payment_request, current_user)
|
8
|
+
result = service.process_disbursement
|
9
|
+
|
10
|
+
if result[:success]
|
11
|
+
render_success(
|
12
|
+
"disbursement.success.create",
|
13
|
+
data: {
|
14
|
+
loan: result[:loan],
|
15
|
+
disbursement_details: result[:disbursement_details],
|
16
|
+
credit_line: credit_line,
|
17
|
+
payment_request: payment_request
|
18
|
+
},
|
19
|
+
serializer_options: {
|
20
|
+
include: [
|
21
|
+
:loan_profile,
|
22
|
+
:credit_line,
|
23
|
+
:payment_request,
|
24
|
+
:loan_transactions
|
25
|
+
]
|
26
|
+
}
|
27
|
+
)
|
28
|
+
else
|
29
|
+
render_error(
|
30
|
+
"disbursement.errors.create",
|
31
|
+
errors: [ result[:error] ],
|
32
|
+
status: :unprocessable_entity
|
33
|
+
)
|
34
|
+
end
|
35
|
+
rescue ActiveRecord::RecordNotFound => e
|
36
|
+
render_error(
|
37
|
+
"disbursement.errors.not_found",
|
38
|
+
errors: [ e.message ],
|
39
|
+
status: :not_found
|
40
|
+
)
|
41
|
+
rescue StandardError => e
|
42
|
+
render_error(
|
43
|
+
"disbursement.errors.create",
|
44
|
+
errors: [ e.message ],
|
45
|
+
status: :internal_server_error
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def model_params
|
52
|
+
params.permit(:credit_line_id, :payment_request_id)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|