Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b6841388e |
@@ -144,7 +144,7 @@ jobs:
|
||||
# Backend tests with parallelization
|
||||
backend-tests:
|
||||
<<: *defaults
|
||||
parallelism: 20
|
||||
parallelism: 16
|
||||
steps:
|
||||
- checkout
|
||||
- node/install:
|
||||
|
||||
+2
-2
@@ -541,9 +541,9 @@ GEM
|
||||
net-smtp
|
||||
marcel (1.0.4)
|
||||
maxminddb (0.1.22)
|
||||
meta_request (0.8.5)
|
||||
meta_request (0.8.3)
|
||||
rack-contrib (>= 1.1, < 3)
|
||||
railties (>= 3.0.0, < 9)
|
||||
railties (>= 3.0.0, < 8)
|
||||
method_source (1.1.0)
|
||||
mime-types (3.4.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
|
||||
@@ -158,7 +158,6 @@ class Messages::Instagram::BaseMessageBuilder < Messages::Messenger::MessageBuil
|
||||
account_id: conversation.account_id,
|
||||
inbox_id: conversation.inbox_id,
|
||||
message_type: message_type,
|
||||
status: @outgoing_echo ? :delivered : :sent,
|
||||
source_id: message_identifier,
|
||||
content: message_content,
|
||||
sender: @outgoing_echo ? nil : contact,
|
||||
@@ -167,7 +166,6 @@ class Messages::Instagram::BaseMessageBuilder < Messages::Messenger::MessageBuil
|
||||
}
|
||||
}
|
||||
|
||||
params[:content_attributes][:external_echo] = true if @outgoing_echo
|
||||
params[:content_attributes][:is_unsupported] = true if message_is_unsupported?
|
||||
params
|
||||
end
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
class V2::Reports::Conversations::MetricBuilder < V2::Reports::Conversations::BaseReportBuilder
|
||||
HIDEABLE_METRICS = %w[incoming_messages_count outgoing_messages_count reply_time].freeze
|
||||
|
||||
def summary
|
||||
{
|
||||
conversations_count: count('conversations_count'),
|
||||
incoming_messages_count: count_unless_hidden('incoming_messages_count'),
|
||||
outgoing_messages_count: count_unless_hidden('outgoing_messages_count'),
|
||||
incoming_messages_count: count('incoming_messages_count'),
|
||||
outgoing_messages_count: count('outgoing_messages_count'),
|
||||
avg_first_response_time: count('avg_first_response_time'),
|
||||
avg_resolution_time: count('avg_resolution_time'),
|
||||
resolutions_count: count('resolutions_count'),
|
||||
reply_time: count_unless_hidden('reply_time')
|
||||
}.compact
|
||||
reply_time: count('reply_time')
|
||||
}
|
||||
end
|
||||
|
||||
def bot_summary
|
||||
@@ -26,18 +24,6 @@ class V2::Reports::Conversations::MetricBuilder < V2::Reports::Conversations::Ba
|
||||
builder_class(metric).new(account, builder_params(metric)).aggregate_value
|
||||
end
|
||||
|
||||
def count_unless_hidden(metric)
|
||||
return nil if metric_hidden?(metric)
|
||||
|
||||
count(metric)
|
||||
end
|
||||
|
||||
def metric_hidden?(metric)
|
||||
return false unless HIDEABLE_METRICS.include?(metric)
|
||||
|
||||
(params[:hidden_metrics] || []).include?(metric)
|
||||
end
|
||||
|
||||
def builder_params(metric)
|
||||
params.merge({ metric: metric })
|
||||
end
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
class V2::Reports::FirstResponseTimeDistributionBuilder
|
||||
include DateRangeHelper
|
||||
|
||||
attr_reader :account, :params
|
||||
|
||||
def initialize(account:, params:)
|
||||
@account = account
|
||||
@params = params
|
||||
end
|
||||
|
||||
def build
|
||||
build_distribution
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_distribution
|
||||
results = fetch_aggregated_counts
|
||||
map_to_channel_types(results)
|
||||
end
|
||||
|
||||
def fetch_aggregated_counts
|
||||
ReportingEvent
|
||||
.where(account_id: account.id, name: 'first_response')
|
||||
.where(range_condition)
|
||||
.group(:inbox_id)
|
||||
.select(
|
||||
:inbox_id,
|
||||
bucket_case_statements
|
||||
)
|
||||
end
|
||||
|
||||
def bucket_case_statements
|
||||
<<~SQL.squish
|
||||
COUNT(CASE WHEN value < 3600 THEN 1 END) AS bucket_0_1h,
|
||||
COUNT(CASE WHEN value >= 3600 AND value < 14400 THEN 1 END) AS bucket_1_4h,
|
||||
COUNT(CASE WHEN value >= 14400 AND value < 28800 THEN 1 END) AS bucket_4_8h,
|
||||
COUNT(CASE WHEN value >= 28800 AND value < 86400 THEN 1 END) AS bucket_8_24h,
|
||||
COUNT(CASE WHEN value >= 86400 THEN 1 END) AS bucket_24h_plus
|
||||
SQL
|
||||
end
|
||||
|
||||
def range_condition
|
||||
range.present? ? { created_at: range } : {}
|
||||
end
|
||||
|
||||
def inbox_channel_types
|
||||
@inbox_channel_types ||= account.inboxes.pluck(:id, :channel_type).to_h
|
||||
end
|
||||
|
||||
def map_to_channel_types(results)
|
||||
results.each_with_object({}) do |row, hash|
|
||||
channel_type = inbox_channel_types[row.inbox_id]
|
||||
next unless channel_type
|
||||
|
||||
hash[channel_type] ||= empty_buckets
|
||||
hash[channel_type]['0-1h'] += row.bucket_0_1h
|
||||
hash[channel_type]['1-4h'] += row.bucket_1_4h
|
||||
hash[channel_type]['4-8h'] += row.bucket_4_8h
|
||||
hash[channel_type]['8-24h'] += row.bucket_8_24h
|
||||
hash[channel_type]['24h+'] += row.bucket_24h_plus
|
||||
end
|
||||
end
|
||||
|
||||
def empty_buckets
|
||||
{ '0-1h' => 0, '1-4h' => 0, '4-8h' => 0, '8-24h' => 0, '24h+' => 0 }
|
||||
end
|
||||
end
|
||||
@@ -1,79 +0,0 @@
|
||||
class V2::Reports::OutgoingMessagesCountBuilder
|
||||
include DateRangeHelper
|
||||
attr_reader :account, :params
|
||||
|
||||
def initialize(account, params)
|
||||
@account = account
|
||||
@params = params
|
||||
end
|
||||
|
||||
def build
|
||||
send("build_by_#{params[:group_by]}")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def base_messages
|
||||
account.messages.outgoing.unscope(:order).where(created_at: range)
|
||||
end
|
||||
|
||||
def build_by_agent
|
||||
counts = base_messages
|
||||
.where(sender_type: 'User')
|
||||
.where.not(sender_id: nil)
|
||||
.group(:sender_id)
|
||||
.count
|
||||
|
||||
user_names = account.users.where(id: counts.keys).index_by(&:id)
|
||||
|
||||
counts.map do |user_id, count|
|
||||
user = user_names[user_id]
|
||||
{ id: user_id, name: user&.name, outgoing_messages_count: count }
|
||||
end
|
||||
end
|
||||
|
||||
def build_by_team
|
||||
counts = base_messages
|
||||
.joins('INNER JOIN conversations ON messages.conversation_id = conversations.id')
|
||||
.where.not(conversations: { team_id: nil })
|
||||
.group('conversations.team_id')
|
||||
.count
|
||||
|
||||
team_names = account.teams.where(id: counts.keys).index_by(&:id)
|
||||
|
||||
counts.map do |team_id, count|
|
||||
team = team_names[team_id]
|
||||
{ id: team_id, name: team&.name, outgoing_messages_count: count }
|
||||
end
|
||||
end
|
||||
|
||||
def build_by_inbox
|
||||
counts = base_messages
|
||||
.group(:inbox_id)
|
||||
.count
|
||||
|
||||
inbox_names = account.inboxes.where(id: counts.keys).index_by(&:id)
|
||||
|
||||
counts.map do |inbox_id, count|
|
||||
inbox = inbox_names[inbox_id]
|
||||
{ id: inbox_id, name: inbox&.name, outgoing_messages_count: count }
|
||||
end
|
||||
end
|
||||
|
||||
def build_by_label
|
||||
counts = base_messages
|
||||
.joins('INNER JOIN conversations ON messages.conversation_id = conversations.id')
|
||||
.joins("INNER JOIN taggings ON taggings.taggable_id = conversations.id
|
||||
AND taggings.taggable_type = 'Conversation' AND taggings.context = 'labels'")
|
||||
.joins('INNER JOIN tags ON tags.id = taggings.tag_id')
|
||||
.group('tags.name')
|
||||
.count
|
||||
|
||||
label_ids = account.labels.where(title: counts.keys).index_by(&:title)
|
||||
|
||||
counts.map do |label_name, count|
|
||||
label = label_ids[label_name]
|
||||
{ id: label&.id, name: label_name, outgoing_messages_count: count }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -70,10 +70,8 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
|
||||
def transcript
|
||||
render json: { error: 'email param missing' }, status: :unprocessable_entity and return if params[:email].blank?
|
||||
return head :too_many_requests unless @conversation.account.within_email_rate_limit?
|
||||
|
||||
ConversationReplyMailer.with(account: @conversation.account).conversation_transcript(@conversation, params[:email])&.deliver_later
|
||||
@conversation.account.increment_email_sent_count
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
||||
@@ -35,9 +35,12 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||
end
|
||||
|
||||
def transcript
|
||||
return head :too_many_requests unless conversation.present? && conversation.account.within_email_rate_limit?
|
||||
|
||||
send_transcript_email
|
||||
if conversation.present? && conversation.contact.present? && conversation.contact.email.present?
|
||||
ConversationReplyMailer.with(account: conversation.account).conversation_transcript(
|
||||
conversation,
|
||||
conversation.contact.email
|
||||
)&.deliver_later
|
||||
end
|
||||
head :ok
|
||||
end
|
||||
|
||||
@@ -74,16 +77,6 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||
|
||||
private
|
||||
|
||||
def send_transcript_email
|
||||
return if conversation.contact&.email.blank?
|
||||
|
||||
ConversationReplyMailer.with(account: conversation.account).conversation_transcript(
|
||||
conversation,
|
||||
conversation.contact.email
|
||||
)&.deliver_later
|
||||
conversation.account.increment_email_sent_count
|
||||
end
|
||||
|
||||
def trigger_typing_event(event)
|
||||
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: conversation, user: @contact)
|
||||
end
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||
include Api::V2::Accounts::ReportsHelper
|
||||
include Api::V2::Accounts::HeatmapHelper
|
||||
include Reports::MetricFilter
|
||||
|
||||
before_action :check_authorization
|
||||
|
||||
def index
|
||||
# Return empty timeseries array for hidden metrics (preserves expected response shape)
|
||||
return render json: [] if metric_hidden?(params[:metric])
|
||||
|
||||
builder = V2::Reports::Conversations::ReportBuilder.new(Current.account, report_params)
|
||||
data = builder.timeseries
|
||||
render json: data
|
||||
@@ -74,23 +70,6 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||
render json: builder.build
|
||||
end
|
||||
|
||||
def first_response_time_distribution
|
||||
builder = V2::Reports::FirstResponseTimeDistributionBuilder.new(
|
||||
account: Current.account,
|
||||
params: first_response_time_distribution_params
|
||||
)
|
||||
render json: builder.build
|
||||
end
|
||||
|
||||
OUTGOING_MESSAGES_ALLOWED_GROUP_BY = %w[agent team inbox label].freeze
|
||||
|
||||
def outgoing_messages_count
|
||||
return head :unprocessable_entity unless OUTGOING_MESSAGES_ALLOWED_GROUP_BY.include?(params[:group_by])
|
||||
|
||||
builder = V2::Reports::OutgoingMessagesCountBuilder.new(Current.account, outgoing_messages_count_params)
|
||||
render json: builder.build
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_csv(filename, template)
|
||||
@@ -116,8 +95,7 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||
common_params.merge({
|
||||
since: range[:current][:since],
|
||||
until: range[:current][:until],
|
||||
timezone_offset: params[:timezone_offset],
|
||||
hidden_metrics: hidden_metrics
|
||||
timezone_offset: params[:timezone_offset]
|
||||
})
|
||||
end
|
||||
|
||||
@@ -125,8 +103,7 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||
common_params.merge({
|
||||
since: range[:previous][:since],
|
||||
until: range[:previous][:until],
|
||||
timezone_offset: params[:timezone_offset],
|
||||
hidden_metrics: hidden_metrics
|
||||
timezone_offset: params[:timezone_offset]
|
||||
})
|
||||
end
|
||||
|
||||
@@ -179,19 +156,4 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||
label_ids: params[:label_ids]
|
||||
}
|
||||
end
|
||||
|
||||
def first_response_time_distribution_params
|
||||
{
|
||||
since: params[:since],
|
||||
until: params[:until]
|
||||
}
|
||||
end
|
||||
|
||||
def outgoing_messages_count_params
|
||||
{
|
||||
group_by: params[:group_by],
|
||||
since: params[:since],
|
||||
until: params[:until]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseController
|
||||
include Reports::MetricFilter
|
||||
|
||||
before_action :check_authorization
|
||||
before_action :prepare_builder_params, only: [:agent, :team, :inbox, :label, :channel]
|
||||
|
||||
@@ -42,7 +40,7 @@ class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseContr
|
||||
|
||||
def render_report_with(builder_class)
|
||||
builder = builder_class.new(account: Current.account, params: @builder_params)
|
||||
render json: filter_hidden_metrics_from_array(builder.build)
|
||||
render json: builder.build
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
module Reports::MetricFilter
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
HIDEABLE_METRICS = %w[outgoing_messages_count incoming_messages_count reply_time].freeze
|
||||
|
||||
# Alias mapping (canonical form)
|
||||
METRIC_ALIASES = {
|
||||
'avg_reply_time' => 'reply_time'
|
||||
}.freeze
|
||||
|
||||
private
|
||||
|
||||
def filter_hidden_metrics_from_hash(data)
|
||||
return data if hidden_metrics.blank?
|
||||
|
||||
data.reject { |key, _| metric_key_hidden?(key) }
|
||||
end
|
||||
|
||||
def filter_hidden_metrics_from_array(data)
|
||||
return data if hidden_metrics.blank?
|
||||
|
||||
data.map { |item| item.reject { |key, _| metric_key_hidden?(key) } }
|
||||
end
|
||||
|
||||
# Filter summary data including nested 'previous' key (handles both symbol and string keys)
|
||||
def filter_summary_with_previous(data)
|
||||
return data if hidden_metrics.blank?
|
||||
|
||||
filtered = filter_hidden_metrics_from_hash(data)
|
||||
previous_data = filtered[:previous] || filtered['previous']
|
||||
if previous_data.present?
|
||||
filtered_previous = filter_hidden_metrics_from_hash(previous_data)
|
||||
# Preserve the original key type
|
||||
if filtered.key?(:previous)
|
||||
filtered[:previous] = filtered_previous
|
||||
else
|
||||
filtered['previous'] = filtered_previous
|
||||
end
|
||||
end
|
||||
filtered
|
||||
end
|
||||
|
||||
def hidden_metrics
|
||||
@hidden_metrics ||= (Current.account.report_hidden_metrics || []) & HIDEABLE_METRICS
|
||||
end
|
||||
|
||||
def metric_hidden?(metric_name)
|
||||
# Normalize to canonical form (e.g., avg_reply_time -> reply_time)
|
||||
normalized = METRIC_ALIASES[metric_name.to_s] || metric_name.to_s
|
||||
hidden_metrics.include?(normalized)
|
||||
end
|
||||
|
||||
# Check if a hash key should be hidden (handles both symbol and string keys, plus aliases)
|
||||
def metric_key_hidden?(key)
|
||||
key_str = key.to_s
|
||||
normalized = METRIC_ALIASES[key_str] || key_str
|
||||
hidden_metrics.include?(normalized)
|
||||
end
|
||||
end
|
||||
@@ -42,7 +42,7 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
|
||||
'facebook' => %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET IG_VERIFY_TOKEN FACEBOOK_API_VERSION ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT],
|
||||
'shopify' => %w[SHOPIFY_CLIENT_ID SHOPIFY_CLIENT_SECRET],
|
||||
'microsoft' => %w[AZURE_APP_ID AZURE_APP_SECRET],
|
||||
'email' => %w[MAILER_INBOUND_EMAIL_DOMAIN ACCOUNT_EMAILS_LIMIT ACCOUNT_EMAILS_PLAN_LIMITS],
|
||||
'email' => ['MAILER_INBOUND_EMAIL_DOMAIN'],
|
||||
'linear' => %w[LINEAR_CLIENT_ID LINEAR_CLIENT_SECRET],
|
||||
'slack' => %w[SLACK_CLIENT_ID SLACK_CLIENT_SECRET],
|
||||
'instagram' => %w[INSTAGRAM_APP_ID INSTAGRAM_APP_SECRET INSTAGRAM_VERIFY_TOKEN INSTAGRAM_API_VERSION ENABLE_INSTAGRAM_CHANNEL_HUMAN_AGENT],
|
||||
|
||||
+1
-3
@@ -18,9 +18,7 @@ const dialogRef = ref(null);
|
||||
|
||||
const uiFlags = useMapGetter('captainResponses/getUIFlags');
|
||||
const responses = useMapGetter('captainResponses/getRecords');
|
||||
const meta = useMapGetter('captainResponses/getMeta');
|
||||
const isFetching = computed(() => uiFlags.value.fetchingList);
|
||||
const totalCount = computed(() => meta.value.totalCount || 0);
|
||||
|
||||
const handleClose = () => {
|
||||
emit('close');
|
||||
@@ -39,7 +37,7 @@ defineExpose({ dialogRef });
|
||||
<Dialog
|
||||
ref="dialogRef"
|
||||
type="edit"
|
||||
:title="`${t('CAPTAIN.DOCUMENTS.RELATED_RESPONSES.TITLE')} (${totalCount})`"
|
||||
:title="t('CAPTAIN.DOCUMENTS.RELATED_RESPONSES.TITLE')"
|
||||
:description="t('CAPTAIN.DOCUMENTS.RELATED_RESPONSES.DESCRIPTION')"
|
||||
:show-cancel-button="false"
|
||||
:show-confirm-button="false"
|
||||
|
||||
@@ -3,14 +3,12 @@ import { onMounted, computed, ref, toRefs } from 'vue';
|
||||
import { useTimeoutFn } from '@vueuse/core';
|
||||
import { provideMessageContext } from './provider.js';
|
||||
import { useTrack } from 'dashboard/composables';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||
import { ACCOUNT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||
import { getInboxIconByType } from 'dashboard/helper/inbox';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import {
|
||||
MESSAGE_TYPES,
|
||||
@@ -141,8 +139,6 @@ const showBackgroundHighlight = ref(false);
|
||||
const showContextMenu = ref(false);
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const inboxGetter = useMapGetter('inboxes/getInbox');
|
||||
const inbox = computed(() => inboxGetter.value(props.inboxId) || {});
|
||||
|
||||
/**
|
||||
* Computes the message variant based on props
|
||||
@@ -166,10 +162,6 @@ const variant = computed(() => {
|
||||
if (props.contentAttributes?.isUnsupported)
|
||||
return MESSAGE_VARIANTS.UNSUPPORTED;
|
||||
|
||||
if (props.contentAttributes?.externalEcho) {
|
||||
return MESSAGE_VARIANTS.AGENT;
|
||||
}
|
||||
|
||||
const isBot = !props.sender || props.sender.type === SENDER_TYPES.AGENT_BOT;
|
||||
if (isBot && props.messageType === MESSAGE_TYPES.OUTGOING) {
|
||||
return MESSAGE_VARIANTS.BOT;
|
||||
@@ -432,18 +424,6 @@ function handleReplyTo() {
|
||||
}
|
||||
|
||||
const avatarInfo = computed(() => {
|
||||
if (props.contentAttributes?.externalEcho) {
|
||||
const { name, avatar_url, channel_type, medium } = inbox.value;
|
||||
const iconName = avatar_url
|
||||
? null
|
||||
: getInboxIconByType(channel_type, medium);
|
||||
return {
|
||||
name: iconName ? '' : name || t('CONVERSATION.NATIVE_APP'),
|
||||
src: avatar_url || '',
|
||||
iconName,
|
||||
};
|
||||
}
|
||||
|
||||
// If no sender, return bot info
|
||||
if (!props.sender) {
|
||||
return {
|
||||
@@ -471,9 +451,6 @@ const avatarInfo = computed(() => {
|
||||
});
|
||||
|
||||
const avatarTooltip = computed(() => {
|
||||
if (props.contentAttributes?.externalEcho) {
|
||||
return t('CONVERSATION.NATIVE_APP_ADVISORY');
|
||||
}
|
||||
if (avatarInfo.value.name === '') return '';
|
||||
return `${t('CONVERSATION.SENT_BY')} ${avatarInfo.value.name}`;
|
||||
});
|
||||
@@ -507,7 +484,7 @@ provideMessageContext({
|
||||
<div
|
||||
v-if="shouldRenderMessage"
|
||||
:id="`message${props.id}`"
|
||||
class="flex w-full mb-2 message-bubble-container"
|
||||
class="flex mb-2 w-full message-bubble-container"
|
||||
:data-message-id="props.id"
|
||||
:class="[
|
||||
flexOrientationClass,
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { computed } from 'vue';
|
||||
import { useAccount } from './useAccount';
|
||||
|
||||
// Alias mapping for consistency (canonical form is reply_time)
|
||||
const METRIC_ALIASES = {
|
||||
avg_reply_time: 'reply_time',
|
||||
};
|
||||
|
||||
export function useHiddenMetrics() {
|
||||
const { currentAccount } = useAccount();
|
||||
|
||||
const hiddenMetrics = computed(() => {
|
||||
return currentAccount.value?.settings?.report_hidden_metrics || [];
|
||||
});
|
||||
|
||||
const isMetricHidden = metricKey => {
|
||||
const normalized = METRIC_ALIASES[metricKey] || metricKey;
|
||||
return hiddenMetrics.value.includes(normalized);
|
||||
};
|
||||
|
||||
const filterVisibleReportKeys = reportKeys => {
|
||||
// Filter out hidden metrics from reportKeys object
|
||||
return Object.fromEntries(
|
||||
Object.entries(reportKeys).filter(([, value]) => !isMetricHidden(value))
|
||||
);
|
||||
};
|
||||
|
||||
return { hiddenMetrics, isMetricHidden, filterVisibleReportKeys };
|
||||
}
|
||||
@@ -127,7 +127,6 @@ const validateSingleAction = action => {
|
||||
'resolve_conversation',
|
||||
'remove_assigned_team',
|
||||
'open_conversation',
|
||||
'pending_conversation',
|
||||
];
|
||||
|
||||
if (
|
||||
|
||||
@@ -150,8 +150,7 @@
|
||||
"ADD_PRIVATE_NOTE": "Add a Private Note",
|
||||
"CHANGE_PRIORITY": "Change Priority",
|
||||
"ADD_SLA": "Add SLA",
|
||||
"OPEN_CONVERSATION": "Open conversation",
|
||||
"PENDING_CONVERSATION": "Mark conversation as pending"
|
||||
"OPEN_CONVERSATION": "Open conversation"
|
||||
},
|
||||
"MESSAGE_TYPES": {
|
||||
"INCOMING": "Incoming Message",
|
||||
|
||||
@@ -253,8 +253,6 @@
|
||||
"MESSAGE_ERROR": "Unable to send this message, please try again later",
|
||||
"SENT_BY": "Sent by:",
|
||||
"BOT": "Bot",
|
||||
"NATIVE_APP": "Native app",
|
||||
"NATIVE_APP_ADVISORY": "This message was sent from the native app. Reply from Chatwoot to maintain the message window.",
|
||||
"SEND_FAILED": "Couldn't send message! Try again",
|
||||
"TRY_AGAIN": "retry",
|
||||
"ASSIGNMENT": {
|
||||
|
||||
@@ -116,10 +116,6 @@ export const AUTOMATIONS = {
|
||||
key: 'open_conversation',
|
||||
name: 'OPEN_CONVERSATION',
|
||||
},
|
||||
{
|
||||
key: 'pending_conversation',
|
||||
name: 'PENDING_CONVERSATION',
|
||||
},
|
||||
{
|
||||
key: 'resolve_conversation',
|
||||
name: 'RESOLVE_CONVERSATION',
|
||||
@@ -236,10 +232,6 @@ export const AUTOMATIONS = {
|
||||
key: 'snooze_conversation',
|
||||
name: 'SNOOZE_CONVERSATION',
|
||||
},
|
||||
{
|
||||
key: 'pending_conversation',
|
||||
name: 'PENDING_CONVERSATION',
|
||||
},
|
||||
{
|
||||
key: 'resolve_conversation',
|
||||
name: 'RESOLVE_CONVERSATION',
|
||||
@@ -368,10 +360,6 @@ export const AUTOMATIONS = {
|
||||
key: 'snooze_conversation',
|
||||
name: 'SNOOZE_CONVERSATION',
|
||||
},
|
||||
{
|
||||
key: 'pending_conversation',
|
||||
name: 'PENDING_CONVERSATION',
|
||||
},
|
||||
{
|
||||
key: 'resolve_conversation',
|
||||
name: 'RESOLVE_CONVERSATION',
|
||||
@@ -494,10 +482,6 @@ export const AUTOMATIONS = {
|
||||
key: 'snooze_conversation',
|
||||
name: 'SNOOZE_CONVERSATION',
|
||||
},
|
||||
{
|
||||
key: 'pending_conversation',
|
||||
name: 'PENDING_CONVERSATION',
|
||||
},
|
||||
{
|
||||
key: 'send_webhook_event',
|
||||
name: 'SEND_WEBHOOK_EVENT',
|
||||
@@ -684,11 +668,6 @@ export const AUTOMATION_ACTION_TYPES = [
|
||||
label: 'OPEN_CONVERSATION',
|
||||
inputType: null,
|
||||
},
|
||||
{
|
||||
key: 'pending_conversation',
|
||||
label: 'PENDING_CONVERSATION',
|
||||
inputType: null,
|
||||
},
|
||||
{
|
||||
key: 'send_webhook_event',
|
||||
label: 'SEND_WEBHOOK_EVENT',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script>
|
||||
import V4Button from 'dashboard/components-next/button/Button.vue';
|
||||
import { useAlert, useTrack } from 'dashboard/composables';
|
||||
import { useHiddenMetrics } from 'dashboard/composables/useHiddenMetrics';
|
||||
import ReportFilterSelector from './components/FilterSelector.vue';
|
||||
import { GROUP_BY_FILTER } from './constants';
|
||||
import { REPORTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||
@@ -9,7 +8,7 @@ import { generateFileName } from 'dashboard/helper/downloadHelper';
|
||||
import ReportContainer from './ReportContainer.vue';
|
||||
import ReportHeader from './components/ReportHeader.vue';
|
||||
|
||||
const ALL_REPORTS_KEYS = {
|
||||
const REPORTS_KEYS = {
|
||||
CONVERSATIONS: 'conversations_count',
|
||||
INCOMING_MESSAGES: 'incoming_messages_count',
|
||||
OUTGOING_MESSAGES: 'outgoing_messages_count',
|
||||
@@ -27,10 +26,6 @@ export default {
|
||||
ReportContainer,
|
||||
V4Button,
|
||||
},
|
||||
setup() {
|
||||
const { filterVisibleReportKeys } = useHiddenMetrics();
|
||||
return { filterVisibleReportKeys };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
from: 0,
|
||||
@@ -39,11 +34,6 @@ export default {
|
||||
businessHours: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
reportKeys() {
|
||||
return this.filterVisibleReportKeys(ALL_REPORTS_KEYS);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
fetchAllData() {
|
||||
this.fetchAccountSummary();
|
||||
@@ -57,10 +47,18 @@ export default {
|
||||
}
|
||||
},
|
||||
fetchChartData() {
|
||||
Object.keys(this.reportKeys).forEach(async key => {
|
||||
[
|
||||
'CONVERSATIONS',
|
||||
'INCOMING_MESSAGES',
|
||||
'OUTGOING_MESSAGES',
|
||||
'FIRST_RESPONSE_TIME',
|
||||
'RESOLUTION_TIME',
|
||||
'RESOLUTION_COUNT',
|
||||
'REPLY_TIME',
|
||||
].forEach(async key => {
|
||||
try {
|
||||
await this.$store.dispatch('fetchAccountReport', {
|
||||
metric: this.reportKeys[key],
|
||||
metric: REPORTS_KEYS[key],
|
||||
...this.getRequestPayload(),
|
||||
});
|
||||
} catch {
|
||||
@@ -123,6 +121,6 @@ export default {
|
||||
show-group-by-filter
|
||||
@filter-change="onFilterChange"
|
||||
/>
|
||||
<ReportContainer :group-by="groupBy" :report-keys="reportKeys" />
|
||||
<ReportContainer :group-by="groupBy" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+32
-38
@@ -2,7 +2,6 @@
|
||||
import ReportFilterSelector from './FilterSelector.vue';
|
||||
import { formatTime } from '@chatwoot/utils';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useHiddenMetrics } from 'dashboard/composables/useHiddenMetrics';
|
||||
import Table from 'dashboard/components/table/Table.vue';
|
||||
import { generateFileName } from 'dashboard/helper/downloadHelper';
|
||||
import {
|
||||
@@ -45,7 +44,6 @@ import SummaryReportLink from './SummaryReportLink.vue';
|
||||
|
||||
const rowItems = useMapGetter([props.getterKey]) || [];
|
||||
const reportMetrics = useMapGetter([props.summaryKey]) || [];
|
||||
const { isMetricHidden } = useHiddenMetrics();
|
||||
|
||||
const getMetrics = id =>
|
||||
reportMetrics.value.find(metrics => metrics.id === Number(id)) || {};
|
||||
@@ -61,42 +59,38 @@ const defaulSpanRender = cellProps =>
|
||||
cellProps.getValue()
|
||||
);
|
||||
|
||||
const columns = computed(() => {
|
||||
const allColumns = [
|
||||
columnHelper.accessor('name', {
|
||||
header: t(`SUMMARY_REPORTS.${props.type.toUpperCase()}`),
|
||||
width: 300,
|
||||
cell: cellProps => h(SummaryReportLink, cellProps),
|
||||
}),
|
||||
columnHelper.accessor('conversationsCount', {
|
||||
header: t('SUMMARY_REPORTS.CONVERSATIONS'),
|
||||
width: 200,
|
||||
cell: defaulSpanRender,
|
||||
}),
|
||||
columnHelper.accessor('avgFirstResponseTime', {
|
||||
header: t('SUMMARY_REPORTS.AVG_FIRST_RESPONSE_TIME'),
|
||||
width: 200,
|
||||
cell: defaulSpanRender,
|
||||
}),
|
||||
columnHelper.accessor('avgResolutionTime', {
|
||||
header: t('SUMMARY_REPORTS.AVG_RESOLUTION_TIME'),
|
||||
width: 200,
|
||||
cell: defaulSpanRender,
|
||||
}),
|
||||
!isMetricHidden('reply_time') &&
|
||||
columnHelper.accessor('avgReplyTime', {
|
||||
header: t('SUMMARY_REPORTS.AVG_REPLY_TIME'),
|
||||
width: 200,
|
||||
cell: defaulSpanRender,
|
||||
}),
|
||||
columnHelper.accessor('resolutionsCount', {
|
||||
header: t('SUMMARY_REPORTS.RESOLUTION_COUNT'),
|
||||
width: 200,
|
||||
cell: defaulSpanRender,
|
||||
}),
|
||||
];
|
||||
return allColumns.filter(Boolean);
|
||||
});
|
||||
const columns = computed(() => [
|
||||
columnHelper.accessor('name', {
|
||||
header: t(`SUMMARY_REPORTS.${props.type.toUpperCase()}`),
|
||||
width: 300,
|
||||
cell: cellProps => h(SummaryReportLink, cellProps),
|
||||
}),
|
||||
columnHelper.accessor('conversationsCount', {
|
||||
header: t('SUMMARY_REPORTS.CONVERSATIONS'),
|
||||
width: 200,
|
||||
cell: defaulSpanRender,
|
||||
}),
|
||||
columnHelper.accessor('avgFirstResponseTime', {
|
||||
header: t('SUMMARY_REPORTS.AVG_FIRST_RESPONSE_TIME'),
|
||||
width: 200,
|
||||
cell: defaulSpanRender,
|
||||
}),
|
||||
columnHelper.accessor('avgResolutionTime', {
|
||||
header: t('SUMMARY_REPORTS.AVG_RESOLUTION_TIME'),
|
||||
width: 200,
|
||||
cell: defaulSpanRender,
|
||||
}),
|
||||
columnHelper.accessor('avgReplyTime', {
|
||||
header: t('SUMMARY_REPORTS.AVG_REPLY_TIME'),
|
||||
width: 200,
|
||||
cell: defaulSpanRender,
|
||||
}),
|
||||
columnHelper.accessor('resolutionsCount', {
|
||||
header: t('SUMMARY_REPORTS.RESOLUTION_COUNT'),
|
||||
width: 200,
|
||||
cell: defaulSpanRender,
|
||||
}),
|
||||
]);
|
||||
|
||||
const renderAvgTime = value => (value ? formatTime(value) : '--');
|
||||
|
||||
|
||||
+1
-8
@@ -1,7 +1,6 @@
|
||||
<script>
|
||||
import V4Button from 'dashboard/components-next/button/Button.vue';
|
||||
import { useAlert, useTrack } from 'dashboard/composables';
|
||||
import { useHiddenMetrics } from 'dashboard/composables/useHiddenMetrics';
|
||||
import ReportFilters from './ReportFilters.vue';
|
||||
import ReportContainer from '../ReportContainer.vue';
|
||||
import { GROUP_BY_FILTER } from '../constants';
|
||||
@@ -64,10 +63,6 @@ export default {
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { filterVisibleReportKeys } = useHiddenMetrics();
|
||||
return { filterVisibleReportKeys };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
from: 0,
|
||||
@@ -87,7 +82,7 @@ export default {
|
||||
return this.type === 'agent';
|
||||
},
|
||||
reportKeys() {
|
||||
const allKeys = {
|
||||
return {
|
||||
CONVERSATIONS: 'conversations_count',
|
||||
...(!this.isAgentType && {
|
||||
INCOMING_MESSAGES: 'incoming_messages_count',
|
||||
@@ -98,8 +93,6 @@ export default {
|
||||
RESOLUTION_COUNT: 'resolutions_count',
|
||||
REPLY_TIME: 'reply_time',
|
||||
};
|
||||
// Filter out hidden metrics
|
||||
return this.filterVisibleReportKeys(allKeys);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@@ -3,7 +3,6 @@ class ConversationReplyEmailJob < ApplicationJob
|
||||
|
||||
def perform(conversation_id, last_queued_id)
|
||||
conversation = Conversation.find(conversation_id)
|
||||
return unless conversation.account.active?
|
||||
|
||||
if conversation.messages.incoming&.last&.content_type == 'incoming_email'
|
||||
ConversationReplyMailer.with(account: conversation.account).reply_without_summary(conversation, last_queued_id).deliver_later
|
||||
|
||||
@@ -54,7 +54,7 @@ class Webhooks::TiktokEventsJob < MutexApplicationJob
|
||||
# Receive real-time notifications if you send a message to a user.
|
||||
def im_send_msg
|
||||
# This can be either an echo message or a message sent directly via tiktok application
|
||||
::Tiktok::MessageService.new(channel: channel, content: content, outgoing_echo: true).perform
|
||||
::Tiktok::MessageService.new(channel: channel, content: content).perform
|
||||
end
|
||||
|
||||
# Receive real-time notifications if a user outside the European Economic Area (EEA), Switzerland, or the UK sends a message to you.
|
||||
|
||||
@@ -9,56 +9,6 @@ class Webhooks::WhatsappEventsJob < ApplicationJob
|
||||
return
|
||||
end
|
||||
|
||||
if message_echo_event?(params)
|
||||
handle_message_echo(channel, params)
|
||||
else
|
||||
handle_message_events(channel, params)
|
||||
end
|
||||
end
|
||||
|
||||
# Detects if the webhook is an SMB message echo event (message sent from WhatsApp Business app)
|
||||
# This is part of WhatsApp coexistence feature where businesses can respond from both
|
||||
# Chatwoot and the WhatsApp Business app, with messages synced to Chatwoot.
|
||||
#
|
||||
# Regular message payload (field: "messages"):
|
||||
# {
|
||||
# "entry": [{
|
||||
# "changes": [{
|
||||
# "field": "messages",
|
||||
# "value": {
|
||||
# "contacts": [{ "wa_id": "919745786257", "profile": { "name": "Customer" } }],
|
||||
# "messages": [{ "from": "919745786257", "id": "wamid...", "text": { "body": "Hello" } }]
|
||||
# }
|
||||
# }]
|
||||
# }]
|
||||
# }
|
||||
#
|
||||
# Echo message payload (field: "smb_message_echoes"):
|
||||
# {
|
||||
# "entry": [{
|
||||
# "changes": [{
|
||||
# "field": "smb_message_echoes",
|
||||
# "value": {
|
||||
# "message_echoes": [{ "from": "971545296927", "to": "919745786257", "id": "wamid...", "text": { "body": "Hi" } }]
|
||||
# }
|
||||
# }]
|
||||
# }]
|
||||
# }
|
||||
#
|
||||
# Key differences:
|
||||
# - field: "smb_message_echoes" instead of "messages"
|
||||
# - message_echoes[] instead of messages[]
|
||||
# - "from" is the business number, "to" is the contact (reversed from regular messages)
|
||||
# - No "contacts" array in echo payload
|
||||
def message_echo_event?(params)
|
||||
params.dig(:entry, 0, :changes, 0, :field) == 'smb_message_echoes'
|
||||
end
|
||||
|
||||
def handle_message_echo(channel, params)
|
||||
Whatsapp::IncomingMessageWhatsappCloudService.new(inbox: channel.inbox, params: params, outgoing_echo: true).perform
|
||||
end
|
||||
|
||||
def handle_message_events(channel, params)
|
||||
case channel.provider
|
||||
when 'whatsapp_cloud'
|
||||
Whatsapp::IncomingMessageWhatsappCloudService.new(inbox: channel.inbox, params: params).perform
|
||||
|
||||
@@ -38,7 +38,6 @@ class ConversationReplyMailer < ApplicationMailer
|
||||
return unless smtp_config_set_or_development?
|
||||
|
||||
init_conversation_attributes(message.conversation)
|
||||
|
||||
@message = message
|
||||
prepare_mail(true)
|
||||
end
|
||||
|
||||
@@ -29,7 +29,6 @@ class Account < ApplicationRecord
|
||||
include Featurable
|
||||
include CacheKeys
|
||||
include CaptainFeaturable
|
||||
include AccountEmailRateLimitable
|
||||
|
||||
SETTINGS_PARAMS_SCHEMA = {
|
||||
'type': 'object',
|
||||
@@ -67,13 +66,6 @@ class Account < ApplicationRecord
|
||||
'help_center_search': { 'type': %w[boolean null] }
|
||||
},
|
||||
'additionalProperties': false
|
||||
},
|
||||
'report_hidden_metrics': {
|
||||
'type': %w[array null],
|
||||
'items': {
|
||||
'type': 'string',
|
||||
'enum': %w[outgoing_messages_count incoming_messages_count reply_time]
|
||||
}
|
||||
}
|
||||
},
|
||||
'required': [],
|
||||
@@ -95,7 +87,6 @@ class Account < ApplicationRecord
|
||||
|
||||
store_accessor :settings, :audio_transcriptions, :auto_resolve_label
|
||||
store_accessor :settings, :captain_models, :captain_features
|
||||
store_accessor :settings, :report_hidden_metrics
|
||||
|
||||
has_many :account_users, dependent: :destroy_async
|
||||
has_many :agent_bot_inboxes, dependent: :destroy_async
|
||||
|
||||
@@ -41,8 +41,8 @@ class AutomationRule < ApplicationRecord
|
||||
|
||||
def actions_attributes
|
||||
%w[send_message add_label remove_label send_email_to_team assign_team assign_agent send_webhook_event mute_conversation
|
||||
send_attachment change_status resolve_conversation open_conversation pending_conversation snooze_conversation change_priority
|
||||
send_email_transcript add_private_note].freeze
|
||||
send_attachment change_status resolve_conversation open_conversation snooze_conversation change_priority send_email_transcript
|
||||
add_private_note].freeze
|
||||
end
|
||||
|
||||
def file_base_data
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
module AccountEmailRateLimitable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
OUTBOUND_EMAIL_TTL = 25.hours.to_i
|
||||
EMAIL_LIMIT_CONFIG_KEY = 'ACCOUNT_EMAILS_LIMIT'.freeze
|
||||
|
||||
def email_rate_limit
|
||||
account_limit || global_limit || default_limit
|
||||
end
|
||||
|
||||
def emails_sent_today
|
||||
Redis::Alfred.get(email_count_cache_key).to_i
|
||||
end
|
||||
|
||||
def within_email_rate_limit?
|
||||
return true if emails_sent_today < email_rate_limit
|
||||
|
||||
Rails.logger.warn("Account #{id} reached daily email rate limit of #{email_rate_limit}. Sent: #{emails_sent_today}")
|
||||
false
|
||||
end
|
||||
|
||||
def increment_email_sent_count
|
||||
Redis::Alfred.incr(email_count_cache_key).tap do |count|
|
||||
Redis::Alfred.expire(email_count_cache_key, OUTBOUND_EMAIL_TTL) if count == 1
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def email_count_cache_key
|
||||
@email_count_cache_key ||= format(
|
||||
Redis::Alfred::ACCOUNT_OUTBOUND_EMAIL_COUNT_KEY,
|
||||
account_id: id,
|
||||
date: Time.zone.today.to_s
|
||||
)
|
||||
end
|
||||
|
||||
def account_limit
|
||||
self[:limits]&.dig('emails')&.to_i
|
||||
end
|
||||
|
||||
def global_limit
|
||||
GlobalConfig.get(EMAIL_LIMIT_CONFIG_KEY)[EMAIL_LIMIT_CONFIG_KEY]&.to_i
|
||||
end
|
||||
|
||||
def default_limit
|
||||
ChatwootApp.max_limit.to_i
|
||||
end
|
||||
end
|
||||
@@ -344,11 +344,10 @@ class Message < ApplicationRecord
|
||||
# if the sender is not a user, it's not a human response
|
||||
# if automation rule id is present, it's not a human response
|
||||
# if campaign id is present, it's not a human response
|
||||
# external echo messages are responses sent from the native app (WhatsApp Business, Instagram)
|
||||
outgoing? &&
|
||||
content_attributes['automation_rule_id'].blank? &&
|
||||
additional_attributes['campaign_id'].blank? &&
|
||||
(sender.is_a?(User) || content_attributes['external_echo'].present?)
|
||||
sender.is_a?(User)
|
||||
end
|
||||
|
||||
def bot_response?
|
||||
|
||||
@@ -22,10 +22,6 @@ class ActionService
|
||||
@conversation.open!
|
||||
end
|
||||
|
||||
def pending_conversation(_params)
|
||||
@conversation.pending!
|
||||
end
|
||||
|
||||
def change_status(status)
|
||||
@conversation.update!(status: status[0])
|
||||
end
|
||||
|
||||
@@ -9,7 +9,8 @@ class Messages::MarkdownRendererService
|
||||
'Channel::Line' => :render_line,
|
||||
'Channel::TwitterProfile' => :render_plain_text,
|
||||
'Channel::Sms' => :render_plain_text,
|
||||
'Channel::TwilioSms' => :render_plain_text
|
||||
'Channel::TwilioSms' => :render_plain_text,
|
||||
'Channel::Api' => :render_api_message
|
||||
}.freeze
|
||||
|
||||
def initialize(content, channel_type, channel = nil)
|
||||
@@ -56,6 +57,11 @@ class Messages::MarkdownRendererService
|
||||
restore_multiple_newlines(result)
|
||||
end
|
||||
|
||||
def render_api_message
|
||||
# Convert literal \n strings to actual newlines for API channel messages
|
||||
@content.gsub('\\n', "\n")
|
||||
end
|
||||
|
||||
def render_whatsapp
|
||||
# Strip whitespace from whitespace-only lines to normalize newlines
|
||||
normalized_content = @content.gsub(/^[ \t]+$/m, '')
|
||||
|
||||
@@ -13,7 +13,6 @@ class Messages::SendEmailNotificationService
|
||||
return unless Redis::Alfred.set(conversation_mail_key, message.id, nx: true, ex: 1.hour.to_i)
|
||||
|
||||
ConversationReplyEmailJob.set(wait: 2.minutes).perform_later(conversation.id, message.id)
|
||||
message.account.increment_email_sent_count
|
||||
end
|
||||
|
||||
private
|
||||
@@ -21,7 +20,6 @@ class Messages::SendEmailNotificationService
|
||||
def should_send_email_notification?
|
||||
return false unless message.email_notifiable_message?
|
||||
return false if message.conversation.contact.email.blank?
|
||||
return false unless message.account.within_email_rate_limit?
|
||||
|
||||
email_reply_enabled?
|
||||
end
|
||||
|
||||
@@ -7,22 +7,15 @@ class Notification::EmailNotificationService
|
||||
# don't send emails if user is not confirmed
|
||||
return if notification.user.confirmed_at.nil?
|
||||
return unless user_subscribed_to_notification?
|
||||
return unless notification.account.within_email_rate_limit?
|
||||
|
||||
send_notification_email
|
||||
notification.account.increment_email_sent_count
|
||||
# TODO : Clean up whatever happening over here
|
||||
# Segregate the mailers properly
|
||||
AgentNotifications::ConversationNotificationsMailer.with(account: notification.account).public_send(notification
|
||||
.notification_type.to_s, notification.primary_actor, notification.user, notification.secondary_actor).deliver_later
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# TODO : Clean up whatever happening over here
|
||||
# Segregate the mailers properly
|
||||
def send_notification_email
|
||||
AgentNotifications::ConversationNotificationsMailer.with(account: notification.account).public_send(
|
||||
notification.notification_type.to_s, notification.primary_actor, notification.user, notification.secondary_actor
|
||||
).deliver_later
|
||||
end
|
||||
|
||||
def user_subscribed_to_notification?
|
||||
notification_setting = notification.user.notification_settings.find_by(account_id: notification.account.id)
|
||||
return true if notification_setting.public_send("email_#{notification.notification_type}?")
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
class Tiktok::MessageService
|
||||
include Tiktok::MessagingHelpers
|
||||
|
||||
pattr_initialize [:channel!, :content!, :outgoing_echo]
|
||||
pattr_initialize [:channel!, :content!]
|
||||
|
||||
def perform
|
||||
if outgoing_message?
|
||||
# Skip processing echo messages
|
||||
message = find_message(tt_conversation_id, tt_message_id)
|
||||
return if message.present?
|
||||
end
|
||||
@@ -38,7 +39,7 @@ class Tiktok::MessageService
|
||||
updated_at: tt_message_time
|
||||
)
|
||||
|
||||
message.sender = contact_inbox.contact if incoming_message? && !outgoing_echo
|
||||
message.sender = contact_inbox.contact if incoming_message?
|
||||
message.status = :delivered if outgoing_message?
|
||||
|
||||
create_message_attachments(message)
|
||||
@@ -90,7 +91,6 @@ class Tiktok::MessageService
|
||||
attributes = {}
|
||||
attributes[:in_reply_to_external_id] = tt_referenced_message_id if tt_referenced_message_id
|
||||
attributes[:is_unsupported] = true unless supported_message?
|
||||
attributes[:external_echo] = true if outgoing_echo
|
||||
attributes
|
||||
end
|
||||
|
||||
|
||||
@@ -61,36 +61,16 @@ class Whatsapp::FacebookApiClient
|
||||
end
|
||||
|
||||
def subscribe_waba_webhook(waba_id, callback_url, verify_token)
|
||||
# Step 1: Subscribe app to WABA first (required before override)
|
||||
# Meta requires the app to be subscribed before using override_callback_uri
|
||||
# See: https://github.com/chatwoot/chatwoot/issues/13097
|
||||
subscribe_app_to_waba(waba_id)
|
||||
|
||||
# Step 2: Override callback URL for this specific WABA
|
||||
override_waba_callback(waba_id, callback_url, verify_token)
|
||||
end
|
||||
|
||||
def subscribe_app_to_waba(waba_id)
|
||||
response = HTTParty.post(
|
||||
"#{BASE_URI}/#{@api_version}/#{waba_id}/subscribed_apps",
|
||||
headers: request_headers
|
||||
)
|
||||
|
||||
handle_response(response, 'App subscription to WABA failed')
|
||||
end
|
||||
|
||||
def override_waba_callback(waba_id, callback_url, verify_token)
|
||||
response = HTTParty.post(
|
||||
"#{BASE_URI}/#{@api_version}/#{waba_id}/subscribed_apps",
|
||||
headers: request_headers,
|
||||
body: {
|
||||
override_callback_uri: callback_url,
|
||||
verify_token: verify_token,
|
||||
subscribed_fields: %w[messages smb_message_echoes]
|
||||
verify_token: verify_token
|
||||
}.to_json
|
||||
)
|
||||
|
||||
handle_response(response, 'Webhook callback override failed')
|
||||
handle_response(response, 'Webhook subscription failed')
|
||||
end
|
||||
|
||||
def unsubscribe_waba_webhook(waba_id)
|
||||
|
||||
@@ -4,23 +4,18 @@
|
||||
class Whatsapp::IncomingMessageBaseService
|
||||
include ::Whatsapp::IncomingMessageServiceHelpers
|
||||
|
||||
pattr_initialize [:inbox!, :params!, :outgoing_echo]
|
||||
pattr_initialize [:inbox!, :params!]
|
||||
|
||||
def perform
|
||||
processed_params
|
||||
|
||||
if processed_params.try(:[], :statuses).present?
|
||||
process_statuses
|
||||
elsif messages_data.present?
|
||||
elsif processed_params.try(:[], :messages).present?
|
||||
process_messages
|
||||
end
|
||||
end
|
||||
|
||||
# Returns messages array for both regular messages and echo events
|
||||
def messages_data
|
||||
@processed_params&.dig(:messages) || @processed_params&.dig(:message_echoes)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_messages
|
||||
@@ -31,7 +26,7 @@ class Whatsapp::IncomingMessageBaseService
|
||||
# Multiple webhook event can be received against the same message due to misconfigurations in the Meta
|
||||
# business manager account. While we have not found the core reason yet, the following line ensure that
|
||||
# there are no duplicate messages created.
|
||||
return if find_message_by_source_id(messages_data.first[:id]) || message_under_process?
|
||||
return if find_message_by_source_id(@processed_params[:messages].first[:id]) || message_under_process?
|
||||
|
||||
cache_message_source_id_in_redis
|
||||
set_contact
|
||||
@@ -62,7 +57,7 @@ class Whatsapp::IncomingMessageBaseService
|
||||
end
|
||||
|
||||
def create_messages
|
||||
message = messages_data.first
|
||||
message = @processed_params[:messages].first
|
||||
log_error(message) && return if error_webhook_event?(message)
|
||||
|
||||
process_in_reply_to(message)
|
||||
@@ -72,44 +67,20 @@ class Whatsapp::IncomingMessageBaseService
|
||||
|
||||
def create_contact_messages(message)
|
||||
message['contacts'].each do |contact|
|
||||
# Pass source_id from parent message since contact objects don't have :id
|
||||
create_message(contact, source_id: message[:id])
|
||||
create_message(contact)
|
||||
attach_contact(contact)
|
||||
@message.save!
|
||||
end
|
||||
end
|
||||
|
||||
def create_regular_message(message)
|
||||
create_message(message, source_id: message[:id])
|
||||
create_message(message)
|
||||
attach_files
|
||||
attach_location if message_type == 'location'
|
||||
@message.save!
|
||||
end
|
||||
|
||||
def set_contact
|
||||
if outgoing_echo
|
||||
set_contact_from_echo
|
||||
else
|
||||
set_contact_from_message
|
||||
end
|
||||
end
|
||||
|
||||
def set_contact_from_echo
|
||||
# For echo messages, contact phone is in the 'to' field
|
||||
phone_number = messages_data.first[:to]
|
||||
waid = processed_waid(phone_number)
|
||||
|
||||
contact_inbox = ::ContactInboxWithContactBuilder.new(
|
||||
source_id: waid,
|
||||
inbox: inbox,
|
||||
contact_attributes: { name: "+#{phone_number}", phone_number: "+#{phone_number}" }
|
||||
).perform
|
||||
|
||||
@contact_inbox = contact_inbox
|
||||
@contact = contact_inbox.contact
|
||||
end
|
||||
|
||||
def set_contact_from_message
|
||||
contact_params = @processed_params[:contacts]&.first
|
||||
return if contact_params.blank?
|
||||
|
||||
@@ -118,7 +89,7 @@ class Whatsapp::IncomingMessageBaseService
|
||||
contact_inbox = ::ContactInboxWithContactBuilder.new(
|
||||
source_id: waid,
|
||||
inbox: inbox,
|
||||
contact_attributes: { name: contact_params.dig(:profile, :name), phone_number: "+#{messages_data.first[:from]}" }
|
||||
contact_attributes: { name: contact_params.dig(:profile, :name), phone_number: "+#{@processed_params[:messages].first[:from]}" }
|
||||
).perform
|
||||
|
||||
@contact_inbox = contact_inbox
|
||||
@@ -144,7 +115,7 @@ class Whatsapp::IncomingMessageBaseService
|
||||
def attach_files
|
||||
return if %w[text button interactive location contacts].include?(message_type)
|
||||
|
||||
attachment_payload = messages_data.first[message_type.to_sym]
|
||||
attachment_payload = @processed_params[:messages].first[message_type.to_sym]
|
||||
@message.content ||= attachment_payload[:caption]
|
||||
|
||||
attachment_file = download_attachment_file(attachment_payload)
|
||||
@@ -162,7 +133,7 @@ class Whatsapp::IncomingMessageBaseService
|
||||
end
|
||||
|
||||
def attach_location
|
||||
location = messages_data.first['location']
|
||||
location = @processed_params[:messages].first['location']
|
||||
location_name = location['name'] ? "#{location['name']}, #{location['address']}" : ''
|
||||
@message.attachments.new(
|
||||
account_id: @message.account_id,
|
||||
@@ -174,17 +145,14 @@ class Whatsapp::IncomingMessageBaseService
|
||||
)
|
||||
end
|
||||
|
||||
def create_message(message, source_id: nil)
|
||||
def create_message(message)
|
||||
@message = @conversation.messages.build(
|
||||
content: message_content(message),
|
||||
account_id: @inbox.account_id,
|
||||
inbox_id: @inbox.id,
|
||||
message_type: outgoing_echo ? :outgoing : :incoming,
|
||||
# Set status to :delivered for echo messages to prevent SendReplyJob from trying to send them
|
||||
status: outgoing_echo ? :delivered : :sent,
|
||||
sender: outgoing_echo ? nil : @contact,
|
||||
source_id: (source_id || message[:id]).to_s,
|
||||
content_attributes: outgoing_echo ? { external_echo: true } : {},
|
||||
message_type: :incoming,
|
||||
sender: @contact,
|
||||
source_id: message[:id].to_s,
|
||||
in_reply_to_external_id: @in_reply_to_external_id
|
||||
)
|
||||
end
|
||||
@@ -221,7 +189,7 @@ class Whatsapp::IncomingMessageBaseService
|
||||
end
|
||||
|
||||
def contact_name_matches_phone_number?
|
||||
phone_number = "+#{messages_data.first[:from]}"
|
||||
phone_number = "+#{@processed_params[:messages].first[:from]}"
|
||||
formatted_phone_number = TelephoneNumber.parse(phone_number).international_number
|
||||
@contact.name == phone_number || @contact.name == formatted_phone_number
|
||||
end
|
||||
|
||||
@@ -21,7 +21,7 @@ module Whatsapp::IncomingMessageServiceHelpers
|
||||
end
|
||||
|
||||
def message_type
|
||||
messages_data.first[:type]
|
||||
@processed_params[:messages].first[:type]
|
||||
end
|
||||
|
||||
def message_content(message)
|
||||
@@ -70,19 +70,19 @@ module Whatsapp::IncomingMessageServiceHelpers
|
||||
end
|
||||
|
||||
def message_under_process?
|
||||
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: messages_data.first[:id])
|
||||
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: @processed_params[:messages].first[:id])
|
||||
Redis::Alfred.get(key)
|
||||
end
|
||||
|
||||
def cache_message_source_id_in_redis
|
||||
return if messages_data.blank?
|
||||
return if @processed_params.try(:[], :messages).blank?
|
||||
|
||||
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: messages_data.first[:id])
|
||||
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: @processed_params[:messages].first[:id])
|
||||
::Redis::Alfred.setex(key, true)
|
||||
end
|
||||
|
||||
def clear_message_source_id_from_redis
|
||||
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: messages_data.first[:id])
|
||||
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: @processed_params[:messages].first[:id])
|
||||
::Redis::Alfred.delete(key)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -107,16 +107,6 @@
|
||||
value:
|
||||
description: 'The support email address for your installation'
|
||||
locked: false
|
||||
- name: ACCOUNT_EMAILS_LIMIT
|
||||
display_title: 'Account Email Sending Limit (Daily)'
|
||||
description: 'Maximum number of non-channel emails an account can send per day'
|
||||
value: 100
|
||||
locked: false
|
||||
- name: ACCOUNT_EMAILS_PLAN_LIMITS
|
||||
display_title: 'Account Email Plan Limits (Daily)'
|
||||
description: 'Per-plan daily email sending limits as JSON'
|
||||
value:
|
||||
type: code
|
||||
# ------- End of Email Related Config ------- #
|
||||
|
||||
# ------- Facebook Channel Related Config ------- #
|
||||
|
||||
@@ -445,8 +445,6 @@ Rails.application.routes.draw do
|
||||
get :conversation_traffic
|
||||
get :bot_metrics
|
||||
get :inbox_label_matrix
|
||||
get :first_response_time_distribution
|
||||
get :outgoing_messages_count
|
||||
end
|
||||
end
|
||||
resource :year_in_review, only: [:show]
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
class AddIndexToReportingEventsForResponseDistribution < ActiveRecord::Migration[7.1]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def change
|
||||
add_index :reporting_events,
|
||||
[:account_id, :name, :inbox_id, :created_at],
|
||||
name: 'index_reporting_events_for_response_distribution',
|
||||
algorithm: :concurrently,
|
||||
if_not_exists: true
|
||||
end
|
||||
end
|
||||
+1
-2
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.1].define(version: 2026_01_30_061021) do
|
||||
ActiveRecord::Schema[7.1].define(version: 2026_01_20_121402) do
|
||||
# These extensions should be enabled to support this database
|
||||
enable_extension "pg_stat_statements"
|
||||
enable_extension "pg_trgm"
|
||||
@@ -1115,7 +1115,6 @@ ActiveRecord::Schema[7.1].define(version: 2026_01_30_061021) do
|
||||
t.datetime "event_start_time", precision: nil
|
||||
t.datetime "event_end_time", precision: nil
|
||||
t.index ["account_id", "name", "created_at"], name: "reporting_events__account_id__name__created_at"
|
||||
t.index ["account_id", "name", "inbox_id", "created_at"], name: "index_reporting_events_for_response_distribution"
|
||||
t.index ["account_id"], name: "index_reporting_events_on_account_id"
|
||||
t.index ["conversation_id"], name: "index_reporting_events_on_conversation_id"
|
||||
t.index ["created_at"], name: "index_reporting_events_on_created_at"
|
||||
|
||||
@@ -2,9 +2,6 @@ require 'administrate/field/base'
|
||||
|
||||
class AccountLimitsField < Administrate::Field::Base
|
||||
def to_s
|
||||
defaults = { agents: nil, inboxes: nil, captain_responses: nil, captain_documents: nil, emails: nil }
|
||||
overrides = (data.presence || {}).to_h.symbolize_keys.compact
|
||||
|
||||
defaults.merge(overrides).to_json
|
||||
data.present? ? data.to_json : { agents: nil, inboxes: nil, captain_responses: nil, captain_documents: nil }.to_json
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
module Enterprise::Account::PlanUsageAndLimits # rubocop:disable Metrics/ModuleLength
|
||||
module Enterprise::Account::PlanUsageAndLimits
|
||||
CAPTAIN_RESPONSES = 'captain_responses'.freeze
|
||||
CAPTAIN_DOCUMENTS = 'captain_documents'.freeze
|
||||
CAPTAIN_RESPONSES_USAGE = 'captain_responses_usage'.freeze
|
||||
@@ -32,10 +32,6 @@ module Enterprise::Account::PlanUsageAndLimits # rubocop:disable Metrics/ModuleL
|
||||
save
|
||||
end
|
||||
|
||||
def email_rate_limit
|
||||
account_limit || plan_email_limit || global_limit || default_limit
|
||||
end
|
||||
|
||||
def subscribed_features
|
||||
plan_features = InstallationConfig.find_by(name: 'CHATWOOT_CLOUD_PLAN_FEATURES')&.value
|
||||
return [] if plan_features.blank?
|
||||
@@ -72,16 +68,6 @@ module Enterprise::Account::PlanUsageAndLimits # rubocop:disable Metrics/ModuleL
|
||||
}
|
||||
end
|
||||
|
||||
def plan_email_limit
|
||||
config = InstallationConfig.find_by(name: 'ACCOUNT_EMAILS_PLAN_LIMITS')&.value
|
||||
return nil if config.blank? || plan_name.blank?
|
||||
|
||||
parsed = config.is_a?(String) ? JSON.parse(config) : config
|
||||
parsed[plan_name.downcase]&.to_i
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
|
||||
def default_captain_limits
|
||||
max_limits = { documents: ChatwootApp.max_limit, responses: ChatwootApp.max_limit }.with_indifferent_access
|
||||
zero_limits = { documents: 0, responses: 0 }.with_indifferent_access
|
||||
@@ -133,8 +119,7 @@ module Enterprise::Account::PlanUsageAndLimits # rubocop:disable Metrics/ModuleL
|
||||
'inboxes' => { 'type': 'number' },
|
||||
'agents' => { 'type': 'number' },
|
||||
'captain_responses' => { 'type': 'number' },
|
||||
'captain_documents' => { 'type': 'number' },
|
||||
'emails' => { 'type': 'number' }
|
||||
'captain_documents' => { 'type': 'number' }
|
||||
},
|
||||
'required' => [],
|
||||
'additionalProperties' => false
|
||||
|
||||
@@ -49,7 +49,4 @@ module Redis::RedisKeys
|
||||
# Track conversation assignments to agents for rate limiting
|
||||
ASSIGNMENT_KEY = 'ASSIGNMENT::%<inbox_id>d::AGENT::%<agent_id>d::CONVERSATION::%<conversation_id>d'.freeze
|
||||
ASSIGNMENT_KEY_PATTERN = 'ASSIGNMENT::%<inbox_id>d::AGENT::%<agent_id>d::*'.freeze
|
||||
|
||||
## Account Email Rate Limiting
|
||||
ACCOUNT_OUTBOUND_EMAIL_COUNT_KEY = 'OUTBOUND_EMAIL_COUNT::%<account_id>d::%<date>s'.freeze
|
||||
end
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe V2::Reports::FirstResponseTimeDistributionBuilder do
|
||||
let!(:account) { create(:account) }
|
||||
let!(:web_widget_inbox) { create(:inbox, account: account, channel: create(:channel_widget, account: account)) }
|
||||
let!(:email_inbox) { create(:inbox, account: account, channel: create(:channel_email, account: account)) }
|
||||
let(:params) do
|
||||
{
|
||||
since: 1.week.ago.beginning_of_day.to_i.to_s,
|
||||
until: Time.current.end_of_day.to_i.to_s
|
||||
}
|
||||
end
|
||||
let(:builder) { described_class.new(account: account, params: params) }
|
||||
|
||||
describe '#build' do
|
||||
subject(:report) { builder.build }
|
||||
|
||||
context 'when there are first response events across channels and time buckets' do
|
||||
before do
|
||||
# Web Widget: 0-1h bucket (30 minutes = 1800 seconds)
|
||||
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
|
||||
value: 1_800, created_at: 2.days.ago)
|
||||
# Web Widget: 1-4h bucket (2 hours = 7200 seconds)
|
||||
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
|
||||
value: 7_200, created_at: 2.days.ago)
|
||||
# Web Widget: 4-8h bucket (6 hours = 21600 seconds)
|
||||
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
|
||||
value: 21_600, created_at: 3.days.ago)
|
||||
# Email: 8-24h bucket (12 hours = 43200 seconds)
|
||||
create(:reporting_event, account: account, inbox: email_inbox, name: 'first_response',
|
||||
value: 43_200, created_at: 2.days.ago)
|
||||
# Email: 24h+ bucket (48 hours = 172800 seconds)
|
||||
create(:reporting_event, account: account, inbox: email_inbox, name: 'first_response',
|
||||
value: 172_800, created_at: 1.day.ago)
|
||||
end
|
||||
|
||||
it 'returns correct distribution for web widget channel' do
|
||||
expect(report['Channel::WebWidget']).to eq({
|
||||
'0-1h' => 1,
|
||||
'1-4h' => 1,
|
||||
'4-8h' => 1,
|
||||
'8-24h' => 0,
|
||||
'24h+' => 0
|
||||
})
|
||||
end
|
||||
|
||||
it 'returns correct distribution for email channel' do
|
||||
expect(report['Channel::Email']).to eq({
|
||||
'0-1h' => 0,
|
||||
'1-4h' => 0,
|
||||
'4-8h' => 0,
|
||||
'8-24h' => 1,
|
||||
'24h+' => 1
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filtering by date range' do
|
||||
before do
|
||||
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
|
||||
value: 1_800, created_at: 2.days.ago)
|
||||
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
|
||||
value: 1_800, created_at: 2.weeks.ago)
|
||||
end
|
||||
|
||||
it 'only counts events within the date range' do
|
||||
expect(report['Channel::WebWidget']['0-1h']).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are no first response events' do
|
||||
it 'returns an empty hash' do
|
||||
expect(report).to eq({})
|
||||
end
|
||||
end
|
||||
|
||||
context 'when events belong to another account' do
|
||||
let(:other_account) { create(:account) }
|
||||
let(:other_inbox) { create(:inbox, account: other_account) }
|
||||
|
||||
before do
|
||||
create(:reporting_event, account: other_account, inbox: other_inbox, name: 'first_response',
|
||||
value: 1_800, created_at: 2.days.ago)
|
||||
end
|
||||
|
||||
it 'does not include events from other accounts' do
|
||||
expect(report).to eq({})
|
||||
end
|
||||
end
|
||||
|
||||
context 'when events have different names' do
|
||||
before do
|
||||
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
|
||||
value: 1_800, created_at: 2.days.ago)
|
||||
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'conversation_resolved',
|
||||
value: 1_800, created_at: 2.days.ago)
|
||||
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'reply_time',
|
||||
value: 1_800, created_at: 2.days.ago)
|
||||
end
|
||||
|
||||
it 'only counts first_response events' do
|
||||
expect(report['Channel::WebWidget']['0-1h']).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when no date range params are provided' do
|
||||
let(:params) { {} }
|
||||
|
||||
before do
|
||||
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
|
||||
value: 1_800, created_at: 2.days.ago)
|
||||
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
|
||||
value: 1_800, created_at: 2.months.ago)
|
||||
end
|
||||
|
||||
it 'returns all events without date filtering' do
|
||||
expect(report['Channel::WebWidget']['0-1h']).to eq(2)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with boundary values for time buckets' do
|
||||
before do
|
||||
# Exactly at 1 hour boundary (should be in 1-4h bucket)
|
||||
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
|
||||
value: 3_600, created_at: 2.days.ago)
|
||||
# Just under 1 hour (should be in 0-1h bucket)
|
||||
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
|
||||
value: 3_599, created_at: 2.days.ago)
|
||||
# Exactly at 24 hour boundary (should be in 24h+ bucket)
|
||||
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
|
||||
value: 86_400, created_at: 2.days.ago)
|
||||
end
|
||||
|
||||
it 'correctly assigns boundary values to buckets' do
|
||||
expect(report['Channel::WebWidget']).to eq({
|
||||
'0-1h' => 1,
|
||||
'1-4h' => 1,
|
||||
'4-8h' => 0,
|
||||
'8-24h' => 0,
|
||||
'24h+' => 1
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -248,178 +248,4 @@ RSpec.describe Api::V2::Accounts::ReportsController, type: :request do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v2/accounts/{account.id}/reports/first_response_time_distribution' do
|
||||
let!(:web_widget_inbox) { create(:inbox, account: account, channel: create(:channel_widget, account: account)) }
|
||||
|
||||
context 'when unauthenticated' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/first_response_time_distribution"
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated as agent' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/first_response_time_distribution",
|
||||
headers: agent.create_new_auth_token, as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated as admin' do
|
||||
before do
|
||||
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
|
||||
value: 1_800, created_at: 2.days.ago)
|
||||
end
|
||||
|
||||
it 'returns the first response time distribution' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/first_response_time_distribution",
|
||||
params: { since: 1.week.ago.to_i.to_s, until: Time.current.to_i.to_s },
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
body = response.parsed_body
|
||||
expect(body).to be_a(Hash)
|
||||
expect(body['Channel::WebWidget']).to include('0-1h', '1-4h', '4-8h', '8-24h', '24h+')
|
||||
end
|
||||
|
||||
it 'returns correct counts in buckets' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/first_response_time_distribution",
|
||||
params: { since: 1.week.ago.to_i.to_s, until: Time.current.to_i.to_s },
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
|
||||
body = response.parsed_body
|
||||
expect(body['Channel::WebWidget']['0-1h']).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v2/accounts/{account.id}/reports/outgoing_messages_count' do
|
||||
let(:since_epoch) { 1.week.ago.to_i.to_s }
|
||||
let(:until_epoch) { 1.day.from_now.to_i.to_s }
|
||||
|
||||
context 'when unauthenticated' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
|
||||
params: { group_by: 'agent', since: since_epoch, until: until_epoch }
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated as agent' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
|
||||
params: { group_by: 'agent', since: since_epoch, until: until_epoch },
|
||||
headers: agent.create_new_auth_token, as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated as admin' do
|
||||
let(:agent2) { create(:user, account: account, role: :agent) }
|
||||
let(:team) { create(:team, account: account) }
|
||||
let(:inbox2) { create(:inbox, account: account) }
|
||||
|
||||
# Separate conversations for agent and team grouping because
|
||||
# model callbacks clear assignee_id when team is set.
|
||||
before do
|
||||
conv_agent = create(:conversation, account: account, inbox: inbox, assignee: agent)
|
||||
conv_agent2 = create(:conversation, account: account, inbox: inbox2, assignee: agent2)
|
||||
conv_team = create(:conversation, account: account, inbox: inbox, team: team)
|
||||
|
||||
create_list(:message, 3, account: account, conversation: conv_agent, inbox: inbox, message_type: :outgoing, sender: agent)
|
||||
create_list(:message, 2, account: account, conversation: conv_agent2, inbox: inbox2, message_type: :outgoing, sender: agent2)
|
||||
create_list(:message, 4, account: account, conversation: conv_team, inbox: inbox, message_type: :outgoing)
|
||||
# incoming message should not be counted
|
||||
create(:message, account: account, conversation: conv_agent, inbox: inbox, message_type: :incoming)
|
||||
end
|
||||
|
||||
it 'returns unprocessable_entity for invalid group_by' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
|
||||
params: { group_by: 'invalid', since: since_epoch, until: until_epoch },
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
|
||||
it 'returns outgoing message counts grouped by agent' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
|
||||
params: { group_by: 'agent', since: since_epoch, until: until_epoch },
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
data = response.parsed_body
|
||||
expect(data).to be_an(Array)
|
||||
|
||||
agent_entry = data.find { |e| e['id'] == agent.id }
|
||||
agent2_entry = data.find { |e| e['id'] == agent2.id }
|
||||
expect(agent_entry['outgoing_messages_count']).to eq(3)
|
||||
expect(agent2_entry['outgoing_messages_count']).to eq(2)
|
||||
end
|
||||
|
||||
it 'returns outgoing message counts grouped by team' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
|
||||
params: { group_by: 'team', since: since_epoch, until: until_epoch },
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
data = response.parsed_body
|
||||
expect(data).to be_an(Array)
|
||||
expect(data.length).to eq(1)
|
||||
expect(data.first['id']).to eq(team.id)
|
||||
expect(data.first['outgoing_messages_count']).to eq(4)
|
||||
end
|
||||
|
||||
it 'returns outgoing message counts grouped by inbox' do
|
||||
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
|
||||
params: { group_by: 'inbox', since: since_epoch, until: until_epoch },
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
data = response.parsed_body
|
||||
expect(data).to be_an(Array)
|
||||
|
||||
inbox_entry = data.find { |e| e['id'] == inbox.id }
|
||||
inbox2_entry = data.find { |e| e['id'] == inbox2.id }
|
||||
expect(inbox_entry['outgoing_messages_count']).to eq(7)
|
||||
expect(inbox2_entry['outgoing_messages_count']).to eq(2)
|
||||
end
|
||||
|
||||
it 'returns outgoing message counts grouped by label' do
|
||||
label = create(:label, account: account, title: 'support')
|
||||
conversation = account.conversations.first
|
||||
conversation.label_list.add('support')
|
||||
conversation.save!
|
||||
|
||||
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
|
||||
params: { group_by: 'label', since: since_epoch, until: until_epoch },
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
data = response.parsed_body
|
||||
expect(data).to be_an(Array)
|
||||
expect(data.length).to eq(1)
|
||||
expect(data.first['id']).to eq(label.id)
|
||||
expect(data.first['name']).to eq('support')
|
||||
end
|
||||
|
||||
it 'excludes bot messages when grouped by agent' do
|
||||
bot = create(:agent_bot)
|
||||
bot_conversation = create(:conversation, account: account, inbox: inbox)
|
||||
create(:message, account: account, conversation: bot_conversation, inbox: inbox,
|
||||
message_type: :outgoing, sender: bot)
|
||||
|
||||
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
|
||||
params: { group_by: 'agent', since: since_epoch, until: until_epoch },
|
||||
headers: admin.create_new_auth_token, as: :json
|
||||
|
||||
data = response.parsed_body
|
||||
agent_entry = data.find { |e| e['id'] == agent.id }
|
||||
# 3 from before block; bot message excluded (sender_type != 'User')
|
||||
expect(agent_entry['outgoing_messages_count']).to eq(3)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe AccountEmailRateLimitable do
|
||||
let(:account) { create(:account) }
|
||||
|
||||
describe '#email_rate_limit' do
|
||||
it 'returns account-level override when set' do
|
||||
account.update!(limits: { 'emails' => 50 })
|
||||
expect(account.email_rate_limit).to eq(50)
|
||||
end
|
||||
|
||||
it 'returns global config when no account override' do
|
||||
InstallationConfig.where(name: 'ACCOUNT_EMAILS_LIMIT').first_or_create(value: 200)
|
||||
expect(account.email_rate_limit).to eq(200)
|
||||
end
|
||||
|
||||
it 'returns account override over global config' do
|
||||
InstallationConfig.where(name: 'ACCOUNT_EMAILS_LIMIT').first_or_create(value: 200)
|
||||
account.update!(limits: { 'emails' => 50 })
|
||||
expect(account.email_rate_limit).to eq(50)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#within_email_rate_limit?' do
|
||||
before do
|
||||
account.update!(limits: { 'emails' => 2 })
|
||||
end
|
||||
|
||||
it 'returns true when under limit' do
|
||||
expect(account).to be_within_email_rate_limit
|
||||
end
|
||||
|
||||
it 'returns false when at limit' do
|
||||
2.times { account.increment_email_sent_count }
|
||||
expect(account).not_to be_within_email_rate_limit
|
||||
end
|
||||
end
|
||||
|
||||
describe '#increment_email_sent_count' do
|
||||
it 'increments the counter' do
|
||||
expect { account.increment_email_sent_count }.to change(account, :emails_sent_today).by(1)
|
||||
end
|
||||
|
||||
it 'sets TTL on first increment' do
|
||||
key = format(Redis::Alfred::ACCOUNT_OUTBOUND_EMAIL_COUNT_KEY, account_id: account.id, date: Time.zone.today.to_s)
|
||||
allow(Redis::Alfred).to receive(:incr).and_return(1)
|
||||
allow(Redis::Alfred).to receive(:expire)
|
||||
|
||||
account.increment_email_sent_count
|
||||
|
||||
expect(Redis::Alfred).to have_received(:expire).with(key, AccountEmailRateLimitable::OUTBOUND_EMAIL_TTL)
|
||||
end
|
||||
|
||||
it 'does not reset TTL on subsequent increments' do
|
||||
allow(Redis::Alfred).to receive(:incr).and_return(2)
|
||||
allow(Redis::Alfred).to receive(:expire)
|
||||
|
||||
account.increment_email_sent_count
|
||||
|
||||
expect(Redis::Alfred).not_to have_received(:expire)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -493,6 +493,31 @@ RSpec.describe Messages::MarkdownRendererService, type: :service do
|
||||
result = described_class.new(content, channel_type).render
|
||||
expect(result).to eq("- Item 1\n- Item 2")
|
||||
end
|
||||
|
||||
it 'converts literal \\n strings to actual newlines' do
|
||||
content = 'Hi \\ntext message \\nwith line \\nbreak'
|
||||
result = described_class.new(content, channel_type).render
|
||||
expect(result).to eq("Hi \ntext message \nwith line \nbreak")
|
||||
end
|
||||
|
||||
it 'handles multiple literal \\n in sequence' do
|
||||
content = 'Line 1\\n\\n\\nLine 2'
|
||||
result = described_class.new(content, channel_type).render
|
||||
expect(result).to eq("Line 1\n\n\nLine 2")
|
||||
end
|
||||
|
||||
it 'preserves markdown formatting while converting \\n' do
|
||||
content = '**bold**\\n_italic_\\n`code`'
|
||||
result = described_class.new(content, channel_type).render
|
||||
expect(result).to eq("**bold**\n_italic_\n`code`")
|
||||
end
|
||||
|
||||
it 'handles real-world API payload with literal \\n strings' do
|
||||
content = 'Hi \\ntext message \\nwith line \\nbreak'
|
||||
result = described_class.new(content, channel_type).render
|
||||
expect(result).to eq("Hi \ntext message \nwith line \nbreak")
|
||||
expect(result).not_to include('\\n')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when channel is Channel::TwitterProfile' do
|
||||
|
||||
@@ -99,20 +99,6 @@ describe Messages::SendEmailNotificationService do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account email rate limit is exceeded' do
|
||||
let(:inbox) { create(:inbox, account: account, channel: create(:channel_widget, account: account, continuity_via_email: true)) }
|
||||
let(:conversation) { create(:conversation, account: account, inbox: inbox) }
|
||||
|
||||
before do
|
||||
conversation.contact.update!(email: 'test@example.com')
|
||||
allow_any_instance_of(Account).to receive(:within_email_rate_limit?).and_return(false) # rubocop:disable RSpec/AnyInstance
|
||||
end
|
||||
|
||||
it 'does not enqueue job' do
|
||||
expect { service.perform }.not_to have_enqueued_job(ConversationReplyEmailJob)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when channel does not support email notifications' do
|
||||
let(:inbox) { create(:inbox, account: account, channel: create(:channel_sms, account: account)) }
|
||||
let(:conversation) { create(:conversation, account: account, inbox: inbox) }
|
||||
|
||||
@@ -161,23 +161,10 @@ describe Whatsapp::FacebookApiClient do
|
||||
|
||||
context 'when successful' do
|
||||
before do
|
||||
# Step 1: Subscribe app to WABA (no body)
|
||||
stub_request(:post, "https://graph.facebook.com/#{api_version}/#{waba_id}/subscribed_apps")
|
||||
.with(
|
||||
headers: { 'Authorization' => "Bearer #{access_token}", 'Content-Type' => 'application/json' }
|
||||
)
|
||||
.to_return(
|
||||
status: 200,
|
||||
body: { success: true }.to_json,
|
||||
headers: { 'Content-Type' => 'application/json' }
|
||||
)
|
||||
|
||||
# Step 2: Override callback URL (with body)
|
||||
stub_request(:post, "https://graph.facebook.com/#{api_version}/#{waba_id}/subscribed_apps")
|
||||
.with(
|
||||
headers: { 'Authorization' => "Bearer #{access_token}", 'Content-Type' => 'application/json' },
|
||||
body: { override_callback_uri: callback_url, verify_token: verify_token,
|
||||
subscribed_fields: %w[messages smb_message_echoes] }.to_json
|
||||
body: { override_callback_uri: callback_url, verify_token: verify_token }.to_json
|
||||
)
|
||||
.to_return(
|
||||
status: 200,
|
||||
@@ -192,45 +179,18 @@ describe Whatsapp::FacebookApiClient do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when app subscription fails' do
|
||||
context 'when failed' do
|
||||
before do
|
||||
stub_request(:post, "https://graph.facebook.com/#{api_version}/#{waba_id}/subscribed_apps")
|
||||
.with(
|
||||
headers: { 'Authorization' => "Bearer #{access_token}", 'Content-Type' => 'application/json' }
|
||||
)
|
||||
.to_return(status: 400, body: { error: 'App subscription to WABA failed' }.to_json)
|
||||
end
|
||||
|
||||
it 'raises an error' do
|
||||
expect { api_client.subscribe_waba_webhook(waba_id, callback_url, verify_token) }.to raise_error(/App subscription to WABA failed/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when callback override fails' do
|
||||
before do
|
||||
# Step 1 succeeds
|
||||
stub_request(:post, "https://graph.facebook.com/#{api_version}/#{waba_id}/subscribed_apps")
|
||||
.with(
|
||||
headers: { 'Authorization' => "Bearer #{access_token}", 'Content-Type' => 'application/json' }
|
||||
)
|
||||
.to_return(
|
||||
status: 200,
|
||||
body: { success: true }.to_json,
|
||||
headers: { 'Content-Type' => 'application/json' }
|
||||
)
|
||||
|
||||
# Step 2 fails
|
||||
stub_request(:post, "https://graph.facebook.com/#{api_version}/#{waba_id}/subscribed_apps")
|
||||
.with(
|
||||
headers: { 'Authorization' => "Bearer #{access_token}", 'Content-Type' => 'application/json' },
|
||||
body: { override_callback_uri: callback_url, verify_token: verify_token,
|
||||
subscribed_fields: %w[messages smb_message_echoes] }.to_json
|
||||
body: { override_callback_uri: callback_url, verify_token: verify_token }.to_json
|
||||
)
|
||||
.to_return(status: 400, body: { error: 'Webhook callback override failed' }.to_json)
|
||||
.to_return(status: 400, body: { error: 'Webhook subscription failed' }.to_json)
|
||||
end
|
||||
|
||||
it 'raises an error' do
|
||||
expect { api_client.subscribe_waba_webhook(waba_id, callback_url, verify_token) }.to raise_error(/Webhook callback override failed/)
|
||||
expect { api_client.subscribe_waba_webhook(waba_id, callback_url, verify_token) }.to raise_error(/Webhook subscription failed/)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -225,18 +225,6 @@ agent_conversation_metrics:
|
||||
$ref: './resource/reports/conversation/agent.yml'
|
||||
channel_summary:
|
||||
$ref: './resource/reports/channel_summary.yml'
|
||||
first_response_time_distribution:
|
||||
$ref: './resource/reports/first_response_time_distribution.yml'
|
||||
inbox_label_matrix:
|
||||
$ref: './resource/reports/inbox_label_matrix.yml'
|
||||
outgoing_messages_count:
|
||||
$ref: './resource/reports/outgoing_messages_count.yml'
|
||||
inbox_summary:
|
||||
$ref: './resource/reports/inbox_summary.yml'
|
||||
agent_summary:
|
||||
$ref: './resource/reports/agent_summary.yml'
|
||||
team_summary:
|
||||
$ref: './resource/reports/team_summary.yml'
|
||||
|
||||
contact_detail:
|
||||
$ref: ./resource/contact_detail.yml
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
type: array
|
||||
description: Agent summary report containing conversation statistics grouped by agent.
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: number
|
||||
description: The agent (user) ID
|
||||
conversations_count:
|
||||
type: number
|
||||
description: Number of conversations assigned to the agent during the date range
|
||||
resolved_conversations_count:
|
||||
type: number
|
||||
description: Number of conversations resolved by the agent during the date range
|
||||
avg_resolution_time:
|
||||
type: number
|
||||
nullable: true
|
||||
description: Average time (in seconds) to resolve conversations. Null if no data available.
|
||||
avg_first_response_time:
|
||||
type: number
|
||||
nullable: true
|
||||
description: Average time (in seconds) for the first response. Null if no data available.
|
||||
avg_reply_time:
|
||||
type: number
|
||||
nullable: true
|
||||
description: Average time (in seconds) between replies. Null if no data available.
|
||||
example:
|
||||
- id: 1
|
||||
conversations_count: 150
|
||||
resolved_conversations_count: 120
|
||||
avg_resolution_time: 3600
|
||||
avg_first_response_time: 300
|
||||
avg_reply_time: 600
|
||||
- id: 2
|
||||
conversations_count: 75
|
||||
resolved_conversations_count: 60
|
||||
avg_resolution_time: 1800
|
||||
avg_first_response_time: 180
|
||||
avg_reply_time: 420
|
||||
@@ -1,34 +0,0 @@
|
||||
type: object
|
||||
description: First response time distribution report grouped by channel type. Shows the count of conversations with first response times in different time buckets.
|
||||
additionalProperties:
|
||||
type: object
|
||||
description: First response time distribution for a specific channel type (e.g., Channel::WebWidget, Channel::Api)
|
||||
properties:
|
||||
0-1h:
|
||||
type: number
|
||||
description: Number of conversations with first response time less than 1 hour
|
||||
1-4h:
|
||||
type: number
|
||||
description: Number of conversations with first response time between 1-4 hours
|
||||
4-8h:
|
||||
type: number
|
||||
description: Number of conversations with first response time between 4-8 hours
|
||||
8-24h:
|
||||
type: number
|
||||
description: Number of conversations with first response time between 8-24 hours
|
||||
24h+:
|
||||
type: number
|
||||
description: Number of conversations with first response time greater than 24 hours
|
||||
example:
|
||||
Channel::WebWidget:
|
||||
0-1h: 150
|
||||
1-4h: 80
|
||||
4-8h: 45
|
||||
8-24h: 30
|
||||
24h+: 15
|
||||
Channel::Api:
|
||||
0-1h: 75
|
||||
1-4h: 40
|
||||
4-8h: 20
|
||||
8-24h: 10
|
||||
24h+: 5
|
||||
@@ -1,50 +0,0 @@
|
||||
type: object
|
||||
description: Inbox-label matrix report showing the count of conversations for each inbox-label combination.
|
||||
properties:
|
||||
inboxes:
|
||||
type: array
|
||||
description: List of inboxes included in the report
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: number
|
||||
description: The inbox ID
|
||||
name:
|
||||
type: string
|
||||
description: The inbox name
|
||||
labels:
|
||||
type: array
|
||||
description: List of labels included in the report
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: number
|
||||
description: The label ID
|
||||
title:
|
||||
type: string
|
||||
description: The label title
|
||||
matrix:
|
||||
type: array
|
||||
description: 2D array where matrix[i][j] represents the count of conversations in inboxes[i] with labels[j]
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
type: number
|
||||
example:
|
||||
inboxes:
|
||||
- id: 1
|
||||
name: Website Chat
|
||||
- id: 2
|
||||
name: Email Support
|
||||
labels:
|
||||
- id: 1
|
||||
title: bug
|
||||
- id: 2
|
||||
title: feature-request
|
||||
- id: 3
|
||||
title: urgent
|
||||
matrix:
|
||||
- [10, 5, 3]
|
||||
- [8, 12, 2]
|
||||
@@ -1,39 +0,0 @@
|
||||
type: array
|
||||
description: Inbox summary report containing conversation statistics grouped by inbox.
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: number
|
||||
description: The inbox ID
|
||||
conversations_count:
|
||||
type: number
|
||||
description: Number of conversations created in the inbox during the date range
|
||||
resolved_conversations_count:
|
||||
type: number
|
||||
description: Number of conversations resolved in the inbox during the date range
|
||||
avg_resolution_time:
|
||||
type: number
|
||||
nullable: true
|
||||
description: Average time (in seconds) to resolve conversations. Null if no data available.
|
||||
avg_first_response_time:
|
||||
type: number
|
||||
nullable: true
|
||||
description: Average time (in seconds) for the first response. Null if no data available.
|
||||
avg_reply_time:
|
||||
type: number
|
||||
nullable: true
|
||||
description: Average time (in seconds) between replies. Null if no data available.
|
||||
example:
|
||||
- id: 1
|
||||
conversations_count: 150
|
||||
resolved_conversations_count: 120
|
||||
avg_resolution_time: 3600
|
||||
avg_first_response_time: 300
|
||||
avg_reply_time: 600
|
||||
- id: 2
|
||||
conversations_count: 75
|
||||
resolved_conversations_count: 60
|
||||
avg_resolution_time: 1800
|
||||
avg_first_response_time: 180
|
||||
avg_reply_time: 420
|
||||
@@ -1,21 +0,0 @@
|
||||
type: array
|
||||
description: Outgoing messages count report grouped by entity (agent, team, inbox, or label).
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: number
|
||||
description: The ID of the grouped entity (agent, team, inbox, or label).
|
||||
name:
|
||||
type: string
|
||||
description: The name of the grouped entity.
|
||||
outgoing_messages_count:
|
||||
type: number
|
||||
description: The total number of outgoing messages for this entity in the given time range.
|
||||
example:
|
||||
- id: 1
|
||||
name: Agent One
|
||||
outgoing_messages_count: 42
|
||||
- id: 2
|
||||
name: Agent Two
|
||||
outgoing_messages_count: 18
|
||||
@@ -1,39 +0,0 @@
|
||||
type: array
|
||||
description: Team summary report containing conversation statistics grouped by team.
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: number
|
||||
description: The team ID
|
||||
conversations_count:
|
||||
type: number
|
||||
description: Number of conversations assigned to the team during the date range
|
||||
resolved_conversations_count:
|
||||
type: number
|
||||
description: Number of conversations resolved by the team during the date range
|
||||
avg_resolution_time:
|
||||
type: number
|
||||
nullable: true
|
||||
description: Average time (in seconds) to resolve conversations. Null if no data available.
|
||||
avg_first_response_time:
|
||||
type: number
|
||||
nullable: true
|
||||
description: Average time (in seconds) for the first response. Null if no data available.
|
||||
avg_reply_time:
|
||||
type: number
|
||||
nullable: true
|
||||
description: Average time (in seconds) between replies. Null if no data available.
|
||||
example:
|
||||
- id: 1
|
||||
conversations_count: 250
|
||||
resolved_conversations_count: 200
|
||||
avg_resolution_time: 2800
|
||||
avg_first_response_time: 240
|
||||
avg_reply_time: 500
|
||||
- id: 2
|
||||
conversations_count: 180
|
||||
resolved_conversations_count: 150
|
||||
avg_resolution_time: 2400
|
||||
avg_first_response_time: 200
|
||||
avg_reply_time: 450
|
||||
+1
-1
@@ -18,6 +18,6 @@
|
||||
</head>
|
||||
<body>
|
||||
<redoc spec-url='/swagger/swagger.json'></redoc>
|
||||
<script src="https://cdn.jsdelivr.net/npm/redoc@2.1.5/bundles/redoc.standalone.js"> </script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"> </script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
tags:
|
||||
- Reports
|
||||
operationId: get-agent-summary-report
|
||||
summary: Get conversation statistics grouped by agent
|
||||
security:
|
||||
- userApiKey: []
|
||||
description: |
|
||||
Get conversation statistics grouped by agent for a given date range.
|
||||
Returns metrics for each agent including conversation counts, resolution counts,
|
||||
average first response time, average resolution time, and average reply time.
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/agent_summary'
|
||||
'403':
|
||||
description: Access denied
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/bad_request_error'
|
||||
@@ -1,24 +0,0 @@
|
||||
tags:
|
||||
- Reports
|
||||
operationId: get-first-response-time-distribution
|
||||
summary: Get first response time distribution by channel
|
||||
security:
|
||||
- userApiKey: []
|
||||
description: |
|
||||
Get the distribution of first response times grouped by channel type.
|
||||
Returns conversation counts in different time buckets (0-1h, 1-4h, 4-8h, 8-24h, 24h+) for each channel type.
|
||||
|
||||
**Note:** This API endpoint is available only in Chatwoot version 4.11.0 and above.
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/first_response_time_distribution'
|
||||
'403':
|
||||
description: Access denied
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/bad_request_error'
|
||||
@@ -1,25 +0,0 @@
|
||||
tags:
|
||||
- Reports
|
||||
operationId: get-inbox-label-matrix
|
||||
summary: Get inbox-label matrix report
|
||||
security:
|
||||
- userApiKey: []
|
||||
description: |
|
||||
Get a matrix showing the count of conversations for each inbox-label combination.
|
||||
Returns a list of inboxes, labels, and a 2D matrix where each cell contains the count of conversations
|
||||
in a specific inbox that have a specific label applied.
|
||||
|
||||
**Note:** This API endpoint is available only in Chatwoot version 4.11.0 and above.
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/inbox_label_matrix'
|
||||
'403':
|
||||
description: Access denied
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/bad_request_error'
|
||||
@@ -1,23 +0,0 @@
|
||||
tags:
|
||||
- Reports
|
||||
operationId: get-inbox-summary-report
|
||||
summary: Get conversation statistics grouped by inbox
|
||||
security:
|
||||
- userApiKey: []
|
||||
description: |
|
||||
Get conversation statistics grouped by inbox for a given date range.
|
||||
Returns metrics for each inbox including conversation counts, resolution counts,
|
||||
average first response time, average resolution time, and average reply time.
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/inbox_summary'
|
||||
'403':
|
||||
description: Access denied
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/bad_request_error'
|
||||
@@ -1,36 +0,0 @@
|
||||
tags:
|
||||
- Reports
|
||||
operationId: get-outgoing-messages-count
|
||||
summary: Get outgoing messages count grouped by entity
|
||||
security:
|
||||
- userApiKey: []
|
||||
description: |
|
||||
Get the count of outgoing messages grouped by a specified entity (agent, team, inbox, or label).
|
||||
When grouped by agent, messages sent by bots (AgentBot, Captain::Assistant) are excluded.
|
||||
|
||||
**Note:** This API endpoint is available only in Chatwoot version 4.11.0 and above.
|
||||
parameters:
|
||||
- in: query
|
||||
name: group_by
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- agent
|
||||
- team
|
||||
- inbox
|
||||
- label
|
||||
description: The entity to group outgoing message counts by.
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/outgoing_messages_count'
|
||||
'403':
|
||||
description: Access denied
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/bad_request_error'
|
||||
@@ -1,23 +0,0 @@
|
||||
tags:
|
||||
- Reports
|
||||
operationId: get-team-summary-report
|
||||
summary: Get conversation statistics grouped by team
|
||||
security:
|
||||
- userApiKey: []
|
||||
description: |
|
||||
Get conversation statistics grouped by team for a given date range.
|
||||
Returns metrics for each team including conversation counts, resolution counts,
|
||||
average first response time, average resolution time, and average reply time.
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/team_summary'
|
||||
'403':
|
||||
description: Access denied
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/bad_request_error'
|
||||
+5
-131
@@ -653,140 +653,14 @@
|
||||
schema:
|
||||
type: string
|
||||
description: The timestamp from where report should stop (Unix timestamp).
|
||||
- in: query
|
||||
name: business_hours
|
||||
schema:
|
||||
type: boolean
|
||||
description: Whether to filter by business hours.
|
||||
get:
|
||||
$ref: './application/reports/channel_summary.yml'
|
||||
|
||||
# Inbox summary report
|
||||
/api/v2/accounts/{account_id}/summary_reports/inbox:
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/account_id'
|
||||
- in: query
|
||||
name: since
|
||||
schema:
|
||||
type: string
|
||||
description: The timestamp from where report should start (Unix timestamp).
|
||||
- in: query
|
||||
name: until
|
||||
schema:
|
||||
type: string
|
||||
description: The timestamp from where report should stop (Unix timestamp).
|
||||
- in: query
|
||||
name: business_hours
|
||||
schema:
|
||||
type: boolean
|
||||
description: Whether to calculate metrics using business hours only.
|
||||
get:
|
||||
$ref: './application/reports/inbox_summary.yml'
|
||||
|
||||
# Agent summary report
|
||||
/api/v2/accounts/{account_id}/summary_reports/agent:
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/account_id'
|
||||
- in: query
|
||||
name: since
|
||||
schema:
|
||||
type: string
|
||||
description: The timestamp from where report should start (Unix timestamp).
|
||||
- in: query
|
||||
name: until
|
||||
schema:
|
||||
type: string
|
||||
description: The timestamp from where report should stop (Unix timestamp).
|
||||
- in: query
|
||||
name: business_hours
|
||||
schema:
|
||||
type: boolean
|
||||
description: Whether to calculate metrics using business hours only.
|
||||
get:
|
||||
$ref: './application/reports/agent_summary.yml'
|
||||
|
||||
# Team summary report
|
||||
/api/v2/accounts/{account_id}/summary_reports/team:
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/account_id'
|
||||
- in: query
|
||||
name: since
|
||||
schema:
|
||||
type: string
|
||||
description: The timestamp from where report should start (Unix timestamp).
|
||||
- in: query
|
||||
name: until
|
||||
schema:
|
||||
type: string
|
||||
description: The timestamp from where report should stop (Unix timestamp).
|
||||
- in: query
|
||||
name: business_hours
|
||||
schema:
|
||||
type: boolean
|
||||
description: Whether to calculate metrics using business hours only.
|
||||
get:
|
||||
$ref: './application/reports/team_summary.yml'
|
||||
|
||||
# First response time distribution report
|
||||
/api/v2/accounts/{account_id}/reports/first_response_time_distribution:
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/account_id'
|
||||
- in: query
|
||||
name: since
|
||||
schema:
|
||||
type: string
|
||||
description: The timestamp from where report should start (Unix timestamp).
|
||||
- in: query
|
||||
name: until
|
||||
schema:
|
||||
type: string
|
||||
description: The timestamp from where report should stop (Unix timestamp).
|
||||
get:
|
||||
$ref: './application/reports/first_response_time_distribution.yml'
|
||||
|
||||
# Inbox-label matrix report
|
||||
/api/v2/accounts/{account_id}/reports/inbox_label_matrix:
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/account_id'
|
||||
- in: query
|
||||
name: since
|
||||
schema:
|
||||
type: string
|
||||
description: The timestamp from where report should start (Unix timestamp).
|
||||
- in: query
|
||||
name: until
|
||||
schema:
|
||||
type: string
|
||||
description: The timestamp from where report should stop (Unix timestamp).
|
||||
- in: query
|
||||
name: inbox_ids
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
description: Filter by specific inbox IDs.
|
||||
- in: query
|
||||
name: label_ids
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
description: Filter by specific label IDs.
|
||||
get:
|
||||
$ref: './application/reports/inbox_label_matrix.yml'
|
||||
|
||||
# Outgoing messages count report
|
||||
/api/v2/accounts/{account_id}/reports/outgoing_messages_count:
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/account_id'
|
||||
- in: query
|
||||
name: since
|
||||
schema:
|
||||
type: string
|
||||
description: The timestamp from where report should start (Unix timestamp).
|
||||
- in: query
|
||||
name: until
|
||||
schema:
|
||||
type: string
|
||||
description: The timestamp from where report should stop (Unix timestamp).
|
||||
get:
|
||||
$ref: './application/reports/outgoing_messages_count.yml'
|
||||
|
||||
# Conversations Messages
|
||||
/accounts/{account_id}/conversations/{conversation_id}/messages:
|
||||
parameters:
|
||||
|
||||
+8
-740
@@ -7890,6 +7890,14 @@
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The timestamp from where report should stop (Unix timestamp)."
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "business_hours",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": "Whether to filter by business hours."
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
@@ -7938,417 +7946,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/accounts/{account_id}/summary_reports/inbox": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/account_id"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "since",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The timestamp from where report should start (Unix timestamp)."
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "until",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The timestamp from where report should stop (Unix timestamp)."
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "business_hours",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": "Whether to calculate metrics using business hours only."
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"tags": [
|
||||
"Reports"
|
||||
],
|
||||
"operationId": "get-inbox-summary-report",
|
||||
"summary": "Get conversation statistics grouped by inbox",
|
||||
"security": [
|
||||
{
|
||||
"userApiKey": []
|
||||
}
|
||||
],
|
||||
"description": "Get conversation statistics grouped by inbox for a given date range.\nReturns metrics for each inbox including conversation counts, resolution counts,\naverage first response time, average resolution time, and average reply time.\n",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/inbox_summary"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Access denied",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/bad_request_error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/accounts/{account_id}/summary_reports/agent": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/account_id"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "since",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The timestamp from where report should start (Unix timestamp)."
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "until",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The timestamp from where report should stop (Unix timestamp)."
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "business_hours",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": "Whether to calculate metrics using business hours only."
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"tags": [
|
||||
"Reports"
|
||||
],
|
||||
"operationId": "get-agent-summary-report",
|
||||
"summary": "Get conversation statistics grouped by agent",
|
||||
"security": [
|
||||
{
|
||||
"userApiKey": []
|
||||
}
|
||||
],
|
||||
"description": "Get conversation statistics grouped by agent for a given date range.\nReturns metrics for each agent including conversation counts, resolution counts,\naverage first response time, average resolution time, and average reply time.\n",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/agent_summary"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Access denied",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/bad_request_error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/accounts/{account_id}/summary_reports/team": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/account_id"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "since",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The timestamp from where report should start (Unix timestamp)."
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "until",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The timestamp from where report should stop (Unix timestamp)."
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "business_hours",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": "Whether to calculate metrics using business hours only."
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"tags": [
|
||||
"Reports"
|
||||
],
|
||||
"operationId": "get-team-summary-report",
|
||||
"summary": "Get conversation statistics grouped by team",
|
||||
"security": [
|
||||
{
|
||||
"userApiKey": []
|
||||
}
|
||||
],
|
||||
"description": "Get conversation statistics grouped by team for a given date range.\nReturns metrics for each team including conversation counts, resolution counts,\naverage first response time, average resolution time, and average reply time.\n",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/team_summary"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Access denied",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/bad_request_error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/accounts/{account_id}/reports/first_response_time_distribution": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/account_id"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "since",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The timestamp from where report should start (Unix timestamp)."
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "until",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The timestamp from where report should stop (Unix timestamp)."
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"tags": [
|
||||
"Reports"
|
||||
],
|
||||
"operationId": "get-first-response-time-distribution",
|
||||
"summary": "Get first response time distribution by channel",
|
||||
"security": [
|
||||
{
|
||||
"userApiKey": []
|
||||
}
|
||||
],
|
||||
"description": "Get the distribution of first response times grouped by channel type.\nReturns conversation counts in different time buckets (0-1h, 1-4h, 4-8h, 8-24h, 24h+) for each channel type.\n\n**Note:** This API endpoint is available only in Chatwoot version 4.11.0 and above.\n",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/first_response_time_distribution"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Access denied",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/bad_request_error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/accounts/{account_id}/reports/inbox_label_matrix": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/account_id"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "since",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The timestamp from where report should start (Unix timestamp)."
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "until",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The timestamp from where report should stop (Unix timestamp)."
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "inbox_ids",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"description": "Filter by specific inbox IDs."
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "label_ids",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"description": "Filter by specific label IDs."
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"tags": [
|
||||
"Reports"
|
||||
],
|
||||
"operationId": "get-inbox-label-matrix",
|
||||
"summary": "Get inbox-label matrix report",
|
||||
"security": [
|
||||
{
|
||||
"userApiKey": []
|
||||
}
|
||||
],
|
||||
"description": "Get a matrix showing the count of conversations for each inbox-label combination.\nReturns a list of inboxes, labels, and a 2D matrix where each cell contains the count of conversations\nin a specific inbox that have a specific label applied.\n\n**Note:** This API endpoint is available only in Chatwoot version 4.11.0 and above.\n",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/inbox_label_matrix"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Access denied",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/bad_request_error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/accounts/{account_id}/reports/outgoing_messages_count": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/account_id"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "since",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The timestamp from where report should start (Unix timestamp)."
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "until",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The timestamp from where report should stop (Unix timestamp)."
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"tags": [
|
||||
"Reports"
|
||||
],
|
||||
"operationId": "get-outgoing-messages-count",
|
||||
"summary": "Get outgoing messages count grouped by entity",
|
||||
"security": [
|
||||
{
|
||||
"userApiKey": []
|
||||
}
|
||||
],
|
||||
"description": "Get the count of outgoing messages grouped by a specified entity (agent, team, inbox, or label).\nWhen grouped by agent, messages sent by bots (AgentBot, Captain::Assistant) are excluded.\n\n**Note:** This API endpoint is available only in Chatwoot version 4.11.0 and above.\n",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "query",
|
||||
"name": "group_by",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"agent",
|
||||
"team",
|
||||
"inbox",
|
||||
"label"
|
||||
]
|
||||
},
|
||||
"description": "The entity to group outgoing message counts by."
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/outgoing_messages_count"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Access denied",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/bad_request_error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/accounts/{account_id}/conversations/{conversation_id}/messages": {
|
||||
"parameters": [
|
||||
{
|
||||
@@ -12184,335 +11781,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"first_response_time_distribution": {
|
||||
"type": "object",
|
||||
"description": "First response time distribution report grouped by channel type. Shows the count of conversations with first response times in different time buckets.",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"description": "First response time distribution for a specific channel type (e.g., Channel::WebWidget, Channel::Api)",
|
||||
"properties": {
|
||||
"0-1h": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations with first response time less than 1 hour"
|
||||
},
|
||||
"1-4h": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations with first response time between 1-4 hours"
|
||||
},
|
||||
"4-8h": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations with first response time between 4-8 hours"
|
||||
},
|
||||
"8-24h": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations with first response time between 8-24 hours"
|
||||
},
|
||||
"24h+": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations with first response time greater than 24 hours"
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"Channel::WebWidget": {
|
||||
"0-1h": 150,
|
||||
"1-4h": 80,
|
||||
"4-8h": 45,
|
||||
"8-24h": 30,
|
||||
"24h+": 15
|
||||
},
|
||||
"Channel::Api": {
|
||||
"0-1h": 75,
|
||||
"1-4h": 40,
|
||||
"4-8h": 20,
|
||||
"8-24h": 10,
|
||||
"24h+": 5
|
||||
}
|
||||
}
|
||||
},
|
||||
"inbox_label_matrix": {
|
||||
"type": "object",
|
||||
"description": "Inbox-label matrix report showing the count of conversations for each inbox-label combination.",
|
||||
"properties": {
|
||||
"inboxes": {
|
||||
"type": "array",
|
||||
"description": "List of inboxes included in the report",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The inbox ID"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The inbox name"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"labels": {
|
||||
"type": "array",
|
||||
"description": "List of labels included in the report",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The label ID"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "The label title"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"matrix": {
|
||||
"type": "array",
|
||||
"description": "2D array where matrix[i][j] represents the count of conversations in inboxes[i] with labels[j]",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"inboxes": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Website Chat"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Email Support"
|
||||
}
|
||||
],
|
||||
"labels": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "bug"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "feature-request"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "urgent"
|
||||
}
|
||||
],
|
||||
"matrix": [
|
||||
[
|
||||
10,
|
||||
5,
|
||||
3
|
||||
],
|
||||
[
|
||||
8,
|
||||
12,
|
||||
2
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"outgoing_messages_count": {
|
||||
"type": "array",
|
||||
"description": "Outgoing messages count report grouped by entity (agent, team, inbox, or label).",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The ID of the grouped entity (agent, team, inbox, or label)."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the grouped entity."
|
||||
},
|
||||
"outgoing_messages_count": {
|
||||
"type": "number",
|
||||
"description": "The total number of outgoing messages for this entity in the given time range."
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Agent One",
|
||||
"outgoing_messages_count": 42
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Agent Two",
|
||||
"outgoing_messages_count": 18
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbox_summary": {
|
||||
"type": "array",
|
||||
"description": "Inbox summary report containing conversation statistics grouped by inbox.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The inbox ID"
|
||||
},
|
||||
"conversations_count": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations created in the inbox during the date range"
|
||||
},
|
||||
"resolved_conversations_count": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations resolved in the inbox during the date range"
|
||||
},
|
||||
"avg_resolution_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) to resolve conversations. Null if no data available."
|
||||
},
|
||||
"avg_first_response_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) for the first response. Null if no data available."
|
||||
},
|
||||
"avg_reply_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) between replies. Null if no data available."
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": [
|
||||
{
|
||||
"id": 1,
|
||||
"conversations_count": 150,
|
||||
"resolved_conversations_count": 120,
|
||||
"avg_resolution_time": 3600,
|
||||
"avg_first_response_time": 300,
|
||||
"avg_reply_time": 600
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"conversations_count": 75,
|
||||
"resolved_conversations_count": 60,
|
||||
"avg_resolution_time": 1800,
|
||||
"avg_first_response_time": 180,
|
||||
"avg_reply_time": 420
|
||||
}
|
||||
]
|
||||
},
|
||||
"agent_summary": {
|
||||
"type": "array",
|
||||
"description": "Agent summary report containing conversation statistics grouped by agent.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The agent (user) ID"
|
||||
},
|
||||
"conversations_count": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations assigned to the agent during the date range"
|
||||
},
|
||||
"resolved_conversations_count": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations resolved by the agent during the date range"
|
||||
},
|
||||
"avg_resolution_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) to resolve conversations. Null if no data available."
|
||||
},
|
||||
"avg_first_response_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) for the first response. Null if no data available."
|
||||
},
|
||||
"avg_reply_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) between replies. Null if no data available."
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": [
|
||||
{
|
||||
"id": 1,
|
||||
"conversations_count": 150,
|
||||
"resolved_conversations_count": 120,
|
||||
"avg_resolution_time": 3600,
|
||||
"avg_first_response_time": 300,
|
||||
"avg_reply_time": 600
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"conversations_count": 75,
|
||||
"resolved_conversations_count": 60,
|
||||
"avg_resolution_time": 1800,
|
||||
"avg_first_response_time": 180,
|
||||
"avg_reply_time": 420
|
||||
}
|
||||
]
|
||||
},
|
||||
"team_summary": {
|
||||
"type": "array",
|
||||
"description": "Team summary report containing conversation statistics grouped by team.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The team ID"
|
||||
},
|
||||
"conversations_count": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations assigned to the team during the date range"
|
||||
},
|
||||
"resolved_conversations_count": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations resolved by the team during the date range"
|
||||
},
|
||||
"avg_resolution_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) to resolve conversations. Null if no data available."
|
||||
},
|
||||
"avg_first_response_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) for the first response. Null if no data available."
|
||||
},
|
||||
"avg_reply_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) between replies. Null if no data available."
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": [
|
||||
{
|
||||
"id": 1,
|
||||
"conversations_count": 250,
|
||||
"resolved_conversations_count": 200,
|
||||
"avg_resolution_time": 2800,
|
||||
"avg_first_response_time": 240,
|
||||
"avg_reply_time": 500
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"conversations_count": 180,
|
||||
"resolved_conversations_count": 150,
|
||||
"avg_resolution_time": 2400,
|
||||
"avg_first_response_time": 200,
|
||||
"avg_reply_time": 450
|
||||
}
|
||||
]
|
||||
},
|
||||
"contact_detail": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -6433,6 +6433,14 @@
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The timestamp from where report should stop (Unix timestamp)."
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "business_hours",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": "Whether to filter by business hours."
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
@@ -6480,417 +6488,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/accounts/{account_id}/summary_reports/inbox": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/account_id"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "since",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The timestamp from where report should start (Unix timestamp)."
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "until",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The timestamp from where report should stop (Unix timestamp)."
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "business_hours",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": "Whether to calculate metrics using business hours only."
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"tags": [
|
||||
"Reports"
|
||||
],
|
||||
"operationId": "get-inbox-summary-report",
|
||||
"summary": "Get conversation statistics grouped by inbox",
|
||||
"security": [
|
||||
{
|
||||
"userApiKey": []
|
||||
}
|
||||
],
|
||||
"description": "Get conversation statistics grouped by inbox for a given date range.\nReturns metrics for each inbox including conversation counts, resolution counts,\naverage first response time, average resolution time, and average reply time.\n",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/inbox_summary"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Access denied",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/bad_request_error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/accounts/{account_id}/summary_reports/agent": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/account_id"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "since",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The timestamp from where report should start (Unix timestamp)."
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "until",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The timestamp from where report should stop (Unix timestamp)."
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "business_hours",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": "Whether to calculate metrics using business hours only."
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"tags": [
|
||||
"Reports"
|
||||
],
|
||||
"operationId": "get-agent-summary-report",
|
||||
"summary": "Get conversation statistics grouped by agent",
|
||||
"security": [
|
||||
{
|
||||
"userApiKey": []
|
||||
}
|
||||
],
|
||||
"description": "Get conversation statistics grouped by agent for a given date range.\nReturns metrics for each agent including conversation counts, resolution counts,\naverage first response time, average resolution time, and average reply time.\n",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/agent_summary"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Access denied",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/bad_request_error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/accounts/{account_id}/summary_reports/team": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/account_id"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "since",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The timestamp from where report should start (Unix timestamp)."
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "until",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The timestamp from where report should stop (Unix timestamp)."
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "business_hours",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": "Whether to calculate metrics using business hours only."
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"tags": [
|
||||
"Reports"
|
||||
],
|
||||
"operationId": "get-team-summary-report",
|
||||
"summary": "Get conversation statistics grouped by team",
|
||||
"security": [
|
||||
{
|
||||
"userApiKey": []
|
||||
}
|
||||
],
|
||||
"description": "Get conversation statistics grouped by team for a given date range.\nReturns metrics for each team including conversation counts, resolution counts,\naverage first response time, average resolution time, and average reply time.\n",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/team_summary"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Access denied",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/bad_request_error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/accounts/{account_id}/reports/first_response_time_distribution": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/account_id"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "since",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The timestamp from where report should start (Unix timestamp)."
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "until",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The timestamp from where report should stop (Unix timestamp)."
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"tags": [
|
||||
"Reports"
|
||||
],
|
||||
"operationId": "get-first-response-time-distribution",
|
||||
"summary": "Get first response time distribution by channel",
|
||||
"security": [
|
||||
{
|
||||
"userApiKey": []
|
||||
}
|
||||
],
|
||||
"description": "Get the distribution of first response times grouped by channel type.\nReturns conversation counts in different time buckets (0-1h, 1-4h, 4-8h, 8-24h, 24h+) for each channel type.\n\n**Note:** This API endpoint is available only in Chatwoot version 4.11.0 and above.\n",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/first_response_time_distribution"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Access denied",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/bad_request_error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/accounts/{account_id}/reports/inbox_label_matrix": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/account_id"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "since",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The timestamp from where report should start (Unix timestamp)."
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "until",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The timestamp from where report should stop (Unix timestamp)."
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "inbox_ids",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"description": "Filter by specific inbox IDs."
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "label_ids",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"description": "Filter by specific label IDs."
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"tags": [
|
||||
"Reports"
|
||||
],
|
||||
"operationId": "get-inbox-label-matrix",
|
||||
"summary": "Get inbox-label matrix report",
|
||||
"security": [
|
||||
{
|
||||
"userApiKey": []
|
||||
}
|
||||
],
|
||||
"description": "Get a matrix showing the count of conversations for each inbox-label combination.\nReturns a list of inboxes, labels, and a 2D matrix where each cell contains the count of conversations\nin a specific inbox that have a specific label applied.\n\n**Note:** This API endpoint is available only in Chatwoot version 4.11.0 and above.\n",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/inbox_label_matrix"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Access denied",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/bad_request_error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/accounts/{account_id}/reports/outgoing_messages_count": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/account_id"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "since",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The timestamp from where report should start (Unix timestamp)."
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "until",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The timestamp from where report should stop (Unix timestamp)."
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"tags": [
|
||||
"Reports"
|
||||
],
|
||||
"operationId": "get-outgoing-messages-count",
|
||||
"summary": "Get outgoing messages count grouped by entity",
|
||||
"security": [
|
||||
{
|
||||
"userApiKey": []
|
||||
}
|
||||
],
|
||||
"description": "Get the count of outgoing messages grouped by a specified entity (agent, team, inbox, or label).\nWhen grouped by agent, messages sent by bots (AgentBot, Captain::Assistant) are excluded.\n\n**Note:** This API endpoint is available only in Chatwoot version 4.11.0 and above.\n",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "query",
|
||||
"name": "group_by",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"agent",
|
||||
"team",
|
||||
"inbox",
|
||||
"label"
|
||||
]
|
||||
},
|
||||
"description": "The entity to group outgoing message counts by."
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/outgoing_messages_count"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Access denied",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/bad_request_error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
@@ -10691,335 +10288,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"first_response_time_distribution": {
|
||||
"type": "object",
|
||||
"description": "First response time distribution report grouped by channel type. Shows the count of conversations with first response times in different time buckets.",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"description": "First response time distribution for a specific channel type (e.g., Channel::WebWidget, Channel::Api)",
|
||||
"properties": {
|
||||
"0-1h": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations with first response time less than 1 hour"
|
||||
},
|
||||
"1-4h": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations with first response time between 1-4 hours"
|
||||
},
|
||||
"4-8h": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations with first response time between 4-8 hours"
|
||||
},
|
||||
"8-24h": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations with first response time between 8-24 hours"
|
||||
},
|
||||
"24h+": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations with first response time greater than 24 hours"
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"Channel::WebWidget": {
|
||||
"0-1h": 150,
|
||||
"1-4h": 80,
|
||||
"4-8h": 45,
|
||||
"8-24h": 30,
|
||||
"24h+": 15
|
||||
},
|
||||
"Channel::Api": {
|
||||
"0-1h": 75,
|
||||
"1-4h": 40,
|
||||
"4-8h": 20,
|
||||
"8-24h": 10,
|
||||
"24h+": 5
|
||||
}
|
||||
}
|
||||
},
|
||||
"inbox_label_matrix": {
|
||||
"type": "object",
|
||||
"description": "Inbox-label matrix report showing the count of conversations for each inbox-label combination.",
|
||||
"properties": {
|
||||
"inboxes": {
|
||||
"type": "array",
|
||||
"description": "List of inboxes included in the report",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The inbox ID"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The inbox name"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"labels": {
|
||||
"type": "array",
|
||||
"description": "List of labels included in the report",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The label ID"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "The label title"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"matrix": {
|
||||
"type": "array",
|
||||
"description": "2D array where matrix[i][j] represents the count of conversations in inboxes[i] with labels[j]",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"inboxes": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Website Chat"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Email Support"
|
||||
}
|
||||
],
|
||||
"labels": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "bug"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "feature-request"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "urgent"
|
||||
}
|
||||
],
|
||||
"matrix": [
|
||||
[
|
||||
10,
|
||||
5,
|
||||
3
|
||||
],
|
||||
[
|
||||
8,
|
||||
12,
|
||||
2
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"outgoing_messages_count": {
|
||||
"type": "array",
|
||||
"description": "Outgoing messages count report grouped by entity (agent, team, inbox, or label).",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The ID of the grouped entity (agent, team, inbox, or label)."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the grouped entity."
|
||||
},
|
||||
"outgoing_messages_count": {
|
||||
"type": "number",
|
||||
"description": "The total number of outgoing messages for this entity in the given time range."
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Agent One",
|
||||
"outgoing_messages_count": 42
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Agent Two",
|
||||
"outgoing_messages_count": 18
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbox_summary": {
|
||||
"type": "array",
|
||||
"description": "Inbox summary report containing conversation statistics grouped by inbox.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The inbox ID"
|
||||
},
|
||||
"conversations_count": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations created in the inbox during the date range"
|
||||
},
|
||||
"resolved_conversations_count": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations resolved in the inbox during the date range"
|
||||
},
|
||||
"avg_resolution_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) to resolve conversations. Null if no data available."
|
||||
},
|
||||
"avg_first_response_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) for the first response. Null if no data available."
|
||||
},
|
||||
"avg_reply_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) between replies. Null if no data available."
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": [
|
||||
{
|
||||
"id": 1,
|
||||
"conversations_count": 150,
|
||||
"resolved_conversations_count": 120,
|
||||
"avg_resolution_time": 3600,
|
||||
"avg_first_response_time": 300,
|
||||
"avg_reply_time": 600
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"conversations_count": 75,
|
||||
"resolved_conversations_count": 60,
|
||||
"avg_resolution_time": 1800,
|
||||
"avg_first_response_time": 180,
|
||||
"avg_reply_time": 420
|
||||
}
|
||||
]
|
||||
},
|
||||
"agent_summary": {
|
||||
"type": "array",
|
||||
"description": "Agent summary report containing conversation statistics grouped by agent.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The agent (user) ID"
|
||||
},
|
||||
"conversations_count": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations assigned to the agent during the date range"
|
||||
},
|
||||
"resolved_conversations_count": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations resolved by the agent during the date range"
|
||||
},
|
||||
"avg_resolution_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) to resolve conversations. Null if no data available."
|
||||
},
|
||||
"avg_first_response_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) for the first response. Null if no data available."
|
||||
},
|
||||
"avg_reply_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) between replies. Null if no data available."
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": [
|
||||
{
|
||||
"id": 1,
|
||||
"conversations_count": 150,
|
||||
"resolved_conversations_count": 120,
|
||||
"avg_resolution_time": 3600,
|
||||
"avg_first_response_time": 300,
|
||||
"avg_reply_time": 600
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"conversations_count": 75,
|
||||
"resolved_conversations_count": 60,
|
||||
"avg_resolution_time": 1800,
|
||||
"avg_first_response_time": 180,
|
||||
"avg_reply_time": 420
|
||||
}
|
||||
]
|
||||
},
|
||||
"team_summary": {
|
||||
"type": "array",
|
||||
"description": "Team summary report containing conversation statistics grouped by team.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The team ID"
|
||||
},
|
||||
"conversations_count": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations assigned to the team during the date range"
|
||||
},
|
||||
"resolved_conversations_count": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations resolved by the team during the date range"
|
||||
},
|
||||
"avg_resolution_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) to resolve conversations. Null if no data available."
|
||||
},
|
||||
"avg_first_response_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) for the first response. Null if no data available."
|
||||
},
|
||||
"avg_reply_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) between replies. Null if no data available."
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": [
|
||||
{
|
||||
"id": 1,
|
||||
"conversations_count": 250,
|
||||
"resolved_conversations_count": 200,
|
||||
"avg_resolution_time": 2800,
|
||||
"avg_first_response_time": 240,
|
||||
"avg_reply_time": 500
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"conversations_count": 180,
|
||||
"resolved_conversations_count": 150,
|
||||
"avg_resolution_time": 2400,
|
||||
"avg_first_response_time": 200,
|
||||
"avg_reply_time": 450
|
||||
}
|
||||
]
|
||||
},
|
||||
"contact_detail": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -4424,335 +4424,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"first_response_time_distribution": {
|
||||
"type": "object",
|
||||
"description": "First response time distribution report grouped by channel type. Shows the count of conversations with first response times in different time buckets.",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"description": "First response time distribution for a specific channel type (e.g., Channel::WebWidget, Channel::Api)",
|
||||
"properties": {
|
||||
"0-1h": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations with first response time less than 1 hour"
|
||||
},
|
||||
"1-4h": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations with first response time between 1-4 hours"
|
||||
},
|
||||
"4-8h": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations with first response time between 4-8 hours"
|
||||
},
|
||||
"8-24h": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations with first response time between 8-24 hours"
|
||||
},
|
||||
"24h+": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations with first response time greater than 24 hours"
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"Channel::WebWidget": {
|
||||
"0-1h": 150,
|
||||
"1-4h": 80,
|
||||
"4-8h": 45,
|
||||
"8-24h": 30,
|
||||
"24h+": 15
|
||||
},
|
||||
"Channel::Api": {
|
||||
"0-1h": 75,
|
||||
"1-4h": 40,
|
||||
"4-8h": 20,
|
||||
"8-24h": 10,
|
||||
"24h+": 5
|
||||
}
|
||||
}
|
||||
},
|
||||
"inbox_label_matrix": {
|
||||
"type": "object",
|
||||
"description": "Inbox-label matrix report showing the count of conversations for each inbox-label combination.",
|
||||
"properties": {
|
||||
"inboxes": {
|
||||
"type": "array",
|
||||
"description": "List of inboxes included in the report",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The inbox ID"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The inbox name"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"labels": {
|
||||
"type": "array",
|
||||
"description": "List of labels included in the report",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The label ID"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "The label title"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"matrix": {
|
||||
"type": "array",
|
||||
"description": "2D array where matrix[i][j] represents the count of conversations in inboxes[i] with labels[j]",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"inboxes": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Website Chat"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Email Support"
|
||||
}
|
||||
],
|
||||
"labels": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "bug"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "feature-request"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "urgent"
|
||||
}
|
||||
],
|
||||
"matrix": [
|
||||
[
|
||||
10,
|
||||
5,
|
||||
3
|
||||
],
|
||||
[
|
||||
8,
|
||||
12,
|
||||
2
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"outgoing_messages_count": {
|
||||
"type": "array",
|
||||
"description": "Outgoing messages count report grouped by entity (agent, team, inbox, or label).",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The ID of the grouped entity (agent, team, inbox, or label)."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the grouped entity."
|
||||
},
|
||||
"outgoing_messages_count": {
|
||||
"type": "number",
|
||||
"description": "The total number of outgoing messages for this entity in the given time range."
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Agent One",
|
||||
"outgoing_messages_count": 42
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Agent Two",
|
||||
"outgoing_messages_count": 18
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbox_summary": {
|
||||
"type": "array",
|
||||
"description": "Inbox summary report containing conversation statistics grouped by inbox.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The inbox ID"
|
||||
},
|
||||
"conversations_count": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations created in the inbox during the date range"
|
||||
},
|
||||
"resolved_conversations_count": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations resolved in the inbox during the date range"
|
||||
},
|
||||
"avg_resolution_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) to resolve conversations. Null if no data available."
|
||||
},
|
||||
"avg_first_response_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) for the first response. Null if no data available."
|
||||
},
|
||||
"avg_reply_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) between replies. Null if no data available."
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": [
|
||||
{
|
||||
"id": 1,
|
||||
"conversations_count": 150,
|
||||
"resolved_conversations_count": 120,
|
||||
"avg_resolution_time": 3600,
|
||||
"avg_first_response_time": 300,
|
||||
"avg_reply_time": 600
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"conversations_count": 75,
|
||||
"resolved_conversations_count": 60,
|
||||
"avg_resolution_time": 1800,
|
||||
"avg_first_response_time": 180,
|
||||
"avg_reply_time": 420
|
||||
}
|
||||
]
|
||||
},
|
||||
"agent_summary": {
|
||||
"type": "array",
|
||||
"description": "Agent summary report containing conversation statistics grouped by agent.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The agent (user) ID"
|
||||
},
|
||||
"conversations_count": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations assigned to the agent during the date range"
|
||||
},
|
||||
"resolved_conversations_count": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations resolved by the agent during the date range"
|
||||
},
|
||||
"avg_resolution_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) to resolve conversations. Null if no data available."
|
||||
},
|
||||
"avg_first_response_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) for the first response. Null if no data available."
|
||||
},
|
||||
"avg_reply_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) between replies. Null if no data available."
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": [
|
||||
{
|
||||
"id": 1,
|
||||
"conversations_count": 150,
|
||||
"resolved_conversations_count": 120,
|
||||
"avg_resolution_time": 3600,
|
||||
"avg_first_response_time": 300,
|
||||
"avg_reply_time": 600
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"conversations_count": 75,
|
||||
"resolved_conversations_count": 60,
|
||||
"avg_resolution_time": 1800,
|
||||
"avg_first_response_time": 180,
|
||||
"avg_reply_time": 420
|
||||
}
|
||||
]
|
||||
},
|
||||
"team_summary": {
|
||||
"type": "array",
|
||||
"description": "Team summary report containing conversation statistics grouped by team.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The team ID"
|
||||
},
|
||||
"conversations_count": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations assigned to the team during the date range"
|
||||
},
|
||||
"resolved_conversations_count": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations resolved by the team during the date range"
|
||||
},
|
||||
"avg_resolution_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) to resolve conversations. Null if no data available."
|
||||
},
|
||||
"avg_first_response_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) for the first response. Null if no data available."
|
||||
},
|
||||
"avg_reply_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) between replies. Null if no data available."
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": [
|
||||
{
|
||||
"id": 1,
|
||||
"conversations_count": 250,
|
||||
"resolved_conversations_count": 200,
|
||||
"avg_resolution_time": 2800,
|
||||
"avg_first_response_time": 240,
|
||||
"avg_reply_time": 500
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"conversations_count": 180,
|
||||
"resolved_conversations_count": 150,
|
||||
"avg_resolution_time": 2400,
|
||||
"avg_first_response_time": 200,
|
||||
"avg_reply_time": 450
|
||||
}
|
||||
]
|
||||
},
|
||||
"contact_detail": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -3839,335 +3839,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"first_response_time_distribution": {
|
||||
"type": "object",
|
||||
"description": "First response time distribution report grouped by channel type. Shows the count of conversations with first response times in different time buckets.",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"description": "First response time distribution for a specific channel type (e.g., Channel::WebWidget, Channel::Api)",
|
||||
"properties": {
|
||||
"0-1h": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations with first response time less than 1 hour"
|
||||
},
|
||||
"1-4h": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations with first response time between 1-4 hours"
|
||||
},
|
||||
"4-8h": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations with first response time between 4-8 hours"
|
||||
},
|
||||
"8-24h": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations with first response time between 8-24 hours"
|
||||
},
|
||||
"24h+": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations with first response time greater than 24 hours"
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"Channel::WebWidget": {
|
||||
"0-1h": 150,
|
||||
"1-4h": 80,
|
||||
"4-8h": 45,
|
||||
"8-24h": 30,
|
||||
"24h+": 15
|
||||
},
|
||||
"Channel::Api": {
|
||||
"0-1h": 75,
|
||||
"1-4h": 40,
|
||||
"4-8h": 20,
|
||||
"8-24h": 10,
|
||||
"24h+": 5
|
||||
}
|
||||
}
|
||||
},
|
||||
"inbox_label_matrix": {
|
||||
"type": "object",
|
||||
"description": "Inbox-label matrix report showing the count of conversations for each inbox-label combination.",
|
||||
"properties": {
|
||||
"inboxes": {
|
||||
"type": "array",
|
||||
"description": "List of inboxes included in the report",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The inbox ID"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The inbox name"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"labels": {
|
||||
"type": "array",
|
||||
"description": "List of labels included in the report",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The label ID"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "The label title"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"matrix": {
|
||||
"type": "array",
|
||||
"description": "2D array where matrix[i][j] represents the count of conversations in inboxes[i] with labels[j]",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"inboxes": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Website Chat"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Email Support"
|
||||
}
|
||||
],
|
||||
"labels": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "bug"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "feature-request"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "urgent"
|
||||
}
|
||||
],
|
||||
"matrix": [
|
||||
[
|
||||
10,
|
||||
5,
|
||||
3
|
||||
],
|
||||
[
|
||||
8,
|
||||
12,
|
||||
2
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"outgoing_messages_count": {
|
||||
"type": "array",
|
||||
"description": "Outgoing messages count report grouped by entity (agent, team, inbox, or label).",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The ID of the grouped entity (agent, team, inbox, or label)."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the grouped entity."
|
||||
},
|
||||
"outgoing_messages_count": {
|
||||
"type": "number",
|
||||
"description": "The total number of outgoing messages for this entity in the given time range."
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Agent One",
|
||||
"outgoing_messages_count": 42
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Agent Two",
|
||||
"outgoing_messages_count": 18
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbox_summary": {
|
||||
"type": "array",
|
||||
"description": "Inbox summary report containing conversation statistics grouped by inbox.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The inbox ID"
|
||||
},
|
||||
"conversations_count": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations created in the inbox during the date range"
|
||||
},
|
||||
"resolved_conversations_count": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations resolved in the inbox during the date range"
|
||||
},
|
||||
"avg_resolution_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) to resolve conversations. Null if no data available."
|
||||
},
|
||||
"avg_first_response_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) for the first response. Null if no data available."
|
||||
},
|
||||
"avg_reply_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) between replies. Null if no data available."
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": [
|
||||
{
|
||||
"id": 1,
|
||||
"conversations_count": 150,
|
||||
"resolved_conversations_count": 120,
|
||||
"avg_resolution_time": 3600,
|
||||
"avg_first_response_time": 300,
|
||||
"avg_reply_time": 600
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"conversations_count": 75,
|
||||
"resolved_conversations_count": 60,
|
||||
"avg_resolution_time": 1800,
|
||||
"avg_first_response_time": 180,
|
||||
"avg_reply_time": 420
|
||||
}
|
||||
]
|
||||
},
|
||||
"agent_summary": {
|
||||
"type": "array",
|
||||
"description": "Agent summary report containing conversation statistics grouped by agent.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The agent (user) ID"
|
||||
},
|
||||
"conversations_count": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations assigned to the agent during the date range"
|
||||
},
|
||||
"resolved_conversations_count": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations resolved by the agent during the date range"
|
||||
},
|
||||
"avg_resolution_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) to resolve conversations. Null if no data available."
|
||||
},
|
||||
"avg_first_response_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) for the first response. Null if no data available."
|
||||
},
|
||||
"avg_reply_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) between replies. Null if no data available."
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": [
|
||||
{
|
||||
"id": 1,
|
||||
"conversations_count": 150,
|
||||
"resolved_conversations_count": 120,
|
||||
"avg_resolution_time": 3600,
|
||||
"avg_first_response_time": 300,
|
||||
"avg_reply_time": 600
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"conversations_count": 75,
|
||||
"resolved_conversations_count": 60,
|
||||
"avg_resolution_time": 1800,
|
||||
"avg_first_response_time": 180,
|
||||
"avg_reply_time": 420
|
||||
}
|
||||
]
|
||||
},
|
||||
"team_summary": {
|
||||
"type": "array",
|
||||
"description": "Team summary report containing conversation statistics grouped by team.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The team ID"
|
||||
},
|
||||
"conversations_count": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations assigned to the team during the date range"
|
||||
},
|
||||
"resolved_conversations_count": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations resolved by the team during the date range"
|
||||
},
|
||||
"avg_resolution_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) to resolve conversations. Null if no data available."
|
||||
},
|
||||
"avg_first_response_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) for the first response. Null if no data available."
|
||||
},
|
||||
"avg_reply_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) between replies. Null if no data available."
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": [
|
||||
{
|
||||
"id": 1,
|
||||
"conversations_count": 250,
|
||||
"resolved_conversations_count": 200,
|
||||
"avg_resolution_time": 2800,
|
||||
"avg_first_response_time": 240,
|
||||
"avg_reply_time": 500
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"conversations_count": 180,
|
||||
"resolved_conversations_count": 150,
|
||||
"avg_resolution_time": 2400,
|
||||
"avg_first_response_time": 200,
|
||||
"avg_reply_time": 450
|
||||
}
|
||||
]
|
||||
},
|
||||
"contact_detail": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -4600,335 +4600,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"first_response_time_distribution": {
|
||||
"type": "object",
|
||||
"description": "First response time distribution report grouped by channel type. Shows the count of conversations with first response times in different time buckets.",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"description": "First response time distribution for a specific channel type (e.g., Channel::WebWidget, Channel::Api)",
|
||||
"properties": {
|
||||
"0-1h": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations with first response time less than 1 hour"
|
||||
},
|
||||
"1-4h": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations with first response time between 1-4 hours"
|
||||
},
|
||||
"4-8h": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations with first response time between 4-8 hours"
|
||||
},
|
||||
"8-24h": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations with first response time between 8-24 hours"
|
||||
},
|
||||
"24h+": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations with first response time greater than 24 hours"
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"Channel::WebWidget": {
|
||||
"0-1h": 150,
|
||||
"1-4h": 80,
|
||||
"4-8h": 45,
|
||||
"8-24h": 30,
|
||||
"24h+": 15
|
||||
},
|
||||
"Channel::Api": {
|
||||
"0-1h": 75,
|
||||
"1-4h": 40,
|
||||
"4-8h": 20,
|
||||
"8-24h": 10,
|
||||
"24h+": 5
|
||||
}
|
||||
}
|
||||
},
|
||||
"inbox_label_matrix": {
|
||||
"type": "object",
|
||||
"description": "Inbox-label matrix report showing the count of conversations for each inbox-label combination.",
|
||||
"properties": {
|
||||
"inboxes": {
|
||||
"type": "array",
|
||||
"description": "List of inboxes included in the report",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The inbox ID"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The inbox name"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"labels": {
|
||||
"type": "array",
|
||||
"description": "List of labels included in the report",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The label ID"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "The label title"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"matrix": {
|
||||
"type": "array",
|
||||
"description": "2D array where matrix[i][j] represents the count of conversations in inboxes[i] with labels[j]",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"inboxes": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Website Chat"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Email Support"
|
||||
}
|
||||
],
|
||||
"labels": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "bug"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "feature-request"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "urgent"
|
||||
}
|
||||
],
|
||||
"matrix": [
|
||||
[
|
||||
10,
|
||||
5,
|
||||
3
|
||||
],
|
||||
[
|
||||
8,
|
||||
12,
|
||||
2
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"outgoing_messages_count": {
|
||||
"type": "array",
|
||||
"description": "Outgoing messages count report grouped by entity (agent, team, inbox, or label).",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The ID of the grouped entity (agent, team, inbox, or label)."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the grouped entity."
|
||||
},
|
||||
"outgoing_messages_count": {
|
||||
"type": "number",
|
||||
"description": "The total number of outgoing messages for this entity in the given time range."
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Agent One",
|
||||
"outgoing_messages_count": 42
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Agent Two",
|
||||
"outgoing_messages_count": 18
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbox_summary": {
|
||||
"type": "array",
|
||||
"description": "Inbox summary report containing conversation statistics grouped by inbox.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The inbox ID"
|
||||
},
|
||||
"conversations_count": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations created in the inbox during the date range"
|
||||
},
|
||||
"resolved_conversations_count": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations resolved in the inbox during the date range"
|
||||
},
|
||||
"avg_resolution_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) to resolve conversations. Null if no data available."
|
||||
},
|
||||
"avg_first_response_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) for the first response. Null if no data available."
|
||||
},
|
||||
"avg_reply_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) between replies. Null if no data available."
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": [
|
||||
{
|
||||
"id": 1,
|
||||
"conversations_count": 150,
|
||||
"resolved_conversations_count": 120,
|
||||
"avg_resolution_time": 3600,
|
||||
"avg_first_response_time": 300,
|
||||
"avg_reply_time": 600
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"conversations_count": 75,
|
||||
"resolved_conversations_count": 60,
|
||||
"avg_resolution_time": 1800,
|
||||
"avg_first_response_time": 180,
|
||||
"avg_reply_time": 420
|
||||
}
|
||||
]
|
||||
},
|
||||
"agent_summary": {
|
||||
"type": "array",
|
||||
"description": "Agent summary report containing conversation statistics grouped by agent.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The agent (user) ID"
|
||||
},
|
||||
"conversations_count": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations assigned to the agent during the date range"
|
||||
},
|
||||
"resolved_conversations_count": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations resolved by the agent during the date range"
|
||||
},
|
||||
"avg_resolution_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) to resolve conversations. Null if no data available."
|
||||
},
|
||||
"avg_first_response_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) for the first response. Null if no data available."
|
||||
},
|
||||
"avg_reply_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) between replies. Null if no data available."
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": [
|
||||
{
|
||||
"id": 1,
|
||||
"conversations_count": 150,
|
||||
"resolved_conversations_count": 120,
|
||||
"avg_resolution_time": 3600,
|
||||
"avg_first_response_time": 300,
|
||||
"avg_reply_time": 600
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"conversations_count": 75,
|
||||
"resolved_conversations_count": 60,
|
||||
"avg_resolution_time": 1800,
|
||||
"avg_first_response_time": 180,
|
||||
"avg_reply_time": 420
|
||||
}
|
||||
]
|
||||
},
|
||||
"team_summary": {
|
||||
"type": "array",
|
||||
"description": "Team summary report containing conversation statistics grouped by team.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The team ID"
|
||||
},
|
||||
"conversations_count": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations assigned to the team during the date range"
|
||||
},
|
||||
"resolved_conversations_count": {
|
||||
"type": "number",
|
||||
"description": "Number of conversations resolved by the team during the date range"
|
||||
},
|
||||
"avg_resolution_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) to resolve conversations. Null if no data available."
|
||||
},
|
||||
"avg_first_response_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) for the first response. Null if no data available."
|
||||
},
|
||||
"avg_reply_time": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Average time (in seconds) between replies. Null if no data available."
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": [
|
||||
{
|
||||
"id": 1,
|
||||
"conversations_count": 250,
|
||||
"resolved_conversations_count": 200,
|
||||
"avg_resolution_time": 2800,
|
||||
"avg_first_response_time": 240,
|
||||
"avg_reply_time": 500
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"conversations_count": 180,
|
||||
"resolved_conversations_count": 150,
|
||||
"avg_resolution_time": 2400,
|
||||
"avg_first_response_time": 200,
|
||||
"avg_reply_time": 450
|
||||
}
|
||||
]
|
||||
},
|
||||
"contact_detail": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
Reference in New Issue
Block a user