diff --git a/.circleci/config.yml b/.circleci/config.yml
index 5c9b27f03..974b981ab 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -12,8 +12,8 @@ defaults: &defaults
# Specify service dependencies here if necessary
# CircleCI maintains a library of pre-built images
# documented at https://circleci.com/docs/2.0/circleci-images/
- - image: circleci/postgres:alpine
- - image: circleci/redis:alpine
+ - image: cimg/postgres:14.1
+ - image: cimg/redis:6.2.6
environment:
- CC_TEST_REPORTER_ID: b1b5c4447bf93f6f0b06a64756e35afd0810ea83649f03971cbf303b4449456f
- RAILS_LOG_TO_STDOUT: false
@@ -110,7 +110,7 @@ jobs:
- run:
name: Run backend tests
command: |
- bundle exec rspec $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) --profile=10
+ bundle exec rspec $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) --profile=10 --format documentation
~/tmp/cc-test-reporter format-coverage -t simplecov -o ~/tmp/codeclimate.backend.json coverage/backend/.resultset.json
- persist_to_workspace:
root: ~/tmp
diff --git a/app/builders/contact_builder.rb b/app/builders/contact_builder.rb
index 14f26aa82..b63d72627 100644
--- a/app/builders/contact_builder.rb
+++ b/app/builders/contact_builder.rb
@@ -70,7 +70,7 @@ class ContactBuilder
update_contact_avatar(contact)
contact_inbox
rescue StandardError => e
- Rails.logger.info e
+ Rails.logger.error e
raise e
end
end
diff --git a/app/builders/messages/facebook/message_builder.rb b/app/builders/messages/facebook/message_builder.rb
index 5410aa3c4..204b452d6 100644
--- a/app/builders/messages/facebook/message_builder.rb
+++ b/app/builders/messages/facebook/message_builder.rb
@@ -27,7 +27,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
end
ensure_contact_avatar
rescue Koala::Facebook::AuthenticationError
- Rails.logger.info "Facebook Authorization expired for Inbox #{@inbox.id}"
+ Rails.logger.error "Facebook Authorization expired for Inbox #{@inbox.id}"
rescue StandardError => e
Sentry.capture_exception(e)
true
diff --git a/app/builders/v2/report_builder.rb b/app/builders/v2/report_builder.rb
index c45226397..67b80b7a5 100644
--- a/app/builders/v2/report_builder.rb
+++ b/app/builders/v2/report_builder.rb
@@ -1,5 +1,6 @@
class V2::ReportBuilder
include DateRangeHelper
+ include ReportHelper
attr_reader :account, :params
DEFAULT_GROUP_BY = 'day'.freeze
@@ -18,8 +19,14 @@ class V2::ReportBuilder
# For backward compatible with old report
def build
- timeseries.each_with_object([]) do |p, arr|
- arr << { value: p[1], timestamp: p[0].in_time_zone(@timezone).to_i }
+ if %w[avg_first_response_time avg_resolution_time].include?(params[:metric])
+ timeseries.each_with_object([]) do |p, arr|
+ arr << { value: p[1], timestamp: p[0].in_time_zone(@timezone).to_i, count: @grouped_values.count[p[0]] }
+ end
+ else
+ timeseries.each_with_object([]) do |p, arr|
+ arr << { value: p[1], timestamp: p[0].in_time_zone(@timezone).to_i }
+ end
end
end
@@ -34,23 +41,16 @@ class V2::ReportBuilder
}
end
- private
-
- def scope
- case params[:type]
- when :account
- account
- when :inbox
- inbox
- when :agent
- user
- when :label
- label
- when :team
- team
+ def conversation_metrics
+ if params[:type].equal?(:account)
+ conversations
+ else
+ agent_metrics
end
end
+ private
+
def inbox
@inbox ||= account.inboxes.find(params[:id])
end
@@ -68,7 +68,7 @@ class V2::ReportBuilder
end
def get_grouped_values(object_scope)
- object_scope.group_by_period(
+ @grouped_values = object_scope.group_by_period(
params[:group_by] || DEFAULT_GROUP_BY,
:created_at,
default_value: 0,
@@ -78,47 +78,26 @@ class V2::ReportBuilder
)
end
- def conversations_count
- (get_grouped_values scope.conversations).count
+ def agent_metrics
+ users = @account.users
+ users = users.where(id: params[:user_id]) if params[:user_id].present?
+ users.each_with_object([]) do |user, arr|
+ @user = user
+ arr << {
+ user: { id: user.id, name: user.name, thumbnail: user.avatar_url },
+ metric: conversations
+ }
+ end
end
- def incoming_messages_count
- (get_grouped_values scope.messages.incoming.unscope(:order)).count
- end
-
- def outgoing_messages_count
- (get_grouped_values scope.messages.outgoing.unscope(:order)).count
- end
-
- def resolutions_count
- (get_grouped_values scope.conversations.resolved).count
- end
-
- def avg_first_response_time
- (get_grouped_values scope.reporting_events.where(name: 'first_response')).average(:value)
- end
-
- def avg_resolution_time
- (get_grouped_values scope.reporting_events.where(name: 'conversation_resolved')).average(:value)
- end
-
- def avg_resolution_time_summary
- avg_rt = scope.reporting_events
- .where(name: 'conversation_resolved', created_at: range)
- .average(:value)
-
- return 0 if avg_rt.blank?
-
- avg_rt
- end
-
- def avg_first_response_time_summary
- avg_frt = scope.reporting_events
- .where(name: 'first_response', created_at: range)
- .average(:value)
-
- return 0 if avg_frt.blank?
-
- avg_frt
+ def conversations
+ @open_conversations = scope.conversations.open
+ first_response_count = scope.reporting_events.where(name: 'first_response', conversation_id: @open_conversations.pluck('id')).count
+ metric = {
+ open: @open_conversations.count,
+ unattended: @open_conversations.count - first_response_count
+ }
+ metric[:unassigned] = @open_conversations.unassigned.count if params[:type].equal?(:account)
+ metric
end
end
diff --git a/app/controllers/api/v1/accounts/automation_rules_controller.rb b/app/controllers/api/v1/accounts/automation_rules_controller.rb
index 3971dbcaf..dc4f7b6c8 100644
--- a/app/controllers/api/v1/accounts/automation_rules_controller.rb
+++ b/app/controllers/api/v1/accounts/automation_rules_controller.rb
@@ -7,13 +7,22 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
end
def create
- @automation_rule = Current.account.automation_rules.create(automation_rules_permit)
+ @automation_rule = Current.account.automation_rules.new(automation_rules_permit)
+ @automation_rule.actions = params[:actions]
+
+ render json: { error: @automation_rule.errors.messages }, status: :unprocessable_entity and return unless @automation_rule.valid?
+
+ @automation_rule.save!
+ process_attachments
+ @automation_rule
end
def show; end
def update
@automation_rule.update(automation_rules_permit)
+ process_attachments
+ @automation_rule
end
def destroy
@@ -30,11 +39,19 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
private
+ def process_attachments
+ return if params[:attachments].blank?
+
+ params[:attachments].each do |uploaded_attachment|
+ @automation_rule.files.attach(uploaded_attachment)
+ end
+ end
+
def automation_rules_permit
params.permit(
:name, :description, :event_name, :account_id, :active,
conditions: [:attribute_key, :filter_operator, :query_operator, { values: [] }],
- actions: [:action_name, { action_params: [{}] }]
+ actions: [:action_name, { action_params: [] }]
)
end
diff --git a/app/controllers/api/v1/accounts/callbacks_controller.rb b/app/controllers/api/v1/accounts/callbacks_controller.rb
index 7c2469c05..842930874 100644
--- a/app/controllers/api/v1/accounts/callbacks_controller.rb
+++ b/app/controllers/api/v1/accounts/callbacks_controller.rb
@@ -77,7 +77,7 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
koala = Koala::Facebook::OAuth.new(GlobalConfigService.load('FB_APP_ID', ''), GlobalConfigService.load('FB_APP_SECRET', ''))
koala.exchange_access_token_info(omniauth_token)['access_token']
rescue StandardError => e
- Rails.logger.info e
+ Rails.logger.error e
end
def mark_already_existing_facebook_pages(data)
diff --git a/app/controllers/api/v2/accounts/reports_controller.rb b/app/controllers/api/v2/accounts/reports_controller.rb
index efa09a43c..b2932e34f 100644
--- a/app/controllers/api/v2/accounts/reports_controller.rb
+++ b/app/controllers/api/v2/accounts/reports_controller.rb
@@ -35,6 +35,12 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
render layout: false, template: 'api/v2/accounts/reports/teams.csv.erb', format: 'csv'
end
+ def conversations
+ return head :unprocessable_entity if params[:type].blank?
+
+ render json: conversation_metrics
+ end
+
private
def check_authorization
@@ -73,6 +79,13 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
}
end
+ def conversation_params
+ {
+ type: params[:type].to_sym,
+ user_id: params[:user_id]
+ }
+ end
+
def range
{
current: {
@@ -91,4 +104,8 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
summary[:previous] = V2::ReportBuilder.new(Current.account, previous_summary_params).summary
summary
end
+
+ def conversation_metrics
+ V2::ReportBuilder.new(Current.account, conversation_params).conversation_metrics
+ end
end
diff --git a/app/controllers/twitter/callbacks_controller.rb b/app/controllers/twitter/callbacks_controller.rb
index d484b0871..32fffbceb 100644
--- a/app/controllers/twitter/callbacks_controller.rb
+++ b/app/controllers/twitter/callbacks_controller.rb
@@ -14,7 +14,7 @@ class Twitter::CallbacksController < Twitter::BaseController
redirect_to app_twitter_inbox_agents_url(account_id: account.id, inbox_id: inbox.id)
end
rescue StandardError => e
- Rails.logger.info e
+ Rails.logger.error e
redirect_to twitter_app_redirect_url
end
diff --git a/app/controllers/webhooks/instagram_controller.rb b/app/controllers/webhooks/instagram_controller.rb
index 2338ca7d1..e6fe93566 100644
--- a/app/controllers/webhooks/instagram_controller.rb
+++ b/app/controllers/webhooks/instagram_controller.rb
@@ -17,7 +17,7 @@ class Webhooks::InstagramController < ApplicationController
::Webhooks::InstagramEventsJob.perform_later(params.to_unsafe_hash[:entry])
render json: :ok
else
- Rails.logger.info("Message is not received from the instagram webhook event: #{params['object']}")
+ Rails.logger.warn("Message is not received from the instagram webhook event: #{params['object']}")
head :unprocessable_entity
end
end
diff --git a/app/helpers/report_helper.rb b/app/helpers/report_helper.rb
new file mode 100644
index 000000000..31e2c1ce8
--- /dev/null
+++ b/app/helpers/report_helper.rb
@@ -0,0 +1,62 @@
+module ReportHelper
+ private
+
+ def scope
+ case params[:type]
+ when :account
+ account
+ when :inbox
+ inbox
+ when :agent
+ user
+ when :label
+ label
+ when :team
+ team
+ end
+ end
+
+ def conversations_count
+ (get_grouped_values scope.conversations).count
+ end
+
+ def incoming_messages_count
+ (get_grouped_values scope.messages.incoming.unscope(:order)).count
+ end
+
+ def outgoing_messages_count
+ (get_grouped_values scope.messages.outgoing.unscope(:order)).count
+ end
+
+ def resolutions_count
+ (get_grouped_values scope.conversations.resolved).count
+ end
+
+ def avg_first_response_time
+ (get_grouped_values scope.reporting_events.where(name: 'first_response')).average(:value)
+ end
+
+ def avg_resolution_time
+ (get_grouped_values scope.reporting_events.where(name: 'conversation_resolved')).average(:value)
+ end
+
+ def avg_resolution_time_summary
+ avg_rt = scope.reporting_events
+ .where(name: 'conversation_resolved', created_at: range)
+ .average(:value)
+
+ return 0 if avg_rt.blank?
+
+ avg_rt
+ end
+
+ def avg_first_response_time_summary
+ avg_frt = scope.reporting_events
+ .where(name: 'first_response', created_at: range)
+ .average(:value)
+
+ return 0 if avg_frt.blank?
+
+ avg_frt
+ end
+end
diff --git a/app/javascript/dashboard/assets/scss/widgets/_report.scss b/app/javascript/dashboard/assets/scss/widgets/_report.scss
index 6e8b2fe5c..444dccb7c 100644
--- a/app/javascript/dashboard/assets/scss/widgets/_report.scss
+++ b/app/javascript/dashboard/assets/scss/widgets/_report.scss
@@ -18,6 +18,13 @@
font-size: $font-size-small;
font-weight: $font-weight-bold;
color: $color-heading;
+ display: flex;
+ align-items: center;
+ }
+
+ .info-icon {
+ color: var(--b-400);
+ margin-left: var(--space-micro);
}
.metric-wrap {
diff --git a/app/javascript/dashboard/components/widgets/AutomationActionInput.vue b/app/javascript/dashboard/components/widgets/AutomationActionInput.vue
index 22521af91..0c8178a9b 100644
--- a/app/javascript/dashboard/components/widgets/AutomationActionInput.vue
+++ b/app/javascript/dashboard/components/widgets/AutomationActionInput.vue
@@ -7,7 +7,8 @@
@@ -187,7 +188,13 @@ export default {
required,
$each: {
action_params: {
- required,
+ required: requiredIf(prop => {
+ return !(
+ prop.action_name === 'mute_conversation' ||
+ prop.action_name === 'snooze_convresation' ||
+ prop.action_name === 'resolve_convresation'
+ );
+ }),
},
},
},
@@ -351,7 +358,6 @@ export default {
getActionDropdownValues(type) {
switch (type) {
case 'assign_team':
- case 'send_email_to_team':
return this.$store.getters['teams/getTeams'];
case 'add_label':
return this.$store.getters['labels/getLabels'].map(i => {
@@ -424,6 +430,9 @@ export default {
).filterOperators[0].value;
this.automation.conditions[index].values = '';
},
+ resetAction(index) {
+ this.automation.actions[index].action_params = [];
+ },
showUserInput(operatorType) {
if (operatorType === 'is_present' || operatorType === 'is_not_present')
return false;
diff --git a/app/javascript/dashboard/routes/dashboard/settings/automation/EditAutomationRule.vue b/app/javascript/dashboard/routes/dashboard/settings/automation/EditAutomationRule.vue
index d55ce419b..6add27ba5 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/automation/EditAutomationRule.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/automation/EditAutomationRule.vue
@@ -351,7 +351,6 @@ export default {
getActionDropdownValues(type) {
switch (type) {
case 'assign_team':
- case 'send_email_to_team':
return this.$store.getters['teams/getTeams'];
case 'add_label':
return this.$store.getters['labels/getLabels'].map(i => {
diff --git a/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js b/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js
index 90e3a7395..dd11700bd 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js
+++ b/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js
@@ -79,8 +79,33 @@ export const AUTOMATIONS = {
// {
// key: 'send_email_to_team',
// name: 'Send an email to team',
- // attributeI18nKey: 'SEND_EMAIL_TO_TEAM',
+ // attributeI18nKey: 'SEND_MESSAGE',
// },
+ {
+ key: 'send_email_transcript',
+ name: 'Send an email transcript',
+ attributeI18nKey: 'SEND_EMAIL_TRANSCRIPT',
+ },
+ {
+ key: 'mute_conversation',
+ name: 'Mute conversation',
+ attributeI18nKey: 'MUTE_CONVERSATION',
+ },
+ {
+ key: 'snooze_convresation',
+ name: 'Snooze conversation',
+ attributeI18nKey: 'MUTE_CONVERSATION',
+ },
+ {
+ key: 'resolve_convresation',
+ name: 'Resolve conversation',
+ attributeI18nKey: 'RESOLVE_CONVERSATION',
+ },
+ {
+ key: 'send_webhook_event',
+ name: 'Send Webhook Event',
+ attributeI18nKey: 'SEND_WEBHOOK_EVENT',
+ },
],
},
conversation_created: {
@@ -120,15 +145,40 @@ export const AUTOMATIONS = {
name: 'Assign a team',
attributeI18nKey: 'ASSIGN_TEAM',
},
+ {
+ key: 'assign_agent',
+ name: 'Assign an agent',
+ attributeI18nKey: 'ASSIGN_AGENT',
+ },
// {
// key: 'send_email_to_team',
// name: 'Send an email to team',
// attributeI18nKey: 'SEND_MESSAGE',
// },
{
- key: 'assign_agent',
- name: 'Assign an agent',
- attributeI18nKey: 'ASSIGN_AGENT',
+ key: 'send_email_transcript',
+ name: 'Send an email transcript',
+ attributeI18nKey: 'SEND_EMAIL_TRANSCRIPT',
+ },
+ {
+ key: 'mute_conversation',
+ name: 'Mute conversation',
+ attributeI18nKey: 'MUTE_CONVERSATION',
+ },
+ {
+ key: 'snooze_convresation',
+ name: 'Snooze conversation',
+ attributeI18nKey: 'MUTE_CONVERSATION',
+ },
+ {
+ key: 'resolve_convresation',
+ name: 'Resolve conversation',
+ attributeI18nKey: 'RESOLVE_CONVERSATION',
+ },
+ {
+ key: 'send_webhook_event',
+ name: 'Send Webhook Event',
+ attributeI18nKey: 'SEND_WEBHOOK_EVENT',
},
],
},
@@ -183,16 +233,40 @@ export const AUTOMATIONS = {
name: 'Assign a team',
attributeI18nKey: 'ASSIGN_TEAM',
},
- // {
- // key: 'send_email_to_team',
- // name: 'Send an email to team',
- // attributeI18nKey: 'SEND_EMAIL_TO_TEAM',
- // },
{
key: 'assign_agent',
name: 'Assign an agent',
attributeI18nKey: 'ASSIGN_AGENT',
- attributeKey: 'assignee_id',
+ },
+ // {
+ // key: 'send_email_to_team',
+ // name: 'Send an email to team',
+ // attributeI18nKey: 'SEND_MESSAGE',
+ // },
+ {
+ key: 'send_email_transcript',
+ name: 'Send an email transcript',
+ attributeI18nKey: 'SEND_EMAIL_TRANSCRIPT',
+ },
+ {
+ key: 'mute_conversation',
+ name: 'Mute conversation',
+ attributeI18nKey: 'MUTE_CONVERSATION',
+ },
+ {
+ key: 'snooze_convresation',
+ name: 'Snooze conversation',
+ attributeI18nKey: 'MUTE_CONVERSATION',
+ },
+ {
+ key: 'resolve_convresation',
+ name: 'Resolve conversation',
+ attributeI18nKey: 'RESOLVE_CONVERSATION',
+ },
+ {
+ key: 'send_webhook_event',
+ name: 'Send Webhook Event',
+ attributeI18nKey: 'SEND_WEBHOOK_EVENT',
},
],
},
@@ -217,13 +291,41 @@ export const AUTOMATION_ACTION_TYPES = [
{
key: 'assign_team',
label: 'Assign a team',
+ inputType: 'multi_select',
},
{
key: 'add_label',
label: 'Add a label',
+ inputType: 'multi_select',
},
// {
// key: 'send_email_to_team',
// label: 'Send an email to team',
+ // inputType: 'multi_select',
// },
+ {
+ key: 'send_email_transcript',
+ label: 'Send an email transcript',
+ inputType: 'email',
+ },
+ {
+ key: 'mute_conversation',
+ label: 'Mute conversation',
+ inputType: null,
+ },
+ {
+ key: 'snooze_convresation',
+ label: 'Snooze conversation',
+ inputType: null,
+ },
+ {
+ key: 'resolve_convresation',
+ label: 'Resolve conversation',
+ inputType: null,
+ },
+ {
+ key: 'send_webhook_event',
+ label: 'Send Webhook Event',
+ inputType: 'url',
+ },
];
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue
index fe0240d74..1b46c474a 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue
@@ -22,6 +22,7 @@
:key="metric.NAME"
:desc="metric.DESC"
:heading="metric.NAME"
+ :info-text="displayInfoText(metric.KEY)"
:index="index"
:on-click="changeSelection"
:point="displayMetric(metric.KEY)"
@@ -35,7 +36,11 @@
:message="$t('REPORT.LOADING_CHART')"
/>
-
+
{{ $t('REPORT.NO_ENOUGH_DATA') }}
@@ -49,7 +54,7 @@ import { mapGetters } from 'vuex';
import fromUnixTime from 'date-fns/fromUnixTime';
import format from 'date-fns/format';
import ReportFilterSelector from './components/FilterSelector';
-import { GROUP_BY_FILTER } from './constants';
+import { GROUP_BY_FILTER, METRIC_CHART } from './constants';
import reportMixin from '../../../../mixins/reportMixin';
const REPORTS_KEYS = {
@@ -108,16 +113,38 @@ export default {
}
return format(fromUnixTime(element.timestamp), 'dd-MMM-yyyy');
});
- const data = this.accountReport.data.map(element => element.value);
+
+ const datasets = METRIC_CHART[
+ this.metrics[this.currentSelection].KEY
+ ].datasets.map(dataset => {
+ switch (dataset.type) {
+ case 'bar':
+ return {
+ ...dataset,
+ yAxisID: 'y-left',
+ label: this.metrics[this.currentSelection].NAME,
+ data: this.accountReport.data.map(element => element.value),
+ };
+ case 'line':
+ return {
+ ...dataset,
+ yAxisID: 'y-right',
+ label: this.metrics[0].NAME,
+ data: this.accountReport.data.map(element => element.count),
+ };
+ default:
+ return dataset;
+ }
+ });
+
return {
labels,
- datasets: [
- {
- label: this.metrics[this.currentSelection].NAME,
- backgroundColor: '#1f93ff',
- data,
- },
- ],
+ datasets,
+ };
+ },
+ chartOptions() {
+ return {
+ scales: METRIC_CHART[this.metrics[this.currentSelection].KEY].scales,
};
},
metrics() {
@@ -133,6 +160,7 @@ export default {
NAME: this.$t(`REPORT.METRICS.${key}.NAME`),
KEY: REPORTS_KEYS[key],
DESC: this.$t(`REPORT.METRICS.${key}.DESC`),
+ INFO_TEXT: this.$t(`REPORT.METRICS.${key}.INFO_TEXT`),
}));
},
},
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue
index 8ce264080..c5cf3993b 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue
@@ -25,6 +25,7 @@
:key="metric.NAME"
:desc="metric.DESC"
:heading="metric.NAME"
+ :info-text="displayInfoText(metric.KEY)"
:index="index"
:on-click="changeSelection"
:point="displayMetric(metric.KEY)"
@@ -41,6 +42,7 @@
{{ $t('REPORT.NO_ENOUGH_DATA') }}
@@ -55,7 +57,7 @@
import ReportFilters from './ReportFilters';
import fromUnixTime from 'date-fns/fromUnixTime';
import format from 'date-fns/format';
-import { GROUP_BY_FILTER } from '../constants';
+import { GROUP_BY_FILTER, METRIC_CHART } from '../constants';
import reportMixin from '../../../../../mixins/reportMixin';
const REPORTS_KEYS = {
@@ -137,16 +139,38 @@ export default {
}
return format(fromUnixTime(element.timestamp), 'dd-MMM-yyyy');
});
- const data = this.accountReport.data.map(element => element.value);
+
+ const datasets = METRIC_CHART[
+ this.metrics[this.currentSelection].KEY
+ ].datasets.map(dataset => {
+ switch (dataset.type) {
+ case 'bar':
+ return {
+ ...dataset,
+ yAxisID: 'y-left',
+ label: this.metrics[this.currentSelection].NAME,
+ data: this.accountReport.data.map(element => element.value),
+ };
+ case 'line':
+ return {
+ ...dataset,
+ yAxisID: 'y-right',
+ label: this.metrics[0].NAME,
+ data: this.accountReport.data.map(element => element.count),
+ };
+ default:
+ return dataset;
+ }
+ });
+
return {
labels,
- datasets: [
- {
- label: this.metrics[this.currentSelection].NAME,
- backgroundColor: '#1f93ff',
- data,
- },
- ],
+ datasets,
+ };
+ },
+ chartOptions() {
+ return {
+ scales: METRIC_CHART[this.metrics[this.currentSelection].KEY].scales,
};
},
metrics() {
@@ -168,6 +192,7 @@ export default {
NAME: this.$t(`REPORT.METRICS.${key}.NAME`),
KEY: REPORTS_KEYS[key],
DESC: this.$t(`REPORT.METRICS.${key}.DESC`),
+ INFO_TEXT: this.$t(`REPORT.METRICS.${key}.INFO_TEXT`),
}));
},
},
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/constants.js b/app/javascript/dashboard/routes/dashboard/settings/reports/constants.js
index f285061cb..91348543e 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/reports/constants.js
+++ b/app/javascript/dashboard/routes/dashboard/settings/reports/constants.js
@@ -4,3 +4,142 @@ export const GROUP_BY_FILTER = {
3: { id: 3, period: 'month' },
4: { id: 4, period: 'year' },
};
+
+export const CHART_FONT_FAMILY =
+ '-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';
+
+export const DEFAULT_LINE_CHART = {
+ type: 'line',
+ fill: false,
+ borderColor: '#779BBB',
+ pointBackgroundColor: '#779BBB',
+};
+
+export const DEFAULT_BAR_CHART = {
+ type: 'bar',
+ backgroundColor: 'rgb(31, 147, 255, 0.5)',
+};
+
+export const DEFAULT_CHART = {
+ datasets: [DEFAULT_BAR_CHART],
+ scales: {
+ xAxes: [
+ {
+ ticks: {
+ fontFamily: CHART_FONT_FAMILY,
+ },
+ gridLines: {
+ drawOnChartArea: false,
+ },
+ },
+ ],
+ yAxes: [
+ {
+ id: 'y-left',
+ type: 'linear',
+ position: 'left',
+ ticks: {
+ fontFamily: CHART_FONT_FAMILY,
+ beginAtZero: true,
+ stepSize: 1,
+ },
+ gridLines: {
+ drawOnChartArea: false,
+ },
+ },
+ ],
+ },
+};
+
+export const METRIC_CHART = {
+ conversations_count: DEFAULT_CHART,
+ incoming_messages_count: DEFAULT_CHART,
+ outgoing_messages_count: DEFAULT_CHART,
+ avg_first_response_time: {
+ datasets: [DEFAULT_BAR_CHART, DEFAULT_LINE_CHART],
+ scales: {
+ xAxes: [
+ {
+ ticks: {
+ fontFamily: CHART_FONT_FAMILY,
+ },
+ gridLines: {
+ drawOnChartArea: false,
+ },
+ },
+ ],
+ yAxes: [
+ {
+ id: 'y-left',
+ type: 'linear',
+ position: 'left',
+ ticks: {
+ fontFamily: CHART_FONT_FAMILY,
+ beginAtZero: true,
+ precision: 2,
+ },
+ gridLines: {
+ drawOnChartArea: false,
+ },
+ },
+ {
+ id: 'y-right',
+ type: 'linear',
+ position: 'right',
+ ticks: {
+ fontFamily: CHART_FONT_FAMILY,
+ beginAtZero: true,
+ stepSize: 1,
+ },
+ gridLines: {
+ drawOnChartArea: false,
+ },
+ },
+ ],
+ },
+ },
+ avg_resolution_time: {
+ datasets: [DEFAULT_BAR_CHART, DEFAULT_LINE_CHART],
+ scales: {
+ xAxes: [
+ {
+ ticks: {
+ fontFamily: CHART_FONT_FAMILY,
+ },
+ gridLines: {
+ drawOnChartArea: false,
+ },
+ },
+ ],
+ yAxes: [
+ {
+ id: 'y-left',
+ type: 'linear',
+ position: 'left',
+ ticks: {
+ fontFamily: CHART_FONT_FAMILY,
+ beginAtZero: true,
+ precision: 2,
+ },
+ gridLines: {
+ drawOnChartArea: false,
+ },
+ },
+ {
+ id: 'y-right',
+ type: 'linear',
+ position: 'right',
+ ticks: {
+ fontFamily: CHART_FONT_FAMILY,
+ beginAtZero: true,
+ stepSize: 1,
+ },
+ gridLines: {
+ drawOnChartArea: false,
+ },
+ },
+ ],
+ },
+ },
+ resolutions_count: DEFAULT_CHART,
+};
diff --git a/app/javascript/dashboard/store/modules/notifications/actions.js b/app/javascript/dashboard/store/modules/notifications/actions.js
index 8bc4fdfcd..d28020b37 100644
--- a/app/javascript/dashboard/store/modules/notifications/actions.js
+++ b/app/javascript/dashboard/store/modules/notifications/actions.js
@@ -52,4 +52,8 @@ export const actions = {
throw new Error(error);
}
},
+
+ addNotification({ commit }, data) {
+ commit(types.ADD_NOTIFICATION, data);
+ },
};
diff --git a/app/javascript/dashboard/store/modules/notifications/mutations.js b/app/javascript/dashboard/store/modules/notifications/mutations.js
index ca896835f..96e6816a6 100644
--- a/app/javascript/dashboard/store/modules/notifications/mutations.js
+++ b/app/javascript/dashboard/store/modules/notifications/mutations.js
@@ -45,4 +45,14 @@ export const mutations = {
Vue.set($state.records[item.id], 'read_at', true);
});
},
+
+ [types.ADD_NOTIFICATION]($state, data) {
+ const { notification, unread_count: unreadCount, count } = data;
+ Vue.set($state.records, notification.id, {
+ ...($state.records[notification.id] || {}),
+ ...notification,
+ });
+ Vue.set($state.meta, 'unreadCount', unreadCount);
+ Vue.set($state.meta, 'count', count);
+ },
};
diff --git a/app/javascript/dashboard/store/modules/specs/notifications/actions.spec.js b/app/javascript/dashboard/store/modules/specs/notifications/actions.spec.js
index fd94d9a67..7351905a0 100644
--- a/app/javascript/dashboard/store/modules/specs/notifications/actions.spec.js
+++ b/app/javascript/dashboard/store/modules/specs/notifications/actions.spec.js
@@ -90,4 +90,12 @@ describe('#actions', () => {
await expect(actions.readAll({ commit })).rejects.toThrow(Error);
});
});
+ describe('#addNotification', () => {
+ it('sends correct actions if API is success', async () => {
+ await actions.addNotification({ commit }, { data: 1 });
+ expect(commit.mock.calls).toEqual([
+ [types.ADD_NOTIFICATION, { data: 1 }],
+ ]);
+ });
+ });
});
diff --git a/app/javascript/dashboard/store/modules/specs/notifications/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/notifications/mutations.spec.js
index 15a097df7..ab8027c6e 100644
--- a/app/javascript/dashboard/store/modules/specs/notifications/mutations.spec.js
+++ b/app/javascript/dashboard/store/modules/specs/notifications/mutations.spec.js
@@ -93,4 +93,29 @@ describe('#mutations', () => {
});
});
});
+
+ describe('#ADD_NOTIFICATION', () => {
+ it('add notification', () => {
+ const state = {
+ meta: { unreadCount: 4, count: 231 },
+ records: {
+ 1: { id: 1, primary_actor_id: 1 },
+ 2: { id: 2, primary_actor_id: 2 },
+ },
+ };
+ const data = {
+ notification: { id: 3, primary_actor_id: 3 },
+ unread_count: 5,
+ count: 232,
+ };
+ mutations[types.ADD_NOTIFICATION](state, data);
+ expect(state.records).toEqual({
+ 1: { id: 1, primary_actor_id: 1 },
+ 2: { id: 2, primary_actor_id: 2 },
+ 3: { id: 3, primary_actor_id: 3 },
+ });
+ expect(state.meta.unreadCount).toEqual(5);
+ expect(state.meta.count).toEqual(232);
+ });
+ });
});
diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js
index 734e0b5e5..9a24fa3b4 100755
--- a/app/javascript/dashboard/store/mutation-types.js
+++ b/app/javascript/dashboard/store/mutation-types.js
@@ -116,6 +116,7 @@ export default {
SET_NOTIFICATIONS_UNREAD_COUNT: 'SET_NOTIFICATIONS_UNREAD_COUNT',
SET_NOTIFICATIONS_UI_FLAG: 'SET_NOTIFICATIONS_UI_FLAG',
UPDATE_NOTIFICATION: 'UPDATE_NOTIFICATION',
+ ADD_NOTIFICATION: 'ADD_NOTIFICATION',
UPDATE_ALL_NOTIFICATIONS: 'UPDATE_ALL_NOTIFICATIONS',
SET_NOTIFICATIONS_ITEM: 'SET_NOTIFICATIONS_ITEM',
SET_NOTIFICATIONS: 'SET_NOTIFICATIONS',
diff --git a/app/javascript/packs/sdk.js b/app/javascript/packs/sdk.js
index 66f6ba790..76e1a2cd8 100755
--- a/app/javascript/packs/sdk.js
+++ b/app/javascript/packs/sdk.js
@@ -31,6 +31,14 @@ const runSDK = ({ baseUrl, websiteToken }) => {
IFrameHelper.events.toggleBubble(state);
},
+ popoutChatWindow() {
+ IFrameHelper.events.popoutChatWindow({
+ baseUrl: window.$chatwoot.baseUrl,
+ websiteToken: window.$chatwoot.websiteToken,
+ locale: window.$chatwoot.locale,
+ });
+ },
+
setUser(identifier, user) {
if (typeof identifier !== 'string' && typeof identifier !== 'number') {
throw new Error('Identifier should be a string or a number');
diff --git a/app/javascript/sdk/IFrameHelper.js b/app/javascript/sdk/IFrameHelper.js
index ba495bbc8..df36d8e0d 100644
--- a/app/javascript/sdk/IFrameHelper.js
+++ b/app/javascript/sdk/IFrameHelper.js
@@ -31,6 +31,7 @@ import {
initOnEvents,
} from 'shared/helpers/AudioNotificationHelper';
import { isFlatWidgetStyle } from './settingsHelper';
+import { popoutChatWindow } from '../widget/helpers/popoutHelper';
export const IFrameHelper = {
getUrl({ baseUrl, websiteToken }) {
@@ -190,6 +191,12 @@ export const IFrameHelper = {
onBubbleClick(bubbleState);
},
+ popoutChatWindow: ({ baseUrl, websiteToken, locale }) => {
+ const cwCookie = Cookies.get('cw_conversation');
+ window.$chatwoot.toggle('close');
+ popoutChatWindow(baseUrl, websiteToken, locale, cwCookie);
+ },
+
closeWindow: () => {
onBubbleClick({ toggleValue: false });
removeUnreadClass();
diff --git a/app/javascript/widget/components/HeaderActions.vue b/app/javascript/widget/components/HeaderActions.vue
index 4d6e5faa9..1090d54b0 100644
--- a/app/javascript/widget/components/HeaderActions.vue
+++ b/app/javascript/widget/components/HeaderActions.vue
@@ -29,7 +29,7 @@
diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb
index 87deded55..adefee312 100644
--- a/config/initializers/rack_attack.rb
+++ b/config/initializers/rack_attack.rb
@@ -91,7 +91,7 @@ end
# Log blocked events
ActiveSupport::Notifications.subscribe('throttle.rack_attack') do |_name, _start, _finish, _request_id, payload|
- Rails.logger.info "[Rack::Attack][Blocked] remote_ip: \"#{payload[:request].remote_ip}\", path: \"#{payload[:request].path}\""
+ Rails.logger.warn "[Rack::Attack][Blocked] remote_ip: \"#{payload[:request].remote_ip}\", path: \"#{payload[:request].path}\""
end
Rack::Attack.enabled = Rails.env.production? ? ActiveModel::Type::Boolean.new.cast(ENV.fetch('ENABLE_RACK_ATTACK', true)) : false
diff --git a/config/routes.rb b/config/routes.rb
index 6e4296e62..a2afa4f09 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -52,8 +52,8 @@ Rails.application.routes.draw do
post :reauthorize_page
end
end
- resources :canned_responses, except: [:show, :edit, :new]
- resources :automation_rules, except: [:edit] do
+ resources :canned_responses, only: [:index, :create, :update, :destroy]
+ resources :automation_rules, only: [:index, :create, :show, :update, :destroy] do
post :clone
end
resources :campaigns, only: [:index, :create, :show, :update, :destroy]
@@ -145,7 +145,7 @@ Rails.application.routes.draw do
resource :authorization, only: [:create]
end
- resources :webhooks, except: [:show]
+ resources :webhooks, only: [:index, :create, :update, :destroy]
namespace :integrations do
resources :apps, only: [:index, :show]
resources :hooks, only: [:create, :update, :destroy]
@@ -212,6 +212,7 @@ Rails.application.routes.draw do
get :inboxes
get :labels
get :teams
+ get :conversations
end
end
end
diff --git a/config/sidekiq.yml b/config/sidekiq.yml
index 2c6d89933..e83ddf8f0 100644
--- a/config/sidekiq.yml
+++ b/config/sidekiq.yml
@@ -18,6 +18,7 @@
- [bots, 1]
- [active_storage_analysis, 1]
- [action_mailbox_incineration, 1]
+ - [active_storage_purge, 1]
- [integrations, 2]
- [default, 2]
- [mailers, 2]
diff --git a/lib/chatwoot_hub.rb b/lib/chatwoot_hub.rb
index c8ef38de0..655060c7e 100644
--- a/lib/chatwoot_hub.rb
+++ b/lib/chatwoot_hub.rb
@@ -39,7 +39,7 @@ class ChatwootHub
response = RestClient.post(PING_URL, info.to_json, { content_type: :json, accept: :json })
version = JSON.parse(response)['version']
rescue *ExceptionList::REST_CLIENT_EXCEPTIONS => e
- Rails.logger.info "Exception: #{e.message}"
+ Rails.logger.error "Exception: #{e.message}"
rescue StandardError => e
Sentry.capture_exception(e)
end
@@ -50,7 +50,7 @@ class ChatwootHub
info = { company_name: company_name, owner_name: owner_name, owner_email: owner_email, subscribed_to_mailers: true }
RestClient.post(REGISTRATION_URL, info.merge(instance_config).to_json, { content_type: :json, accept: :json })
rescue *ExceptionList::REST_CLIENT_EXCEPTIONS => e
- Rails.logger.info "Exception: #{e.message}"
+ Rails.logger.error "Exception: #{e.message}"
rescue StandardError => e
Sentry.capture_exception(e)
end
@@ -59,7 +59,7 @@ class ChatwootHub
info = { fcm_token_list: fcm_token_list, fcm_options: fcm_options }
RestClient.post(PUSH_NOTIFICATION_URL, info.merge(instance_config).to_json, { content_type: :json, accept: :json })
rescue *ExceptionList::REST_CLIENT_EXCEPTIONS => e
- Rails.logger.info "Exception: #{e.message}"
+ Rails.logger.error "Exception: #{e.message}"
rescue StandardError => e
Sentry.capture_exception(e)
end
@@ -70,7 +70,7 @@ class ChatwootHub
info = { event_name: event_name, event_data: event_data }
RestClient.post(EVENTS_URL, info.merge(instance_config).to_json, { content_type: :json, accept: :json })
rescue *ExceptionList::REST_CLIENT_EXCEPTIONS => e
- Rails.logger.info "Exception: #{e.message}"
+ Rails.logger.error "Exception: #{e.message}"
rescue StandardError => e
Sentry.capture_exception(e)
end
diff --git a/lib/filters/filter_keys.json b/lib/filters/filter_keys.json
index 2736473b7..3d662d19b 100644
--- a/lib/filters/filter_keys.json
+++ b/lib/filters/filter_keys.json
@@ -142,6 +142,13 @@
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain" ],
"attribute_type": "additional_attributes"
},
+ "company": {
+ "attribute_name": "Company",
+ "input_type": "textbox",
+ "data_type": "text",
+ "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain" ],
+ "attribute_type": "additional_attributes"
+ },
"labels": {
"attribute_name": "Labels",
"input_type": "tags",
diff --git a/lib/integrations/slack/send_on_slack_service.rb b/lib/integrations/slack/send_on_slack_service.rb
index 6f780ae38..1eb91b3ea 100644
--- a/lib/integrations/slack/send_on_slack_service.rb
+++ b/lib/integrations/slack/send_on_slack_service.rb
@@ -48,7 +48,7 @@ class Integrations::Slack::SendOnSlackService < Base::SendOnChannelService
post_message if message_content.present?
upload_file if message.attachments.any?
rescue Slack::Web::Api::Errors::AccountInactive => e
- Rails.logger.info e
+ Rails.logger.error e
hook.authorization_error!
hook.disable if hook.enabled?
end
diff --git a/spec/controllers/api/v1/accounts/automation_rules_controller_spec.rb b/spec/controllers/api/v1/accounts/automation_rules_controller_spec.rb
index 5755bf95a..0d3b3e784 100644
--- a/spec/controllers/api/v1/accounts/automation_rules_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/automation_rules_controller_spec.rb
@@ -84,6 +84,23 @@ RSpec.describe 'Api::V1::Accounts::AutomationRulesController', type: :request do
}.with_indifferent_access
end
+ it 'throws an error for unknown attributes in condtions' do
+ expect(account.automation_rules.count).to eq(0)
+ params[:conditions] << {
+ attribute_key: 'unknown_attribute',
+ filter_operator: 'equal_to',
+ values: ['en'],
+ query_operator: 'AND'
+ }
+
+ post "/api/v1/accounts/#{account.id}/automation_rules",
+ headers: administrator.create_new_auth_token,
+ params: params
+
+ expect(response).to have_http_status(:unprocessable_entity)
+ expect(account.automation_rules.count).to eq(0)
+ end
+
it 'Saves for automation_rules for account with country_code and browser_language conditions' do
expect(account.automation_rules.count).to eq(0)
@@ -113,6 +130,67 @@ RSpec.describe 'Api::V1::Accounts::AutomationRulesController', type: :request do
expect(response).to have_http_status(:success)
expect(account.automation_rules.count).to eq(1)
end
+
+ it 'Saves file in the automation actions to send an attachments' do
+ file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
+ params[:attachments] = [file]
+ params[:actions] = [
+ {
+ action_name: :send_message,
+ action_params: ['Welcome to the chatwoot platform.']
+ },
+ {
+ action_name: :update_additional_attributes,
+ action_params: [{ intiated_at: '2021-12-03 17:25:26.844536 +0530' }]
+ },
+ {
+ action_name: :send_attachments
+ }
+ ]
+
+ expect(account.automation_rules.count).to eq(0)
+
+ post "/api/v1/accounts/#{account.id}/automation_rules",
+ headers: administrator.create_new_auth_token,
+ params: params
+
+ expect(response).to have_http_status(:success)
+ expect(account.automation_rules.count).to eq(1)
+ automation_rule = account.automation_rules.first
+ expect(automation_rule.files.presence).to be_truthy
+ expect(automation_rule.files.count).to eq(1)
+ end
+
+ it 'Saves files in the automation actions to send multiple attachments' do
+ file_1 = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
+ file_2 = fixture_file_upload(Rails.root.join('spec/assets/sample.pdf'), 'image/png')
+ params[:attachments] = [file_1, file_2]
+ params[:actions] = [
+ {
+ action_name: :send_message,
+ action_params: ['Welcome to the chatwoot platform.']
+ },
+ {
+ action_name: :update_additional_attributes,
+ action_params: [{ intiated_at: '2021-12-03 17:25:26.844536 +0530' }]
+ },
+ {
+ action_name: :send_attachments
+ }
+ ]
+
+ expect(account.automation_rules.count).to eq(0)
+
+ post "/api/v1/accounts/#{account.id}/automation_rules",
+ headers: administrator.create_new_auth_token,
+ params: params
+
+ expect(response).to have_http_status(:success)
+ expect(account.automation_rules.count).to eq(1)
+ automation_rule = account.automation_rules.first
+ expect(automation_rule.files.presence).to be_truthy
+ expect(automation_rule.files.count).to eq(2)
+ end
end
end
diff --git a/spec/controllers/api/v1/accounts/conversations/assignments_controller_spec.rb b/spec/controllers/api/v1/accounts/conversations/assignments_controller_spec.rb
index 5d65c70ca..6583eaf0e 100644
--- a/spec/controllers/api/v1/accounts/conversations/assignments_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/conversations/assignments_controller_spec.rb
@@ -35,6 +35,9 @@ RSpec.describe 'Conversation Assignment API', type: :request do
end
it 'assigns a team to the conversation' do
+ team_member = create(:user, account: account, role: :agent)
+ create(:inbox_member, inbox: conversation.inbox, user: team_member)
+ create(:team_member, team: team, user: team_member)
params = { team_id: team.id }
post api_v1_account_conversation_assignments_url(account_id: account.id, conversation_id: conversation.display_id),
@@ -44,6 +47,8 @@ RSpec.describe 'Conversation Assignment API', type: :request do
expect(response).to have_http_status(:success)
expect(conversation.reload.team).to eq(team)
+ # assignee will be from team
+ expect(conversation.reload.assignee).to eq(team_member)
end
end
diff --git a/spec/controllers/api/v2/accounts/report_controller_spec.rb b/spec/controllers/api/v2/accounts/report_controller_spec.rb
index 7fbc3ed18..cb2b9b72d 100644
--- a/spec/controllers/api/v2/accounts/report_controller_spec.rb
+++ b/spec/controllers/api/v2/accounts/report_controller_spec.rb
@@ -7,7 +7,8 @@ RSpec.describe 'Reports API', type: :request do
let!(:user) { create(:user, account: account) }
let!(:inbox) { create(:inbox, account: account) }
let(:inbox_member) { create(:inbox_member, user: user, inbox: inbox) }
- let(:date_timestamp) { Time.current.beginning_of_day.to_i }
+ let(:default_timezone) { ActiveSupport::TimeZone[0]&.name }
+ let(:date_timestamp) { Time.current.in_time_zone(default_timezone).beginning_of_day.to_i }
let(:params) { { timezone_offset: Time.zone.utc_offset } }
before do
@@ -56,6 +57,68 @@ RSpec.describe 'Reports API', type: :request do
expect(current_day_metric.length).to eq(1)
expect(current_day_metric[0]['value']).to eq(10)
end
+
+ it 'return conversation metrics in account level' do
+ unassigned_conversation = create(:conversation, account: account, inbox: inbox,
+ assignee: nil, created_at: Time.zone.today)
+ unassigned_conversation.save!
+
+ get "/api/v2/accounts/#{account.id}/reports/conversations",
+ params: {
+ type: :account
+ },
+ headers: admin.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ json_response = JSON.parse(response.body)
+
+ expect(json_response['open']).to eq(11)
+ expect(json_response['unattended']).to eq(11)
+ expect(json_response['unassigned']).to eq(1)
+ end
+
+ it 'return conversation metrics for user in account level' do
+ create_list(:conversation, 2, account: account, inbox: inbox,
+ assignee: admin, created_at: Time.zone.today)
+
+ get "/api/v2/accounts/#{account.id}/reports/conversations",
+ params: {
+ type: :agent
+ },
+ headers: admin.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:success)
+
+ json_response = JSON.parse(response.body)
+ expect(json_response.blank?).to be false
+ user_metrics = json_response.find { |item| item['user']['name'] == admin[:name] }
+ expect(user_metrics.present?).to be true
+
+ expect(user_metrics['metric']['open']).to eq(2)
+ expect(user_metrics['metric']['unattended']).to eq(2)
+ end
+
+ it 'return conversation metrics for specific user in account level' do
+ create_list(:conversation, 2, account: account, inbox: inbox,
+ assignee: admin, created_at: Time.zone.today)
+
+ get "/api/v2/accounts/#{account.id}/reports/conversations",
+ params: {
+ type: :agent,
+ user_id: user.id
+ },
+ headers: admin.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:success)
+
+ json_response = JSON.parse(response.body)
+ expect(json_response.blank?).to be false
+ expect(json_response[0]['metric']['open']).to eq(10)
+ expect(json_response[0]['metric']['unattended']).to eq(10)
+ end
end
end
diff --git a/spec/cypress/app_commands/log_fail.rb b/spec/cypress/app_commands/log_fail.rb
index 8c64be394..264d9ea66 100644
--- a/spec/cypress/app_commands/log_fail.rb
+++ b/spec/cypress/app_commands/log_fail.rb
@@ -13,7 +13,7 @@ if defined?(ActiveRecord::Base)
ActiveRecord::Base.descendants.each_with_object({}) do |record_class, records|
records[record_class.to_s] = record_class.limit(100).map(&:attributes)
rescue StandardError => e
- Rails.logger.info e.message
+ Rails.logger.error e.message
end
end
diff --git a/spec/listeners/automation_rule_listener_spec.rb b/spec/listeners/automation_rule_listener_spec.rb
index ff289a6dd..6c4fc0c2a 100644
--- a/spec/listeners/automation_rule_listener_spec.rb
+++ b/spec/listeners/automation_rule_listener_spec.rb
@@ -15,6 +15,12 @@ describe AutomationRuleListener do
end
before do
+ create(:custom_attribute_definition,
+ attribute_key: 'customer_type',
+ account: account,
+ attribute_model: 'contact_attribute',
+ attribute_display_type: 'list',
+ attribute_values: %w[regular platinum gold])
create(:team_member, user: user_1, team: team)
create(:team_member, user: user_2, team: team)
create(:account_user, user: user_2, account: account)
@@ -31,13 +37,17 @@ describe AutomationRuleListener do
},
{ 'action_name' => 'assign_team', 'action_params' => [team.id] },
{ 'action_name' => 'add_label', 'action_params' => %w[support priority_customer] },
- { 'action_name' => 'send_webhook_events', 'action_params' => 'https://www.example.com' },
+ { 'action_name' => 'send_webhook_event', 'action_params' => ['https://www.example.com'] },
{ 'action_name' => 'assign_best_agent', 'action_params' => [user_1.id] },
{ 'action_name' => 'send_email_transcript', 'action_params' => 'new_agent@example.com' },
{ 'action_name' => 'mute_conversation', 'action_params' => nil },
{ 'action_name' => 'change_status', 'action_params' => ['snoozed'] },
- { 'action_name' => 'send_message', 'action_params' => ['Send this message.'] }
+ { 'action_name' => 'send_message', 'action_params' => ['Send this message.'] },
+ { 'action_name' => 'send_attachments' }
])
+ file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
+ automation_rule.files.attach(file)
+ automation_rule.save
end
describe '#conversation_status_changed' do
@@ -77,6 +87,7 @@ describe AutomationRuleListener do
listener.conversation_status_changed(event)
conversation.reload
+
expect(conversation.labels.pluck(:name)).to contain_exactly('support', 'priority_customer')
end
@@ -105,7 +116,7 @@ describe AutomationRuleListener do
conversation.reload
- expect(conversation.messages.last.content).to eq('Send this message.')
+ expect(conversation.messages.first.content).to eq('Send this message.')
end
it 'triggers automation rule changes status to snoozed' do
@@ -143,6 +154,120 @@ describe AutomationRuleListener do
listener.conversation_status_changed(event)
end
+
+ it 'triggers automation rule send attachments in messages' do
+ automation_rule
+
+ expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation)
+
+ listener.conversation_status_changed(event)
+
+ conversation.reload
+
+ expect(conversation.messages.last.attachments.count).to eq(1)
+ end
+ end
+ end
+
+ describe '#conversation_updated with contacts attributes' do
+ before do
+ conversation.contact.update!(custom_attributes: { customer_type: 'platinum', signed_in_at: '2022-01-19' },
+ additional_attributes: { 'company': 'Marvel' })
+
+ automation_rule.update!(
+ event_name: 'conversation_updated',
+ name: 'Call actions conversation updated',
+ description: 'Add labels, assign team after conversation updated',
+ conditions: [
+ {
+ attribute_key: 'company',
+ filter_operator: 'equal_to',
+ values: ['Marvel'],
+ query_operator: 'AND'
+ }.with_indifferent_access,
+ {
+ attribute_key: 'customer_type',
+ filter_operator: 'equal_to',
+ values: ['platinum'],
+ query_operator: nil
+ }.with_indifferent_access
+ ]
+ )
+ end
+
+ let!(:event) do
+ Events::Base.new('conversation_updated', Time.zone.now, { conversation: conversation })
+ end
+
+ context 'when rule matches with additional_attributes and custom_attributes' do
+ it 'triggers automation rule to assign team' do
+ expect(conversation.team_id).not_to eq(team.id)
+
+ listener.conversation_updated(event)
+
+ conversation.reload
+ expect(conversation.team_id).to eq(team.id)
+ end
+
+ it 'triggers automation rule to add label and assign best agents' do
+ expect(conversation.labels).to eq([])
+ expect(conversation.assignee).to be_nil
+
+ listener.conversation_updated(event)
+
+ conversation.reload
+ expect(conversation.labels.pluck(:name)).to contain_exactly('support', 'priority_customer')
+ expect(conversation.assignee).to eq(user_1)
+ end
+
+ it 'triggers automation rule send email transcript to the mentioned email' do
+ mailer = double
+
+ expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_updated)
+
+ listener.conversation_updated(event)
+
+ conversation.reload
+
+ allow(mailer).to receive(:conversation_transcript)
+ end
+
+ it 'triggers automation rule send message to the contacts' do
+ expect(conversation.messages).to be_empty
+
+ expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_updated)
+
+ listener.conversation_updated(event)
+
+ conversation.reload
+
+ expect(conversation.messages.first.content).to eq('Send this message.')
+ end
+
+ it 'triggers automation_rule with contact standard attributes' do
+ automation_rule.update!(
+ conditions: [
+ {
+ attribute_key: 'email',
+ filter_operator: 'contains',
+ values: ['example.com'],
+ query_operator: 'AND'
+ }.with_indifferent_access,
+ {
+ attribute_key: 'customer_type',
+ filter_operator: 'equal_to',
+ values: ['platinum'],
+ query_operator: nil
+ }.with_indifferent_access
+ ]
+ )
+ expect(conversation.team_id).not_to eq(team.id)
+
+ listener.conversation_updated(event)
+
+ conversation.reload
+ expect(conversation.team_id).to eq(team.id)
+ end
end
end
@@ -224,7 +349,7 @@ describe AutomationRuleListener do
conversation.reload
- expect(conversation.messages.last.content).to eq('Send this message.')
+ expect(conversation.messages.first.content).to eq('Send this message.')
end
end
end
diff --git a/spec/listeners/webhook_listener_spec.rb b/spec/listeners/webhook_listener_spec.rb
index 0bebc8d15..bcc990c5b 100644
--- a/spec/listeners/webhook_listener_spec.rb
+++ b/spec/listeners/webhook_listener_spec.rb
@@ -105,4 +105,80 @@ describe WebhookListener do
end
end
end
+
+ describe '#conversation_resolved' do
+ let!(:conversation_resolved_event) do
+ Events::Base.new(event_name, Time.zone.now, conversation: conversation.reload, changed_attributes: { status: [:open, :resolved] })
+ end
+ let(:event_name) { :'conversation.resolved' }
+
+ context 'when webhook is not configured' do
+ it 'does not trigger webhook' do
+ expect(WebhookJob).to receive(:perform_later).exactly(0).times
+ listener.conversation_resolved(conversation_resolved_event)
+ end
+ end
+
+ context 'when webhook is configured' do
+ it 'triggers webhook' do
+ webhook = create(:webhook, inbox: inbox, account: account)
+
+ conversation.update(status: :resolved)
+
+ expect(WebhookJob).to receive(:perform_later).with(webhook.url,
+ conversation.webhook_data.merge(event: 'conversation_resolved',
+ changed_attributes: [{ status: {
+ current_value: :resolved, previous_value: :open
+ } }])).once
+
+ listener.conversation_resolved(conversation_resolved_event)
+ end
+ end
+ end
+
+ describe '#conversation_updated' do
+ let(:custom_attributes) { { test: nil } }
+ let!(:conversation_updated_event) do
+ Events::Base.new(
+ event_name, Time.zone.now,
+ conversation: conversation.reload,
+ changed_attributes: {
+ custom_attributes: [{ test: nil }, { test: 'testing custom attri webhook' }]
+ }
+ )
+ end
+ let(:event_name) { :'conversation.updated' }
+
+ context 'when webhook is not configured' do
+ it 'does not trigger webhook' do
+ expect(WebhookJob).to receive(:perform_later).exactly(0).times
+ listener.conversation_updated(conversation_updated_event)
+ end
+ end
+
+ context 'when webhook is configured' do
+ it 'triggers webhook' do
+ webhook = create(:webhook, inbox: inbox, account: account)
+
+ conversation.update(custom_attributes: { test: 'testing custom attri webhook' })
+
+ expect(WebhookJob).to receive(:perform_later).with(
+ webhook.url,
+ conversation.webhook_data.merge(
+ event: 'conversation_updated',
+ changed_attributes: [
+ {
+ custom_attributes: {
+ previous_value: { test: nil },
+ current_value: { test: 'testing custom attri webhook' }
+ }
+ }
+ ]
+ )
+ ).once
+
+ listener.conversation_updated(conversation_updated_event)
+ end
+ end
+ end
end
diff --git a/spec/models/conversation_spec.rb b/spec/models/conversation_spec.rb
index a1d67f7d0..182073fbc 100644
--- a/spec/models/conversation_spec.rb
+++ b/spec/models/conversation_spec.rb
@@ -54,7 +54,8 @@ RSpec.describe Conversation, type: :model do
it 'runs after_create callbacks' do
# send_events
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
- .with(described_class::CONVERSATION_CREATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: false)
+ .with(described_class::CONVERSATION_CREATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: false,
+ changed_attributes: nil)
end
end
@@ -115,14 +116,21 @@ RSpec.describe Conversation, type: :model do
assignee: new_assignee,
label_list: [label.title]
)
+ status_change = conversation.status_change
+ changed_attributes = conversation.previous_changes
+
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
- .with(described_class::CONVERSATION_RESOLVED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true)
+ .with(described_class::CONVERSATION_RESOLVED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true,
+ changed_attributes: status_change)
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
- .with(described_class::CONVERSATION_READ, kind_of(Time), conversation: conversation, notifiable_assignee_change: true)
+ .with(described_class::CONVERSATION_READ, kind_of(Time), conversation: conversation, notifiable_assignee_change: true,
+ changed_attributes: nil)
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
- .with(described_class::ASSIGNEE_CHANGED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true)
+ .with(described_class::ASSIGNEE_CHANGED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true,
+ changed_attributes: nil)
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
- .with(described_class::CONVERSATION_UPDATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true)
+ .with(described_class::CONVERSATION_UPDATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true,
+ changed_attributes: changed_attributes)
end
it 'will not run conversation_updated event for empty updates' do
diff --git a/spec/services/round_robin/manage_service_spec.rb b/spec/services/round_robin/manage_service_spec.rb
index 0deef3109..75110d325 100644
--- a/spec/services/round_robin/manage_service_spec.rb
+++ b/spec/services/round_robin/manage_service_spec.rb
@@ -1,7 +1,7 @@
require 'rails_helper'
describe RoundRobin::ManageService do
- subject(:round_robin_manage_service) { ::RoundRobin::ManageService.new(inbox: inbox) }
+ subject(:round_robin_manage_service) { described_class.new(inbox: inbox) }
let!(:account) { create(:account) }
let!(:inbox) { create(:inbox, account: account) }
@@ -18,8 +18,9 @@ describe RoundRobin::ManageService do
it 'gets intersection of priority list and agent queue. get and move agent to the end of the list' do
expected_queue = [inbox_members[2].user_id, inbox_members[4].user_id, inbox_members[3].user_id, inbox_members[1].user_id,
inbox_members[0].user_id].map(&:to_s)
- expect(round_robin_manage_service.available_agent(priority_list: [inbox_members[3].user_id,
- inbox_members[2].user_id])).to eq inbox_members[2].user
+ # prority list will be ids in string, since thats what redis supplies to us
+ expect(round_robin_manage_service.available_agent(priority_list: [inbox_members[3].user_id.to_s,
+ inbox_members[2].user_id.to_s])).to eq inbox_members[2].user
expect(round_robin_manage_service.send(:queue)).to eq(expected_queue)
end
@@ -39,5 +40,32 @@ describe RoundRobin::ManageService do
# the service have refreshed the redis queue before performing
expect(round_robin_manage_service.send(:queue).sort.map(&:to_i)).to eq(inbox_members.map(&:user_id).sort)
end
+
+ context 'when allowed_member_ids is passed' do
+ it 'will get the first allowed member and move it to the end of the queue' do
+ expected_queue = [inbox_members[3].user_id, inbox_members[2].user_id, inbox_members[4].user_id, inbox_members[1].user_id,
+ inbox_members[0].user_id].map(&:to_s)
+ expect(described_class.new(inbox: inbox,
+ allowed_member_ids: [inbox_members[3].user_id,
+ inbox_members[2].user_id]).available_agent).to eq inbox_members[2].user
+ expect(described_class.new(inbox: inbox,
+ allowed_member_ids: [inbox_members[3].user_id,
+ inbox_members[2].user_id]).available_agent).to eq inbox_members[3].user
+ expect(round_robin_manage_service.send(:queue)).to eq(expected_queue)
+ end
+
+ it 'will get union of priority list and allowed_member_ids and move it to the end of the queue' do
+ expected_queue = [inbox_members[3].user_id, inbox_members[4].user_id, inbox_members[2].user_id, inbox_members[1].user_id,
+ inbox_members[0].user_id].map(&:to_s)
+ # prority list will be ids in string, since thats what redis supplies to us
+ expect(described_class.new(inbox: inbox,
+ allowed_member_ids: [inbox_members[3].user_id,
+ inbox_members[2].user_id])
+ .available_agent(
+ priority_list: [inbox_members[3].user_id.to_s]
+ )).to eq inbox_members[3].user
+ expect(round_robin_manage_service.send(:queue)).to eq(expected_queue)
+ end
+ end
end
end
diff --git a/swagger/definitions/index.yml b/swagger/definitions/index.yml
index fb24800de..c120320c8 100644
--- a/swagger/definitions/index.yml
+++ b/swagger/definitions/index.yml
@@ -12,6 +12,8 @@ request_error:
generic_id:
$ref: ./resource/extension/generic.yml
+canned_response:
+ $ref: ./resource/canned_response.yml
contact:
$ref: ./resource/contact.yml
conversation:
@@ -64,6 +66,9 @@ agent_bot_create_update_payload:
user_create_update_payload:
$ref: ./request/user/create_update_payload.yml
+canned_response_create_update_payload:
+ $ref: ./request/canned_response/create_update_payload.yml
+
## contact
contact_create:
$ref: ./request/contact/create.yml
@@ -144,10 +149,11 @@ extended_message:
- $ref: ./resource/extension/message/with_source_sender.yml
-## report list
-report:
- type: array
- description: 'array of conversation count based on date'
- items:
- allOf:
- - $ref: './resource/report.yml'
+## report
+account_summary:
+ $ref: './resource/reports/summary.yml'
+agent_conversation_metrics:
+ $ref: './resource/reports/conversation/agent.yml'
+
+
+
diff --git a/swagger/definitions/request/canned_response/create_update_payload.yml b/swagger/definitions/request/canned_response/create_update_payload.yml
new file mode 100644
index 000000000..ef6f7f74a
--- /dev/null
+++ b/swagger/definitions/request/canned_response/create_update_payload.yml
@@ -0,0 +1,8 @@
+type: object
+properties:
+ content:
+ type: string
+ description: Message content for canned response
+ short_code:
+ type: string
+ description: Short Code for quick access of the canned response
diff --git a/swagger/definitions/resource/canned_response.yml b/swagger/definitions/resource/canned_response.yml
new file mode 100644
index 000000000..b044b703a
--- /dev/null
+++ b/swagger/definitions/resource/canned_response.yml
@@ -0,0 +1,14 @@
+type: object
+properties:
+ id:
+ type: integer
+ description: ID of the canned response
+ content:
+ type: string
+ description: Message content for canned response
+ short_code:
+ type: string
+ description: Short Code for quick access of the canned response
+ account_id:
+ type: integer
+ description: Account Id
diff --git a/swagger/definitions/resource/reports/conversation/agent.yml b/swagger/definitions/resource/reports/conversation/agent.yml
new file mode 100644
index 000000000..7077b767b
--- /dev/null
+++ b/swagger/definitions/resource/reports/conversation/agent.yml
@@ -0,0 +1,18 @@
+type: object
+properties:
+ user:
+ type: object
+ properties:
+ id:
+ type: number
+ name:
+ type: string
+ thumbnail:
+ type: string
+ metric:
+ type: object
+ properties:
+ open:
+ type: number
+ unattended:
+ type: number
\ No newline at end of file
diff --git a/swagger/definitions/resource/report.yml b/swagger/definitions/resource/reports/summary.yml
similarity index 100%
rename from swagger/definitions/resource/report.yml
rename to swagger/definitions/resource/reports/summary.yml
diff --git a/swagger/index.yml b/swagger/index.yml
index 2420658e3..28f20ce5a 100644
--- a/swagger/index.yml
+++ b/swagger/index.yml
@@ -54,6 +54,7 @@ x-tagGroups:
tags:
- Account AgentBots
- Agent
+ - Canned Response
- Contact
- Conversation
- Conversation Assignment
diff --git a/swagger/paths/application/canned_responses/create.yml b/swagger/paths/application/canned_responses/create.yml
new file mode 100644
index 000000000..69b3673bc
--- /dev/null
+++ b/swagger/paths/application/canned_responses/create.yml
@@ -0,0 +1,21 @@
+tags:
+ - Canned Response
+operationId: add-new-canned-response-to-account
+summary: Add a New Canned Response
+description: Add a new Canned Response to Account
+security:
+ - userApiKey: []
+parameters:
+ - name: data
+ in: body
+ required: true
+ schema:
+ $ref: '#/definitions/canned_response_create_update_payload'
+responses:
+ 200:
+ description: Success
+ schema:
+ description: 'Newly Created Canned Response'
+ $ref: '#/definitions/canned_response'
+ 403:
+ description: Access denied
diff --git a/swagger/paths/application/canned_responses/delete.yml b/swagger/paths/application/canned_responses/delete.yml
new file mode 100644
index 000000000..4b82e5068
--- /dev/null
+++ b/swagger/paths/application/canned_responses/delete.yml
@@ -0,0 +1,20 @@
+tags:
+ - Canned Response
+operationId: delete-canned-response-from-account
+summary: Remove a Canned Response from Account
+description: Remove a Canned Response from Account
+security:
+ - userApiKey: []
+parameters:
+ - in: path
+ name: id
+ type: integer
+ required: true
+ description: The ID of the canned response to be deleted
+responses:
+ 200:
+ description: Success
+ 404:
+ description: Canned Response not found
+ 403:
+ description: Access denied
diff --git a/swagger/paths/application/canned_responses/index.yml b/swagger/paths/application/canned_responses/index.yml
new file mode 100644
index 000000000..4e4a24c5d
--- /dev/null
+++ b/swagger/paths/application/canned_responses/index.yml
@@ -0,0 +1,17 @@
+tags:
+ - Canned Response
+operationId: get-account-canned-response
+summary: List all Canned Responses in an Account
+description: Get Details of Canned Responses in an Account
+security:
+ - userApiKey: []
+responses:
+ 200:
+ description: Success
+ schema:
+ type: array
+ description: 'Array of all canned responses'
+ items:
+ $ref: '#/definitions/canned_response'
+ 403:
+ description: Access denied
diff --git a/swagger/paths/application/canned_responses/update.yml b/swagger/paths/application/canned_responses/update.yml
new file mode 100644
index 000000000..7ea801f2f
--- /dev/null
+++ b/swagger/paths/application/canned_responses/update.yml
@@ -0,0 +1,28 @@
+tags:
+ - Canned Response
+operationId: update-canned-response-in-account
+summary: Update Canned Response in Account
+description: Update a Canned Response in Account
+security:
+ - userApiKey: []
+parameters:
+ - in: path
+ name: id
+ type: integer
+ required: true
+ description: The ID of the canned response to be updated.
+ - name: data
+ in: body
+ required: true
+ schema:
+ $ref: '#/definitions/canned_response_create_update_payload'
+responses:
+ 200:
+ description: Success
+ schema:
+ description: 'The updated canned response'
+ $ref: '#/definitions/canned_response'
+ 404:
+ description: Agent not found
+ 403:
+ description: Access denied
diff --git a/swagger/paths/application/reports/conversation/account.yml b/swagger/paths/application/reports/conversation/account.yml
new file mode 100644
index 000000000..28aac72e7
--- /dev/null
+++ b/swagger/paths/application/reports/conversation/account.yml
@@ -0,0 +1,23 @@
+tags:
+ - Reports
+operationId: get-account-conversation-metrics
+summary: Account Conversation Metrics
+description: Get conversation metrics for Account
+responses:
+ 200:
+ description: Success
+ schema:
+ type: object
+ description: 'Object of account conversation metrics'
+ properties:
+ open:
+ type: number
+ unattended:
+ type: number
+ unassigned:
+ type: number
+
+ 404:
+ description: reports not found
+ 403:
+ description: Access denied
diff --git a/swagger/paths/application/reports/conversation/agent.yml b/swagger/paths/application/reports/conversation/agent.yml
new file mode 100644
index 000000000..90f433e4a
--- /dev/null
+++ b/swagger/paths/application/reports/conversation/agent.yml
@@ -0,0 +1,18 @@
+tags:
+ - Reports
+operationId: get-agent-conversation-metrics
+summary: Agent Conversation Metrics
+description: Get conversation metrics for Agent
+responses:
+ 200:
+ description: Success
+ schema:
+ type: array
+ description: 'Array of agent based conversation metrics'
+ items:
+ $ref: '#/definitions/agent_conversation_metrics'
+
+ 404:
+ description: reports not found
+ 403:
+ description: Access denied
diff --git a/swagger/paths/application/reports/index.yml b/swagger/paths/application/reports/index.yml
index dacf77a13..3d86df38d 100644
--- a/swagger/paths/application/reports/index.yml
+++ b/swagger/paths/application/reports/index.yml
@@ -10,7 +10,12 @@ responses:
type: array
description: 'Array of date based conversation statistics'
items:
- $ref: '#/definitions/report'
+ type: object
+ properties:
+ value:
+ type: string
+ timestamp:
+ type: number
404:
description: reports not found
403:
diff --git a/swagger/paths/application/reports/summary.yml b/swagger/paths/application/reports/summary.yml
index ec659e8e8..f9538a9f8 100644
--- a/swagger/paths/application/reports/summary.yml
+++ b/swagger/paths/application/reports/summary.yml
@@ -7,10 +7,8 @@ responses:
200:
description: Success
schema:
- type: array
- description: 'Array of date based conversation statistics'
- items:
- $ref: '#/definitions/report'
+ description: 'Object of summary metrics'
+ $ref: '#/definitions/account_summary'
404:
description: reports not found
403:
diff --git a/swagger/paths/index.yml b/swagger/paths/index.yml
index 8a4154bb8..eeb2539f1 100644
--- a/swagger/paths/index.yml
+++ b/swagger/paths/index.yml
@@ -154,6 +154,21 @@
delete:
$ref: ./application/agents/delete.yml
+# Agents
+/api/v1/accounts/{account_id}/canned_responses:
+ parameters:
+ - $ref: '#/parameters/account_id'
+ get:
+ $ref: ./application/canned_responses/index.yml
+ post:
+ $ref: ./application/canned_responses/create.yml
+/api/v1/accounts/{account_id}/canned_responses/{id}:
+ parameters:
+ - $ref: '#/parameters/account_id'
+ patch:
+ $ref: ./application/canned_responses/update.yml
+ delete:
+ $ref: ./application/canned_responses/delete.yml
# Contacts
/api/v1/accounts/{account_id}/contacts:
@@ -376,3 +391,35 @@
description: The timestamp from where report should stop.
get:
$ref: './application/reports/summary.yml'
+
+# Conversation metrics for account
+/api/v2/accounts/{account_id}/reports/conversations:
+ parameters:
+ - $ref: '#/parameters/account_id'
+ - in: query
+ name: type
+ type: string
+ enum:
+ - account
+ required: true
+ description: Type of report
+ get:
+ $ref: './application/reports/conversation/account.yml'
+
+# Conversation metrics for agent
+/api/v2/accounts/{account_id}/reports/conversations/:
+ parameters:
+ - $ref: '#/parameters/account_id'
+ - in: query
+ name: type
+ type: string
+ enum:
+ - agent
+ required: true
+ description: Type of report
+ - in: query
+ name: user_id
+ type: string
+ description: The numeric ID of the user
+ get:
+ $ref: './application/reports/conversation/agent.yml'
\ No newline at end of file
diff --git a/swagger/swagger.json b/swagger/swagger.json
index 8e574fedc..b17f9c586 100644
--- a/swagger/swagger.json
+++ b/swagger/swagger.json
@@ -1333,6 +1333,167 @@
}
}
},
+ "/api/v1/accounts/{account_id}/canned_responses": {
+ "parameters": [
+ {
+ "$ref": "#/parameters/account_id"
+ }
+ ],
+ "get": {
+ "tags": [
+ "Canned Response"
+ ],
+ "operationId": "get-account-canned-response",
+ "summary": "List all Canned Responses in an Account",
+ "description": "Get Details of Canned Responses in an Account",
+ "security": [
+ {
+ "userApiKey": [
+
+ ]
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Success",
+ "schema": {
+ "type": "array",
+ "description": "Array of all canned responses",
+ "items": {
+ "$ref": "#/definitions/canned_response"
+ }
+ }
+ },
+ "403": {
+ "description": "Access denied"
+ }
+ }
+ },
+ "post": {
+ "tags": [
+ "Canned Response"
+ ],
+ "operationId": "add-new-canned-response-to-account",
+ "summary": "Add a New Canned Response",
+ "description": "Add a new Canned Response to Account",
+ "security": [
+ {
+ "userApiKey": [
+
+ ]
+ }
+ ],
+ "parameters": [
+ {
+ "name": "data",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/canned_response_create_update_payload"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Success",
+ "schema": {
+ "$ref": "#/definitions/canned_response"
+ }
+ },
+ "403": {
+ "description": "Access denied"
+ }
+ }
+ }
+ },
+ "/api/v1/accounts/{account_id}/canned_responses/{id}": {
+ "parameters": [
+ {
+ "$ref": "#/parameters/account_id"
+ }
+ ],
+ "patch": {
+ "tags": [
+ "Canned Response"
+ ],
+ "operationId": "update-canned-response-in-account",
+ "summary": "Update Canned Response in Account",
+ "description": "Update a Canned Response in Account",
+ "security": [
+ {
+ "userApiKey": [
+
+ ]
+ }
+ ],
+ "parameters": [
+ {
+ "in": "path",
+ "name": "id",
+ "type": "integer",
+ "required": true,
+ "description": "The ID of the canned response to be updated."
+ },
+ {
+ "name": "data",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/canned_response_create_update_payload"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Success",
+ "schema": {
+ "$ref": "#/definitions/canned_response"
+ }
+ },
+ "404": {
+ "description": "Agent not found"
+ },
+ "403": {
+ "description": "Access denied"
+ }
+ }
+ },
+ "delete": {
+ "tags": [
+ "Canned Response"
+ ],
+ "operationId": "delete-canned-response-from-account",
+ "summary": "Remove a Canned Response from Account",
+ "description": "Remove a Canned Response from Account",
+ "security": [
+ {
+ "userApiKey": [
+
+ ]
+ }
+ ],
+ "parameters": [
+ {
+ "in": "path",
+ "name": "id",
+ "type": "integer",
+ "required": true,
+ "description": "The ID of the canned response to be deleted"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Success"
+ },
+ "404": {
+ "description": "Canned Response not found"
+ },
+ "403": {
+ "description": "Access denied"
+ }
+ }
+ }
+ },
"/api/v1/accounts/{account_id}/contacts": {
"get": {
"tags": [
@@ -3443,7 +3604,15 @@
"type": "array",
"description": "Array of date based conversation statistics",
"items": {
- "$ref": "#/definitions/report"
+ "type": "object",
+ "properties": {
+ "value": {
+ "type": "string"
+ },
+ "timestamp": {
+ "type": "number"
+ }
+ }
}
}
},
@@ -3490,14 +3659,110 @@
"operationId": "list-all-conversation-statistics-summary",
"summary": "Get Account reports summary",
"description": "Get Account reports summary for a specific type and date range",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "schema": {
+ "$ref": "#/definitions/account_summary"
+ }
+ },
+ "404": {
+ "description": "reports not found"
+ },
+ "403": {
+ "description": "Access denied"
+ }
+ }
+ }
+ },
+ "/api/v2/accounts/{account_id}/reports/conversations": {
+ "parameters": [
+ {
+ "$ref": "#/parameters/account_id"
+ },
+ {
+ "in": "query",
+ "name": "type",
+ "type": "string",
+ "enum": [
+ "account"
+ ],
+ "required": true,
+ "description": "Type of report"
+ }
+ ],
+ "get": {
+ "tags": [
+ "Reports"
+ ],
+ "operationId": "get-account-conversation-metrics",
+ "summary": "Account Conversation Metrics",
+ "description": "Get conversation metrics for Account",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "schema": {
+ "type": "object",
+ "description": "Object of account conversation metrics",
+ "properties": {
+ "open": {
+ "type": "number"
+ },
+ "unattended": {
+ "type": "number"
+ },
+ "unassigned": {
+ "type": "number"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "reports not found"
+ },
+ "403": {
+ "description": "Access denied"
+ }
+ }
+ }
+ },
+ "/api/v2/accounts/{account_id}/reports/conversations/": {
+ "parameters": [
+ {
+ "$ref": "#/parameters/account_id"
+ },
+ {
+ "in": "query",
+ "name": "type",
+ "type": "string",
+ "enum": [
+ "agent"
+ ],
+ "required": true,
+ "description": "Type of report"
+ },
+ {
+ "in": "query",
+ "name": "user_id",
+ "type": "string",
+ "description": "The numeric ID of the user"
+ }
+ ],
+ "get": {
+ "tags": [
+ "Reports"
+ ],
+ "operationId": "get-agent-conversation-metrics",
+ "summary": "Agent Conversation Metrics",
+ "description": "Get conversation metrics for Agent",
"responses": {
"200": {
"description": "Success",
"schema": {
"type": "array",
- "description": "Array of date based conversation statistics",
+ "description": "Array of agent based conversation metrics",
"items": {
- "$ref": "#/definitions/report"
+ "$ref": "#/definitions/agent_conversation_metrics"
}
}
},
@@ -3549,6 +3814,27 @@
}
}
},
+ "canned_response": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "description": "ID of the canned response"
+ },
+ "content": {
+ "type": "string",
+ "description": "Message content for canned response"
+ },
+ "short_code": {
+ "type": "string",
+ "description": "Short Code for quick access of the canned response"
+ },
+ "account_id": {
+ "type": "integer",
+ "description": "Account Id"
+ }
+ }
+ },
"contact": {
"type": "object",
"properties": {
@@ -4196,6 +4482,19 @@
}
}
},
+ "canned_response_create_update_payload": {
+ "type": "object",
+ "properties": {
+ "content": {
+ "type": "string",
+ "description": "Message content for canned response"
+ },
+ "short_code": {
+ "type": "string",
+ "description": "Short Code for quick access of the canned response"
+ }
+ }
+ },
"contact_create": {
"type": "object",
"required": [
@@ -4684,58 +4983,80 @@
}
]
},
- "report": {
- "type": "array",
- "description": "array of conversation count based on date",
- "items": {
- "allOf": [
- {
- "type": "object",
- "properties": {
- "avg_first_response_time": {
- "type": "string"
- },
- "avg_resolution_time": {
- "type": "string"
- },
- "conversations_count": {
- "type": "number"
- },
- "incoming_messages_count": {
- "type": "number"
- },
- "outgoing_messages_count": {
- "type": "number"
- },
- "resolutions_count": {
- "type": "number"
- },
- "previous": {
- "type": "object",
- "properties": {
- "avg_first_response_time": {
- "type": "string"
- },
- "avg_resolution_time": {
- "type": "string"
- },
- "conversations_count": {
- "type": "number"
- },
- "incoming_messages_count": {
- "type": "number"
- },
- "outgoing_messages_count": {
- "type": "number"
- },
- "resolutions_count": {
- "type": "number"
- }
- }
- }
+ "account_summary": {
+ "type": "object",
+ "properties": {
+ "avg_first_response_time": {
+ "type": "string"
+ },
+ "avg_resolution_time": {
+ "type": "string"
+ },
+ "conversations_count": {
+ "type": "number"
+ },
+ "incoming_messages_count": {
+ "type": "number"
+ },
+ "outgoing_messages_count": {
+ "type": "number"
+ },
+ "resolutions_count": {
+ "type": "number"
+ },
+ "previous": {
+ "type": "object",
+ "properties": {
+ "avg_first_response_time": {
+ "type": "string"
+ },
+ "avg_resolution_time": {
+ "type": "string"
+ },
+ "conversations_count": {
+ "type": "number"
+ },
+ "incoming_messages_count": {
+ "type": "number"
+ },
+ "outgoing_messages_count": {
+ "type": "number"
+ },
+ "resolutions_count": {
+ "type": "number"
}
}
- ]
+ }
+ }
+ },
+ "agent_conversation_metrics": {
+ "type": "object",
+ "properties": {
+ "user": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "number"
+ },
+ "name": {
+ "type": "string"
+ },
+ "thumbnail": {
+ "type": "string"
+ }
+ }
+ },
+ "metric": {
+ "type": "object",
+ "properties": {
+ "open": {
+ "type": "number"
+ },
+ "unattended": {
+ "type": "number"
+ }
+ }
+ }
}
}
},
@@ -4894,6 +5215,7 @@
"tags": [
"Account AgentBots",
"Agent",
+ "Canned Response",
"Contact",
"Conversation",
"Conversation Assignment",