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.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/dscf/core/authenticatable.rb +81 -0
  3. data/app/controllers/concerns/dscf/core/common.rb +200 -0
  4. data/app/controllers/concerns/dscf/core/filterable.rb +12 -0
  5. data/app/controllers/concerns/dscf/core/json_response.rb +77 -0
  6. data/app/controllers/concerns/dscf/core/pagination.rb +71 -0
  7. data/app/controllers/concerns/dscf/core/token_authenticatable.rb +53 -0
  8. data/app/controllers/concerns/dscf/credit/reviewable.rb +112 -0
  9. data/app/controllers/dscf/credit/categories_controller.rb +6 -5
  10. data/app/controllers/dscf/credit/credit_limit_calculations_controller.rb +50 -0
  11. data/app/controllers/dscf/credit/credit_line_specs_controller.rb +2 -1
  12. data/app/controllers/dscf/credit/credit_lines_controller.rb +11 -38
  13. data/app/controllers/dscf/credit/disbursements_controller.rb +55 -0
  14. data/app/controllers/dscf/credit/eligible_credit_lines_controller.rb +50 -0
  15. data/app/controllers/dscf/credit/facilitators_controller.rb +39 -150
  16. data/app/controllers/dscf/credit/loan_profiles_controller.rb +138 -0
  17. data/app/controllers/dscf/credit/payment_requests_controller.rb +54 -5
  18. data/app/controllers/dscf/credit/repayments_controller.rb +53 -0
  19. data/app/controllers/dscf/credit/scoring_param_types_controller.rb +31 -0
  20. data/app/controllers/dscf/credit/scoring_parameters_controller.rb +13 -8
  21. data/app/controllers/dscf/credit/scoring_tables_controller.rb +8 -8
  22. data/app/controllers/dscf/credit/system_configs_controller.rb +10 -7
  23. data/app/models/dscf/credit/bank_branch.rb +2 -1
  24. data/app/models/dscf/credit/category.rb +4 -2
  25. data/app/models/dscf/credit/credit_line.rb +9 -6
  26. data/app/models/dscf/credit/credit_line_spec.rb +3 -3
  27. data/app/models/dscf/credit/eligible_credit_line.rb +28 -0
  28. data/app/models/dscf/credit/facilitator.rb +5 -4
  29. data/app/models/dscf/credit/facilitator_performance.rb +1 -2
  30. data/app/models/dscf/credit/loan_profile.rb +8 -4
  31. data/app/models/dscf/credit/loan_profile_scoring_spec.rb +4 -6
  32. data/app/models/dscf/credit/scoring_param_type.rb +17 -0
  33. data/app/models/dscf/credit/scoring_parameter.rb +8 -7
  34. data/app/models/dscf/credit/scoring_table.rb +4 -4
  35. data/app/models/dscf/credit/system_config.rb +5 -4
  36. data/app/serializers/dscf/credit/bank_branch_serializer.rb +1 -0
  37. data/app/serializers/dscf/credit/category_serializer.rb +3 -1
  38. data/app/serializers/dscf/credit/credit_line_serializer.rb +4 -2
  39. data/app/serializers/dscf/credit/credit_line_spec_serializer.rb +1 -1
  40. data/app/serializers/dscf/credit/daily_routine_transaction_serializer.rb +8 -0
  41. data/app/serializers/dscf/credit/eligible_credit_line_serializer.rb +8 -0
  42. data/app/serializers/dscf/credit/facilitator_performance_serializer.rb +1 -1
  43. data/app/serializers/dscf/credit/facilitator_serializer.rb +2 -2
  44. data/app/serializers/dscf/credit/loan_profile_scoring_spec_serializer.rb +8 -0
  45. data/app/serializers/dscf/credit/loan_profile_serializer.rb +15 -0
  46. data/app/serializers/dscf/credit/loan_serializer.rb +12 -0
  47. data/app/serializers/dscf/credit/loan_transaction_serializer.rb +8 -0
  48. data/app/serializers/dscf/credit/payment_request_serializer.rb +10 -0
  49. data/app/serializers/dscf/credit/payment_serializer.rb +8 -0
  50. data/app/serializers/dscf/credit/scoring_param_type_serializer.rb +7 -0
  51. data/app/serializers/dscf/credit/scoring_parameter_serializer.rb +6 -3
  52. data/app/serializers/dscf/credit/scoring_table_serializer.rb +1 -1
  53. data/app/serializers/dscf/credit/system_config_serializer.rb +2 -2
  54. data/app/services/dscf/credit/credit_limit_calculation_service.rb +153 -0
  55. data/app/services/dscf/credit/disbursement_service.rb +180 -0
  56. data/app/services/dscf/credit/facilitator_approval_service.rb +4 -3
  57. data/app/services/dscf/credit/facilitator_creation_service.rb +157 -0
  58. data/app/services/dscf/credit/repayment_service.rb +216 -0
  59. data/app/services/dscf/credit/risk_application_service.rb +27 -0
  60. data/app/services/dscf/credit/scoring_service.rb +297 -0
  61. data/config/locales/en.yml +125 -8
  62. data/config/routes.rb +42 -11
  63. data/db/migrate/20250822091011_create_dscf_credit_categories.rb +2 -0
  64. data/db/migrate/20250822091131_create_dscf_credit_credit_lines.rb +7 -4
  65. data/db/migrate/20250822091527_create_dscf_credit_credit_line_specs.rb +1 -0
  66. data/db/migrate/20250822091820_create_dscf_credit_system_configs.rb +5 -2
  67. data/db/migrate/20250822092040_create_dscf_credit_scoring_param_types.rb +12 -0
  68. data/db/migrate/20250822092050_create_dscf_credit_scoring_parameters.rb +11 -6
  69. data/db/migrate/20250822092246_create_dscf_credit_loan_profiles.rb +6 -3
  70. data/db/migrate/20250822092417_create_dscf_credit_loan_profile_scoring_specs.rb +5 -7
  71. data/db/migrate/20250822092436_create_dscf_credit_facilitators.rb +5 -2
  72. data/db/migrate/20250822092528_create_dscf_credit_facilitator_performances.rb +0 -3
  73. data/db/migrate/20250901172842_create_dscf_credit_scoring_tables.rb +2 -2
  74. data/db/migrate/20250917120000_create_dscf_credit_eligible_credit_lines.rb +18 -0
  75. data/db/seeds.rb +134 -40
  76. data/lib/dscf/credit/version.rb +1 -1
  77. data/spec/factories/dscf/credit/categories.rb +1 -0
  78. data/spec/factories/dscf/credit/credit_line_specs.rb +1 -0
  79. data/spec/factories/dscf/credit/credit_lines.rb +9 -7
  80. data/spec/factories/dscf/credit/eligible_credit_lines.rb +33 -0
  81. data/spec/factories/dscf/credit/facilitator_performances.rb +0 -5
  82. data/spec/factories/dscf/credit/facilitators.rb +6 -1
  83. data/spec/factories/dscf/credit/loan_profile_scoring_specs.rb +1 -7
  84. data/spec/factories/dscf/credit/loan_profiles.rb +11 -6
  85. data/spec/factories/dscf/credit/scoring_param_types.rb +31 -0
  86. data/spec/factories/dscf/credit/scoring_parameters.rb +26 -4
  87. data/spec/factories/dscf/credit/scoring_tables.rb +1 -1
  88. data/spec/factories/dscf/credit/system_configs.rb +8 -2
  89. metadata +50 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 48da97a9ea6344abf871d97bda581670dbe6091760ff9a0eab34d1816d548236
4
- data.tar.gz: 1eee4be4d65744fde0baa13e865a82dc1482789a6c6cdabbd16deab05c2f6683
3
+ metadata.gz: b35f1bc98830d1d76e49d13013a97834b16869ec489051f568c46c4903cef9ec
4
+ data.tar.gz: 44a24524ca9e31bf9cdb5cff42f646797e0e0b37272af32a16e3360639bc4265
5
5
  SHA512:
6
- metadata.gz: 5b5ecf3fb9ccd373f8ba718348f52d5b06a5c033e0b7a706ef8cc2eb60e35b7a43d5a315e5bb3b9cf7896198ff935cdce46ef6ac2b65f0d6eb6891c90a5a4a42
7
- data.tar.gz: 66377466c007cb2880434c37fc25db5da940c644e1d2c8c5131e39e7e451fb5e73d1a38e2fa44b383b4692bfc9913e4a8bfbc73dd9fa657dd7fa8b451a0536f2
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,12 @@
1
+ module Dscf
2
+ module Core
3
+ module Filterable
4
+ extend ActiveSupport::Concern
5
+
6
+ def filter_records(records)
7
+ @q = records.ransack(params[:q])
8
+ @q.result(distinct: true)
9
+ end
10
+ end
11
+ end
12
+ 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