diff --git a/app/builders/v2/reports/conversations/base_aggregator.rb b/app/builders/v2/reports/conversations/base_aggregator.rb new file mode 100644 index 000000000..f872d1d89 --- /dev/null +++ b/app/builders/v2/reports/conversations/base_aggregator.rb @@ -0,0 +1,13 @@ +class V2::Reports::Conversations::BaseAggregator < V2::Reports::Timeseries::BaseTimeseriesBuilder + def metrics + return {} if range.blank? + + compute_metrics + end + + private + + def compute_metrics + raise NotImplementedError, 'Subclasses must implement #compute_metrics' + end +end diff --git a/app/builders/v2/reports/conversations/better_metric_builder.rb b/app/builders/v2/reports/conversations/better_metric_builder.rb new file mode 100644 index 000000000..7d023a638 --- /dev/null +++ b/app/builders/v2/reports/conversations/better_metric_builder.rb @@ -0,0 +1,67 @@ +class V2::Reports::Conversations::BetterMetricBuilder < V2::Reports::Conversations::BaseReportBuilder + COUNT_METRICS = %i[ + conversations_count + incoming_messages_count + outgoing_messages_count + resolutions_count + bot_resolutions_count + bot_handoffs_count + ].freeze + + AVERAGE_METRICS = %i[ + avg_first_response_time + avg_resolution_time + reply_time + ].freeze + + SUMMARY_METRICS = %i[ + conversations_count + outgoing_messages_count + avg_first_response_time + avg_resolution_time + resolutions_count + reply_time + ].freeze + + BOT_SUMMARY_METRICS = %i[ + bot_resolutions_count + bot_handoffs_count + ].freeze + + AGGREGATOR_CLASSES = [ + V2::Reports::Conversations::ConversationsAggregator, + V2::Reports::Conversations::MessagesAggregator, + V2::Reports::Conversations::ReportingEventsAggregator + ].freeze + + def summary + result = SUMMARY_METRICS.index_with { |metric| fetch_metric(metric) } + result[:incoming_messages_count] = fetch_metric(:incoming_messages_count) unless params[:type] == :agent + result + end + + def bot_summary + BOT_SUMMARY_METRICS.index_with { |metric| fetch_metric(metric) } + end + + private + + def fetch_metric(key) + aggregated_metrics.fetch(key, default_for(key)) + end + + def aggregated_metrics + @aggregated_metrics ||= AGGREGATOR_CLASSES.each_with_object({}) do |aggregator_class, memo| + memo.merge!(aggregator_class.new(account, params).metrics) do |_metric, existing_value, new_value| + new_value.nil? ? existing_value : new_value + end + end + end + + def default_for(metric) + return 0 if COUNT_METRICS.include?(metric) + return nil if AVERAGE_METRICS.include?(metric) + + nil + end +end diff --git a/app/builders/v2/reports/conversations/conversations_aggregator.rb b/app/builders/v2/reports/conversations/conversations_aggregator.rb new file mode 100644 index 000000000..7ee609980 --- /dev/null +++ b/app/builders/v2/reports/conversations/conversations_aggregator.rb @@ -0,0 +1,16 @@ +class V2::Reports::Conversations::ConversationsAggregator < V2::Reports::Conversations::BaseAggregator + private + + def compute_metrics + { + conversations_count: base_relation.count + } + end + + def base_relation + scope.conversations + .where(account_id: account.id, created_at: range) + .unscope(:order) + .reorder(nil) + end +end diff --git a/app/builders/v2/reports/conversations/messages_aggregator.rb b/app/builders/v2/reports/conversations/messages_aggregator.rb new file mode 100644 index 000000000..b186a8a9e --- /dev/null +++ b/app/builders/v2/reports/conversations/messages_aggregator.rb @@ -0,0 +1,26 @@ +class V2::Reports::Conversations::MessagesAggregator < V2::Reports::Conversations::BaseAggregator + private + + def compute_metrics + incoming_count, outgoing_count = base_relation.pluck( + Arel.sql(count_sql(Message.message_types[:incoming])), + Arel.sql(count_sql(Message.message_types[:outgoing])) + ).first || [0, 0] + + { + incoming_messages_count: incoming_count || 0, + outgoing_messages_count: outgoing_count || 0 + } + end + + def base_relation + scope.messages + .where(account_id: account.id, created_at: range) + .unscope(:order) + .reorder(nil) + end + + def count_sql(message_type) + "COUNT(*) FILTER (WHERE message_type = #{message_type})" + end +end diff --git a/app/builders/v2/reports/conversations/reporting_events_aggregator.rb b/app/builders/v2/reports/conversations/reporting_events_aggregator.rb new file mode 100644 index 000000000..527cd2dda --- /dev/null +++ b/app/builders/v2/reports/conversations/reporting_events_aggregator.rb @@ -0,0 +1,61 @@ +class V2::Reports::Conversations::ReportingEventsAggregator < V2::Reports::Conversations::BaseAggregator + COUNT_MAPPINGS = { + resolutions_count: :conversation_resolved, + bot_resolutions_count: :conversation_bot_resolved, + bot_handoffs_count: :conversation_bot_handoff + }.freeze + + AVERAGE_MAPPINGS = { + avg_first_response_time: :first_response, + avg_resolution_time: :conversation_resolved, + reply_time: :reply_time + }.freeze + + private + + def compute_metrics + row = base_relation.pluck(*select_fragments).first || default_row + + counts_hash(row.first(COUNT_MAPPINGS.size)).merge(averages_hash(row.last(AVERAGE_MAPPINGS.size))) + end + + def counts_hash(values) + COUNT_MAPPINGS.keys.zip(values).to_h.transform_values { |value| value || 0 } + end + + def averages_hash(values) + AVERAGE_MAPPINGS.keys.zip(values).to_h + end + + def select_fragments + COUNT_MAPPINGS.map { |_metric, event_name| count_fragment(event_name) } + + AVERAGE_MAPPINGS.map { |_metric, event_name| average_fragment(event_name) } + end + + def count_fragment(event_name) + if event_name == :conversation_bot_handoff + Arel.sql("COUNT(DISTINCT CASE WHEN name = '#{event_name}' THEN conversation_id END)") + else + Arel.sql("COUNT(*) FILTER (WHERE name = '#{event_name}')") + end + end + + def average_fragment(event_name) + Arel.sql("AVG(CASE WHEN name = '#{event_name}' THEN #{value_column} END)") + end + + def base_relation + scope.reporting_events + .where(account_id: account.id, created_at: range) + .unscope(:order) + .reorder(nil) + end + + def value_column + params[:business_hours].present? ? 'value_in_business_hours' : 'value' + end + + def default_row + Array.new(COUNT_MAPPINGS.size, 0) + Array.new(AVERAGE_MAPPINGS.size) + end +end diff --git a/app/controllers/api/v2/accounts/reports_controller.rb b/app/controllers/api/v2/accounts/reports_controller.rb index 6e2d0ff4c..c4babd53d 100644 --- a/app/controllers/api/v2/accounts/reports_controller.rb +++ b/app/controllers/api/v2/accounts/reports_controller.rb @@ -125,7 +125,7 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController end def build_summary(method) - builder = V2::Reports::Conversations::MetricBuilder + builder = V2::Reports::Conversations::BetterMetricBuilder current_summary = builder.new(Current.account, current_summary_params).send(method) previous_summary = builder.new(Current.account, previous_summary_params).send(method) current_summary.merge(previous: previous_summary)