Skip to content

Commit 55b287c

Browse files
committed
feat: implement service layer for business logic
- Application service base class with error handling - Dashboard stats service for real-time metrics calculation - User management service for CRUD operations - User search service with filtering and pagination - Separation of concerns between controllers and business logic
1 parent ecb54f7 commit 55b287c

File tree

4 files changed

+299
-0
lines changed

4 files changed

+299
-0
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Base service class following Command pattern
2+
class ApplicationService
3+
class << self
4+
# Call the service with given arguments
5+
def call(*args, **kwargs)
6+
new(*args, **kwargs).call
7+
end
8+
end
9+
10+
# Override in subclasses
11+
def call
12+
raise NotImplementedError, "Subclasses must implement the call method"
13+
end
14+
15+
private
16+
17+
# Success result
18+
def success(data = nil)
19+
ServiceResult.new(success: true, data: data)
20+
end
21+
22+
# Error result
23+
def failure(errors)
24+
ServiceResult.new(success: false, errors: errors)
25+
end
26+
end
27+
28+
# Service result object
29+
class ServiceResult
30+
attr_reader :data, :errors
31+
32+
def initialize(success:, data: nil, errors: nil)
33+
@success = success
34+
@data = data
35+
@errors = errors || []
36+
end
37+
38+
def success?
39+
@success
40+
end
41+
42+
def failure?
43+
!@success
44+
end
45+
46+
def error_messages
47+
return [] if errors.blank?
48+
49+
case errors
50+
when String
51+
[ errors ]
52+
when Array
53+
errors
54+
when ActiveModel::Errors
55+
errors.full_messages
56+
else
57+
[ errors.to_s ]
58+
end
59+
end
60+
end
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
class DashboardStatsService < ApplicationService
2+
def initialize(current_user)
3+
@current_user = current_user
4+
end
5+
6+
def call
7+
return failure("Unauthorized access") unless can_access_dashboard?
8+
9+
stats = calculate_dashboard_stats
10+
success(stats)
11+
end
12+
13+
private
14+
15+
attr_reader :current_user
16+
17+
def can_access_dashboard?
18+
current_user&.admin?
19+
end
20+
21+
def calculate_dashboard_stats
22+
{
23+
users: user_statistics,
24+
imports: import_statistics,
25+
activity: activity_statistics,
26+
growth: growth_statistics
27+
}
28+
end
29+
30+
def user_statistics
31+
{
32+
total: User.count,
33+
admins: User.where(role: "admin").count,
34+
regular_users: User.where(role: "user").count,
35+
recent: User.with_attached_avatar_image.order(created_at: :desc).limit(5),
36+
created_today: User.where(created_at: Date.current.beginning_of_day..Date.current.end_of_day).count,
37+
created_this_week: User.where(created_at: 1.week.ago..Time.current).count,
38+
created_this_month: User.where(created_at: 1.month.ago..Time.current).count
39+
}
40+
end
41+
42+
def import_statistics
43+
{
44+
total: Import.count,
45+
pending: Import.where(status: "pending").count,
46+
processing: Import.where(status: "processing").count,
47+
completed: Import.where(status: "completed").count,
48+
failed: Import.where(status: "failed").count,
49+
recent: Import.order(created_at: :desc).limit(5)
50+
}
51+
end
52+
53+
def activity_statistics
54+
{
55+
users_created_today: User.where(created_at: Date.current.beginning_of_day..Date.current.end_of_day).count,
56+
imports_started_today: Import.where(created_at: Date.current.beginning_of_day..Date.current.end_of_day).count,
57+
last_activity: [
58+
User.maximum(:updated_at),
59+
Import.maximum(:updated_at)
60+
].compact.max
61+
}
62+
end
63+
64+
def growth_statistics
65+
# Calculate user growth over the last 30 days
66+
growth_data = (0..29).map do |days_ago|
67+
date = days_ago.days.ago.to_date
68+
{
69+
date: date,
70+
users_created: User.where(created_at: date.beginning_of_day..date.end_of_day).count
71+
}
72+
end.reverse
73+
74+
{
75+
daily_growth: growth_data,
76+
total_growth_30_days: User.where(created_at: 30.days.ago..Time.current).count,
77+
average_daily_growth: growth_data.sum { |d| d[:users_created] } / 30.0
78+
}
79+
end
80+
end
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
class UserManagementService < ApplicationService
2+
def initialize(user_params, current_user)
3+
@user_params = user_params
4+
@current_user = current_user
5+
end
6+
7+
def call
8+
if can_perform_action?
9+
create_or_update_user
10+
else
11+
failure("You do not have permission to perform this action")
12+
end
13+
end
14+
15+
private
16+
17+
attr_reader :user_params, :current_user
18+
19+
def can_perform_action?
20+
current_user&.admin?
21+
end
22+
23+
def create_or_update_user
24+
if user_params[:id].present?
25+
update_existing_user
26+
else
27+
create_new_user
28+
end
29+
end
30+
31+
def update_existing_user
32+
user = User.find(user_params[:id])
33+
34+
if user.update(filtered_params)
35+
success(user)
36+
else
37+
failure(user.errors)
38+
end
39+
rescue ActiveRecord::RecordNotFound
40+
failure("User not found")
41+
end
42+
43+
def create_new_user
44+
# Generate random password for new users
45+
params_with_password = filtered_params.merge(
46+
password: generate_secure_password,
47+
password_confirmation: nil
48+
)
49+
50+
user = User.new(params_with_password)
51+
52+
if user.save
53+
# Send welcome email with password
54+
UserMailer.welcome_email(user, params_with_password[:password]).deliver_later
55+
success(user)
56+
else
57+
failure(user.errors)
58+
end
59+
end
60+
61+
def filtered_params
62+
user_params.permit(:full_name, :email, :role, :avatar_url, :avatar_image)
63+
end
64+
65+
def generate_secure_password
66+
SecureRandom.alphanumeric(12)
67+
end
68+
end
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
class UserSearchService < ApplicationService
2+
def initialize(search_params, current_user)
3+
@search_params = search_params
4+
@current_user = current_user
5+
end
6+
7+
def call
8+
return failure("Unauthorized access") unless can_search?
9+
10+
users = build_search_query
11+
success({
12+
users: users,
13+
total_count: users.count,
14+
filters_applied: filters_applied?
15+
})
16+
end
17+
18+
private
19+
20+
attr_reader :search_params, :current_user
21+
22+
def can_search?
23+
current_user&.admin?
24+
end
25+
26+
def build_search_query
27+
query = User.all
28+
query = apply_search_filter(query)
29+
query = apply_role_filter(query)
30+
query = apply_date_filter(query)
31+
query = apply_sorting(query)
32+
query
33+
end
34+
35+
def apply_search_filter(query)
36+
return query if search_params[:search].blank?
37+
38+
search_term = "%#{search_params[:search].strip}%"
39+
query.where(
40+
"full_name ILIKE ? OR email ILIKE ?",
41+
search_term, search_term
42+
)
43+
end
44+
45+
def apply_role_filter(query)
46+
return query if search_params[:role].blank?
47+
48+
query.where(role: search_params[:role])
49+
end
50+
51+
def apply_date_filter(query)
52+
return query unless search_params[:date_from].present? || search_params[:date_to].present?
53+
54+
if search_params[:date_from].present?
55+
query = query.where("created_at >= ?", Date.parse(search_params[:date_from]))
56+
end
57+
58+
if search_params[:date_to].present?
59+
query = query.where("created_at <= ?", Date.parse(search_params[:date_to]).end_of_day)
60+
end
61+
62+
query
63+
rescue Date::Error
64+
query
65+
end
66+
67+
def apply_sorting(query)
68+
sort_by = search_params[:sort_by]&.to_s
69+
sort_direction = search_params[:sort_direction]&.to_s
70+
71+
case sort_by
72+
when "name"
73+
query.order("full_name #{sort_direction == 'desc' ? 'DESC' : 'ASC'}")
74+
when "email"
75+
query.order("email #{sort_direction == 'desc' ? 'DESC' : 'ASC'}")
76+
when "role"
77+
query.order("role #{sort_direction == 'desc' ? 'DESC' : 'ASC'}")
78+
when "created_at"
79+
query.order("created_at #{sort_direction == 'desc' ? 'DESC' : 'ASC'}")
80+
else
81+
query.order(created_at: :desc)
82+
end
83+
end
84+
85+
def filters_applied?
86+
search_params[:search].present? ||
87+
search_params[:role].present? ||
88+
search_params[:date_from].present? ||
89+
search_params[:date_to].present?
90+
end
91+
end

0 commit comments

Comments
 (0)