dscf-core 0.2.4 → 0.2.6
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/auditable_controller.rb +539 -0
- data/app/controllers/concerns/dscf/core/common.rb +5 -0
- data/app/controllers/concerns/dscf/core/reviewable_controller.rb +26 -4
- data/app/controllers/dscf/core/businesses_controller.rb +94 -0
- data/app/jobs/dscf/core/audit_logger_job.rb +256 -0
- data/app/models/concerns/dscf/core/auditable_model.rb +34 -0
- data/app/models/concerns/dscf/core/reviewable_model.rb +34 -1
- data/app/models/dscf/core/audit_log.rb +26 -0
- data/app/models/dscf/core/business.rb +4 -0
- data/app/models/dscf/core/document.rb +6 -5
- data/app/serializers/dscf/core/audit_log_serializer.rb +40 -0
- data/app/serializers/dscf/core/business_serializer.rb +1 -0
- data/app/serializers/dscf/core/document_serializer.rb +17 -0
- data/config/locales/en.yml +24 -0
- data/config/routes.rb +13 -0
- data/db/migrate/20250926102025_create_dscf_core_reviews.rb +1 -1
- data/db/migrate/20251023000000_create_dscf_core_audit_logs.rb +25 -0
- data/lib/dscf/core/version.rb +1 -1
- data/spec/factories/auditable_test_model.rb +11 -0
- data/spec/factories/dscf/core/audit_logs.rb +12 -0
- data/spec/factories/dscf/core/documents.rb +1 -1
- metadata +12 -2
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
module Dscf
|
|
2
|
+
module Core
|
|
3
|
+
class BusinessesController < ApplicationController
|
|
4
|
+
include Dscf::Core::Common
|
|
5
|
+
include Dscf::Core::ReviewableController
|
|
6
|
+
|
|
7
|
+
def create
|
|
8
|
+
super do
|
|
9
|
+
business = @clazz.new(model_params)
|
|
10
|
+
business.user = current_user
|
|
11
|
+
business.reviews.build(
|
|
12
|
+
status: "draft",
|
|
13
|
+
context: "default"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
if business.save && params[:business][:business_license].present?
|
|
17
|
+
document = business.documents.create!(
|
|
18
|
+
document_type: :business_license
|
|
19
|
+
)
|
|
20
|
+
document.files.attach(params[:business][:business_license])
|
|
21
|
+
document.save!
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
business
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def update
|
|
29
|
+
unless @obj.editable?
|
|
30
|
+
return render_error(
|
|
31
|
+
errors: ["Cannot update Business after submission. Use modification request workflow instead."],
|
|
32
|
+
status: :unprocessable_entity
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
super
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def my_business
|
|
40
|
+
index do
|
|
41
|
+
current_user.businesses
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
# Enable authentication for this controller
|
|
48
|
+
def authentication_required?
|
|
49
|
+
true
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def model_params
|
|
53
|
+
params.require(:business).permit(
|
|
54
|
+
:name,
|
|
55
|
+
:description,
|
|
56
|
+
:contact_email,
|
|
57
|
+
:contact_phone,
|
|
58
|
+
:tin_number,
|
|
59
|
+
:business_type_id,
|
|
60
|
+
business_license: :file
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def eager_loaded_associations
|
|
65
|
+
[
|
|
66
|
+
:business_type, :user, :documents,
|
|
67
|
+
reviews: {reviewed_by: :user_profile}
|
|
68
|
+
]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def allowed_order_columns
|
|
72
|
+
%w[id name description contact_email contact_phone tin_number created_at updated_at]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def default_serializer_includes
|
|
76
|
+
{
|
|
77
|
+
index: [
|
|
78
|
+
:business_type, :user, :documents,
|
|
79
|
+
reviews: {reviewed_by: :user_profile}
|
|
80
|
+
],
|
|
81
|
+
show: [
|
|
82
|
+
:business_type, :user, :documents,
|
|
83
|
+
reviews: {reviewed_by: :user_profile}
|
|
84
|
+
],
|
|
85
|
+
create: %i[business_type user documents reviews],
|
|
86
|
+
update: [
|
|
87
|
+
:business_type, :user, :documents,
|
|
88
|
+
reviews: {reviewed_by: :user_profile}
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
module Dscf
|
|
2
|
+
module Core
|
|
3
|
+
class AuditLoggerJob < ApplicationJob
|
|
4
|
+
queue_as :audit
|
|
5
|
+
retry_on StandardError, wait: 5.seconds, attempts: 3
|
|
6
|
+
discard_on ActiveRecord::RecordNotFound
|
|
7
|
+
|
|
8
|
+
def perform(record_id:, record_type:, before_main:, after_main:, before_associated:, after_associated:,
|
|
9
|
+
action_name:, controller_name:, actor_id: nil, actor_type: nil,
|
|
10
|
+
request_uuid: nil, ip_address: nil, user_agent: nil, configs: [])
|
|
11
|
+
# Find the auditable record
|
|
12
|
+
record = record_type.constantize.find_by(id: record_id)
|
|
13
|
+
return unless record
|
|
14
|
+
|
|
15
|
+
# Find the actor if present
|
|
16
|
+
actor = actor_id && actor_type ? actor_type.constantize.find_by(id: actor_id) : nil
|
|
17
|
+
|
|
18
|
+
# Calculate changes
|
|
19
|
+
all_changes = calculate_changes(
|
|
20
|
+
before_main: before_main,
|
|
21
|
+
after_main: after_main,
|
|
22
|
+
before_associated: before_associated,
|
|
23
|
+
after_associated: after_associated,
|
|
24
|
+
configs: configs
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Skip if no changes detected
|
|
28
|
+
# Check for empty structured format
|
|
29
|
+
return if all_changes.blank? ||
|
|
30
|
+
(all_changes.is_a?(Hash) &&
|
|
31
|
+
all_changes[:main].blank? &&
|
|
32
|
+
all_changes[:associated].blank?)
|
|
33
|
+
|
|
34
|
+
# Resolve action label
|
|
35
|
+
action_label = resolve_action_label(action_name, configs)
|
|
36
|
+
|
|
37
|
+
# Create audit log
|
|
38
|
+
record.record_audit!(
|
|
39
|
+
action_label,
|
|
40
|
+
audited_changes: all_changes,
|
|
41
|
+
metadata: {
|
|
42
|
+
controller: controller_name,
|
|
43
|
+
action: action_name,
|
|
44
|
+
request_uuid: request_uuid
|
|
45
|
+
},
|
|
46
|
+
actor: actor,
|
|
47
|
+
request_uuid: request_uuid,
|
|
48
|
+
ip_address: ip_address,
|
|
49
|
+
user_agent: user_agent
|
|
50
|
+
)
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
Rails.logger.error "AuditLoggerJob failed: #{e.message}\n#{e.backtrace.first(5).join("\n")}"
|
|
53
|
+
raise
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
# Calculate all changes by comparing before/after snapshots
|
|
59
|
+
# Returns structured format: { main: { changes: {...} }, associated: { assoc_name: [...] } }
|
|
60
|
+
def calculate_changes(before_main:, after_main:, before_associated:, after_associated:, configs:)
|
|
61
|
+
structured_changes = {
|
|
62
|
+
main: {},
|
|
63
|
+
associated: {}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# Process each config
|
|
67
|
+
configs.each do |config|
|
|
68
|
+
# 1. Main record changes
|
|
69
|
+
main_changes = diff_records(before_main, after_main)
|
|
70
|
+
if main_changes.present?
|
|
71
|
+
filtered = filter_changes(main_changes, config[:only], config[:except])
|
|
72
|
+
if filtered.present?
|
|
73
|
+
structured_changes[:main][:changes] ||= {}
|
|
74
|
+
structured_changes[:main][:changes].merge!(filtered)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# 2. Associated record changes
|
|
79
|
+
config[:associated].each do |assoc_name|
|
|
80
|
+
assoc_key = assoc_name.to_sym
|
|
81
|
+
before_assoc = before_associated[assoc_key] || []
|
|
82
|
+
after_assoc = after_associated[assoc_key] || []
|
|
83
|
+
|
|
84
|
+
assoc_changes = diff_associated_records(before_assoc, after_assoc, assoc_name)
|
|
85
|
+
next unless assoc_changes.present?
|
|
86
|
+
|
|
87
|
+
# Apply filtering to associated changes
|
|
88
|
+
filtered_assoc_changes = filter_associated_changes(assoc_changes, config[:only], config[:except])
|
|
89
|
+
next unless filtered_assoc_changes.present?
|
|
90
|
+
|
|
91
|
+
# Structure: { "reviews.123" => { action: "created", changes: {...} } }
|
|
92
|
+
# Convert to: { reviews: [{ id: 123, action: "created", model: "Review", changes: {...} }] }
|
|
93
|
+
structured_changes[:associated][assoc_name.to_s] ||= []
|
|
94
|
+
|
|
95
|
+
filtered_assoc_changes.each do |key, value|
|
|
96
|
+
# Extract record ID from key like "reviews.123"
|
|
97
|
+
record_id = key.to_s.split(".").last
|
|
98
|
+
model_name = after_assoc.find { |r| r[:id].to_s == record_id }&.dig(:type) ||
|
|
99
|
+
before_assoc.find { |r| r[:id].to_s == record_id }&.dig(:type) ||
|
|
100
|
+
assoc_name.to_s.singularize.camelize
|
|
101
|
+
|
|
102
|
+
structured_changes[:associated][assoc_name.to_s] << {
|
|
103
|
+
id: record_id.to_i,
|
|
104
|
+
action: value[:action] || value["action"],
|
|
105
|
+
model: model_name,
|
|
106
|
+
changes: value[:changes] || value["changes"] || {}
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Return flat hash if no changes (for compatibility)
|
|
113
|
+
return {} if structured_changes[:main].empty? && structured_changes[:associated].empty?
|
|
114
|
+
|
|
115
|
+
structured_changes
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Diff two record states (before/after)
|
|
119
|
+
def diff_records(before, after)
|
|
120
|
+
return {} if after.blank?
|
|
121
|
+
|
|
122
|
+
# For create actions, before is nil - treat all attributes as new
|
|
123
|
+
if before.blank?
|
|
124
|
+
changes = {}
|
|
125
|
+
after_attrs = after[:attributes] || {}
|
|
126
|
+
after_attrs.each do |key, new_val|
|
|
127
|
+
# Skip timestamps and IDs for create
|
|
128
|
+
next if %w[id created_at updated_at].include?(key.to_s)
|
|
129
|
+
# Skip nil valbues
|
|
130
|
+
next if new_val.nil?
|
|
131
|
+
|
|
132
|
+
changes[key] = {old: nil, new: new_val}
|
|
133
|
+
end
|
|
134
|
+
return changes
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
return {} if before[:id] != after[:id]
|
|
138
|
+
|
|
139
|
+
changes = {}
|
|
140
|
+
before_attrs = before[:attributes] || {}
|
|
141
|
+
after_attrs = after[:attributes] || {}
|
|
142
|
+
|
|
143
|
+
# Find changed attributes
|
|
144
|
+
all_keys = (before_attrs.keys + after_attrs.keys).uniq
|
|
145
|
+
all_keys.each do |key|
|
|
146
|
+
old_val = before_attrs[key]
|
|
147
|
+
new_val = after_attrs[key]
|
|
148
|
+
|
|
149
|
+
# Skip if unchanged
|
|
150
|
+
next if old_val == new_val
|
|
151
|
+
|
|
152
|
+
changes[key] = {old: old_val, new: new_val}
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
changes
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Diff associated records (handle create/update/delete)
|
|
159
|
+
def diff_associated_records(before_list, after_list, assoc_name)
|
|
160
|
+
changes = {}
|
|
161
|
+
|
|
162
|
+
# Build ID maps
|
|
163
|
+
before_map = before_list.each_with_object({}) { |rec, h| h[rec[:id]] = rec if rec[:id] }
|
|
164
|
+
after_map = after_list.each_with_object({}) { |rec, h| h[rec[:id]] = rec if rec[:id] }
|
|
165
|
+
|
|
166
|
+
# Detect created records (in after but not in before)
|
|
167
|
+
after_map.each do |id, after_rec|
|
|
168
|
+
next if before_map[id]
|
|
169
|
+
|
|
170
|
+
changes["#{assoc_name}.#{id}"] = {
|
|
171
|
+
action: "created",
|
|
172
|
+
attributes: after_rec[:attributes]
|
|
173
|
+
}
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Detect updated records (in both, but with differences)
|
|
177
|
+
before_map.each do |id, before_rec|
|
|
178
|
+
after_rec = after_map[id]
|
|
179
|
+
next unless after_rec
|
|
180
|
+
|
|
181
|
+
record_changes = diff_records(before_rec, after_rec)
|
|
182
|
+
next unless record_changes.present?
|
|
183
|
+
|
|
184
|
+
changes["#{assoc_name}.#{id}"] = {
|
|
185
|
+
action: "updated",
|
|
186
|
+
changes: record_changes
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
# Detect deleted records (in before but not in after)
|
|
190
|
+
next if after_map[id]
|
|
191
|
+
|
|
192
|
+
changes["#{assoc_name}.#{id}"] = {
|
|
193
|
+
action: "deleted",
|
|
194
|
+
attributes: before_rec[:attributes]
|
|
195
|
+
}
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
changes
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Filter changes based on only/except rules
|
|
202
|
+
def filter_changes(changes, only, except)
|
|
203
|
+
return changes if only.blank? && except.blank?
|
|
204
|
+
|
|
205
|
+
filtered = changes.dup
|
|
206
|
+
filtered = filtered.slice(*only.map(&:to_s)) if only.present?
|
|
207
|
+
filtered = filtered.except(*except.map(&:to_s)) if except.present?
|
|
208
|
+
filtered
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Filter associated changes
|
|
212
|
+
# Input: { "reviews.46" => { action: "updated", changes: {...} } }
|
|
213
|
+
# Output: Same structure but with filtered changes
|
|
214
|
+
def filter_associated_changes(assoc_changes, only, except)
|
|
215
|
+
return assoc_changes if only.blank? && except.blank?
|
|
216
|
+
|
|
217
|
+
filtered = {}
|
|
218
|
+
|
|
219
|
+
assoc_changes.each do |key, record_data|
|
|
220
|
+
# Filter attributes (for created/deleted records)
|
|
221
|
+
if record_data[:attributes].present?
|
|
222
|
+
filtered_attrs = filter_changes(record_data[:attributes], only, except)
|
|
223
|
+
next if filtered_attrs.blank?
|
|
224
|
+
|
|
225
|
+
filtered[key] = record_data.merge(attributes: filtered_attrs)
|
|
226
|
+
# Filter changes (for updated records)
|
|
227
|
+
elsif record_data[:changes].present?
|
|
228
|
+
filtered_changes = filter_changes(record_data[:changes], only, except)
|
|
229
|
+
next if filtered_changes.blank?
|
|
230
|
+
|
|
231
|
+
filtered[key] = record_data.merge(changes: filtered_changes)
|
|
232
|
+
else
|
|
233
|
+
filtered[key] = record_data
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
filtered
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Resolve human-readable action label
|
|
241
|
+
def resolve_action_label(action, configs)
|
|
242
|
+
config = configs.find { |c| c[:action].present? }
|
|
243
|
+
return action.to_s.humanize unless config
|
|
244
|
+
|
|
245
|
+
case config[:action]
|
|
246
|
+
when String
|
|
247
|
+
config[:action]
|
|
248
|
+
when Hash
|
|
249
|
+
config[:action][action.to_sym] || config[:action][action.to_s] || action.to_s.humanize
|
|
250
|
+
else
|
|
251
|
+
action.to_s.humanize
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Dscf
|
|
2
|
+
module Core
|
|
3
|
+
module AuditableModel
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
included do
|
|
7
|
+
has_many :audit_logs,
|
|
8
|
+
as: :auditable,
|
|
9
|
+
class_name: "Dscf::Core::AuditLog",
|
|
10
|
+
dependent: :destroy
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def record_audit!(action_name, audited_changes: {}, metadata: {}, actor: nil, request_uuid: nil, ip_address: nil, user_agent: nil)
|
|
14
|
+
audit_logs.create!(
|
|
15
|
+
action: action_name,
|
|
16
|
+
audited_changes: audited_changes.presence || {},
|
|
17
|
+
metadata: metadata.presence || {},
|
|
18
|
+
actor: actor,
|
|
19
|
+
request_uuid: request_uuid,
|
|
20
|
+
ip_address: ip_address,
|
|
21
|
+
user_agent: user_agent
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def audit_history(limit: 50)
|
|
26
|
+
audit_logs.order(created_at: :desc).limit(limit)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def last_audit
|
|
30
|
+
audit_logs.order(created_at: :desc).first
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -20,12 +20,45 @@ module Dscf
|
|
|
20
20
|
return current_review.status if current_review
|
|
21
21
|
|
|
22
22
|
# Return default initial status for zero-config
|
|
23
|
-
"
|
|
23
|
+
"draft"
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def build_review_for(context = :default)
|
|
27
27
|
reviews.build(context: context.to_s)
|
|
28
28
|
end
|
|
29
|
+
|
|
30
|
+
# Helper methods for common status checks
|
|
31
|
+
def draft?(context = :default)
|
|
32
|
+
current_status_for(context) == "draft"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def pending?(context = :default)
|
|
36
|
+
current_status_for(context) == "pending"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def approved?(context = :default)
|
|
40
|
+
current_status_for(context) == "approved"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def rejected?(context = :default)
|
|
44
|
+
current_status_for(context) == "rejected"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def modification_requested?(context = :default)
|
|
48
|
+
current_status_for(context) == "modify"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Check if resource can be edited via regular update endpoint
|
|
52
|
+
# Only draft records can be edited freely
|
|
53
|
+
# For modify status, use resubmit action with update_model: true
|
|
54
|
+
def editable?(context = :default)
|
|
55
|
+
draft?(context)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Check if resource has been submitted
|
|
59
|
+
def submitted?(context = :default)
|
|
60
|
+
!draft?(context)
|
|
61
|
+
end
|
|
29
62
|
end
|
|
30
63
|
end
|
|
31
64
|
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# app/models/dscf/core/audit_log.rb
|
|
2
|
+
module Dscf
|
|
3
|
+
module Core
|
|
4
|
+
class AuditLog < ApplicationRecord
|
|
5
|
+
self.table_name = "dscf_core_audit_logs"
|
|
6
|
+
|
|
7
|
+
belongs_to :auditable, polymorphic: true
|
|
8
|
+
belongs_to :actor, polymorphic: true, optional: true
|
|
9
|
+
|
|
10
|
+
validates :action, presence: true
|
|
11
|
+
|
|
12
|
+
scope :for_auditable, ->(obj) { where(auditable_type: obj.class.name, auditable_id: obj.id) }
|
|
13
|
+
scope :by_actor, ->(actor) { where(actor_type: actor.class.name, actor_id: actor.id) }
|
|
14
|
+
scope :for_action, ->(name) { where(action: name.to_s) }
|
|
15
|
+
scope :recent, -> { order(created_at: :desc) }
|
|
16
|
+
|
|
17
|
+
def self.ransackable_attributes(_auth_object = nil)
|
|
18
|
+
%w[id auditable_type auditable_id action created_at updated_at]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.ransackable_associations(_auth_object = nil)
|
|
22
|
+
%w[auditable actor]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -22,6 +22,10 @@ module Dscf
|
|
|
22
22
|
%w[business_type_id contact_email contact_phone created_at description id id_value name tin_number
|
|
23
23
|
updated_at user_id]
|
|
24
24
|
end
|
|
25
|
+
|
|
26
|
+
def self.ransackable_associations(_auth_object = nil)
|
|
27
|
+
%w[user business_type documents reviews]
|
|
28
|
+
end
|
|
25
29
|
end
|
|
26
30
|
end
|
|
27
31
|
end
|
|
@@ -5,14 +5,15 @@ module Dscf
|
|
|
5
5
|
belongs_to :verified_by, polymorphic: true, optional: true
|
|
6
6
|
validates :document_type, presence: true
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
has_many_attached :files
|
|
10
9
|
enum :document_type, {business_license: 0, drivers_license: 1, claim: 2}
|
|
11
10
|
|
|
12
|
-
def
|
|
13
|
-
return
|
|
11
|
+
def file_urls
|
|
12
|
+
return [] unless files.attached?
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
files.map do |file|
|
|
15
|
+
Rails.application.routes.url_helpers.rails_blob_url(file, only_path: true)
|
|
16
|
+
end
|
|
16
17
|
end
|
|
17
18
|
end
|
|
18
19
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Dscf
|
|
2
|
+
module Core
|
|
3
|
+
class AuditLogSerializer < ActiveModel::Serializer
|
|
4
|
+
attributes :id, :action, :audited_changes, :metadata,
|
|
5
|
+
:request_uuid, :ip_address, :user_agent,
|
|
6
|
+
:created_at, :updated_at
|
|
7
|
+
|
|
8
|
+
attribute :actor do
|
|
9
|
+
if object.actor
|
|
10
|
+
{
|
|
11
|
+
id: object.actor.id,
|
|
12
|
+
type: object.actor_type,
|
|
13
|
+
name: self.class.actor_display_name(object.actor)
|
|
14
|
+
}
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attribute :auditable do
|
|
19
|
+
{
|
|
20
|
+
id: object.auditable_id,
|
|
21
|
+
type: object.auditable_type
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.actor_display_name(actor)
|
|
26
|
+
return nil unless actor
|
|
27
|
+
|
|
28
|
+
if actor.respond_to?(:full_name) && actor.full_name.present?
|
|
29
|
+
actor.full_name
|
|
30
|
+
elsif actor.respond_to?(:name) && actor.name.present?
|
|
31
|
+
actor.name
|
|
32
|
+
elsif actor.respond_to?(:email)
|
|
33
|
+
actor.email
|
|
34
|
+
else
|
|
35
|
+
"#{actor.class.name.demodulize} ##{actor.id}"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -6,6 +6,7 @@ module Dscf
|
|
|
6
6
|
belongs_to :user, serializer: Dscf::Core::UserSerializer
|
|
7
7
|
belongs_to :business_type, serializer: Dscf::Core::BusinessTypeSerializer
|
|
8
8
|
has_many :reviews, serializer: Dscf::Core::ReviewSerializer
|
|
9
|
+
has_many :documents, serializer: Dscf::Core::DocumentSerializer
|
|
9
10
|
end
|
|
10
11
|
end
|
|
11
12
|
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Dscf
|
|
2
|
+
module Core
|
|
3
|
+
class DocumentSerializer < ActiveModel::Serializer
|
|
4
|
+
attributes :id, :document_type, :file_urls, :is_verified, :verified_at, :created_at, :updated_at
|
|
5
|
+
include Rails.application.routes.url_helpers
|
|
6
|
+
|
|
7
|
+
belongs_to :documentable, polymorphic: true
|
|
8
|
+
belongs_to :verified_by, polymorphic: true, optional: true
|
|
9
|
+
|
|
10
|
+
def file_urls
|
|
11
|
+
return [] unless object.files.attached?
|
|
12
|
+
|
|
13
|
+
object.files.map { |file| url_for(file) }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/config/locales/en.yml
CHANGED
|
@@ -33,3 +33,27 @@ en:
|
|
|
33
33
|
show: "Failed to retrieve business type details"
|
|
34
34
|
create: "Failed to create business type"
|
|
35
35
|
update: "Failed to update business type"
|
|
36
|
+
|
|
37
|
+
business:
|
|
38
|
+
success:
|
|
39
|
+
index: "Businesses retrieved successfully"
|
|
40
|
+
show: "Business details retrieved successfully"
|
|
41
|
+
create: "Business created successfully"
|
|
42
|
+
update: "Business updated successfully"
|
|
43
|
+
submit: "Business submitted successfully"
|
|
44
|
+
approve: "Business approved successfully"
|
|
45
|
+
reject: "Business rejected successfully"
|
|
46
|
+
request_modification: "Business modification requested successfully"
|
|
47
|
+
resubmit: "Business resubmitted successfully"
|
|
48
|
+
my_business: "Businesses retrieved successfully"
|
|
49
|
+
errors:
|
|
50
|
+
index: "Failed to retrieve businesses"
|
|
51
|
+
show: "Business not found"
|
|
52
|
+
create: "Failed to create business"
|
|
53
|
+
update: "Failed to update business"
|
|
54
|
+
submit: "Failed to submit business"
|
|
55
|
+
approve: "Failed to approve business"
|
|
56
|
+
reject: "Failed to reject business"
|
|
57
|
+
request_modification: "Failed to request modification for business"
|
|
58
|
+
resubmit: "Failed to resubmit business"
|
|
59
|
+
my_business: "Failed to retrieve businesses"
|
data/config/routes.rb
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
Dscf::Core::Engine.routes.draw do
|
|
2
|
+
resources :businesses do
|
|
3
|
+
collection do
|
|
4
|
+
get "my_business"
|
|
5
|
+
end
|
|
6
|
+
member do
|
|
7
|
+
patch "submit"
|
|
8
|
+
patch "approve"
|
|
9
|
+
patch "reject"
|
|
10
|
+
patch "request_modification"
|
|
11
|
+
patch "resubmit"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
2
15
|
post "auth/login", to: "auth#login"
|
|
3
16
|
post "auth/logout", to: "auth#logout"
|
|
4
17
|
post "auth/signup", to: "auth#signup"
|
|
@@ -3,7 +3,7 @@ class CreateDscfCoreReviews < ActiveRecord::Migration[8.0]
|
|
|
3
3
|
create_table :dscf_core_reviews do |t|
|
|
4
4
|
t.references :reviewable, polymorphic: true, null: false
|
|
5
5
|
t.string :context
|
|
6
|
-
t.string :status, default: "
|
|
6
|
+
t.string :status, default: "draft"
|
|
7
7
|
t.jsonb :feedback
|
|
8
8
|
t.references :reviewed_by, polymorphic: true
|
|
9
9
|
t.datetime :reviewed_at
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
class CreateDscfCoreAuditLogs < ActiveRecord::Migration[6.1]
|
|
2
|
+
def change
|
|
3
|
+
create_table :dscf_core_audit_logs do |t|
|
|
4
|
+
t.references :auditable, polymorphic: true, null: false
|
|
5
|
+
|
|
6
|
+
t.string :action, null: false
|
|
7
|
+
t.jsonb :audited_changes, null: false, default: {}
|
|
8
|
+
t.jsonb :metadata, default: {}
|
|
9
|
+
|
|
10
|
+
t.references :actor, polymorphic: true
|
|
11
|
+
|
|
12
|
+
t.string :request_uuid
|
|
13
|
+
t.string :ip_address
|
|
14
|
+
t.string :user_agent
|
|
15
|
+
|
|
16
|
+
t.timestamps
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
add_index :dscf_core_audit_logs, %i[auditable_type auditable_id], name: "index_audit_logs_on_auditable"
|
|
20
|
+
add_index :dscf_core_audit_logs, %i[actor_type actor_id], name: "index_audit_logs_on_actor"
|
|
21
|
+
add_index :dscf_core_audit_logs, :request_uuid
|
|
22
|
+
add_index :dscf_core_audit_logs, :created_at
|
|
23
|
+
add_index :dscf_core_audit_logs, :action
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/dscf/core/version.rb
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
FactoryBot.define do
|
|
2
|
+
factory :auditable_test_model, class: "AuditableTestModel" do
|
|
3
|
+
name { Faker::Company.name }
|
|
4
|
+
description { Faker::Company.catch_phrase }
|
|
5
|
+
contact_email { Faker::Internet.email }
|
|
6
|
+
contact_phone { Faker::PhoneNumber.phone_number }
|
|
7
|
+
tin_number { Faker::Number.number(digits: 10).to_s }
|
|
8
|
+
user
|
|
9
|
+
business_type
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
FactoryBot.define do
|
|
2
|
+
factory :audit_log, class: "Dscf::Core::AuditLog" do
|
|
3
|
+
auditable { association :business }
|
|
4
|
+
action { "update" }
|
|
5
|
+
audited_changes { {"name" => ["Old name", "New name"]} }
|
|
6
|
+
metadata { {} }
|
|
7
|
+
actor { association :user }
|
|
8
|
+
request_uuid { SecureRandom.uuid }
|
|
9
|
+
ip_address { "127.0.0.1" }
|
|
10
|
+
user_agent { "RSpec" }
|
|
11
|
+
end
|
|
12
|
+
end
|