diff --git a/app/controllers/api/v1/accounts/agent_capacity_policies_controller.rb b/app/controllers/api/v1/accounts/agent_capacity_policies_controller.rb index c8ea2d2ae..eedea1aa1 100644 --- a/app/controllers/api/v1/accounts/agent_capacity_policies_controller.rb +++ b/app/controllers/api/v1/accounts/agent_capacity_policies_controller.rb @@ -6,7 +6,7 @@ class Api::V1::Accounts::AgentCapacityPoliciesController < Api::V1::Accounts::Ba before_action :check_authorization def index - @agent_capacity_policies = Current.account.agent_capacity_policies.includes(:users, :inbox_capacity_limits) + @agent_capacity_policies = Enterprise::AgentCapacityPolicy.where(account_id: Current.account.id).includes(:users, :inbox_capacity_limits) render json: { agent_capacity_policies: serialize_agent_capacity_policies(@agent_capacity_policies) } end @@ -15,7 +15,8 @@ class Api::V1::Accounts::AgentCapacityPoliciesController < Api::V1::Accounts::Ba end def create - @agent_capacity_policy = Current.account.agent_capacity_policies.build(agent_capacity_policy_params) + @agent_capacity_policy = Enterprise::AgentCapacityPolicy.new(agent_capacity_policy_params) + @agent_capacity_policy.account_id = Current.account.id if @agent_capacity_policy.save render json: { agent_capacity_policy: serialize_agent_capacity_policy(@agent_capacity_policy) }, status: :created @@ -44,10 +45,10 @@ class Api::V1::Accounts::AgentCapacityPoliciesController < Api::V1::Accounts::Ba def set_inbox_limit inbox = Current.account.inboxes.find(params[:inbox_id]) inbox_limit = @agent_capacity_policy.inbox_capacity_limits.find_or_initialize_by(inbox: inbox) - + if inbox_limit.update(conversation_limit: params[:conversation_limit]) - render json: { - inbox_capacity_limit: serialize_inbox_capacity_limit(inbox_limit) + render json: { + inbox_capacity_limit: serialize_inbox_capacity_limit(inbox_limit) } else render json: { errors: inbox_limit.errors.full_messages }, status: :unprocessable_entity @@ -58,7 +59,7 @@ class Api::V1::Accounts::AgentCapacityPoliciesController < Api::V1::Accounts::Ba def remove_inbox_limit inbox = Current.account.inboxes.find(params[:inbox_id]) inbox_limit = @agent_capacity_policy.inbox_capacity_limits.find_by(inbox: inbox) - + if inbox_limit if inbox_limit.destroy head :ok @@ -73,33 +74,30 @@ class Api::V1::Accounts::AgentCapacityPoliciesController < Api::V1::Accounts::Ba # Assign a user to a capacity policy def assign_user user = Current.account.users.find(params[:user_id]) - - # Remove user from any existing capacity policy - Enterprise::AgentCapacityPolicyUser.where(user: user).destroy_all - - # Assign to new policy - policy_user = @agent_capacity_policy.agent_capacity_policy_users.build(user: user) - - if policy_user.save - render json: { + account_user = Current.account.account_users.find_by!(user: user) + + # Update the account_user to assign to this policy + if account_user.update(agent_capacity_policy_id: @agent_capacity_policy.id) + render json: { message: 'User assigned successfully', - agent_capacity_policy_user: serialize_policy_user(policy_user) + user_id: user.id, + agent_capacity_policy_id: @agent_capacity_policy.id } else - render json: { errors: policy_user.errors.full_messages }, status: :unprocessable_entity + render json: { errors: account_user.errors.full_messages }, status: :unprocessable_entity end end # Remove a user from a capacity policy def remove_user user = Current.account.users.find(params[:user_id]) - policy_user = @agent_capacity_policy.agent_capacity_policy_users.find_by(user: user) - - if policy_user - if policy_user.destroy + account_user = Current.account.account_users.find_by!(user: user) + + if account_user.agent_capacity_policy_id == @agent_capacity_policy.id + if account_user.update(agent_capacity_policy_id: nil) head :ok else - render json: { errors: policy_user.errors.full_messages }, status: :unprocessable_entity + render json: { errors: account_user.errors.full_messages }, status: :unprocessable_entity end else render json: { error: 'User not assigned to this policy' }, status: :not_found @@ -110,29 +108,30 @@ class Api::V1::Accounts::AgentCapacityPoliciesController < Api::V1::Accounts::Ba def agent_capacity user = Current.account.users.find(params[:agent_id]) inbox = params[:inbox_id] ? Current.account.inboxes.find(params[:inbox_id]) : nil - + capacity_service = Enterprise::AssignmentV2::CapacityService.new capacity_data = if inbox capacity_service.get_agent_capacity(user, inbox) else capacity_service.get_agent_overall_capacity(user) end - + render json: { agent_capacity: capacity_data } end private def ensure_enterprise_account - unless Current.account.feature_enabled?(:enterprise_agent_capacity) - render json: { - error: 'Agent capacity policies are only available for enterprise accounts' - }, status: :forbidden - end + return if Current.account.feature_enabled?(:enterprise_agent_capacity) + + render json: { + error: 'Agent capacity policies are only available for enterprise accounts' + }, status: :forbidden end def fetch_agent_capacity_policy - @agent_capacity_policy = Current.account.agent_capacity_policies.find(params[:id]) + @agent_capacity_policy = Enterprise::AgentCapacityPolicy.where(account_id: Current.account.id).includes(:users, + inbox_capacity_limits: :inbox).find(params[:id]) end def agent_capacity_policy_params @@ -151,6 +150,8 @@ class Api::V1::Accounts::AgentCapacityPoliciesController < Api::V1::Accounts::Ba exclusion_rules: policy.exclusion_rules, user_count: policy.users.count, inbox_limit_count: policy.inbox_capacity_limits.count, + inbox_limits: policy.inbox_capacity_limits.map { |limit| serialize_inbox_capacity_limit(limit) }, + users: policy.users.map { |user| { id: user.id, name: user.name, email: user.email, avatar_url: user.avatar_url } }, created_at: policy.created_at, updated_at: policy.updated_at } @@ -170,14 +171,4 @@ class Api::V1::Accounts::AgentCapacityPoliciesController < Api::V1::Accounts::Ba updated_at: limit.updated_at } end - - def serialize_policy_user(policy_user) - { - id: policy_user.id, - user_id: policy_user.user_id, - user_name: policy_user.user.name, - user_email: policy_user.user.email, - created_at: policy_user.created_at - } - end -end \ No newline at end of file +end diff --git a/app/controllers/api/v1/accounts/assignment_metrics_controller.rb b/app/controllers/api/v1/accounts/assignment_metrics_controller.rb deleted file mode 100644 index 97724565d..000000000 --- a/app/controllers/api/v1/accounts/assignment_metrics_controller.rb +++ /dev/null @@ -1,222 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Accounts::AssignmentMetricsController < Api::V1::Accounts::BaseController - before_action :check_authorization - before_action :validate_date_range - - def index - @metrics = compute_assignment_metrics - render json: { assignment_metrics: @metrics } - end - - def agent_history - @agent = Current.account.users.find(params[:agent_id]) - @assignment_history = fetch_agent_assignment_history(@agent) - - render json: { - agent: serialize_agent(@agent), - assignment_history: @assignment_history, - meta: pagination_meta - } - end - - private - - def compute_assignment_metrics - metrics = { - summary: compute_summary_metrics, - by_period: compute_period_metrics, - by_inbox: compute_inbox_metrics, - by_agent: compute_agent_metrics, - by_policy: compute_policy_metrics - } - - metrics - end - - def compute_summary_metrics - conversations = filter_conversations_by_date_range - - { - total_assignments: conversations.count, - average_assignment_time: calculate_average_assignment_time(conversations), - assignments_per_agent: calculate_assignments_per_agent(conversations), - unassigned_conversations: Current.account.conversations.open.unassigned.count, - policies_active: Current.account.assignment_policies.enabled.count - } - end - - def compute_period_metrics - group_by = params[:group_by] || 'day' - conversations = filter_conversations_by_date_range - - case group_by - when 'hour' - group_by_hour(conversations) - when 'day' - group_by_day(conversations) - when 'week' - group_by_week(conversations) - when 'month' - group_by_month(conversations) - else - group_by_day(conversations) - end - end - - def compute_inbox_metrics - inbox_id = params[:inbox_id] - base_query = filter_conversations_by_date_range - - if inbox_id.present? - base_query = base_query.where(inbox_id: inbox_id) - end - - base_query.joins(:inbox) - .group('inboxes.id', 'inboxes.name') - .count - .map { |k, v| { inbox_id: k[0], inbox_name: k[1], assignment_count: v } } - end - - def compute_agent_metrics - agent_id = params[:agent_id] - base_query = filter_conversations_by_date_range.where.not(assignee_id: nil) - - if agent_id.present? - base_query = base_query.where(assignee_id: agent_id) - end - - base_query.joins(:assignee) - .group('users.id', 'users.name', 'users.email') - .count - .map { |k, v| { agent_id: k[0], agent_name: k[1], agent_email: k[2], assignment_count: v } } - .sort_by { |a| -a[:assignment_count] } - end - - def compute_policy_metrics - # Get metrics grouped by assignment policy - policy_metrics = {} - - Current.account.assignment_policies.includes(:inboxes).each do |policy| - inbox_ids = policy.inboxes.pluck(:id) - next if inbox_ids.empty? - - conversations = filter_conversations_by_date_range.where(inbox_id: inbox_ids) - - policy_metrics[policy.id] = { - policy_id: policy.id, - policy_name: policy.name, - assignment_order: policy.assignment_order, - total_assignments: conversations.count, - average_assignment_time: calculate_average_assignment_time(conversations), - inbox_count: inbox_ids.count - } - end - - policy_metrics.values - end - - def fetch_agent_assignment_history(agent) - conversations = agent.assigned_conversations - .includes(:inbox, :contact) - .where(created_at: date_range) - .order(created_at: :desc) - .page(params[:page]) - .per(params[:per_page] || 50) - - conversations.map do |conversation| - { - conversation_id: conversation.id, - inbox_id: conversation.inbox_id, - inbox_name: conversation.inbox.name, - contact_name: conversation.contact.name, - assigned_at: conversation.assignee_last_seen_at || conversation.created_at, - status: conversation.status, - created_at: conversation.created_at - } - end - end - - def filter_conversations_by_date_range - Current.account.conversations.where(created_at: date_range) - end - - def date_range - start_date = params[:start_date] ? Date.parse(params[:start_date]).beginning_of_day : 30.days.ago - end_date = params[:end_date] ? Date.parse(params[:end_date]).end_of_day : Time.current - - start_date..end_date - end - - def validate_date_range - if params[:start_date].present? && params[:end_date].present? - start_date = Date.parse(params[:start_date]) - end_date = Date.parse(params[:end_date]) - - if start_date > end_date - render json: { error: 'Start date must be before end date' }, status: :bad_request - elsif (end_date - start_date).to_i > 365 - render json: { error: 'Date range cannot exceed 365 days' }, status: :bad_request - end - end - rescue Date::Error - render json: { error: 'Invalid date format' }, status: :bad_request - end - - def calculate_average_assignment_time(conversations) - assigned_conversations = conversations.where.not(assignee_id: nil) - return 0 if assigned_conversations.empty? - - total_time = assigned_conversations.sum do |conv| - assignment_time = conv.assignee_last_seen_at || conv.updated_at - (assignment_time - conv.created_at).to_i - end - - (total_time / assigned_conversations.count / 60).round(2) # Return in minutes - end - - def calculate_assignments_per_agent(conversations) - assigned_count = conversations.where.not(assignee_id: nil).count - agent_count = conversations.where.not(assignee_id: nil).distinct.count(:assignee_id) - - return 0 if agent_count.zero? - - (assigned_count.to_f / agent_count).round(2) - end - - def group_by_hour(conversations) - conversations.group_by_hour(:created_at).count - end - - def group_by_day(conversations) - conversations.group_by_day(:created_at).count - end - - def group_by_week(conversations) - conversations.group_by_week(:created_at).count - end - - def group_by_month(conversations) - conversations.group_by_month(:created_at).count - end - - def serialize_agent(agent) - { - id: agent.id, - name: agent.name, - email: agent.email, - avatar_url: agent.avatar_url - } - end - - def pagination_meta - { - current_page: params[:page] || 1, - per_page: params[:per_page] || 50 - } - end - - def check_authorization - authorize(Conversation, :index?) - end -end \ No newline at end of file diff --git a/app/controllers/api/v1/accounts/assignment_policies_controller.rb b/app/controllers/api/v1/accounts/assignment_policies_controller.rb index 0eec2f377..acc7c1186 100644 --- a/app/controllers/api/v1/accounts/assignment_policies_controller.rb +++ b/app/controllers/api/v1/accounts/assignment_policies_controller.rb @@ -63,6 +63,7 @@ class Api::V1::Accounts::AssignmentPoliciesController < Api::V1::Accounts::BaseC fair_distribution_window: policy.fair_distribution_window, enabled: policy.enabled, inbox_count: policy.inboxes.count, + inboxes: policy.inboxes.map { |inbox| { id: inbox.id, name: inbox.name } }, created_at: policy.created_at, updated_at: policy.updated_at } @@ -75,4 +76,4 @@ class Api::V1::Accounts::AssignmentPoliciesController < Api::V1::Accounts::BaseC def check_authorization authorize(AssignmentPolicy) end -end \ No newline at end of file +end diff --git a/app/controllers/api/v1/accounts/leaves_controller.rb b/app/controllers/api/v1/accounts/leaves_controller.rb index 51cf4653e..31019603a 100644 --- a/app/controllers/api/v1/accounts/leaves_controller.rb +++ b/app/controllers/api/v1/accounts/leaves_controller.rb @@ -2,7 +2,7 @@ class Api::V1::Accounts::LeavesController < Api::V1::Accounts::BaseController before_action :fetch_leave, only: [:show, :update, :destroy, :approve, :reject] - before_action :check_authorization + before_action -> { check_authorization(Leave) }, only: [:index, :create] before_action :authorize_leave, only: [:show, :update, :destroy] before_action :authorize_approval, only: [:approve, :reject] @@ -22,9 +22,9 @@ class Api::V1::Accounts::LeavesController < Api::V1::Accounts::BaseController account_user: account_user, current_user: Current.user ) - + result = service.create(leave_params) - + if result[:success] render json: { leave: serialize_leave(result[:leave]) }, status: :created else @@ -34,7 +34,7 @@ class Api::V1::Accounts::LeavesController < Api::V1::Accounts::BaseController def update result = leave_service.update(@leave, leave_params) - + if result[:success] render json: { leave: serialize_leave(result[:leave]) } else @@ -53,7 +53,7 @@ class Api::V1::Accounts::LeavesController < Api::V1::Accounts::BaseController def approve service = Leaves::LeaveApprovalService.new(leave: @leave, approver: Current.user) result = service.approve(params[:comments]) - + if result[:success] render json: { leave: serialize_leave(result[:leave]) } else @@ -64,7 +64,7 @@ class Api::V1::Accounts::LeavesController < Api::V1::Accounts::BaseController def reject service = Leaves::LeaveApprovalService.new(leave: @leave, approver: Current.user) result = service.reject(params[:reason]) - + if result[:success] render json: { leave: serialize_leave(result[:leave]) } else @@ -96,11 +96,20 @@ class Api::V1::Accounts::LeavesController < Api::V1::Accounts::BaseController end def leave_service - @leave_service ||= Leaves::LeaveService.new( - account: Current.account, - account_user: @leave.account_user, - current_user: Current.user - ) + @leave_service ||= if @leave + Leaves::LeaveService.new( + account: Current.account, + account_user: @leave.account_user, + current_user: Current.user + ) + else + # For index action, we don't have a specific leave + Leaves::LeaveService.new( + account: Current.account, + account_user: nil, + current_user: Current.user + ) + end end def leave_params @@ -136,4 +145,4 @@ class Api::V1::Accounts::LeavesController < Api::V1::Accounts::BaseController def serialize_leaves(leaves) leaves.map { |leave| serialize_leave(leave) } end -end \ No newline at end of file +end diff --git a/app/controllers/api/v1/accounts/reports/assignment_metrics_controller.rb b/app/controllers/api/v1/accounts/reports/assignment_metrics_controller.rb new file mode 100644 index 000000000..dc3503608 --- /dev/null +++ b/app/controllers/api/v1/accounts/reports/assignment_metrics_controller.rb @@ -0,0 +1,380 @@ +# frozen_string_literal: true + +class Api::V1::Accounts::Reports::AssignmentMetricsController < Api::V1::Accounts::BaseController + before_action :check_authorization + before_action :validate_date_range + + def index + @metrics = compute_assignment_metrics + render json: { assignment_metrics: @metrics } + end + + def agent_history + if params[:agent_id].present? + @agent = Current.account.users.find(params[:agent_id]) + @assignment_history = fetch_agent_assignment_history(@agent) + + render json: { + agent: serialize_agent(@agent), + assignment_history: @assignment_history, + meta: pagination_meta + } + else + # Return all agents' history summary + agents_summary = compute_all_agents_history + render json: { agents_history: agents_summary } + end + end + + def policy_performance + @policies = Current.account.assignment_policies.includes(:inboxes) + performance_data = @policies.map do |policy| + compute_policy_performance(policy) + end + + render json: { policy_performance: performance_data } + end + + def agent_utilization + agents = Current.account.users.joins(:account_users).where(account_users: { role: %w[agent administrator] }) + utilization_data = agents.map do |agent| + compute_agent_utilization(agent) + end + + render json: { agent_utilization: utilization_data } + end + + def assignment_distribution + distribution_data = { + by_inbox: compute_distribution_by_inbox, + by_team: compute_distribution_by_team, + by_hour: compute_distribution_by_hour, + by_day_of_week: compute_distribution_by_day_of_week + } + + render json: { assignment_distribution: distribution_data } + end + + def export + type = params[:type] || 'csv' + data = compute_assignment_metrics + + case type + when 'csv' + send_data generate_csv(data), filename: "assignment_metrics_#{Date.current}.csv", type: 'text/csv' + when 'json' + send_data data.to_json, filename: "assignment_metrics_#{Date.current}.json", type: 'application/json' + else + render json: { error: 'Unsupported export type' }, status: :bad_request + end + end + + private + + def compute_assignment_metrics + { + summary: compute_summary_metrics, + by_period: compute_period_metrics, + by_inbox: compute_inbox_metrics, + by_agent: compute_agent_metrics, + by_policy: compute_policy_metrics + } + end + + def compute_summary_metrics + conversations = filter_conversations_by_date_range + + { + total_assignments: conversations.count, + average_assignment_time: calculate_average_assignment_time(conversations), + assignments_per_agent: calculate_assignments_per_agent(conversations), + unassigned_conversations: Current.account.conversations.open.unassigned.count, + policies_active: Current.account.assignment_policies.enabled.count + } + end + + def compute_period_metrics + group_by = params[:group_by] || 'day' + conversations = filter_conversations_by_date_range + + case group_by + when 'hour' + group_by_hour(conversations) + when 'day' + group_by_day(conversations) + when 'week' + group_by_week(conversations) + when 'month' + group_by_month(conversations) + else + group_by_day(conversations) + end + end + + def compute_inbox_metrics + inbox_id = params[:inbox_id] + base_query = filter_conversations_by_date_range + + base_query = base_query.where(inbox_id: inbox_id) if inbox_id.present? + + base_query.joins(:inbox) + .group('inboxes.id', 'inboxes.name') + .count + .map { |k, v| { inbox_id: k[0], inbox_name: k[1], assignment_count: v } } + end + + def compute_agent_metrics + agent_id = params[:agent_id] + base_query = filter_conversations_by_date_range.where.not(assignee_id: nil) + + base_query = base_query.where(assignee_id: agent_id) if agent_id.present? + + base_query.joins(:assignee) + .group('users.id', 'users.name', 'users.email') + .count + .map { |k, v| { agent_id: k[0], agent_name: k[1], agent_email: k[2], assignment_count: v } } + .sort_by { |a| -a[:assignment_count] } + end + + def compute_policy_metrics + # Get metrics grouped by assignment policy + policy_metrics = {} + + Current.account.assignment_policies.includes(:inboxes).each do |policy| + inbox_ids = policy.inboxes.pluck(:id) + next if inbox_ids.empty? + + conversations = filter_conversations_by_date_range.where(inbox_id: inbox_ids) + + policy_metrics[policy.id] = { + policy_id: policy.id, + policy_name: policy.name, + assignment_order: policy.assignment_order, + total_assignments: conversations.count, + average_assignment_time: calculate_average_assignment_time(conversations), + inbox_count: inbox_ids.count + } + end + + policy_metrics.values + end + + def fetch_agent_assignment_history(agent) + conversations = agent.assigned_conversations + .includes(:inbox, :contact) + .where(created_at: date_range) + .order(created_at: :desc) + .page(params[:page]) + .per(params[:per_page] || 50) + + conversations.map do |conversation| + { + conversation_id: conversation.id, + inbox_id: conversation.inbox_id, + inbox_name: conversation.inbox.name, + contact_name: conversation.contact.name, + assigned_at: conversation.assignee_last_seen_at || conversation.created_at, + status: conversation.status, + created_at: conversation.created_at + } + end + end + + def filter_conversations_by_date_range + Current.account.conversations.where(created_at: date_range) + end + + def date_range + start_date = params[:start_date] ? Date.parse(params[:start_date]).beginning_of_day : 30.days.ago + end_date = params[:end_date] ? Date.parse(params[:end_date]).end_of_day : Time.current + + start_date..end_date + end + + def validate_date_range + if params[:start_date].present? && params[:end_date].present? + start_date = Date.parse(params[:start_date]) + end_date = Date.parse(params[:end_date]) + + if start_date > end_date + render json: { error: 'Start date must be before end date' }, status: :bad_request + elsif (end_date - start_date).to_i > 365 + render json: { error: 'Date range cannot exceed 365 days' }, status: :bad_request + end + end + rescue Date::Error + render json: { error: 'Invalid date format' }, status: :bad_request + end + + def calculate_average_assignment_time(conversations) + assigned_conversations = conversations.where.not(assignee_id: nil) + return 0 if assigned_conversations.empty? + + total_time = assigned_conversations.sum do |conv| + assignment_time = conv.assignee_last_seen_at || conv.updated_at + (assignment_time - conv.created_at).to_i + end + + (total_time / assigned_conversations.count / 60).round(2) # Return in minutes + end + + def calculate_assignments_per_agent(conversations) + assigned_count = conversations.where.not(assignee_id: nil).count + agent_count = conversations.where.not(assignee_id: nil).distinct.count(:assignee_id) + + return 0 if agent_count.zero? + + (assigned_count.to_f / agent_count).round(2) + end + + def group_by_hour(conversations) + conversations.group_by_hour(:created_at).count + end + + def group_by_day(conversations) + conversations.group_by_day(:created_at).count + end + + def group_by_week(conversations) + conversations.group_by_week(:created_at).count + end + + def group_by_month(conversations) + conversations.group_by_month(:created_at).count + end + + def serialize_agent(agent) + { + id: agent.id, + name: agent.name, + email: agent.email, + avatar_url: agent.avatar_url + } + end + + def pagination_meta + { + current_page: params[:page] || 1, + per_page: params[:per_page] || 50 + } + end + + def check_authorization + authorize(Conversation, :index?) + end + + def compute_all_agents_history + agents = Current.account.users.joins(:account_users).where(account_users: { role: %w[agent administrator] }) + agents.map do |agent| + conversations = agent.assigned_conversations.where(created_at: date_range) + { + agent: serialize_agent(agent), + total_assignments: conversations.count, + average_resolution_time: calculate_average_resolution_time(conversations), + active_conversations: conversations.open.count + } + end + end + + def compute_policy_performance(policy) + inbox_ids = policy.inboxes.pluck(:id) + conversations = filter_conversations_by_date_range.where(inbox_id: inbox_ids) + + { + policy_id: policy.id, + policy_name: policy.name, + assignment_order: policy.assignment_order, + total_assignments: conversations.count, + average_assignment_time: calculate_average_assignment_time(conversations), + success_rate: calculate_assignment_success_rate(conversations), + agent_count: policy.account_users.count, + inbox_count: inbox_ids.count + } + end + + def compute_agent_utilization(agent) + capacity_policy = agent.account_users.first&.agent_capacity_policy + assigned_conversations = agent.assigned_conversations.open.count + + utilization = if capacity_policy + policy_limit = capacity_policy.inbox_capacity_limits.sum(:conversation_limit) + policy_limit > 0 ? (assigned_conversations.to_f / policy_limit * 100).round(2) : 0 + else + 0 + end + + { + agent: serialize_agent(agent), + assigned_conversations: assigned_conversations, + capacity_policy: capacity_policy&.name, + utilization_percentage: utilization, + available_capacity: capacity_policy ? capacity_policy.inbox_capacity_limits.sum(:conversation_limit) - assigned_conversations : nil + } + end + + def compute_distribution_by_inbox + filter_conversations_by_date_range + .joins(:inbox) + .group('inboxes.id', 'inboxes.name') + .count + .map { |k, v| { inbox_id: k[0], inbox_name: k[1], count: v } } + end + + def compute_distribution_by_team + filter_conversations_by_date_range + .joins(assignee: { team_members: :team }) + .group('teams.id', 'teams.name') + .count + .map { |k, v| { team_id: k[0], team_name: k[1], count: v } } + end + + def compute_distribution_by_hour + filter_conversations_by_date_range + .group_by_hour_of_day(:created_at) + .count + end + + def compute_distribution_by_day_of_week + filter_conversations_by_date_range + .group_by_day_of_week(:created_at) + .count + end + + def calculate_average_resolution_time(conversations) + resolved = conversations.resolved + return 0 if resolved.empty? + + total_time = resolved.sum { |c| (c.resolved_at - c.created_at).to_i } + (total_time / resolved.count / 3600).round(2) # Return in hours + end + + def calculate_assignment_success_rate(conversations) + total = conversations.count + return 0 if total.zero? + + assigned = conversations.where.not(assignee_id: nil).count + (assigned.to_f / total * 100).round(2) + end + + def generate_csv(data) + require 'csv' + + CSV.generate(headers: true) do |csv| + # Summary metrics + csv << ['Summary Metrics'] + csv << %w[Metric Value] + data[:summary].each do |key, value| + csv << [key.to_s.humanize, value] + end + + csv << [] + + # Agent metrics + csv << ['Agent Metrics'] + csv << ['Agent Name', 'Email', 'Assignment Count'] + data[:by_agent].each do |agent| + csv << [agent[:agent_name], agent[:agent_email], agent[:assignment_count]] + end + end + end +end diff --git a/app/models/account.rb b/app/models/account.rb index ab65fdca2..4aa1c4b29 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -98,8 +98,8 @@ class Account < ApplicationRecord has_many :webhooks, dependent: :destroy_async has_many :whatsapp_channels, dependent: :destroy_async, class_name: '::Channel::Whatsapp' has_many :working_hours, dependent: :destroy_async - has_many :leaves, dependent: :destroy_async - + has_many :leaves, dependent: :destroy_async, class_name: 'Leave' + # Assignment V2 associations has_many :assignment_policies, dependent: :destroy_async has_many :agent_capacity_policies, dependent: :destroy_async, class_name: 'Enterprise::AgentCapacityPolicy' if ChatwootApp.enterprise? @@ -164,7 +164,6 @@ class Account < ApplicationRecord ISO_639.find(account_locale)&.english_name&.downcase || 'english' end - private def notify_creation diff --git a/app/models/account_user.rb b/app/models/account_user.rb index e85b7b1e4..5f540d9ad 100644 --- a/app/models/account_user.rb +++ b/app/models/account_user.rb @@ -2,24 +2,26 @@ # # Table name: account_users # -# id :bigint not null, primary key -# active_at :datetime -# auto_offline :boolean default(TRUE), not null -# availability :integer default("online"), not null -# role :integer default("agent") -# created_at :datetime not null -# updated_at :datetime not null -# account_id :bigint -# custom_role_id :bigint -# inviter_id :bigint -# user_id :bigint +# id :bigint not null, primary key +# active_at :datetime +# auto_offline :boolean default(TRUE), not null +# availability :integer default("online"), not null +# role :integer default("agent") +# created_at :datetime not null +# updated_at :datetime not null +# account_id :bigint +# agent_capacity_policy_id :bigint +# custom_role_id :bigint +# inviter_id :bigint +# user_id :bigint # # Indexes # -# index_account_users_on_account_id (account_id) -# index_account_users_on_custom_role_id (custom_role_id) -# index_account_users_on_user_id (user_id) -# uniq_user_id_per_account_id (account_id,user_id) UNIQUE +# index_account_users_on_account_id (account_id) +# index_account_users_on_agent_capacity_policy_id (agent_capacity_policy_id) +# index_account_users_on_custom_role_id (custom_role_id) +# index_account_users_on_user_id (user_id) +# uniq_user_id_per_account_id (account_id,user_id) UNIQUE # class AccountUser < ApplicationRecord @@ -30,7 +32,7 @@ class AccountUser < ApplicationRecord belongs_to :inviter, class_name: 'User', optional: true belongs_to :agent_capacity_policy, class_name: 'Enterprise::AgentCapacityPolicy', optional: true - has_many :leaves, dependent: :destroy + has_many :leaves, dependent: :destroy, class_name: 'Leave' enum role: { agent: 0, administrator: 1 } enum availability: { online: 0, offline: 1, busy: 2 } diff --git a/app/models/assignment_policy.rb b/app/models/assignment_policy.rb index b070c075b..b7a898569 100644 --- a/app/models/assignment_policy.rb +++ b/app/models/assignment_policy.rb @@ -4,26 +4,23 @@ # # Table name: assignment_policies # -# id :bigint not null, primary key -# account_id :bigint not null -# name :string(255) not null -# description :text -# assignment_order :integer not null, default: 0 -# conversation_priority :integer not null, default: 0 -# fair_distribution_limit :integer not null, default: 10 -# fair_distribution_window :integer not null, default: 3600 -# enabled :boolean not null, default: true -# created_at :datetime not null -# updated_at :datetime not null +# id :bigint not null, primary key +# assignment_order :integer default("round_robin"), not null +# conversation_priority :integer default("earliest_created"), not null +# description :text +# enabled :boolean default(TRUE), not null +# fair_distribution_limit :integer default(10), not null +# fair_distribution_window :integer default(3600), not null +# name :string(255) not null +# created_at :datetime not null +# updated_at :datetime not null +# account_id :bigint not null # # Indexes # -# index_assignment_policies_on_account_id (account_id) -# unique_assignment_policy_name_per_account (account_id,name) UNIQUE -# -# Foreign Keys -# -# fk_rails_... (account_id => accounts.id) +# index_assignment_policies_on_account_id (account_id) +# index_assignment_policies_on_enabled (enabled) +# unique_assignment_policy_name_per_account (account_id,name) UNIQUE # class AssignmentPolicy < ApplicationRecord @@ -49,7 +46,7 @@ class AssignmentPolicy < ApplicationRecord # Validate balanced assignment is only available for enterprise validate :validate_balanced_assignment_enterprise_only - + # Server-side validation to prevent bypass before_save :enforce_enterprise_features @@ -89,11 +86,11 @@ class AssignmentPolicy < ApplicationRecord def enforce_enterprise_features # Server-side enforcement to prevent API bypass - if balanced? && !can_use_balanced_assignment? - # Force to round_robin if enterprise not available - self.assignment_order = 'round_robin' - Rails.logger.warn("Assignment V2: Forced assignment_order to round_robin for non-enterprise account #{account_id}") - end + return unless balanced? && !can_use_balanced_assignment? + + # Force to round_robin if enterprise not available + self.assignment_order = 'round_robin' + Rails.logger.warn("Assignment V2: Forced assignment_order to round_robin for non-enterprise account #{account_id}") end def clear_assignment_caches @@ -103,4 +100,4 @@ class AssignmentPolicy < ApplicationRecord Rails.cache.delete("assignment_v2:inbox_policy:#{inbox.id}") end end -end \ No newline at end of file +end diff --git a/app/models/inbox_assignment_policy.rb b/app/models/inbox_assignment_policy.rb index 88440ed20..208d4b8ef 100644 --- a/app/models/inbox_assignment_policy.rb +++ b/app/models/inbox_assignment_policy.rb @@ -5,22 +5,17 @@ # Table name: inbox_assignment_policies # # id :bigint not null, primary key -# inbox_id :bigint not null -# assignment_policy_id :bigint not null # created_at :datetime not null # updated_at :datetime not null +# assignment_policy_id :bigint not null +# inbox_id :bigint not null # # Indexes # # index_inbox_assignment_policies_on_assignment_policy_id (assignment_policy_id) -# index_inbox_assignment_policies_on_inbox_id (inbox_id) +# index_inbox_assignment_policies_on_inbox_id (inbox_id) # unique_inbox_assignment_policy (inbox_id) UNIQUE # -# Foreign Keys -# -# fk_rails_... (assignment_policy_id => assignment_policies.id) -# fk_rails_... (inbox_id => inboxes.id) -# class InboxAssignmentPolicy < ApplicationRecord include AccountCacheRevalidator @@ -35,11 +30,11 @@ class InboxAssignmentPolicy < ApplicationRecord # Delegations delegate :account, to: :inbox - delegate :name, :description, :assignment_order, :conversation_priority, + delegate :name, :description, :assignment_order, :conversation_priority, :fair_distribution_limit, :fair_distribution_window, :enabled?, to: :assignment_policy, prefix: :policy - # Callbacks + # Callbacks after_create_commit :clear_inbox_cache after_update_commit :clear_inbox_cache after_destroy_commit :clear_inbox_cache @@ -61,14 +56,14 @@ class InboxAssignmentPolicy < ApplicationRecord def inbox_belongs_to_same_account return unless inbox && assignment_policy - - if inbox.account_id != assignment_policy.account_id - errors.add(:inbox, 'must belong to the same account as the assignment policy') - end + + return unless inbox.account_id != assignment_policy.account_id + + errors.add(:inbox, 'must belong to the same account as the assignment policy') end def clear_inbox_cache Rails.cache.delete("assignment_v2:inbox_policy:#{inbox_id}") update_account_cache end -end \ No newline at end of file +end diff --git a/app/models/leave.rb b/app/models/leave.rb index cf1ca136c..96e64c9ff 100644 --- a/app/models/leave.rb +++ b/app/models/leave.rb @@ -19,20 +19,14 @@ # # Indexes # -# index_leaves_on_account_and_status (account_id,status) -# index_leaves_on_account_id (account_id) -# index_leaves_on_account_user_and_dates (account_user_id,start_date,end_date) -# index_leaves_on_account_user_id (account_user_id) -# index_leaves_on_approved_by_id (approved_by_id) -# index_leaves_on_end_date (end_date) -# index_leaves_on_start_date (start_date) -# index_leaves_on_status (status) -# -# Foreign Keys -# -# fk_rails_... (account_id => accounts.id) -# fk_rails_... (account_user_id => account_users.id) -# fk_rails_... (approved_by_id => users.id) +# index_leaves_on_account_and_status (account_id,status) +# index_leaves_on_account_id (account_id) +# index_leaves_on_account_user_and_dates (account_user_id,start_date,end_date) +# index_leaves_on_account_user_id (account_user_id) +# index_leaves_on_approved_by_id (approved_by_id) +# index_leaves_on_end_date (end_date) +# index_leaves_on_start_date (start_date) +# index_leaves_on_status (status) # class Leave < ApplicationRecord @@ -79,11 +73,13 @@ class Leave < ApplicationRecord def days_count return 0 unless start_date && end_date + (end_date - start_date).to_i + 1 end def overlaps_with?(other_leave) return false if other_leave == self + start_date <= other_leave.end_date && end_date >= other_leave.start_date end @@ -91,6 +87,7 @@ class Leave < ApplicationRecord def end_date_after_start_date return unless start_date && end_date + errors.add(:end_date, 'must be after or equal to start date') if end_date < start_date end @@ -99,13 +96,13 @@ class Leave < ApplicationRecord .approved .where.not(id: id) .by_date_range(start_date, end_date) - - if overlapping_leaves.exists? - errors.add(:base, 'Leave dates overlap with an existing approved leave') - end + + return unless overlapping_leaves.exists? + + errors.add(:base, 'Leave dates overlap with an existing approved leave') end def set_approved_at self.approved_at = Time.current end -end \ No newline at end of file +end diff --git a/app/policies/agent_capacity_policy_policy.rb b/app/policies/agent_capacity_policy_policy.rb new file mode 100644 index 000000000..b9f7f46d3 --- /dev/null +++ b/app/policies/agent_capacity_policy_policy.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +class AgentCapacityPolicyPolicy < ApplicationPolicy + def index? + return false unless defined?(::Enterprise::AgentCapacityPolicyPolicy) + + ::Enterprise::AgentCapacityPolicyPolicy.new(@user_context, @record).index? + end + + def show? + return false unless defined?(::Enterprise::AgentCapacityPolicyPolicy) + + ::Enterprise::AgentCapacityPolicyPolicy.new(@user_context, @record).show? + end + + def create? + return false unless defined?(::Enterprise::AgentCapacityPolicyPolicy) + + ::Enterprise::AgentCapacityPolicyPolicy.new(@user_context, @record).create? + end + + def update? + return false unless defined?(::Enterprise::AgentCapacityPolicyPolicy) + + ::Enterprise::AgentCapacityPolicyPolicy.new(@user_context, @record).update? + end + + def destroy? + return false unless defined?(::Enterprise::AgentCapacityPolicyPolicy) + + ::Enterprise::AgentCapacityPolicyPolicy.new(@user_context, @record).destroy? + end + + def set_inbox_limit? + return false unless defined?(::Enterprise::AgentCapacityPolicyPolicy) + + ::Enterprise::AgentCapacityPolicyPolicy.new(@user_context, @record).set_inbox_limit? + end + + def remove_inbox_limit? + return false unless defined?(::Enterprise::AgentCapacityPolicyPolicy) + + ::Enterprise::AgentCapacityPolicyPolicy.new(@user_context, @record).remove_inbox_limit? + end + + def assign_user? + return false unless defined?(::Enterprise::AgentCapacityPolicyPolicy) + + ::Enterprise::AgentCapacityPolicyPolicy.new(@user_context, @record).assign_user? + end + + def remove_user? + return false unless defined?(::Enterprise::AgentCapacityPolicyPolicy) + + ::Enterprise::AgentCapacityPolicyPolicy.new(@user_context, @record).remove_user? + end + + def agent_capacity? + return false unless defined?(::Enterprise::AgentCapacityPolicyPolicy) + + ::Enterprise::AgentCapacityPolicyPolicy.new(@user_context, @record).agent_capacity? + end +end diff --git a/app/policies/leave_policy.rb b/app/policies/leave_policy.rb index 7678ec1db..e35258481 100644 --- a/app/policies/leave_policy.rb +++ b/app/policies/leave_policy.rb @@ -12,17 +12,16 @@ class LeavePolicy < ApplicationPolicy def create? # Users can create their own leave requests + # When authorizing the class (not instance), allow any authenticated user + return true if record.is_a?(Class) + record.account_user.user_id == user.id end def update? # Users can update their own pending/rejected leaves # Admins can update any leave - if @account_user.administrator? - true - else - record.account_user.user_id == user.id && record.pending? - end + @account_user.administrator? || (record.account_user.user_id == user.id && record.pending?) end def destroy? @@ -55,4 +54,4 @@ class LeavePolicy < ApplicationPolicy end end end -end \ No newline at end of file +end diff --git a/config/features.yml b/config/features.yml index db2d46700..c5cf8a099 100644 --- a/config/features.yml +++ b/config/features.yml @@ -191,3 +191,7 @@ display_name: CRM V2 enabled: false chatwoot_internal: true +- name: enterprise_agent_capacity + display_name: Enterprise Agent Capacity + enabled: true + premium: true diff --git a/config/routes.rb b/config/routes.rb index f7f496326..3ebfd0293 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -226,11 +226,11 @@ Rails.application.routes.draw do # Assignment V2 Routes resources :assignment_policies - + resources :inboxes, only: [] do resource :assignment_policy, only: [:show, :create, :destroy], controller: 'inbox_assignment_policies' end - + # Agent Capacity Management (Enterprise) resources :agent_capacity_policies do member do @@ -240,13 +240,22 @@ Rails.application.routes.draw do delete 'users/:user_id', to: 'agent_capacity_policies#remove_user' end end - + # Agent capacity status get 'agents/:agent_id/capacity', to: 'agent_capacity_policies#agent_capacity' - + # Assignment Metrics - get 'reports/assignment_metrics', to: 'assignment_metrics#index' - get 'agents/:agent_id/assignment_history', to: 'assignment_metrics#agent_history' + namespace :reports do + resources :assignment_metrics, only: [:index] do + collection do + get 'agent_history' + get 'policy_performance' + get 'agent_utilization' + get 'assignment_distribution' + get 'export' + end + end + end namespace :twitter do resource :authorization, only: [:create] diff --git a/db/schema.rb b/db/schema.rb index eb7409b8b..297aaf6d7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2025_07_22_152516) do +ActiveRecord::Schema[7.1].define(version: 2025_07_30_000000) do # These extensions should be enabled to support this database enable_extension "pg_stat_statements" enable_extension "pg_trgm" @@ -39,8 +39,10 @@ ActiveRecord::Schema[7.1].define(version: 2025_07_22_152516) do t.integer "availability", default: 0, null: false t.boolean "auto_offline", default: true, null: false t.bigint "custom_role_id" + t.bigint "agent_capacity_policy_id" t.index ["account_id", "user_id"], name: "uniq_user_id_per_account_id", unique: true t.index ["account_id"], name: "index_account_users_on_account_id" + t.index ["agent_capacity_policy_id"], name: "index_account_users_on_agent_capacity_policy_id" t.index ["custom_role_id"], name: "index_account_users_on_custom_role_id" t.index ["user_id"], name: "index_account_users_on_user_id" end @@ -169,6 +171,22 @@ ActiveRecord::Schema[7.1].define(version: 2025_07_22_152516) do t.index ["views"], name: "index_articles_on_views" end + create_table "assignment_policies", force: :cascade do |t| + t.bigint "account_id", null: false + t.string "name", limit: 255, null: false + t.text "description" + t.integer "assignment_order", default: 0, null: false + t.integer "conversation_priority", default: 0, null: false + t.integer "fair_distribution_limit", default: 10, null: false + t.integer "fair_distribution_window", default: 3600, null: false + t.boolean "enabled", default: true, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id", "name"], name: "unique_assignment_policy_name_per_account", unique: true + t.index ["account_id"], name: "index_assignment_policies_on_account_id" + t.index ["enabled"], name: "index_assignment_policies_on_enabled" + end + create_table "attachments", id: :serial, force: :cascade do |t| t.integer "file_type", default: 0 t.string "external_url" @@ -720,6 +738,29 @@ ActiveRecord::Schema[7.1].define(version: 2025_07_22_152516) do t.index ["name", "account_id"], name: "index_email_templates_on_name_and_account_id", unique: true end + create_table "enterprise_agent_capacity_policies", force: :cascade do |t| + t.bigint "account_id", null: false + t.string "name", limit: 255, null: false + t.text "description" + t.jsonb "exclusion_rules", default: {}, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id", "name"], name: "unique_capacity_policy_name_per_account", unique: true + t.index ["account_id"], name: "index_enterprise_agent_capacity_policies_on_account_id" + t.index ["exclusion_rules"], name: "index_capacity_policies_on_exclusion_rules", using: :gin + end + + create_table "enterprise_inbox_capacity_limits", force: :cascade do |t| + t.bigint "agent_capacity_policy_id", null: false + t.bigint "inbox_id", null: false + t.integer "conversation_limit", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["agent_capacity_policy_id", "inbox_id"], name: "unique_policy_inbox_limit", unique: true + t.index ["agent_capacity_policy_id"], name: "index_inbox_limits_on_capacity_policy" + t.index ["inbox_id"], name: "index_enterprise_inbox_capacity_limits_on_inbox_id" + end + create_table "folders", force: :cascade do |t| t.integer "account_id", null: false t.integer "category_id", null: false @@ -728,6 +769,16 @@ ActiveRecord::Schema[7.1].define(version: 2025_07_22_152516) do t.datetime "updated_at", null: false end + create_table "inbox_assignment_policies", force: :cascade do |t| + t.bigint "inbox_id", null: false + t.bigint "assignment_policy_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["assignment_policy_id"], name: "index_inbox_assignment_policies_on_assignment_policy_id" + t.index ["inbox_id"], name: "index_inbox_assignment_policies_on_inbox_id" + t.index ["inbox_id"], name: "unique_inbox_assignment_policy", unique: true + end + create_table "inbox_members", id: :serial, force: :cascade do |t| t.integer "user_id", null: false t.integer "inbox_id", null: false @@ -800,6 +851,28 @@ ActiveRecord::Schema[7.1].define(version: 2025_07_22_152516) do t.index ["title", "account_id"], name: "index_labels_on_title_and_account_id", unique: true end + create_table "leaves", force: :cascade do |t| + t.bigint "account_id", null: false + t.bigint "account_user_id", null: false + t.date "start_date", null: false + t.date "end_date", null: false + t.integer "leave_type", default: 0, null: false + t.integer "status", default: 0, null: false + t.text "reason" + t.bigint "approved_by_id" + t.datetime "approved_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id", "status"], name: "index_leaves_on_account_and_status" + t.index ["account_id"], name: "index_leaves_on_account_id" + t.index ["account_user_id", "start_date", "end_date"], name: "index_leaves_on_account_user_and_dates" + t.index ["account_user_id"], name: "index_leaves_on_account_user_id" + t.index ["approved_by_id"], name: "index_leaves_on_approved_by_id" + t.index ["end_date"], name: "index_leaves_on_end_date" + t.index ["start_date"], name: "index_leaves_on_start_date" + t.index ["status"], name: "index_leaves_on_status" + end + create_table "macros", force: :cascade do |t| t.bigint "account_id", null: false t.string "name", null: false diff --git a/enterprise/app/models/enterprise/agent_capacity_policy.rb b/enterprise/app/models/enterprise/agent_capacity_policy.rb index 128244150..ef772f029 100644 --- a/enterprise/app/models/enterprise/agent_capacity_policy.rb +++ b/enterprise/app/models/enterprise/agent_capacity_policy.rb @@ -23,15 +23,15 @@ # module Enterprise - class AgentCapacityPolicy < ApplicationRecord + class AgentCapacityPolicy < ::ApplicationRecord include AccountCacheRevalidator self.table_name = 'enterprise_agent_capacity_policies' # Associations - belongs_to :account - has_many :account_users, dependent: :nullify, foreign_key: :agent_capacity_policy_id - has_many :users, through: :account_users + belongs_to :account, class_name: '::Account' + has_many :account_users, class_name: '::AccountUser', dependent: :nullify + has_many :users, through: :account_users, source: :user, class_name: '::User' has_many :inbox_capacity_limits, dependent: :destroy, class_name: 'Enterprise::InboxCapacityLimit' has_many :inboxes, through: :inbox_capacity_limits @@ -39,7 +39,7 @@ module Enterprise validates :name, presence: true, uniqueness: { scope: :account_id } validates :name, length: { maximum: 255 } validates :description, length: { maximum: 1000 } - validates :exclusion_rules, json: { schema: exclusion_rules_schema } + validate :validate_exclusion_rules_schema # Callbacks before_save :validate_inbox_access @@ -53,14 +53,14 @@ module Enterprise def add_user(user) # Find the account_user for this account and user account_user = account.account_users.find_by!(user: user) - + # Update the capacity policy reference account_user.update!(agent_capacity_policy_id: id) invalidate_user_capacity_cache(user) end def remove_user(user) - account_user = account.account_users.find_by(user: user) + account_user = account.account_users.find_by(user: user, agent_capacity_policy_id: id) account_user&.update!(agent_capacity_policy_id: nil) invalidate_user_capacity_cache(user) end @@ -69,7 +69,7 @@ module Enterprise inbox_capacity_limit = inbox_capacity_limits.find_or_initialize_by(inbox: inbox) inbox_capacity_limit.conversation_limit = limit inbox_capacity_limit.save! - + invalidate_inbox_capacity_cache(inbox) end @@ -95,15 +95,27 @@ module Enterprise private + def validate_exclusion_rules_schema + return if exclusion_rules.blank? + + schema = self.class.exclusion_rules_schema + schemer = JSONSchemer.schema(schema) + validation_errors = schemer.validate(exclusion_rules) + + validation_errors.each do |error| + errors.add(:exclusion_rules, error['error']) + end + end + def validate_inbox_access # Ensure all specified inboxes belong to the same account invalid_inboxes = inbox_capacity_limits.joins(:inbox) - .where.not(inboxes: { account_id: account_id }) + .where.not(inboxes: { account_id: account_id }) - if invalid_inboxes.exists? - errors.add(:inbox_capacity_limits, 'contains inboxes from different accounts') - throw :abort - end + return unless invalid_inboxes.exists? + + errors.add(:inbox_capacity_limits, 'contains inboxes from different accounts') + throw :abort end def invalidate_capacity_caches @@ -130,4 +142,4 @@ module Enterprise }.to_json end end -end \ No newline at end of file +end diff --git a/enterprise/app/models/enterprise/agent_capacity_policy_user.rb b/enterprise/app/models/enterprise/agent_capacity_policy_user.rb index d54d9ae43..586dce94e 100644 --- a/enterprise/app/models/enterprise/agent_capacity_policy_user.rb +++ b/enterprise/app/models/enterprise/agent_capacity_policy_user.rb @@ -21,12 +21,12 @@ # module Enterprise - class AgentCapacityPolicyUser < ApplicationRecord + class AgentCapacityPolicyUser < ::ApplicationRecord self.table_name = 'enterprise_agent_capacity_policy_users' # Associations belongs_to :agent_capacity_policy, class_name: 'Enterprise::AgentCapacityPolicy' - belongs_to :user + belongs_to :user, class_name: '::User' # Validations validates :user_id, uniqueness: true @@ -44,4 +44,4 @@ module Enterprise Rails.cache.delete_matched("assignment_v2:capacity:#{user_id}:*") end end -end \ No newline at end of file +end diff --git a/enterprise/app/models/enterprise/inbox_capacity_limit.rb b/enterprise/app/models/enterprise/inbox_capacity_limit.rb index e3a1fc42c..61938fb3a 100644 --- a/enterprise/app/models/enterprise/inbox_capacity_limit.rb +++ b/enterprise/app/models/enterprise/inbox_capacity_limit.rb @@ -24,14 +24,14 @@ # module Enterprise - class InboxCapacityLimit < ApplicationRecord + class InboxCapacityLimit < ::ApplicationRecord include AccountCacheRevalidator self.table_name = 'enterprise_inbox_capacity_limits' # Associations belongs_to :agent_capacity_policy, class_name: 'Enterprise::AgentCapacityPolicy' - belongs_to :inbox + belongs_to :inbox, class_name: '::Inbox' # Validations validates :agent_capacity_policy_id, uniqueness: { scope: :inbox_id } @@ -67,4 +67,4 @@ module Enterprise update_account_cache end end -end \ No newline at end of file +end diff --git a/lib/redis/alfred.rb b/lib/redis/alfred.rb index b3e156420..b26c53073 100644 --- a/lib/redis/alfred.rb +++ b/lib/redis/alfred.rb @@ -78,6 +78,11 @@ module Redis::Alfred $alfred.with { |conn| conn.hmget(key, *fields) } end + # get all fields and values from redis hash + def hgetall(key) + $alfred.with { |conn| conn.hgetall(key) } + end + # sorted set operations # add score and value for a key @@ -100,5 +105,15 @@ module Redis::Alfred def zremrangebyscore(key, range_start, range_end) $alfred.with { |conn| conn.zremrangebyscore(key, range_start, range_end) } end + + # transaction operations + + def multi(&) + $alfred.with { |conn| conn.multi(&) } + end + + def expire(key, expiry) + $alfred.with { |conn| conn.expire(key, expiry) } + end end end